@openwebf/webf 0.22.6 → 0.22.9

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.
package/src/react.ts CHANGED
@@ -4,13 +4,16 @@ import path from 'path';
4
4
  import {ParameterType} from "./analyzer";
5
5
  import {ClassObject, FunctionArgumentType, FunctionDeclaration, TypeAliasObject} from "./declaration";
6
6
  import {IDLBlob} from "./IDLBlob";
7
- import {getPointerType, isPointerType} from "./utils";
7
+ import {getPointerType, isPointerType, isUnionType} from "./utils";
8
8
 
9
9
  function readTemplate(name: string) {
10
10
  return fs.readFileSync(path.join(__dirname, '../templates/' + name + '.tpl'), {encoding: 'utf-8'});
11
11
  }
12
12
 
13
13
  function generateReturnType(type: ParameterType) {
14
+ if (isUnionType(type)) {
15
+ return (type.value as ParameterType[]).map(v => `'${v.value}'`).join(' | ');
16
+ }
14
17
  if (isPointerType(type)) {
15
18
  const pointerType = getPointerType(type);
16
19
  return pointerType;
@@ -79,6 +82,20 @@ function generateMethodDeclaration(method: FunctionDeclaration) {
79
82
  return `${methodName}(${args}): ${returnType};`;
80
83
  }
81
84
 
85
+ function generateMethodDeclarationWithDocs(method: FunctionDeclaration, indent: string = ''): string {
86
+ let result = '';
87
+ if (method.documentation) {
88
+ result += `${indent}/**\n`;
89
+ const docLines = method.documentation.split('\n');
90
+ docLines.forEach(line => {
91
+ result += `${indent} * ${line}\n`;
92
+ });
93
+ result += `${indent} */\n`;
94
+ }
95
+ result += `${indent}${generateMethodDeclaration(method)}`;
96
+ return result;
97
+ }
98
+
82
99
  function toReactEventName(name: string) {
83
100
  const eventName = 'on-' + name;
84
101
  return _.camelCase(eventName);
@@ -115,10 +132,14 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
115
132
  const events = classObjects.filter(object => {
116
133
  return object.name.endsWith('Events');
117
134
  });
135
+ const methods = classObjects.filter(object => {
136
+ return object.name.endsWith('Methods');
137
+ });
118
138
 
119
139
  const others = classObjects.filter(object => {
120
140
  return !object.name.endsWith('Properties')
121
- && !object.name.endsWith('Events');
141
+ && !object.name.endsWith('Events')
142
+ && !object.name.endsWith('Methods');
122
143
  });
123
144
 
124
145
  // Include type aliases
@@ -128,6 +149,21 @@ export function generateReactComponent(blob: IDLBlob, packageName?: string, rela
128
149
 
129
150
  const dependencies = [
130
151
  typeAliasDeclarations,
152
+ // Include Methods interfaces as dependencies
153
+ methods.map(object => {
154
+ const methodDeclarations = object.methods.map(method => {
155
+ return generateMethodDeclarationWithDocs(method, ' ');
156
+ }).join('\n');
157
+
158
+ let interfaceDoc = '';
159
+ if (object.documentation) {
160
+ interfaceDoc = `/**\n${object.documentation.split('\n').map(line => ` * ${line}`).join('\n')}\n */\n`;
161
+ }
162
+
163
+ return `${interfaceDoc}interface ${object.name} {
164
+ ${methodDeclarations}
165
+ }`;
166
+ }).join('\n\n'),
131
167
  others.map(object => {
132
168
  const props = object.props.map(prop => {
133
169
  if (prop.optional) {
@@ -146,8 +182,8 @@ interface ${object.name} {
146
182
  // Generate all components from this file
147
183
  const components: string[] = [];
148
184
 
149
- // Create a map of component names to their properties and events
150
- const componentMap = new Map<string, { properties?: ClassObject, events?: ClassObject }>();
185
+ // Create a map of component names to their properties, events, and methods
186
+ const componentMap = new Map<string, { properties?: ClassObject, events?: ClassObject, methods?: ClassObject }>();
151
187
 
152
188
  // Process all Properties interfaces
153
189
  properties.forEach(prop => {
@@ -167,6 +203,15 @@ interface ${object.name} {
167
203
  componentMap.get(componentName)!.events = event;
168
204
  });
169
205
 
206
+ // Process all Methods interfaces
207
+ methods.forEach(method => {
208
+ const componentName = method.name.replace(/Methods$/, '');
209
+ if (!componentMap.has(componentName)) {
210
+ componentMap.set(componentName, {});
211
+ }
212
+ componentMap.get(componentName)!.methods = method;
213
+ });
214
+
170
215
  // If we have multiple components, we need to generate a combined file
171
216
  const componentEntries = Array.from(componentMap.entries());
172
217
 
@@ -202,6 +247,7 @@ interface ${object.name} {
202
247
  className: className,
203
248
  properties: component.properties,
204
249
  events: component.events,
250
+ methods: component.methods,
205
251
  classObjectDictionary,
206
252
  dependencies,
207
253
  blob,
@@ -209,6 +255,7 @@ interface ${object.name} {
209
255
  toWebFTagName,
210
256
  generateReturnType,
211
257
  generateMethodDeclaration,
258
+ generateMethodDeclarationWithDocs,
212
259
  generateEventHandlerType,
213
260
  getEventType,
214
261
  });
@@ -240,6 +287,7 @@ interface ${object.name} {
240
287
  className: className,
241
288
  properties: component.properties,
242
289
  events: component.events,
290
+ methods: component.methods,
243
291
  classObjectDictionary,
244
292
  dependencies: '', // Dependencies will be at the top
245
293
  blob,
@@ -247,6 +295,7 @@ interface ${object.name} {
247
295
  toWebFTagName,
248
296
  generateReturnType,
249
297
  generateMethodDeclaration,
298
+ generateMethodDeclarationWithDocs,
250
299
  generateEventHandlerType,
251
300
  getEventType,
252
301
  });
@@ -7,12 +7,18 @@ export interface <%= className %>Props {
7
7
  <% _.forEach(properties?.props, function(prop, index) { %>
8
8
  <% var propName = _.camelCase(prop.name); %>
9
9
  <% var attributeName = _.kebabCase(prop.name); %>
10
+ <% if (prop.documentation) { %>
11
+ /**
12
+ * <%= prop.documentation.split('\n').join('\n * ') %>
13
+ */
14
+ <% } else { %>
10
15
  /**
11
16
  * <%= propName %> property
12
17
  <% if (prop.optional) { %>
13
18
  * @default undefined
14
19
  <% } %>
15
20
  */
21
+ <% } %>
16
22
  <% if (prop.optional) { %>
17
23
  <%= propName %>?: <%= generateReturnType(prop.type) %>;
18
24
  <% } else { %>
@@ -22,12 +28,23 @@ export interface <%= className %>Props {
22
28
  <% }); %>
23
29
  <% _.forEach(events?.props, function(prop, index) { %>
24
30
  <% var propName = toReactEventName(prop.name); %>
31
+ <% if (prop.documentation) { %>
32
+ /**
33
+ * <%= prop.documentation.split('\n').join('\n * ') %>
34
+ */
35
+ <% } else { %>
25
36
  /**
26
37
  * <%= prop.name %> event handler
27
38
  */
39
+ <% } %>
28
40
  <%= propName %>?: (event: <%= getEventType(prop.type) %>) => void;
29
41
 
30
42
  <% }); %>
43
+ /**
44
+ * HTML id attribute
45
+ */
46
+ id?: string;
47
+
31
48
  /**
32
49
  * Additional CSS styles
33
50
  */
@@ -44,24 +61,63 @@ export interface <%= className %>Props {
44
61
  className?: string;
45
62
  }
46
63
 
64
+ <% if (methods && methods.methods.length > 0) { %>
65
+ /**
66
+ * Element interface with methods accessible via ref
67
+ * @example
68
+ * ```tsx
69
+ * const ref = useRef<<%= className %>Element>(null);
70
+ * // Call methods on the element
71
+ * ref.current?.finishRefresh('success');
72
+ * ```
73
+ */
74
+ <% } %>
47
75
  export interface <%= className %>Element extends WebFElementWithMethods<{
48
- <% _.forEach(properties?.methods, function(method, index) { %>
49
- <%= generateMethodDeclaration(method) %>
76
+ <% _.forEach(methods?.methods, function(method, index) { %>
77
+ <%= generateMethodDeclarationWithDocs(method, ' ') %>
50
78
  <% }); %>
51
79
  }> {}
52
80
 
81
+ <% if (properties?.documentation || methods?.documentation || events?.documentation) { %>
82
+ <% const docs = properties?.documentation || methods?.documentation || events?.documentation; %>
83
+ /**
84
+ * <%= docs %>
85
+ *
86
+ * @example
87
+ * ```tsx<% if (methods && methods.methods.length > 0) { %>
88
+ * const ref = useRef<<%= className %>Element>(null);<% } %>
89
+ *
90
+ * <<%= className %><% if (methods && methods.methods.length > 0) { %>
91
+ * ref={ref}<% } %>
92
+ * // Add props here
93
+ * >
94
+ * Content
95
+ * </<%= className %>><% if (methods && methods.methods.length > 0) { %>
96
+ *
97
+ * // Call methods on the element
98
+ * ref.current?.finishRefresh('success');<% } %>
99
+ * ```
100
+ */
101
+ <% } else { %>
53
102
  /**
54
103
  * <%= className %> - WebF <%= className %> component
55
104
  *
56
105
  * @example
57
- * ```tsx
58
- * <<%= className %>
59
- * // Add example props here
106
+ * ```tsx<% if (methods && methods.methods.length > 0) { %>
107
+ * const ref = useRef<<%= className %>Element>(null);<% } %>
108
+ *
109
+ * <<%= className %><% if (methods && methods.methods.length > 0) { %>
110
+ * ref={ref}<% } %>
111
+ * // Add props here
60
112
  * >
61
113
  * Content
62
- * </<%= className %>>
114
+ * </<%= className %>><% if (methods && methods.methods.length > 0) { %>
115
+ *
116
+ * // Call methods on the element
117
+ * ref.current?.finishRefresh('success');<% } %>
63
118
  * ```
64
119
  */
120
+ <% } %>
65
121
  export const <%= className %> = createWebFComponent<<%= className %>Element, <%= className %>Props>({
66
122
  tagName: '<%= toWebFTagName(className) %>',
67
123
  displayName: '<%= className %>',
@@ -7,6 +7,9 @@ export type <%= className %>Props = {
7
7
  '<%= propName %>': <%= generateReturnType(prop.type) %>;
8
8
  <% } %>
9
9
  <% }); %>
10
+ 'id'?: string;
11
+ 'class'?: string;
12
+ 'style'?: string | Record<string, any>;
10
13
  }
11
14
 
12
15
  export interface <%= className %>Element {
@@ -1,6 +1,6 @@
1
1
  import { generateReactComponent } from '../src/react';
2
2
  import { IDLBlob } from '../src/IDLBlob';
3
- import { ClassObject, ClassObjectKind } from '../src/declaration';
3
+ import { ClassObject, ClassObjectKind, PropsDeclaration } from '../src/declaration';
4
4
 
5
5
  // Import the toWebFTagName function for testing
6
6
  import { toWebFTagName } from '../src/react';
@@ -130,5 +130,66 @@ describe('React Generator', () => {
130
130
  // From src/lib/src/html/shimmer to src/utils: ../../../../utils
131
131
  expect(result).toContain('import { createWebFComponent, WebFElementWithMethods } from "../../../../utils/createWebFComponent"');
132
132
  });
133
+
134
+ it('should include standard HTML props (id, className, style) in component interface', () => {
135
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
136
+
137
+ const properties = new ClassObject();
138
+ properties.name = 'TestComponentProperties';
139
+ properties.kind = ClassObjectKind.interface;
140
+ blob.objects = [properties];
141
+
142
+ const result = generateReactComponent(blob);
143
+
144
+ // Should include standard HTML props
145
+ expect(result).toContain('id?: string;');
146
+ expect(result).toContain('style?: React.CSSProperties;');
147
+ expect(result).toContain('children?: React.ReactNode;');
148
+ expect(result).toContain('className?: string;');
149
+
150
+ // Props should have proper JSDoc comments
151
+ expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*HTML id attribute\s*\n\s*\*\/\s*\n\s*id\?: string;/);
152
+ expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Additional CSS styles\s*\n\s*\*\/\s*\n\s*style\?: React\.CSSProperties;/);
153
+ expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Children elements\s*\n\s*\*\/\s*\n\s*children\?: React\.ReactNode;/);
154
+ expect(result).toMatch(/\/\*\*\s*\n\s*\*\s*Additional CSS class names\s*\n\s*\*\/\s*\n\s*className\?: string;/);
155
+ });
156
+
157
+ it('should include standard HTML props even when component has custom properties', () => {
158
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestComponent', 'test', '');
159
+
160
+ const properties = new ClassObject();
161
+ properties.name = 'TestComponentProperties';
162
+ properties.kind = ClassObjectKind.interface;
163
+ const titleProp = new PropsDeclaration();
164
+ titleProp.name = 'title';
165
+ titleProp.type = { value: 'dom_string' };
166
+ titleProp.optional = false;
167
+ titleProp.documentation = 'The component title';
168
+ titleProp.readonly = false;
169
+ titleProp.typeMode = {};
170
+
171
+ const disabledProp = new PropsDeclaration();
172
+ disabledProp.name = 'disabled';
173
+ disabledProp.type = { value: 'boolean' };
174
+ disabledProp.optional = true;
175
+ disabledProp.documentation = 'Whether the component is disabled';
176
+ disabledProp.readonly = false;
177
+ disabledProp.typeMode = {};
178
+
179
+ properties.props = [titleProp, disabledProp];
180
+ blob.objects = [properties];
181
+
182
+ const result = generateReactComponent(blob);
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;');
187
+
188
+ // And still include standard HTML props
189
+ expect(result).toContain('id?: string;');
190
+ expect(result).toContain('style?: React.CSSProperties;');
191
+ expect(result).toContain('children?: React.ReactNode;');
192
+ expect(result).toContain('className?: string;');
193
+ });
133
194
  });
134
195
  });
@@ -0,0 +1,190 @@
1
+ import { generateReactComponent } from '../src/react';
2
+ import { generateVueTypings } from '../src/vue';
3
+ import { IDLBlob } from '../src/IDLBlob';
4
+ import { ClassObject, ClassObjectKind, PropsDeclaration } from '../src/declaration';
5
+
6
+ describe('Standard HTML Props Generation', () => {
7
+ describe('React Components', () => {
8
+ it('should generate id prop in the correct position within the interface', () => {
9
+ const blob = new IDLBlob('/test/source', '/test/target', 'TestButton', 'test', '');
10
+
11
+ const properties = new ClassObject();
12
+ properties.name = 'TestButtonProperties';
13
+ properties.kind = ClassObjectKind.interface;
14
+ const labelProp = new PropsDeclaration();
15
+ labelProp.name = 'label';
16
+ labelProp.type = { value: 'dom_string' };
17
+ labelProp.optional = false;
18
+ labelProp.readonly = false;
19
+ labelProp.typeMode = {};
20
+
21
+ const variantProp = new PropsDeclaration();
22
+ variantProp.name = 'variant';
23
+ variantProp.type = { value: 'dom_string' };
24
+ variantProp.optional = true;
25
+ variantProp.readonly = false;
26
+ variantProp.typeMode = {};
27
+
28
+ properties.props = [labelProp, variantProp];
29
+
30
+ const events = new ClassObject();
31
+ events.name = 'TestButtonEvents';
32
+ events.kind = ClassObjectKind.interface;
33
+ const clickProp = new PropsDeclaration();
34
+ clickProp.name = 'click';
35
+ clickProp.type = { value: 'Event', isArray: false };
36
+ clickProp.optional = true;
37
+ clickProp.readonly = false;
38
+ clickProp.typeMode = {};
39
+
40
+ events.props = [clickProp];
41
+
42
+ blob.objects = [properties, events];
43
+
44
+ const result = generateReactComponent(blob);
45
+
46
+ // Verify the props interface structure - extract full content including newlines
47
+ const propsStart = result.indexOf('export interface TestButtonProps {');
48
+ const propsEnd = result.indexOf('}', propsStart) + 1;
49
+ const propsContent = result.substring(propsStart, propsEnd);
50
+
51
+ // Verify order: custom props, event handlers, then standard HTML props
52
+ const labelIndex = propsContent.indexOf('label: dom_string;');
53
+ const variantIndex = propsContent.indexOf('variant?: dom_string;');
54
+ const onClickIndex = propsContent.indexOf('onClick?: (event: Event) => void;');
55
+ const idIndex = propsContent.indexOf('id?: string;');
56
+ const styleIndex = propsContent.indexOf('style?: React.CSSProperties;');
57
+ const childrenIndex = propsContent.indexOf('children?: React.ReactNode;');
58
+ const classNameIndex = propsContent.indexOf('className?: string;');
59
+
60
+ // All props should exist
61
+ expect(labelIndex).toBeGreaterThan(-1);
62
+ expect(variantIndex).toBeGreaterThan(-1);
63
+ expect(onClickIndex).toBeGreaterThan(-1);
64
+ expect(idIndex).toBeGreaterThan(-1);
65
+ expect(styleIndex).toBeGreaterThan(-1);
66
+ expect(childrenIndex).toBeGreaterThan(-1);
67
+ expect(classNameIndex).toBeGreaterThan(-1);
68
+
69
+ // Verify order
70
+ expect(labelIndex).toBeLessThan(variantIndex);
71
+ expect(variantIndex).toBeLessThan(onClickIndex);
72
+ expect(onClickIndex).toBeLessThan(idIndex);
73
+ expect(idIndex).toBeLessThan(styleIndex);
74
+ expect(styleIndex).toBeLessThan(childrenIndex);
75
+ expect(childrenIndex).toBeLessThan(classNameIndex);
76
+ });
77
+
78
+ it('should properly type the standard props', () => {
79
+ const blob = new IDLBlob('/test/source', '/test/target', 'SimpleComponent', 'test', '');
80
+
81
+ const properties = new ClassObject();
82
+ properties.name = 'SimpleComponentProperties';
83
+ properties.kind = ClassObjectKind.interface;
84
+ blob.objects = [properties];
85
+
86
+ const result = generateReactComponent(blob);
87
+
88
+ // Verify exact type definitions
89
+ expect(result).toMatch(/id\?: string;/);
90
+ expect(result).toMatch(/style\?: React\.CSSProperties;/);
91
+ expect(result).toMatch(/children\?: React\.ReactNode;/);
92
+ expect(result).toMatch(/className\?: string;/);
93
+ });
94
+ });
95
+
96
+ describe('Vue Components', () => {
97
+ it('should generate standard HTML props with correct Vue naming conventions', () => {
98
+ const blob = new IDLBlob('/test/source', '/test/target', 'VueButton', 'test', '');
99
+
100
+ const properties = new ClassObject();
101
+ properties.name = 'VueButtonProperties';
102
+ properties.kind = ClassObjectKind.interface;
103
+ const labelProp = new PropsDeclaration();
104
+ labelProp.name = 'label';
105
+ labelProp.type = { value: 'dom_string' };
106
+ labelProp.optional = false;
107
+ labelProp.readonly = false;
108
+ labelProp.typeMode = {};
109
+
110
+ const isDisabledProp = new PropsDeclaration();
111
+ isDisabledProp.name = 'isDisabled';
112
+ isDisabledProp.type = { value: 'boolean' };
113
+ isDisabledProp.optional = true;
114
+ isDisabledProp.readonly = false;
115
+ isDisabledProp.typeMode = {};
116
+
117
+ properties.props = [labelProp, isDisabledProp];
118
+
119
+ blob.objects = [properties];
120
+
121
+ const result = generateVueTypings([blob]);
122
+
123
+ // Verify Props type includes custom and standard props - extract full content
124
+ const propsStart = result.indexOf('export type VueButtonProps = {');
125
+ const propsEnd = result.indexOf('}', propsStart) + 1;
126
+ const propsContent = result.substring(propsStart, propsEnd);
127
+
128
+ // Custom props should be kebab-case (dom_string is not converted)
129
+ expect(propsContent).toContain("'label': dom_string;");
130
+ expect(propsContent).toContain("'is-disabled'?: boolean;");
131
+
132
+ // Standard HTML props
133
+ expect(propsContent).toContain("'id'?: string;");
134
+ expect(propsContent).toContain("'class'?: string;");
135
+ expect(propsContent).toContain("'style'?: string | Record<string, any>;");
136
+ });
137
+
138
+ it('should handle Vue style prop with both string and object types', () => {
139
+ const blob = new IDLBlob('/test/source', '/test/target', 'StyledComponent', 'test', '');
140
+
141
+ const properties = new ClassObject();
142
+ properties.name = 'StyledComponentProperties';
143
+ properties.kind = ClassObjectKind.interface;
144
+ blob.objects = [properties];
145
+
146
+ const result = generateVueTypings([blob]);
147
+
148
+ // Vue style prop should accept both string and object
149
+ expect(result).toMatch(/'style'\?: string \| Record<string, any>;/);
150
+ });
151
+ });
152
+
153
+ describe('Cross-framework consistency', () => {
154
+ it('should generate equivalent props for both React and Vue', () => {
155
+ const blob = new IDLBlob('/test/source', '/test/target', 'CrossFrameworkComponent', 'test', '');
156
+
157
+ const properties = new ClassObject();
158
+ properties.name = 'CrossFrameworkComponentProperties';
159
+ properties.kind = ClassObjectKind.interface;
160
+ const titleProp = new PropsDeclaration();
161
+ titleProp.name = 'title';
162
+ titleProp.type = { value: 'dom_string' };
163
+ titleProp.optional = false;
164
+ titleProp.readonly = false;
165
+ titleProp.typeMode = {};
166
+
167
+ properties.props = [titleProp];
168
+ blob.objects = [properties];
169
+
170
+ const reactResult = generateReactComponent(blob);
171
+ const vueResult = generateVueTypings([blob]);
172
+
173
+ // Both should have id prop
174
+ expect(reactResult).toContain('id?: string;');
175
+ expect(vueResult).toContain("'id'?: string;");
176
+
177
+ // Both should have style prop (with appropriate types)
178
+ expect(reactResult).toContain('style?: React.CSSProperties;');
179
+ expect(vueResult).toContain("'style'?: string | Record<string, any>;");
180
+
181
+ // React has className, Vue has class
182
+ expect(reactResult).toContain('className?: string;');
183
+ expect(vueResult).toContain("'class'?: string;");
184
+
185
+ // React has children, Vue uses slots (not in props)
186
+ expect(reactResult).toContain('children?: React.ReactNode;');
187
+ expect(vueResult).not.toContain('children');
188
+ });
189
+ });
190
+ });
@@ -0,0 +1,168 @@
1
+ import { analyzer, clearCaches, UnionTypeCollector, ParameterType } from '../src/analyzer';
2
+ import { generateReactComponent } from '../src/react';
3
+ import { IDLBlob } from '../src/IDLBlob';
4
+ import { ClassObject } from '../src/declaration';
5
+
6
+ describe('Union Types and JSDoc Preservation', () => {
7
+ beforeEach(() => {
8
+ clearCaches();
9
+ });
10
+
11
+ test('should generate union types with quoted string literals and preserve JSDoc @default tags', () => {
12
+ // Create test TypeScript content with union types and JSDoc
13
+ const testContent = `
14
+ /**
15
+ * Table cell component
16
+ */
17
+ interface WebFTableCellProperties {
18
+ /**
19
+ * Text alignment
20
+ * @default "left"
21
+ */
22
+ align?: 'left' | 'center' | 'right';
23
+
24
+ /**
25
+ * Cell type (header or data)
26
+ * @default "data"
27
+ */
28
+ type?: 'header' | 'data';
29
+
30
+ /**
31
+ * Column span
32
+ * @default 1
33
+ */
34
+ colspan?: number;
35
+ }
36
+
37
+ interface WebFTableCellEvents {
38
+ /**
39
+ * Cell click event
40
+ */
41
+ click: CustomEvent<{row: number, column: number, value: any}>;
42
+ }
43
+ `;
44
+
45
+ // Create IDL blob
46
+ const blob = new IDLBlob('test.d.ts', 'dist', 'table_cell', 'implement');
47
+ blob.raw = testContent;
48
+
49
+ // Analyze the content
50
+ const definedPropertyCollector = {
51
+ properties: new Set<string>(),
52
+ files: new Set<string>(),
53
+ interfaces: new Set<string>()
54
+ };
55
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
56
+
57
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
58
+
59
+ // Generate React component
60
+ const reactCode = generateReactComponent(blob);
61
+
62
+ // Test 1: Check that union types are properly quoted
63
+ expect(reactCode).toContain("align?: 'left' | 'center' | 'right';");
64
+ expect(reactCode).toContain("type?: 'header' | 'data';");
65
+
66
+ // Test 2: Check that JSDoc is preserved with @default tags
67
+ expect(reactCode).toMatch(/\*\s+Text alignment[\s\S]*?\*\s+@default "left"/);
68
+ expect(reactCode).toMatch(/\*\s+Cell type \(header or data\)[\s\S]*?\*\s+@default "data"/);
69
+
70
+ // Test 3: Check that the generated interface has the correct structure
71
+ expect(reactCode).toContain('export interface WebFTableCellProps {');
72
+ expect(reactCode).toContain('colspan?: number;');
73
+
74
+ // Test 4: Verify the component is created with correct props
75
+ expect(reactCode).toContain("attributeProps: [");
76
+ expect(reactCode).toContain("'align',");
77
+ expect(reactCode).toContain("'type',");
78
+ expect(reactCode).toContain("'colspan',");
79
+ });
80
+
81
+ test('should handle complex union types', () => {
82
+ const testContent = `
83
+ interface TestProperties {
84
+ /**
85
+ * Size property
86
+ * @default "medium"
87
+ */
88
+ size?: 'small' | 'medium' | 'large' | 'xl';
89
+
90
+ /**
91
+ * Status
92
+ * @default "pending"
93
+ */
94
+ status?: 'pending' | 'active' | 'completed' | 'failed';
95
+ }
96
+ `;
97
+
98
+ const blob = new IDLBlob('test.d.ts', 'dist', 'test', 'implement');
99
+ blob.raw = testContent;
100
+
101
+ const definedPropertyCollector = {
102
+ properties: new Set<string>(),
103
+ files: new Set<string>(),
104
+ interfaces: new Set<string>()
105
+ };
106
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
107
+
108
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
109
+ const reactCode = generateReactComponent(blob);
110
+
111
+ // Check complex union types
112
+ expect(reactCode).toContain("size?: 'small' | 'medium' | 'large' | 'xl';");
113
+ expect(reactCode).toContain("status?: 'pending' | 'active' | 'completed' | 'failed';");
114
+ });
115
+
116
+ test('should handle mixed type properties alongside union types', () => {
117
+ const testContent = `
118
+ interface MixedProperties {
119
+ /**
120
+ * String union type
121
+ * @default "auto"
122
+ */
123
+ mode?: 'auto' | 'manual' | 'disabled';
124
+
125
+ /**
126
+ * Regular string
127
+ */
128
+ name?: string;
129
+
130
+ /**
131
+ * Number property
132
+ * @default 42
133
+ */
134
+ count?: number;
135
+
136
+ /**
137
+ * Boolean property
138
+ * @default true
139
+ */
140
+ enabled?: boolean;
141
+ }
142
+ `;
143
+
144
+ const blob = new IDLBlob('test.d.ts', 'dist', 'mixed', 'implement');
145
+ blob.raw = testContent;
146
+
147
+ const definedPropertyCollector = {
148
+ properties: new Set<string>(),
149
+ files: new Set<string>(),
150
+ interfaces: new Set<string>()
151
+ };
152
+ const unionTypeCollector: UnionTypeCollector = { types: new Set<ParameterType[]>() };
153
+
154
+ analyzer(blob, definedPropertyCollector, unionTypeCollector);
155
+ const reactCode = generateReactComponent(blob);
156
+
157
+ // Check that different types are handled correctly
158
+ expect(reactCode).toContain("mode?: 'auto' | 'manual' | 'disabled';");
159
+ expect(reactCode).toContain("name?: string;");
160
+ expect(reactCode).toContain("count?: number;");
161
+ expect(reactCode).toContain("enabled?: boolean;");
162
+
163
+ // Check JSDoc preservation
164
+ expect(reactCode).toMatch(/\*\s+String union type[\s\S]*?\*\s+@default "auto"/);
165
+ expect(reactCode).toMatch(/\*\s+Number property[\s\S]*?\*\s+@default 42/);
166
+ expect(reactCode).toMatch(/\*\s+Boolean property[\s\S]*?\*\s+@default true/);
167
+ });
168
+ });