fabric-view.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144
  1. import { describe, it, expect, beforeEach, afterEach } from 'vitest';
  2. import * as fs from 'node:fs';
  3. import * as path from 'node:path';
  4. import * as os from 'node:os';
  5. import { CodeGraph } from '../src';
  6. import { fabricViewResolver } from '../src/resolution/frameworks/fabric';
  7. describe('Fabric view component extractor (codegenNativeComponent specs)', () => {
  8. it('extracts a component node + prop nodes from a Native*.ts spec', () => {
  9. const source = `
  10. 'use client';
  11. import { codegenNativeComponent } from 'react-native';
  12. import type { ViewProps, CodegenTypes as CT, ColorValue } from 'react-native';
  13. type TapEvent = Readonly<{ x: number; y: number }>;
  14. export interface NativeProps extends ViewProps {
  15. color?: ColorValue;
  16. onTap?: CT.DirectEventHandler<TapEvent>;
  17. caption?: string;
  18. }
  19. export default codegenNativeComponent<NativeProps>('MyView', {});
  20. `;
  21. const result = fabricViewResolver.extract?.('src/MyViewNativeComponent.ts', source);
  22. expect(result).toBeDefined();
  23. const componentNodes = result!.nodes.filter((n) => n.kind === 'component');
  24. const propNodes = result!.nodes.filter((n) => n.kind === 'property');
  25. expect(componentNodes).toHaveLength(1);
  26. expect(componentNodes[0]?.name).toBe('MyView');
  27. expect(propNodes.map((n) => n.name).sort()).toEqual(['caption', 'color', 'onTap']);
  28. });
  29. it('returns nothing for a file without codegenNativeComponent', () => {
  30. const source = `export const x = 1;`;
  31. const result = fabricViewResolver.extract?.('plain.ts', source);
  32. expect(result?.nodes).toHaveLength(0);
  33. });
  34. it('handles a spec with no NativeProps interface (rare but valid)', () => {
  35. const source = `
  36. import { codegenNativeComponent } from 'react-native';
  37. export default codegenNativeComponent('BareComponent');
  38. `;
  39. const result = fabricViewResolver.extract?.('Bare.ts', source);
  40. // Component node exists; no prop nodes.
  41. const components = result!.nodes.filter((n) => n.kind === 'component');
  42. const props = result!.nodes.filter((n) => n.kind === 'property');
  43. expect(components).toHaveLength(1);
  44. expect(components[0]?.name).toBe('BareComponent');
  45. expect(props).toHaveLength(0);
  46. });
  47. });
  48. describe('Fabric end-to-end: JSX consumer → Fabric component → native class', () => {
  49. let dir: string;
  50. beforeEach(() => {
  51. dir = fs.mkdtempSync(path.join(os.tmpdir(), 'fabric-fixture-'));
  52. });
  53. afterEach(() => {
  54. fs.rmSync(dir, { recursive: true, force: true });
  55. });
  56. it('connects <MyView/> JSX to the native ObjC class via Fabric synthesizer', async () => {
  57. fs.writeFileSync(
  58. path.join(dir, 'package.json'),
  59. '{"dependencies":{"react-native":"^0.73"}}'
  60. );
  61. // Fabric spec.
  62. fs.mkdirSync(path.join(dir, 'spec'));
  63. fs.writeFileSync(
  64. path.join(dir, 'spec', 'MyViewNativeComponent.ts'),
  65. `import { codegenNativeComponent } from 'react-native';
  66. import type { ViewProps } from 'react-native';
  67. export interface NativeProps extends ViewProps { color?: string; }
  68. export default codegenNativeComponent<NativeProps>('MyView');`
  69. );
  70. // Native iOS implementation — class named with the `View` suffix
  71. // convention.
  72. fs.mkdirSync(path.join(dir, 'ios'));
  73. fs.writeFileSync(
  74. path.join(dir, 'ios', 'MyView.mm'),
  75. `@interface MyViewView : UIView
  76. @end
  77. @implementation MyViewView
  78. - (void)setColor:(NSString *)c { /* … */ }
  79. @end`
  80. );
  81. // JSX consumer.
  82. fs.mkdirSync(path.join(dir, 'src'));
  83. fs.writeFileSync(
  84. path.join(dir, 'src', 'App.tsx'),
  85. `import React from 'react';
  86. import MyView from '../spec/MyViewNativeComponent';
  87. export function App() {
  88. return <MyView color="red"/>;
  89. }`
  90. );
  91. const cg = await CodeGraph.init(dir, { silent: true });
  92. await cg.indexAll();
  93. const db = (cg as any).db.db;
  94. // 1. The Fabric component node exists.
  95. const componentRows = db
  96. .prepare("SELECT id, name, kind FROM nodes WHERE id LIKE 'fabric-component:%' AND name='MyView'")
  97. .all();
  98. expect(componentRows).toHaveLength(1);
  99. // 2. The native class node exists.
  100. const nativeRows = db
  101. .prepare("SELECT id, name FROM nodes WHERE kind='class' AND language='objc' AND name='MyViewView'")
  102. .all();
  103. expect(nativeRows).toHaveLength(1);
  104. // 3. Fabric synthesizer bridges component → native class.
  105. const bridgeRows = db
  106. .prepare(
  107. `SELECT s.name comp, t.name native FROM edges e
  108. JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target
  109. WHERE json_extract(e.metadata,'$.synthesizedBy')='fabric-native-impl'
  110. AND s.name='MyView' AND t.name='MyViewView'`
  111. )
  112. .all();
  113. expect(bridgeRows).toHaveLength(1);
  114. // 4. JSX synthesizer links the App function → the Fabric component
  115. // (jsx-render edge keyed on the tag name 'MyView').
  116. const jsxRows = db
  117. .prepare(
  118. `SELECT s.name caller, t.name comp FROM edges e
  119. JOIN nodes s ON s.id=e.source JOIN nodes t ON t.id=e.target
  120. WHERE json_extract(e.metadata,'$.synthesizedBy')='jsx-render'
  121. AND t.id LIKE 'fabric-component:%' AND t.name='MyView'`
  122. )
  123. .all();
  124. cg.close?.();
  125. expect(jsxRows.length).toBeGreaterThanOrEqual(1);
  126. expect(jsxRows[0].caller).toBe('App');
  127. // The full flow: App (TSX) → MyView (fabric-component) → MyViewView (ObjC native class)
  128. });
  129. });