Skip to content

Commit 9ad26e6

Browse files
committed
test: add test for navigation codegen
1 parent ccdb606 commit 9ad26e6

4 files changed

Lines changed: 436 additions & 0 deletions

File tree

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import { afterEach, describe, expect, it, vi } from 'vitest';
2+
3+
import { generateKotlinDelegate, generateKotlinModule } from '../generators/android.js';
4+
import { generateObjCImplementation, generateSwiftDelegate } from '../generators/ios.js';
5+
import {
6+
generateIndexDts,
7+
generateIndexTs,
8+
generateTurboModuleSpec,
9+
} from '../generators/ts.js';
10+
import type { MethodSignature } from '../types.js';
11+
12+
const methods: MethodSignature[] = [
13+
{
14+
name: 'openScreen',
15+
params: [
16+
{ name: 'route', type: 'string', optional: false },
17+
{ name: 'params', type: 'Object', optional: true },
18+
],
19+
returnType: 'void',
20+
isAsync: false,
21+
},
22+
];
23+
24+
describe('navigation code generators', () => {
25+
it('generates TurboModule spec and index files', () => {
26+
const turboModuleSpec = generateTurboModuleSpec(methods);
27+
const indexTs = generateIndexTs(methods);
28+
const indexDts = generateIndexDts(methods);
29+
30+
expect(turboModuleSpec).toContain('export interface Spec extends TurboModule');
31+
expect(turboModuleSpec).toContain('openScreen(route: string, params?: Object): void;');
32+
33+
expect(indexTs).toContain('openScreen: (route: string, params?: Object)');
34+
expect(indexTs).toContain('NativeBrownfieldNavigation.openScreen(route, params)');
35+
36+
expect(indexDts).toContain('openScreen: (route: string, params?: Object) => void;');
37+
});
38+
39+
it('generates iOS bindings for sync methods', () => {
40+
const swiftDelegate = generateSwiftDelegate(methods);
41+
const objcImplementation = generateObjCImplementation(methods);
42+
43+
expect(swiftDelegate).toContain('@objc public protocol BrownfieldNavigationDelegate');
44+
expect(swiftDelegate).toContain('@objc func openScreen(_ route: String, params params: [String: Any]?)');
45+
46+
expect(objcImplementation).toContain('- (void)openScreen:(NSString *)route params:(NSDictionary * _Nullable)params');
47+
expect(objcImplementation).toContain(
48+
'[[[BrownfieldNavigationManager shared] getDelegate] openScreen:route params:params];'
49+
);
50+
});
51+
52+
it('generates Android bindings for sync methods', () => {
53+
const kotlinPackageName = 'com.callstack.nativebrownfieldnavigation';
54+
const kotlinDelegate = generateKotlinDelegate(methods, kotlinPackageName);
55+
const kotlinModule = generateKotlinModule(methods, kotlinPackageName);
56+
57+
expect(kotlinDelegate).toContain(`package ${kotlinPackageName}`);
58+
expect(kotlinDelegate).toContain('fun openScreen(route: String, params: ReadableMap?)');
59+
60+
expect(kotlinModule).toContain('import com.facebook.react.bridge.ReadableMap');
61+
expect(kotlinModule).toContain(
62+
'override fun openScreen(route: String, params: ReadableMap?)'
63+
);
64+
expect(kotlinModule).toContain(
65+
'BrownfieldNavigationManager.getDelegate().openScreen(route, params)'
66+
);
67+
});
68+
});
69+
70+
describe('transpileWithConsumerBabel dependency errors', () => {
71+
afterEach(() => {
72+
vi.resetModules();
73+
vi.unmock('node:module');
74+
});
75+
76+
it('throws a clear error when @babel/core cannot be resolved', async () => {
77+
vi.doMock('node:module', () => ({
78+
createRequire: () => ({
79+
resolve: () => {
80+
throw new Error('module not found');
81+
},
82+
}),
83+
}));
84+
85+
const { transpileWithConsumerBabel } = await import('../generators/ts.js');
86+
87+
expect(() =>
88+
transpileWithConsumerBabel(
89+
'const value: string = "hello"; export default value;',
90+
'/tmp/project',
91+
'/tmp/package'
92+
)
93+
).toThrow('Could not resolve "@babel/core". Install it in your app devDependencies.');
94+
});
95+
});
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, describe, expect, it, vi } from 'vitest';
5+
6+
const { addSourceMock, quicktypeMock } = vi.hoisted(() => ({
7+
addSourceMock: vi.fn(async (_source: unknown) => undefined),
8+
quicktypeMock: vi.fn(async ({ lang }: { lang: 'swift' | 'kotlin' }) => ({
9+
lines:
10+
lang === 'swift'
11+
? ['public struct UserProfile {}', 'public struct SessionResult {}']
12+
: ['data class UserProfile()', 'data class SessionResult()'],
13+
})),
14+
}));
15+
16+
vi.mock('quicktype-core', () => ({
17+
FetchingJSONSchemaStore: class {},
18+
InputData: class {
19+
addInput(): void {}
20+
},
21+
JSONSchemaInput: class {
22+
async addSource(source: unknown): Promise<void> {
23+
await addSourceMock(source);
24+
}
25+
},
26+
quicktype: quicktypeMock,
27+
}));
28+
29+
vi.mock('quicktype-typescript-input', () => ({
30+
schemaForTypeScriptSources: () => ({
31+
schema: JSON.stringify({
32+
definitions: {
33+
UserProfile: { type: 'object' },
34+
SessionResult: { type: 'object' },
35+
InternalOnly: { type: 'object' },
36+
},
37+
}),
38+
}),
39+
}));
40+
41+
import { generateNavigationModels } from '../generators/models.js';
42+
import type { MethodSignature } from '../types.js';
43+
44+
function createTempSpecFile(contents: string): string {
45+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'navigation-models-'));
46+
const specPath = path.join(tempDir, 'brownfield.navigation.ts');
47+
fs.writeFileSync(specPath, contents);
48+
return specPath;
49+
}
50+
51+
function cleanupTempSpecFile(specPath: string): void {
52+
fs.rmSync(path.dirname(specPath), { recursive: true, force: true });
53+
}
54+
55+
describe('generateNavigationModels', () => {
56+
const tempSpecFiles: string[] = [];
57+
58+
afterEach(() => {
59+
for (const specPath of tempSpecFiles) {
60+
cleanupTempSpecFile(specPath);
61+
}
62+
tempSpecFiles.length = 0;
63+
vi.clearAllMocks();
64+
});
65+
66+
it('generates models for complex referenced types', async () => {
67+
const specPath = createTempSpecFile(`
68+
export interface BrownfieldNavigationSpec {
69+
openProfile(profile: UserProfile): void;
70+
}
71+
72+
export interface UserProfile {}
73+
`);
74+
tempSpecFiles.push(specPath);
75+
76+
const methods: MethodSignature[] = [
77+
{
78+
name: 'openProfile',
79+
params: [{ name: 'profile', type: 'UserProfile', optional: false }],
80+
returnType: 'void',
81+
isAsync: false,
82+
},
83+
];
84+
85+
const models = await generateNavigationModels({
86+
specPath,
87+
methods,
88+
kotlinPackageName: 'com.callstack.nativebrownfieldnavigation',
89+
});
90+
91+
expect(models.modelTypeNames).toEqual(['UserProfile']);
92+
expect(models.swiftModels).toContain('public struct UserProfile');
93+
expect(models.kotlinModels).toContain('data class UserProfile');
94+
expect(addSourceMock).toHaveBeenCalled();
95+
expect(quicktypeMock).toHaveBeenCalledTimes(2);
96+
});
97+
98+
it('skips model generation when no complex types are referenced', async () => {
99+
const specPath = createTempSpecFile(`
100+
export interface BrownfieldNavigationSpec {
101+
open(route: string): void;
102+
}
103+
`);
104+
tempSpecFiles.push(specPath);
105+
106+
const methods: MethodSignature[] = [
107+
{
108+
name: 'open',
109+
params: [{ name: 'route', type: 'string', optional: false }],
110+
returnType: 'void',
111+
isAsync: false,
112+
},
113+
];
114+
115+
const models = await generateNavigationModels({
116+
specPath,
117+
methods,
118+
kotlinPackageName: 'com.callstack.nativebrownfieldnavigation',
119+
});
120+
121+
expect(models).toEqual({ modelTypeNames: [] });
122+
expect(quicktypeMock).not.toHaveBeenCalled();
123+
});
124+
});
Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import fs from 'node:fs';
2+
import os from 'node:os';
3+
import path from 'node:path';
4+
import { afterEach, describe, expect, it } from 'vitest';
5+
6+
import { parseNavigationSpec } from '../parser.js';
7+
8+
function createTempSpecFile(contents: string): string {
9+
const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'navigation-parser-'));
10+
const specPath = path.join(tempDir, 'brownfield.navigation.ts');
11+
fs.writeFileSync(specPath, contents);
12+
return specPath;
13+
}
14+
15+
function cleanupTempSpecFile(specPath: string): void {
16+
fs.rmSync(path.dirname(specPath), { recursive: true, force: true });
17+
}
18+
19+
describe('parseNavigationSpec', () => {
20+
const tempSpecFiles: string[] = [];
21+
22+
afterEach(() => {
23+
for (const specPath of tempSpecFiles) {
24+
cleanupTempSpecFile(specPath);
25+
}
26+
tempSpecFiles.length = 0;
27+
});
28+
29+
it('parses methods from BrownfieldNavigationSpec interface', () => {
30+
const specPath = createTempSpecFile(`
31+
export interface BrownfieldNavigationSpec {
32+
openScreen(route: string, params?: Object): void;
33+
}
34+
`);
35+
tempSpecFiles.push(specPath);
36+
37+
const methods = parseNavigationSpec(specPath);
38+
39+
expect(methods).toEqual([
40+
{
41+
name: 'openScreen',
42+
params: [
43+
{ name: 'route', type: 'string', optional: false },
44+
{ name: 'params', type: 'Object', optional: true },
45+
],
46+
returnType: 'void',
47+
isAsync: false,
48+
},
49+
]);
50+
});
51+
52+
it('falls back to Spec interface when BrownfieldNavigationSpec is absent', () => {
53+
const specPath = createTempSpecFile(`
54+
export interface Spec {
55+
presentModal(id: string): number;
56+
}
57+
`);
58+
tempSpecFiles.push(specPath);
59+
60+
const methods = parseNavigationSpec(specPath);
61+
62+
expect(methods).toEqual([
63+
{
64+
name: 'presentModal',
65+
params: [{ name: 'id', type: 'string', optional: false }],
66+
returnType: 'number',
67+
isAsync: false,
68+
},
69+
]);
70+
});
71+
72+
it('throws when no valid spec interface is present', () => {
73+
const specPath = createTempSpecFile(`
74+
export interface NavigationSpec {
75+
noOp(): void;
76+
}
77+
`);
78+
tempSpecFiles.push(specPath);
79+
80+
expect(() => parseNavigationSpec(specPath)).toThrow(
81+
'Could not find BrownfieldNavigationSpec or Spec interface in spec file'
82+
);
83+
});
84+
85+
it('throws when spec file does not exist', () => {
86+
expect(() => parseNavigationSpec('/tmp/non-existent-navigation-spec.ts')).toThrow(
87+
'Spec file not found: /tmp/non-existent-navigation-spec.ts'
88+
);
89+
});
90+
});

0 commit comments

Comments
 (0)