ux4g-components-web 1.4.0 → 1.5.0

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.
Files changed (124) hide show
  1. package/README.md +76 -0
  2. package/dist/__tests__/css-bundle.integration.test.d.ts +11 -0
  3. package/dist/__tests__/css-bundle.integration.test.js +1102 -0
  4. package/dist/__tests__/css-bundle.phase10.property.test.d.ts +9 -0
  5. package/dist/__tests__/css-bundle.phase10.property.test.js +64 -0
  6. package/dist/__tests__/css-bundle.phase5.property.test.d.ts +9 -0
  7. package/dist/__tests__/css-bundle.phase5.property.test.js +126 -0
  8. package/dist/__tests__/css-bundle.phase6.property.test.d.ts +9 -0
  9. package/dist/__tests__/css-bundle.phase6.property.test.js +73 -0
  10. package/dist/__tests__/css-bundle.phase7.property.test.d.ts +9 -0
  11. package/dist/__tests__/css-bundle.phase7.property.test.js +76 -0
  12. package/dist/__tests__/css-bundle.phase8.property.test.d.ts +9 -0
  13. package/dist/__tests__/css-bundle.phase8.property.test.js +67 -0
  14. package/dist/__tests__/css-bundle.phase9.property.test.d.ts +9 -0
  15. package/dist/__tests__/css-bundle.phase9.property.test.js +93 -0
  16. package/dist/__tests__/css-bundle.property.test.d.ts +14 -0
  17. package/dist/__tests__/css-bundle.property.test.js +393 -0
  18. package/dist/__tests__/dom-generators.determinism.property.test.d.ts +1 -0
  19. package/dist/__tests__/dom-generators.determinism.property.test.js +71 -0
  20. package/dist/__tests__/dom-generators.id.property.test.d.ts +1 -0
  21. package/dist/__tests__/dom-generators.id.property.test.js +99 -0
  22. package/dist/__tests__/dom-generators.otp.property.test.d.ts +1 -0
  23. package/dist/__tests__/dom-generators.property.test.d.ts +1 -0
  24. package/dist/__tests__/dom-generators.property.test.js +205 -0
  25. package/dist/__tests__/dom-generators.states.property.test.d.ts +1 -0
  26. package/dist/__tests__/dom-generators.table.property.test.d.ts +1 -0
  27. package/dist/__tests__/dom-generators.tier1.property.test.d.ts +1 -0
  28. package/dist/__tests__/dom-generators.tier1.property.test.js +403 -0
  29. package/dist/__tests__/dom-generators.validation.property.test.d.ts +1 -0
  30. package/dist/__tests__/dom-generators.validation.property.test.js +327 -0
  31. package/dist/__tests__/megamenu.classbuilder.property.test.d.ts +1 -0
  32. package/dist/__tests__/megamenu.classbuilder.property.test.js +88 -0
  33. package/dist/__tests__/smoke.test.d.ts +1 -0
  34. package/dist/__tests__/smoke.test.js +65 -0
  35. package/dist/__tests__/types.phase10.property.test.d.ts +1 -0
  36. package/dist/__tests__/types.phase10.property.test.js +166 -0
  37. package/dist/__tests__/types.phase10.test.d.ts +1 -0
  38. package/dist/__tests__/types.phase10.test.js +76 -0
  39. package/dist/__tests__/types.phase3.property.test.d.ts +1 -0
  40. package/dist/__tests__/types.phase3.property.test.js +83 -0
  41. package/dist/__tests__/types.phase3.test.d.ts +1 -0
  42. package/dist/__tests__/types.phase3.test.js +76 -0
  43. package/dist/__tests__/types.phase4.property.test.d.ts +1 -0
  44. package/dist/__tests__/types.phase4.property.test.js +119 -0
  45. package/dist/__tests__/types.phase4.test.d.ts +1 -0
  46. package/dist/__tests__/types.phase4.test.js +70 -0
  47. package/dist/__tests__/types.phase5.property.test.d.ts +1 -0
  48. package/dist/__tests__/types.phase5.property.test.js +120 -0
  49. package/dist/__tests__/types.phase5.test.d.ts +1 -0
  50. package/dist/__tests__/types.phase5.test.js +64 -0
  51. package/dist/__tests__/types.phase6.property.test.d.ts +1 -0
  52. package/dist/__tests__/types.phase6.property.test.js +189 -0
  53. package/dist/__tests__/types.phase6.test.d.ts +1 -0
  54. package/dist/__tests__/types.phase6.test.js +121 -0
  55. package/dist/__tests__/types.phase7.property.test.d.ts +1 -0
  56. package/dist/__tests__/types.phase7.property.test.js +217 -0
  57. package/dist/__tests__/types.phase7.test.d.ts +1 -0
  58. package/dist/__tests__/types.phase7.test.js +106 -0
  59. package/dist/__tests__/types.phase8.property.test.d.ts +1 -0
  60. package/dist/__tests__/types.phase8.property.test.js +224 -0
  61. package/dist/__tests__/types.phase8.test.d.ts +1 -0
  62. package/dist/__tests__/types.phase8.test.js +114 -0
  63. package/dist/__tests__/types.phase9.property.test.d.ts +1 -0
  64. package/dist/__tests__/types.phase9.property.test.js +347 -0
  65. package/dist/__tests__/types.phase9.test.d.ts +1 -0
  66. package/dist/__tests__/types.phase9.test.js +226 -0
  67. package/dist/__tests__/types.restructure.property.test.d.ts +1 -0
  68. package/dist/__tests__/types.restructure.property.test.js +76 -0
  69. package/dist/__tests__/types.test.d.ts +1 -0
  70. package/dist/__tests__/types.test.js +175 -0
  71. package/dist/dom-generators/accordion.d.ts +23 -0
  72. package/dist/dom-generators/avatar.d.ts +19 -0
  73. package/dist/dom-generators/carousel.d.ts +20 -0
  74. package/dist/dom-generators/chip.d.ts +18 -0
  75. package/dist/dom-generators/combobox.d.ts +28 -0
  76. package/dist/dom-generators/date-picker.d.ts +19 -0
  77. package/dist/dom-generators/dom-generators/accordion.d.ts +21 -0
  78. package/dist/dom-generators/dom-generators/avatar.d.ts +17 -0
  79. package/dist/dom-generators/dom-generators/carousel.d.ts +19 -0
  80. package/dist/dom-generators/dom-generators/chip.d.ts +16 -0
  81. package/dist/dom-generators/dom-generators/combobox.d.ts +26 -0
  82. package/dist/dom-generators/dom-generators/date-picker.d.ts +18 -0
  83. package/dist/dom-generators/dom-generators/drawer.d.ts +17 -0
  84. package/dist/dom-generators/dom-generators/dropdown.d.ts +26 -0
  85. package/dist/dom-generators/dom-generators/file-upload.d.ts +20 -0
  86. package/dist/dom-generators/dom-generators/id-generator.d.ts +9 -0
  87. package/dist/dom-generators/dom-generators/index.d.ts +27 -0
  88. package/dist/dom-generators/dom-generators/modal.d.ts +19 -0
  89. package/dist/dom-generators/dom-generators/otp.d.ts +16 -0
  90. package/dist/dom-generators/dom-generators/popover.d.ts +17 -0
  91. package/dist/dom-generators/dom-generators/progress.d.ts +16 -0
  92. package/dist/dom-generators/dom-generators/search.d.ts +20 -0
  93. package/dist/dom-generators/dom-generators/stepper.d.ts +21 -0
  94. package/dist/dom-generators/dom-generators/table.d.ts +23 -0
  95. package/dist/dom-generators/dom-generators/tabs.d.ts +21 -0
  96. package/dist/dom-generators/dom-generators/time-picker.d.ts +18 -0
  97. package/dist/dom-generators/dom-generators/tooltip.d.ts +17 -0
  98. package/dist/dom-generators/dom-generators/types.d.ts +27 -0
  99. package/dist/dom-generators/dom-generators/validate.d.ts +20 -0
  100. package/dist/dom-generators/drawer.d.ts +19 -0
  101. package/dist/dom-generators/dropdown.d.ts +28 -0
  102. package/dist/dom-generators/file-upload.d.ts +22 -0
  103. package/dist/dom-generators/id-generator.d.ts +9 -0
  104. package/dist/dom-generators/index.bundled.d.ts +654 -0
  105. package/dist/dom-generators/index.cjs +2029 -0
  106. package/dist/dom-generators/index.d.ts +27 -0
  107. package/dist/dom-generators/index.mjs +2001 -0
  108. package/dist/dom-generators/modal.d.ts +21 -0
  109. package/dist/dom-generators/otp.d.ts +18 -0
  110. package/dist/dom-generators/popover.d.ts +19 -0
  111. package/dist/dom-generators/progress.d.ts +18 -0
  112. package/dist/dom-generators/search.d.ts +22 -0
  113. package/dist/dom-generators/stepper.d.ts +23 -0
  114. package/dist/dom-generators/table.d.ts +25 -0
  115. package/dist/dom-generators/tabs.d.ts +23 -0
  116. package/dist/dom-generators/time-picker.d.ts +19 -0
  117. package/dist/dom-generators/tooltip.d.ts +19 -0
  118. package/dist/dom-generators/types.d.ts +155 -0
  119. package/dist/dom-generators/validate.d.ts +20 -0
  120. package/dist/runtime/bootstrap.js +59 -0
  121. package/dist/runtime/index.js +55 -0
  122. package/dist/types.d.ts +155 -0
  123. package/dist/types.js +552 -0
  124. package/package.json +12 -2
@@ -0,0 +1,205 @@
1
+ /**
2
+ * Property-based tests for NodeDescriptor type validation
3
+ * Property 1: DOM Generator returns valid NodeDescriptor
4
+ * Tag: Feature: ux4g-native-framework-experience, Property 1: DOM Generator returns valid NodeDescriptor
5
+ *
6
+ * **Validates: Requirements 3.2**
7
+ */
8
+ import * as fc from 'fast-check';
9
+ import { generateId, resetIdCounter } from '../dom-generators/id-generator';
10
+ // --- Helper: recursive NodeDescriptor validator ---
11
+ /**
12
+ * Recursively validates that a value conforms to the NodeDescriptor contract:
13
+ * - `tag` is a non-empty string
14
+ * - `props` is an object (not null, not array)
15
+ * - `children` is an array where each element is either a string or a valid NodeDescriptor
16
+ */
17
+ function isValidNodeDescriptor(node) {
18
+ if (node === null || node === undefined || typeof node !== 'object') {
19
+ return false;
20
+ }
21
+ const descriptor = node;
22
+ // tag must be a non-empty string
23
+ if (typeof descriptor.tag !== 'string' || descriptor.tag.length === 0) {
24
+ return false;
25
+ }
26
+ // props must be a non-null object (not an array)
27
+ if (descriptor.props === null ||
28
+ descriptor.props === undefined ||
29
+ typeof descriptor.props !== 'object' ||
30
+ Array.isArray(descriptor.props)) {
31
+ return false;
32
+ }
33
+ // children must be an array
34
+ if (!Array.isArray(descriptor.children)) {
35
+ return false;
36
+ }
37
+ // each child must be either a string or a valid NodeDescriptor
38
+ for (const child of descriptor.children) {
39
+ if (typeof child === 'string') {
40
+ continue;
41
+ }
42
+ if (!isValidNodeDescriptor(child)) {
43
+ return false;
44
+ }
45
+ }
46
+ return true;
47
+ }
48
+ // --- Arbitraries: generate random NodeDescriptor trees ---
49
+ /**
50
+ * Creates a recursive fc.Arbitrary<NodeDescriptor> that generates valid NodeDescriptor trees.
51
+ * Uses fc.letrec for safe recursion with bounded depth.
52
+ */
53
+ function nodeDescriptorArbitrary() {
54
+ const { tree } = fc.letrec((tie) => ({
55
+ tree: fc.record({
56
+ tag: fc.stringOf(fc.char().filter((c) => c.trim().length > 0), { minLength: 1, maxLength: 20 }),
57
+ props: fc.dictionary(fc.stringOf(fc.char().filter((c) => /[a-z\-]/.test(c)), { minLength: 1, maxLength: 15 }), fc.oneof(fc.string(), fc.boolean(), fc.integer(), fc.constant(undefined))),
58
+ children: fc.array(fc.oneof(fc.string(), tie('tree')), { maxLength: 4 }),
59
+ }),
60
+ }));
61
+ return tree;
62
+ }
63
+ // --- Property 1: DOM Generator returns valid NodeDescriptor ---
64
+ describe('Property 1: DOM Generator returns valid NodeDescriptor', () => {
65
+ /**
66
+ * For any generated NodeDescriptor tree, the isValidNodeDescriptor helper
67
+ * should confirm it conforms to the type contract.
68
+ *
69
+ * Tag: Feature: ux4g-native-framework-experience, Property 1: DOM Generator returns valid NodeDescriptor
70
+ */
71
+ it('any generated NodeDescriptor tree conforms to the type contract', () => {
72
+ fc.assert(fc.property(nodeDescriptorArbitrary(), (descriptor) => {
73
+ // tag is a non-empty string
74
+ expect(typeof descriptor.tag).toBe('string');
75
+ expect(descriptor.tag.length).toBeGreaterThan(0);
76
+ // props is an object
77
+ expect(typeof descriptor.props).toBe('object');
78
+ expect(descriptor.props).not.toBeNull();
79
+ expect(Array.isArray(descriptor.props)).toBe(false);
80
+ // children is an array
81
+ expect(Array.isArray(descriptor.children)).toBe(true);
82
+ // recursive validation passes
83
+ expect(isValidNodeDescriptor(descriptor)).toBe(true);
84
+ }), { numRuns: 100 });
85
+ });
86
+ it('children elements are either strings or valid NodeDescriptors', () => {
87
+ fc.assert(fc.property(nodeDescriptorArbitrary(), (descriptor) => {
88
+ for (const child of descriptor.children) {
89
+ if (typeof child !== 'string') {
90
+ expect(isValidNodeDescriptor(child)).toBe(true);
91
+ }
92
+ }
93
+ return true;
94
+ }), { numRuns: 100 });
95
+ });
96
+ it('deeply nested NodeDescriptor trees are valid', () => {
97
+ fc.assert(fc.property(nodeDescriptorArbitrary(), (descriptor) => {
98
+ // Recursively walk the tree and confirm every node is valid
99
+ const stack = [descriptor];
100
+ while (stack.length > 0) {
101
+ const current = stack.pop();
102
+ expect(typeof current.tag).toBe('string');
103
+ expect(current.tag.length).toBeGreaterThan(0);
104
+ expect(typeof current.props).toBe('object');
105
+ expect(current.props).not.toBeNull();
106
+ expect(Array.isArray(current.children)).toBe(true);
107
+ for (const child of current.children) {
108
+ if (typeof child !== 'string') {
109
+ stack.push(child);
110
+ }
111
+ }
112
+ }
113
+ return true;
114
+ }), { numRuns: 100 });
115
+ });
116
+ });
117
+ // --- generateId() tests ---
118
+ describe('generateId returns strings matching the pattern', () => {
119
+ beforeEach(() => {
120
+ resetIdCounter();
121
+ });
122
+ it('returns a string in the format prefix-number', () => {
123
+ fc.assert(fc.property(fc.stringOf(fc.char().filter((c) => /[a-z\-]/.test(c)), { minLength: 1, maxLength: 20 }), (prefix) => {
124
+ resetIdCounter();
125
+ const id = generateId(prefix);
126
+ const pattern = new RegExp(`^${prefix.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}-\\d+$`);
127
+ expect(id).toMatch(pattern);
128
+ }), { numRuns: 100 });
129
+ });
130
+ it('generates incrementing counter values', () => {
131
+ resetIdCounter();
132
+ const id1 = generateId('ux4g-test');
133
+ const id2 = generateId('ux4g-test');
134
+ const id3 = generateId('ux4g-test');
135
+ expect(id1).toBe('ux4g-test-1');
136
+ expect(id2).toBe('ux4g-test-2');
137
+ expect(id3).toBe('ux4g-test-3');
138
+ });
139
+ });
140
+ // --- resetIdCounter() tests ---
141
+ describe('resetIdCounter resets properly', () => {
142
+ it('resets counter so next generateId starts from 1', () => {
143
+ generateId('ux4g-component');
144
+ generateId('ux4g-component');
145
+ resetIdCounter();
146
+ const id = generateId('ux4g-component');
147
+ expect(id).toBe('ux4g-component-1');
148
+ });
149
+ it('multiple resets produce consistent results', () => {
150
+ fc.assert(fc.property(fc.integer({ min: 1, max: 50 }), (callsBefore) => {
151
+ // Generate some IDs
152
+ for (let i = 0; i < callsBefore; i++) {
153
+ generateId('ux4g-prefix');
154
+ }
155
+ // Reset
156
+ resetIdCounter();
157
+ // Next ID should always be 1
158
+ const id = generateId('ux4g-prefix');
159
+ expect(id).toBe('ux4g-prefix-1');
160
+ }), { numRuns: 100 });
161
+ });
162
+ });
163
+ // --- NodeDescriptor trees with nested children validation ---
164
+ describe('NodeDescriptor trees with nested children are valid', () => {
165
+ it('manually constructed nested trees pass validation', () => {
166
+ const nestedTree = {
167
+ tag: 'div',
168
+ props: { className: 'ux4g-container', id: 'root' },
169
+ children: [
170
+ {
171
+ tag: 'ul',
172
+ props: { role: 'listbox' },
173
+ children: [
174
+ {
175
+ tag: 'li',
176
+ props: { role: 'option', 'aria-selected': true },
177
+ children: ['Option 1'],
178
+ },
179
+ {
180
+ tag: 'li',
181
+ props: { role: 'option', 'aria-selected': false },
182
+ children: ['Option 2'],
183
+ },
184
+ ],
185
+ },
186
+ 'Some text content',
187
+ ],
188
+ };
189
+ expect(isValidNodeDescriptor(nestedTree)).toBe(true);
190
+ });
191
+ it('invalid descriptors fail validation', () => {
192
+ // Missing tag
193
+ expect(isValidNodeDescriptor({ tag: '', props: {}, children: [] })).toBe(false);
194
+ // Null props
195
+ expect(isValidNodeDescriptor({ tag: 'div', props: null, children: [] })).toBe(false);
196
+ // Children not array
197
+ expect(isValidNodeDescriptor({ tag: 'div', props: {}, children: 'bad' })).toBe(false);
198
+ // Nested invalid child
199
+ expect(isValidNodeDescriptor({
200
+ tag: 'div',
201
+ props: {},
202
+ children: [{ tag: '', props: {}, children: [] }],
203
+ })).toBe(false);
204
+ });
205
+ });
@@ -0,0 +1,403 @@
1
+ /**
2
+ * Property-based tests for Tier 1 DOM Generators (Properties 2, 3, 4)
3
+ * Tag: Feature: ux4g-native-framework-experience
4
+ *
5
+ * Property 2: Generated DOM contains all required data-ux4g-* attributes
6
+ * Property 3: Generated DOM contains correct CSS classes
7
+ * Property 4: Generated DOM satisfies ARIA requirements
8
+ *
9
+ * **Validates: Requirements 3.3, 3.4, 3.5, 9.1-9.6**
10
+ */
11
+ import * as fc from 'fast-check';
12
+ import { resetIdCounter } from '../dom-generators/id-generator';
13
+ import { generateDropdownDOM } from '../dom-generators/dropdown';
14
+ import { generateAccordionDOM } from '../dom-generators/accordion';
15
+ import { generateTabsDOM } from '../dom-generators/tabs';
16
+ import { generateModalDOM } from '../dom-generators/modal';
17
+ import { generateCarouselDOM } from '../dom-generators/carousel';
18
+ import { generateDatePickerDOM } from '../dom-generators/date-picker';
19
+ import { generateTimePickerDOM } from '../dom-generators/time-picker';
20
+ import { generateDrawerDOM } from '../dom-generators/drawer';
21
+ import { generateComboboxDOM } from '../dom-generators/combobox';
22
+ import { generateSearchDOM } from '../dom-generators/search';
23
+ // --- Helpers ---
24
+ /**
25
+ * Recursively searches a NodeDescriptor tree for a node with a given prop key.
26
+ * Optionally matches a specific value.
27
+ */
28
+ function findNodeByProp(tree, key, value) {
29
+ if (tree.props[key] !== undefined) {
30
+ if (value === undefined || tree.props[key] === value) {
31
+ return tree;
32
+ }
33
+ }
34
+ for (const child of tree.children) {
35
+ if (typeof child !== 'string') {
36
+ const found = findNodeByProp(child, key, value);
37
+ if (found)
38
+ return found;
39
+ }
40
+ }
41
+ return undefined;
42
+ }
43
+ /**
44
+ * Recursively searches a NodeDescriptor tree for a node with a given tag.
45
+ */
46
+ function findNodeByTag(tree, tag) {
47
+ if (tree.tag === tag)
48
+ return tree;
49
+ for (const child of tree.children) {
50
+ if (typeof child !== 'string') {
51
+ const found = findNodeByTag(child, tag);
52
+ if (found)
53
+ return found;
54
+ }
55
+ }
56
+ return undefined;
57
+ }
58
+ /**
59
+ * Finds ALL nodes matching a given prop key (optionally with value).
60
+ */
61
+ function findAllNodesByProp(tree, key, value) {
62
+ const results = [];
63
+ if (tree.props[key] !== undefined) {
64
+ if (value === undefined || tree.props[key] === value) {
65
+ results.push(tree);
66
+ }
67
+ }
68
+ for (const child of tree.children) {
69
+ if (typeof child !== 'string') {
70
+ results.push(...findAllNodesByProp(child, key, value));
71
+ }
72
+ }
73
+ return results;
74
+ }
75
+ // --- Arbitraries ---
76
+ const dropdownOptionArb = fc.record({
77
+ label: fc.string({ minLength: 1, maxLength: 20 }),
78
+ value: fc.string({ minLength: 1, maxLength: 20 }),
79
+ disabled: fc.boolean(),
80
+ });
81
+ const dropdownPropsArb = fc.record({
82
+ options: fc.array(dropdownOptionArb, { minLength: 1, maxLength: 10 }),
83
+ value: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }),
84
+ placeholder: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
85
+ searchable: fc.boolean(),
86
+ multiple: fc.boolean(),
87
+ disabled: fc.boolean(),
88
+ size: fc.constantFrom('sm', 'md', 'lg'),
89
+ state: fc.constantFrom('default', 'error', 'success', 'warning'),
90
+ open: fc.boolean(),
91
+ });
92
+ const accordionItemArb = fc.record({
93
+ header: fc.string({ minLength: 1, maxLength: 30 }),
94
+ content: fc.string({ minLength: 1, maxLength: 50 }),
95
+ expanded: fc.boolean(),
96
+ disabled: fc.boolean(),
97
+ });
98
+ const accordionPropsArb = fc.record({
99
+ items: fc.array(accordionItemArb, { minLength: 1, maxLength: 8 }),
100
+ arrowPosition: fc.constantFrom('right', 'left'),
101
+ variant: fc.constantFrom('default', 'bordered'),
102
+ multiExpand: fc.boolean(),
103
+ });
104
+ const tabItemArb = fc.record({
105
+ label: fc.string({ minLength: 1, maxLength: 20 }),
106
+ content: fc.string({ minLength: 1, maxLength: 50 }),
107
+ disabled: fc.boolean(),
108
+ });
109
+ const tabsPropsArb = fc.record({
110
+ tabs: fc.array(tabItemArb, { minLength: 1, maxLength: 8 }),
111
+ activeIndex: fc.nat({ max: 7 }),
112
+ variant: fc.constantFrom('underline', 'pill'),
113
+ size: fc.constantFrom('sm', 'md', 'lg'),
114
+ vertical: fc.boolean(),
115
+ });
116
+ const modalPropsArb = fc.record({
117
+ open: fc.boolean(),
118
+ size: fc.constantFrom('s', 'm', 'l'),
119
+ title: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
120
+ closable: fc.boolean(),
121
+ backdropOpacity: fc.constantFrom('25', '50', '75'),
122
+ backdropBlur: fc.boolean(),
123
+ });
124
+ const carouselSlideArb = fc.record({
125
+ content: fc.string({ minLength: 1, maxLength: 50 }),
126
+ label: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }),
127
+ });
128
+ const carouselPropsArb = fc.record({
129
+ slides: fc.array(carouselSlideArb, { minLength: 1, maxLength: 10 }),
130
+ activeIndex: fc.nat({ max: 9 }),
131
+ showIndicators: fc.boolean(),
132
+ showNavigation: fc.boolean(),
133
+ });
134
+ const datePickerPropsArb = fc.record({
135
+ value: fc.option(fc.tuple(fc.integer({ min: 2000, max: 2030 }), fc.integer({ min: 1, max: 12 }), fc.integer({ min: 1, max: 28 })).map(([y, m, d]) => `${y}-${String(m).padStart(2, '0')}-${String(d).padStart(2, '0')}`), { nil: undefined }),
136
+ displayMonth: fc.integer({ min: 0, max: 11 }),
137
+ displayYear: fc.integer({ min: 2000, max: 2030 }),
138
+ });
139
+ const timePickerPropsArb = fc.record({
140
+ value: fc.option(fc.tuple(fc.integer({ min: 0, max: 23 }), fc.integer({ min: 0, max: 59 }))
141
+ .map(([h, m]) => `${String(h).padStart(2, '0')}:${String(m).padStart(2, '0')}`), { nil: undefined }),
142
+ format: fc.constantFrom('12h', '24h'),
143
+ });
144
+ const drawerPropsArb = fc.record({
145
+ open: fc.boolean(),
146
+ placement: fc.constantFrom('right', 'left', 'top', 'bottom'),
147
+ title: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
148
+ closable: fc.boolean(),
149
+ });
150
+ const comboboxOptionArb = fc.record({
151
+ label: fc.string({ minLength: 1, maxLength: 20 }),
152
+ value: fc.string({ minLength: 1, maxLength: 20 }),
153
+ disabled: fc.boolean(),
154
+ });
155
+ const comboboxPropsArb = fc.record({
156
+ options: fc.array(comboboxOptionArb, { minLength: 1, maxLength: 10 }),
157
+ value: fc.option(fc.string({ minLength: 1, maxLength: 20 }), { nil: undefined }),
158
+ inputValue: fc.option(fc.string({ minLength: 0, maxLength: 20 }), { nil: undefined }),
159
+ placeholder: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
160
+ multiple: fc.boolean(),
161
+ disabled: fc.boolean(),
162
+ size: fc.constantFrom('sm', 'md', 'lg'),
163
+ state: fc.constantFrom('default', 'error', 'success', 'warning'),
164
+ open: fc.boolean(),
165
+ });
166
+ const searchSuggestionArb = fc.record({
167
+ label: fc.string({ minLength: 1, maxLength: 20 }),
168
+ value: fc.string({ minLength: 1, maxLength: 20 }),
169
+ });
170
+ const searchPropsArb = fc.record({
171
+ value: fc.option(fc.string({ minLength: 0, maxLength: 30 }), { nil: undefined }),
172
+ placeholder: fc.option(fc.string({ minLength: 1, maxLength: 30 }), { nil: undefined }),
173
+ suggestions: fc.array(searchSuggestionArb, { minLength: 0, maxLength: 5 }),
174
+ showSuggestions: fc.boolean(),
175
+ size: fc.constantFrom('s', 'm', 'lg'),
176
+ });
177
+ // --- Test Setup ---
178
+ beforeEach(() => {
179
+ resetIdCounter();
180
+ });
181
+ // --- Property 2: data-ux4g-* attributes ---
182
+ describe('Property 2: data-ux4g-* attributes', () => {
183
+ it('generateDropdownDOM → tree contains data-ux4g-dropdown-toggle', () => {
184
+ fc.assert(fc.property(dropdownPropsArb, (props) => {
185
+ resetIdCounter();
186
+ const tree = generateDropdownDOM(props);
187
+ const node = findNodeByProp(tree, 'data-ux4g-dropdown-toggle');
188
+ expect(node).toBeDefined();
189
+ }), { numRuns: 100 });
190
+ });
191
+ it('generateAccordionDOM → tree contains data-ux4g-accordion-toggle on header elements', () => {
192
+ fc.assert(fc.property(accordionPropsArb, (props) => {
193
+ resetIdCounter();
194
+ const tree = generateAccordionDOM(props);
195
+ const toggleNodes = findAllNodesByProp(tree, 'data-ux4g-accordion-toggle');
196
+ // Should have one toggle per item
197
+ expect(toggleNodes.length).toBe(props.items.length);
198
+ // Each toggle should be on a button element
199
+ for (const node of toggleNodes) {
200
+ expect(node.tag).toBe('button');
201
+ }
202
+ }), { numRuns: 100 });
203
+ });
204
+ it('generateModalDOM → tree contains data-ux4g-modal-close when closable=true', () => {
205
+ fc.assert(fc.property(modalPropsArb.filter((p) => p.closable === true), (props) => {
206
+ resetIdCounter();
207
+ const tree = generateModalDOM(props);
208
+ const node = findNodeByProp(tree, 'data-ux4g-modal-close');
209
+ expect(node).toBeDefined();
210
+ }), { numRuns: 100 });
211
+ });
212
+ it('generateCarouselDOM → tree contains data-ux4g-carousel-next and data-ux4g-carousel-prev when showNavigation=true', () => {
213
+ fc.assert(fc.property(carouselPropsArb.filter((p) => p.showNavigation === true), (props) => {
214
+ resetIdCounter();
215
+ const safeProps = { ...props, activeIndex: Math.min(props.activeIndex, props.slides.length - 1) };
216
+ const tree = generateCarouselDOM(safeProps);
217
+ const prevNode = findNodeByProp(tree, 'data-ux4g-carousel-prev');
218
+ const nextNode = findNodeByProp(tree, 'data-ux4g-carousel-next');
219
+ expect(prevNode).toBeDefined();
220
+ expect(nextNode).toBeDefined();
221
+ }), { numRuns: 100 });
222
+ });
223
+ it('generateDrawerDOM → tree contains data-ux4g-drawer-close when closable=true', () => {
224
+ fc.assert(fc.property(drawerPropsArb.filter((p) => p.closable === true), (props) => {
225
+ resetIdCounter();
226
+ const tree = generateDrawerDOM(props);
227
+ const node = findNodeByProp(tree, 'data-ux4g-drawer-close');
228
+ expect(node).toBeDefined();
229
+ }), { numRuns: 100 });
230
+ });
231
+ });
232
+ // --- Property 3: CSS classes ---
233
+ describe('Property 3: CSS classes', () => {
234
+ it('Dropdown → root contains ux4g-dropdown', () => {
235
+ fc.assert(fc.property(dropdownPropsArb, (props) => {
236
+ resetIdCounter();
237
+ const tree = generateDropdownDOM(props);
238
+ expect(tree.props.className).toContain('ux4g-dropdown');
239
+ }), { numRuns: 100 });
240
+ });
241
+ it('Accordion → root contains ux4g-accordion', () => {
242
+ fc.assert(fc.property(accordionPropsArb, (props) => {
243
+ resetIdCounter();
244
+ const tree = generateAccordionDOM(props);
245
+ expect(tree.props.className).toContain('ux4g-accordion');
246
+ }), { numRuns: 100 });
247
+ });
248
+ it('Tabs → root contains ux4g-tab', () => {
249
+ fc.assert(fc.property(tabsPropsArb, (props) => {
250
+ resetIdCounter();
251
+ const safeProps = { ...props, activeIndex: Math.min(props.activeIndex, props.tabs.length - 1) };
252
+ const tree = generateTabsDOM(safeProps);
253
+ expect(tree.props.className).toContain('ux4g-tab');
254
+ }), { numRuns: 100 });
255
+ });
256
+ it('Modal → root contains ux4g-modal', () => {
257
+ fc.assert(fc.property(modalPropsArb, (props) => {
258
+ resetIdCounter();
259
+ const tree = generateModalDOM(props);
260
+ expect(tree.props.className).toContain('ux4g-modal');
261
+ }), { numRuns: 100 });
262
+ });
263
+ it('Carousel → root contains ux4g-carousel', () => {
264
+ fc.assert(fc.property(carouselPropsArb, (props) => {
265
+ resetIdCounter();
266
+ const safeProps = { ...props, activeIndex: Math.min(props.activeIndex, props.slides.length - 1) };
267
+ const tree = generateCarouselDOM(safeProps);
268
+ expect(tree.props.className).toContain('ux4g-carousel');
269
+ }), { numRuns: 100 });
270
+ });
271
+ it('DatePicker → root contains ux4g-date-picker-container', () => {
272
+ fc.assert(fc.property(datePickerPropsArb, (props) => {
273
+ resetIdCounter();
274
+ const tree = generateDatePickerDOM(props);
275
+ expect(tree.props.className).toContain('ux4g-date-picker-container');
276
+ }), { numRuns: 100 });
277
+ });
278
+ it('TimePicker → root contains ux4g-time-picker-container', () => {
279
+ fc.assert(fc.property(timePickerPropsArb, (props) => {
280
+ resetIdCounter();
281
+ const tree = generateTimePickerDOM(props);
282
+ expect(tree.props.className).toContain('ux4g-time-picker-container');
283
+ }), { numRuns: 100 });
284
+ });
285
+ it('Drawer → root contains ux4g-drawer', () => {
286
+ fc.assert(fc.property(drawerPropsArb, (props) => {
287
+ resetIdCounter();
288
+ const tree = generateDrawerDOM(props);
289
+ // The drawer panel (not wrapper) contains the ux4g-drawer class
290
+ const drawerPanel = findAllNodesByProp(tree, 'role', 'dialog');
291
+ expect(drawerPanel.length).toBeGreaterThan(0);
292
+ // Check wrapper or panel contains ux4g-drawer
293
+ const hasDrawerClass = (tree.props.className || '').includes('ux4g-drawer') ||
294
+ drawerPanel.some((n) => (n.props.className || '').includes('ux4g-drawer'));
295
+ expect(hasDrawerClass).toBe(true);
296
+ }), { numRuns: 100 });
297
+ });
298
+ it('Combobox → root contains ux4g-combobox', () => {
299
+ fc.assert(fc.property(comboboxPropsArb, (props) => {
300
+ resetIdCounter();
301
+ const tree = generateComboboxDOM(props);
302
+ expect(tree.props.className).toContain('ux4g-combobox');
303
+ }), { numRuns: 100 });
304
+ });
305
+ it('Search → root contains ux4g-search-container', () => {
306
+ fc.assert(fc.property(searchPropsArb, (props) => {
307
+ resetIdCounter();
308
+ const tree = generateSearchDOM(props);
309
+ expect(tree.props.className).toContain('ux4g-search-container');
310
+ }), { numRuns: 100 });
311
+ });
312
+ });
313
+ // --- Property 4: ARIA requirements ---
314
+ describe('Property 4: ARIA requirements', () => {
315
+ it('Dropdown → menu has role="listbox", trigger has aria-haspopup', () => {
316
+ fc.assert(fc.property(dropdownPropsArb, (props) => {
317
+ resetIdCounter();
318
+ const tree = generateDropdownDOM(props);
319
+ // Menu has role="listbox"
320
+ const listbox = findNodeByProp(tree, 'role', 'listbox');
321
+ expect(listbox).toBeDefined();
322
+ // Trigger has aria-haspopup
323
+ const trigger = findNodeByProp(tree, 'aria-haspopup');
324
+ expect(trigger).toBeDefined();
325
+ }), { numRuns: 100 });
326
+ });
327
+ it('Accordion → panels have role="region", toggles have aria-expanded', () => {
328
+ fc.assert(fc.property(accordionPropsArb, (props) => {
329
+ resetIdCounter();
330
+ const tree = generateAccordionDOM(props);
331
+ // Panels with role="region"
332
+ const regions = findAllNodesByProp(tree, 'role', 'region');
333
+ expect(regions.length).toBe(props.items.length);
334
+ // Toggles with aria-expanded
335
+ const toggles = findAllNodesByProp(tree, 'aria-expanded');
336
+ expect(toggles.length).toBe(props.items.length);
337
+ }), { numRuns: 100 });
338
+ });
339
+ it('Tabs → tablist has role="tablist", tabs have role="tab", panels have role="tabpanel"', () => {
340
+ fc.assert(fc.property(tabsPropsArb, (props) => {
341
+ resetIdCounter();
342
+ const safeProps = { ...props, activeIndex: Math.min(props.activeIndex, props.tabs.length - 1) };
343
+ const tree = generateTabsDOM(safeProps);
344
+ // tablist
345
+ const tablist = findNodeByProp(tree, 'role', 'tablist');
346
+ expect(tablist).toBeDefined();
347
+ // tabs
348
+ const tabs = findAllNodesByProp(tree, 'role', 'tab');
349
+ expect(tabs.length).toBe(props.tabs.length);
350
+ // tabpanels
351
+ const panels = findAllNodesByProp(tree, 'role', 'tabpanel');
352
+ expect(panels.length).toBe(props.tabs.length);
353
+ }), { numRuns: 100 });
354
+ });
355
+ it('Modal → dialog box has role="dialog" and aria-modal="true"', () => {
356
+ fc.assert(fc.property(modalPropsArb, (props) => {
357
+ resetIdCounter();
358
+ const tree = generateModalDOM(props);
359
+ const dialog = findNodeByProp(tree, 'role', 'dialog');
360
+ expect(dialog).toBeDefined();
361
+ expect(dialog.props['aria-modal']).toBe('true');
362
+ }), { numRuns: 100 });
363
+ });
364
+ it('Carousel → root has aria-roledescription="carousel"', () => {
365
+ fc.assert(fc.property(carouselPropsArb, (props) => {
366
+ resetIdCounter();
367
+ const safeProps = { ...props, activeIndex: Math.min(props.activeIndex, props.slides.length - 1) };
368
+ const tree = generateCarouselDOM(safeProps);
369
+ expect(tree.props['aria-roledescription']).toBe('carousel');
370
+ }), { numRuns: 100 });
371
+ });
372
+ it('Combobox → input has role="combobox" and aria-autocomplete="list"', () => {
373
+ fc.assert(fc.property(comboboxPropsArb, (props) => {
374
+ resetIdCounter();
375
+ const tree = generateComboboxDOM(props);
376
+ const combobox = findNodeByProp(tree, 'role', 'combobox');
377
+ expect(combobox).toBeDefined();
378
+ expect(combobox.props['aria-autocomplete']).toBe('list');
379
+ }), { numRuns: 100 });
380
+ });
381
+ it('Search → input has role="searchbox"', () => {
382
+ fc.assert(fc.property(searchPropsArb, (props) => {
383
+ resetIdCounter();
384
+ const tree = generateSearchDOM(props);
385
+ const searchbox = findNodeByProp(tree, 'role', 'searchbox');
386
+ expect(searchbox).toBeDefined();
387
+ }), { numRuns: 100 });
388
+ });
389
+ it('TimePicker → spinbuttons have role="spinbutton"', () => {
390
+ fc.assert(fc.property(timePickerPropsArb, (props) => {
391
+ resetIdCounter();
392
+ const tree = generateTimePickerDOM(props);
393
+ const spinbuttons = findAllNodesByProp(tree, 'role', 'spinbutton');
394
+ // At least 2 spinbuttons (hour + minute), 3 if 12h format (+ period)
395
+ if (props.format === '12h') {
396
+ expect(spinbuttons.length).toBe(3);
397
+ }
398
+ else {
399
+ expect(spinbuttons.length).toBe(2);
400
+ }
401
+ }), { numRuns: 100 });
402
+ });
403
+ });