@openwebf/webf 0.23.0 → 0.23.7

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -2,15 +2,18 @@
2
2
  jest.mock('fs');
3
3
  jest.mock('child_process');
4
4
  jest.mock('../src/generator');
5
+ jest.mock('glob');
5
6
  jest.mock('inquirer');
6
7
  jest.mock('yaml');
7
8
 
8
9
  import fs from 'fs';
9
10
  import path from 'path';
10
11
  import { spawnSync } from 'child_process';
12
+ import { glob } from 'glob';
11
13
 
12
14
  const mockFs = fs as jest.Mocked<typeof fs>;
13
15
  const mockSpawnSync = spawnSync as jest.MockedFunction<typeof spawnSync>;
16
+ const mockGlob = glob as jest.Mocked<typeof glob>;
14
17
 
15
18
  // Set up default mocks before importing commands
16
19
  mockFs.readFileSync = jest.fn().mockImplementation((filePath: any) => {
@@ -84,6 +87,7 @@ describe('Commands', () => {
84
87
  });
85
88
  // Default mock for readdirSync to avoid undefined
86
89
  mockFs.readdirSync.mockReturnValue([] as any);
90
+ mockGlob.globSync.mockReturnValue([]);
87
91
  });
88
92
 
89
93
 
@@ -356,6 +360,36 @@ describe('Commands', () => {
356
360
  consoleSpy.mockRestore();
357
361
  });
358
362
 
363
+ it('should generate only Dart bindings when dartOnly is set', async () => {
364
+ const options = {
365
+ flutterPackageSrc: '/flutter/src',
366
+ dartOnly: true,
367
+ };
368
+
369
+ // Mock TypeScript validation
370
+ mockTypeScriptValidation('/flutter/src');
371
+
372
+ await generateCommand('/dist', options as any);
373
+
374
+ // Should call dartGen for the Flutter package, but not reactGen/vueGen
375
+ expect(mockGenerator.dartGen).toHaveBeenCalledWith({
376
+ source: '/flutter/src',
377
+ target: '/flutter/src',
378
+ command: expect.stringContaining('webf codegen'),
379
+ exclude: undefined,
380
+ });
381
+ expect(mockGenerator.reactGen).not.toHaveBeenCalled();
382
+ expect(mockGenerator.vueGen).not.toHaveBeenCalled();
383
+
384
+ // Should not attempt to build or run npm scripts
385
+ const spawnCalls = (mockSpawnSync as jest.Mock).mock.calls;
386
+ const buildOrPublishCalls = spawnCalls.filter(call => {
387
+ const args = call[1] as any;
388
+ return Array.isArray(args) && (args.includes('run') || args.includes('publish') || args.includes('install'));
389
+ });
390
+ expect(buildOrPublishCalls).toHaveLength(0);
391
+ });
392
+
359
393
  it('should show instructions when --flutter-package-src is missing', async () => {
360
394
  const options = { framework: 'react' };
361
395
 
@@ -420,11 +454,57 @@ describe('Commands', () => {
420
454
 
421
455
  await generateCommand('/dist', options);
422
456
 
423
- expect(mockGenerator.reactGen).toHaveBeenCalledWith({
457
+ expect(mockGenerator.reactGen).toHaveBeenCalledWith(expect.objectContaining({
424
458
  source: '/flutter/src',
425
459
  target: path.resolve('/dist'),
426
460
  command: expect.stringContaining('webf codegen')
461
+ }));
462
+ });
463
+
464
+ it('should generate an aggregated README in dist from markdown docs', async () => {
465
+ const options = {
466
+ flutterPackageSrc: '/flutter/src',
467
+ framework: 'react',
468
+ packageName: 'test-package'
469
+ };
470
+
471
+ // Mock TypeScript validation
472
+ mockTypeScriptValidation('/flutter/src');
473
+
474
+ // Mock .d.ts files so copyMarkdownDocsToDist sees at least one entry
475
+ mockGlob.globSync.mockReturnValue(['lib/src/alert.d.ts'] as any);
476
+
477
+ const originalExistsSync = mockFs.existsSync as jest.Mock;
478
+ mockFs.existsSync = jest.fn().mockImplementation((filePath: any) => {
479
+ const pathStr = filePath.toString();
480
+ const distRoot = path.join(path.resolve('/dist'), 'dist');
481
+ if (pathStr === distRoot) return true;
482
+ if (pathStr === path.join(path.resolve('/dist'), 'package.json')) return true;
483
+ if (pathStr === path.join(path.resolve('/dist'), 'global.d.ts')) return true;
484
+ if (pathStr === path.join(path.resolve('/dist'), 'tsconfig.json')) return true;
485
+ if (pathStr.endsWith('.md') && pathStr.includes('/flutter/src')) return true;
486
+ return originalExistsSync(filePath);
427
487
  });
488
+
489
+ // Ensure that reading a markdown file returns some content
490
+ const originalReadFileSync = mockFs.readFileSync as jest.Mock;
491
+ mockFs.readFileSync = jest.fn().mockImplementation((filePath: any, encoding?: any) => {
492
+ const pathStr = filePath.toString();
493
+ if (pathStr.endsWith('.md')) {
494
+ return '# Test Component\n\nThis is a test component doc.';
495
+ }
496
+ return originalReadFileSync(filePath, encoding);
497
+ });
498
+
499
+ await generateCommand('/dist', options);
500
+
501
+ // README.md should be written into dist directory
502
+ const writeCalls = (mockFs.writeFileSync as jest.Mock).mock.calls;
503
+ const readmeCall = writeCalls.find(call => {
504
+ const p = call[0].toString();
505
+ return p.endsWith(path.join('dist', 'README.md'));
506
+ });
507
+ expect(readmeCall).toBeDefined();
428
508
  });
429
509
 
430
510
  it('should create new project if package.json not found', async () => {
@@ -1245,4 +1325,4 @@ describe('Commands', () => {
1245
1325
  });
1246
1326
  });
1247
1327
  });
1248
- });
1328
+ });
@@ -0,0 +1,58 @@
1
+ import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
2
+ import { generateDartClass } from '../src/dart';
3
+ import { IDLBlob } from '../src/IDLBlob';
4
+
5
+ describe('Dart nullable union properties', () => {
6
+ beforeEach(() => {
7
+ clearCaches();
8
+ });
9
+
10
+ it('handles boolean | null properties and maps "null" attribute to null', () => {
11
+ const tsContent = `
12
+ interface FlutterCupertinoCheckboxProperties {
13
+ /**
14
+ * Whether the checkbox is checked.
15
+ * Default: false.
16
+ */
17
+ checked?: boolean | null;
18
+ }
19
+
20
+ interface FlutterCupertinoCheckboxEvents {
21
+ /**
22
+ * Fired when the checkbox value changes.
23
+ */
24
+ change: CustomEvent<boolean>;
25
+ }
26
+ `;
27
+
28
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
29
+ blob.raw = tsContent;
30
+
31
+ const definedPropertyCollector = {
32
+ properties: new Set<string>(),
33
+ files: new Set<string>(),
34
+ interfaces: new Set<string>(),
35
+ };
36
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
37
+
38
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
39
+
40
+ const dartCode = generateDartClass(blob, 'test');
41
+
42
+ // Should generate bindings class for FlutterCupertinoCheckbox
43
+ expect(dartCode).toContain('abstract class FlutterCupertinoCheckboxBindings extends WidgetElement {');
44
+
45
+ // The checked property should be a nullable bool in Dart bindings.
46
+ expect(dartCode).toContain('bool? get checked;');
47
+
48
+ // Attribute setter should treat the literal "null" as a Dart null.
49
+ expect(dartCode).toContain(
50
+ "setter: (value) => checked = value == 'null' ? null : (value == 'true' || value == ''),"
51
+ );
52
+
53
+ // Deleting the attribute should reset to the default `false`.
54
+ expect(dartCode).toContain(
55
+ "deleter: () => checked = false"
56
+ );
57
+ });
58
+ });
@@ -0,0 +1,30 @@
1
+ import { generateReactComponent } from '../src/react';
2
+ import { IDLBlob } from '../src/IDLBlob';
3
+ import { ConstObject, EnumObject, EnumMemberObject } from '../src/declaration';
4
+
5
+ describe('React generator - declare const support', () => {
6
+ it('emits export declare const for constants from typings', () => {
7
+ const blob = new IDLBlob('/test/source', '/test/target', 'ConstOnly', 'test', '');
8
+ const constObj = new ConstObject();
9
+ constObj.name = 'WEBF_CUPERTINO_SYMBOL';
10
+ constObj.type = 'unique symbol';
11
+
12
+ blob.objects = [constObj as any];
13
+
14
+ const output = generateReactComponent(blob);
15
+ expect(output).toContain('export declare const WEBF_CUPERTINO_SYMBOL: unique symbol;');
16
+ });
17
+
18
+ it('emits export declare enum for enums from typings', () => {
19
+ const blob = new IDLBlob('/test/source', '/test/target', 'EnumOnly', 'test', '');
20
+ const eo = new EnumObject();
21
+ eo.name = 'CupertinoColors';
22
+ const m1 = new EnumMemberObject(); m1.name = "'red'"; m1.initializer = '0x0f';
23
+ const m2 = new EnumMemberObject(); m2.name = "'bbb'"; m2.initializer = '0x00';
24
+ eo.members = [m1, m2];
25
+ blob.objects = [eo as any];
26
+
27
+ const output = generateReactComponent(blob);
28
+ expect(output).toContain("export enum CupertinoColors { 'red' = 0x0f, 'bbb' = 0x00 }");
29
+ });
30
+ });
@@ -0,0 +1,66 @@
1
+ import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
2
+ import { generateReactComponent } from '../src/react';
3
+ import { generateVueTypings } from '../src/vue';
4
+ import { IDLBlob } from '../src/IDLBlob';
5
+
6
+ describe('React/Vue nullable union props', () => {
7
+ beforeEach(() => {
8
+ clearCaches();
9
+ });
10
+
11
+ const tsContent = `
12
+ interface FlutterCupertinoCheckboxProperties {
13
+ /**
14
+ * Whether the checkbox is checked.
15
+ * Default: false.
16
+ */
17
+ checked?: boolean | null;
18
+ }
19
+
20
+ interface FlutterCupertinoCheckboxEvents {
21
+ /**
22
+ * Fired when the checkbox value changes.
23
+ */
24
+ change: CustomEvent<boolean>;
25
+ }
26
+ `;
27
+
28
+ it('emits boolean | null for React props', () => {
29
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
30
+ blob.raw = tsContent;
31
+
32
+ const definedPropertyCollector = {
33
+ properties: new Set<string>(),
34
+ files: new Set<string>(),
35
+ interfaces: new Set<string>(),
36
+ };
37
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
38
+
39
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
40
+
41
+ const reactCode = generateReactComponent(blob);
42
+
43
+ // Prop should allow both undefined and null
44
+ expect(reactCode).toContain('checked?: boolean | null;');
45
+ });
46
+
47
+ it('emits boolean | null for Vue props', () => {
48
+ const blob = new IDLBlob('checkbox.d.ts', 'dist', 'checkbox', 'implement');
49
+ blob.raw = tsContent;
50
+
51
+ const definedPropertyCollector = {
52
+ properties: new Set<string>(),
53
+ files: new Set<string>(),
54
+ interfaces: new Set<string>(),
55
+ };
56
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
57
+
58
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
59
+
60
+ const vueCode = generateVueTypings([blob]);
61
+
62
+ // Vue prop name is kebab-cased and should allow null explicitly
63
+ expect(vueCode).toContain(`'checked'?: boolean | null;`);
64
+ });
65
+ });
66
+
@@ -181,9 +181,9 @@ describe('React Generator', () => {
181
181
 
182
182
  const result = generateReactComponent(blob);
183
183
 
184
- // Should include custom props (dom_string is not converted in raw output)
185
- expect(result).toContain('title: dom_string;');
186
- expect(result).toContain('disabled?: boolean;');
184
+ // Should include custom props with generated TS types
185
+ expect(result).toContain('title: __webfTypes.dom_string;');
186
+ expect(result).toContain('disabled?: __webfTypes.boolean;');
187
187
 
188
188
  // And still include standard HTML props
189
189
  expect(result).toContain('id?: string;');
@@ -191,5 +191,47 @@ describe('React Generator', () => {
191
191
  expect(result).toContain('children?: React.ReactNode;');
192
192
  expect(result).toContain('className?: string;');
193
193
  });
194
+
195
+ it('should preserve JSDoc for supporting option interfaces', () => {
196
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
197
+
198
+ const properties = new ClassObject();
199
+ properties.name = 'TestComponentProperties';
200
+ properties.kind = ClassObjectKind.interface;
201
+
202
+ const options = new ClassObject();
203
+ options.name = 'TestComponentOptions';
204
+ options.kind = ClassObjectKind.interface;
205
+
206
+ const titleProp = new PropsDeclaration();
207
+ titleProp.name = 'title';
208
+ titleProp.type = { value: 'string' } as any;
209
+ titleProp.optional = true;
210
+ titleProp.documentation = 'Optional override title for this show() call.';
211
+ titleProp.readonly = false;
212
+ titleProp.typeMode = {};
213
+
214
+ const messageProp = new PropsDeclaration();
215
+ messageProp.name = 'message';
216
+ messageProp.type = { value: 'string' } as any;
217
+ messageProp.optional = true;
218
+ messageProp.documentation = 'Optional override message for this show() call.';
219
+ messageProp.readonly = false;
220
+ messageProp.typeMode = {};
221
+
222
+ options.props = [titleProp, messageProp];
223
+
224
+ blob.objects = [properties, options];
225
+
226
+ const result = generateReactComponent(blob);
227
+
228
+ expect(result).toContain('interface TestComponentOptions');
229
+ expect(result).toMatch(
230
+ /interface TestComponentOptions[\s\S]*?\/\*\*[\s\S]*?Optional override title for this show\(\) call\.[\s\S]*?\*\/\s*\n\s*title\?:/
231
+ );
232
+ expect(result).toMatch(
233
+ /interface TestComponentOptions[\s\S]*?\/\*\*[\s\S]*?Optional override message for this show\(\) call\.[\s\S]*?\*\/\s*\n\s*message\?:/
234
+ );
235
+ });
194
236
  });
195
- });
237
+ });
package/test/vue.test.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { generateVueTypings } from '../src/vue';
2
2
  import { IDLBlob } from '../src/IDLBlob';
3
- import { ClassObject, ClassObjectKind, PropsDeclaration } from '../src/declaration';
3
+ import { ClassObject, ClassObjectKind, PropsDeclaration, ConstObject } from '../src/declaration';
4
4
 
5
5
  describe('Vue Generator', () => {
6
6
  describe('generateVueTypings', () => {
@@ -153,5 +153,37 @@ describe('Vue Generator', () => {
153
153
  expect(result).toContain("'class'?: string;");
154
154
  expect(result).toContain("'style'?: string | Record<string, any>;");
155
155
  });
156
+
157
+ it('should include declare const variables as exported declarations', () => {
158
+ const blob = new IDLBlob('/test/source', '/test/target', 'ConstOnly', 'test', '');
159
+
160
+ const constObj = new ConstObject();
161
+ constObj.name = 'WEBF_UNIQUE';
162
+ constObj.type = 'unique symbol';
163
+
164
+ blob.objects = [constObj as any];
165
+
166
+ const result = generateVueTypings([blob]);
167
+
168
+ expect(result).toContain('export declare const WEBF_UNIQUE: unique symbol;');
169
+ });
170
+
171
+ it('should include declare enum as exported declaration', () => {
172
+ const blob = new IDLBlob('/test/source', '/test/target', 'EnumOnly', 'test', '');
173
+ // Build a minimal faux EnumObject via analyzer by simulating ast is heavy; create a shape
174
+ // We'll reuse analyzer classes by importing EnumObject is cumbersome in test; instead
175
+ // craft an object literal compatible with instanceof check by constructing real class.
176
+ const { EnumObject, EnumMemberObject } = require('../src/declaration');
177
+ const e = new EnumObject();
178
+ e.name = 'CupertinoColors';
179
+ const m1 = new EnumMemberObject(); m1.name = "'red'"; m1.initializer = '0x0f';
180
+ const m2 = new EnumMemberObject(); m2.name = "'bbb'"; m2.initializer = '0x00';
181
+ e.members = [m1, m2];
182
+
183
+ blob.objects = [e as any];
184
+
185
+ const result = generateVueTypings([blob]);
186
+ expect(result).toContain("export declare enum CupertinoColors { 'red' = 0x0f, 'bbb' = 0x00 }");
187
+ });
156
188
  });
157
- });
189
+ });