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.
- package/README.md +76 -0
- package/dist/__tests__/css-bundle.integration.test.d.ts +11 -0
- package/dist/__tests__/css-bundle.integration.test.js +1102 -0
- package/dist/__tests__/css-bundle.phase10.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase10.property.test.js +64 -0
- package/dist/__tests__/css-bundle.phase5.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase5.property.test.js +126 -0
- package/dist/__tests__/css-bundle.phase6.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase6.property.test.js +73 -0
- package/dist/__tests__/css-bundle.phase7.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase7.property.test.js +76 -0
- package/dist/__tests__/css-bundle.phase8.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase8.property.test.js +67 -0
- package/dist/__tests__/css-bundle.phase9.property.test.d.ts +9 -0
- package/dist/__tests__/css-bundle.phase9.property.test.js +93 -0
- package/dist/__tests__/css-bundle.property.test.d.ts +14 -0
- package/dist/__tests__/css-bundle.property.test.js +393 -0
- package/dist/__tests__/dom-generators.determinism.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.determinism.property.test.js +71 -0
- package/dist/__tests__/dom-generators.id.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.id.property.test.js +99 -0
- package/dist/__tests__/dom-generators.otp.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.property.test.js +205 -0
- package/dist/__tests__/dom-generators.states.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.table.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.tier1.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.tier1.property.test.js +403 -0
- package/dist/__tests__/dom-generators.validation.property.test.d.ts +1 -0
- package/dist/__tests__/dom-generators.validation.property.test.js +327 -0
- package/dist/__tests__/megamenu.classbuilder.property.test.d.ts +1 -0
- package/dist/__tests__/megamenu.classbuilder.property.test.js +88 -0
- package/dist/__tests__/smoke.test.d.ts +1 -0
- package/dist/__tests__/smoke.test.js +65 -0
- package/dist/__tests__/types.phase10.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase10.property.test.js +166 -0
- package/dist/__tests__/types.phase10.test.d.ts +1 -0
- package/dist/__tests__/types.phase10.test.js +76 -0
- package/dist/__tests__/types.phase3.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase3.property.test.js +83 -0
- package/dist/__tests__/types.phase3.test.d.ts +1 -0
- package/dist/__tests__/types.phase3.test.js +76 -0
- package/dist/__tests__/types.phase4.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase4.property.test.js +119 -0
- package/dist/__tests__/types.phase4.test.d.ts +1 -0
- package/dist/__tests__/types.phase4.test.js +70 -0
- package/dist/__tests__/types.phase5.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase5.property.test.js +120 -0
- package/dist/__tests__/types.phase5.test.d.ts +1 -0
- package/dist/__tests__/types.phase5.test.js +64 -0
- package/dist/__tests__/types.phase6.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase6.property.test.js +189 -0
- package/dist/__tests__/types.phase6.test.d.ts +1 -0
- package/dist/__tests__/types.phase6.test.js +121 -0
- package/dist/__tests__/types.phase7.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase7.property.test.js +217 -0
- package/dist/__tests__/types.phase7.test.d.ts +1 -0
- package/dist/__tests__/types.phase7.test.js +106 -0
- package/dist/__tests__/types.phase8.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase8.property.test.js +224 -0
- package/dist/__tests__/types.phase8.test.d.ts +1 -0
- package/dist/__tests__/types.phase8.test.js +114 -0
- package/dist/__tests__/types.phase9.property.test.d.ts +1 -0
- package/dist/__tests__/types.phase9.property.test.js +347 -0
- package/dist/__tests__/types.phase9.test.d.ts +1 -0
- package/dist/__tests__/types.phase9.test.js +226 -0
- package/dist/__tests__/types.restructure.property.test.d.ts +1 -0
- package/dist/__tests__/types.restructure.property.test.js +76 -0
- package/dist/__tests__/types.test.d.ts +1 -0
- package/dist/__tests__/types.test.js +175 -0
- package/dist/dom-generators/accordion.d.ts +23 -0
- package/dist/dom-generators/avatar.d.ts +19 -0
- package/dist/dom-generators/carousel.d.ts +20 -0
- package/dist/dom-generators/chip.d.ts +18 -0
- package/dist/dom-generators/combobox.d.ts +28 -0
- package/dist/dom-generators/date-picker.d.ts +19 -0
- package/dist/dom-generators/dom-generators/accordion.d.ts +21 -0
- package/dist/dom-generators/dom-generators/avatar.d.ts +17 -0
- package/dist/dom-generators/dom-generators/carousel.d.ts +19 -0
- package/dist/dom-generators/dom-generators/chip.d.ts +16 -0
- package/dist/dom-generators/dom-generators/combobox.d.ts +26 -0
- package/dist/dom-generators/dom-generators/date-picker.d.ts +18 -0
- package/dist/dom-generators/dom-generators/drawer.d.ts +17 -0
- package/dist/dom-generators/dom-generators/dropdown.d.ts +26 -0
- package/dist/dom-generators/dom-generators/file-upload.d.ts +20 -0
- package/dist/dom-generators/dom-generators/id-generator.d.ts +9 -0
- package/dist/dom-generators/dom-generators/index.d.ts +27 -0
- package/dist/dom-generators/dom-generators/modal.d.ts +19 -0
- package/dist/dom-generators/dom-generators/otp.d.ts +16 -0
- package/dist/dom-generators/dom-generators/popover.d.ts +17 -0
- package/dist/dom-generators/dom-generators/progress.d.ts +16 -0
- package/dist/dom-generators/dom-generators/search.d.ts +20 -0
- package/dist/dom-generators/dom-generators/stepper.d.ts +21 -0
- package/dist/dom-generators/dom-generators/table.d.ts +23 -0
- package/dist/dom-generators/dom-generators/tabs.d.ts +21 -0
- package/dist/dom-generators/dom-generators/time-picker.d.ts +18 -0
- package/dist/dom-generators/dom-generators/tooltip.d.ts +17 -0
- package/dist/dom-generators/dom-generators/types.d.ts +27 -0
- package/dist/dom-generators/dom-generators/validate.d.ts +20 -0
- package/dist/dom-generators/drawer.d.ts +19 -0
- package/dist/dom-generators/dropdown.d.ts +28 -0
- package/dist/dom-generators/file-upload.d.ts +22 -0
- package/dist/dom-generators/id-generator.d.ts +9 -0
- package/dist/dom-generators/index.bundled.d.ts +654 -0
- package/dist/dom-generators/index.cjs +2029 -0
- package/dist/dom-generators/index.d.ts +27 -0
- package/dist/dom-generators/index.mjs +2001 -0
- package/dist/dom-generators/modal.d.ts +21 -0
- package/dist/dom-generators/otp.d.ts +18 -0
- package/dist/dom-generators/popover.d.ts +19 -0
- package/dist/dom-generators/progress.d.ts +18 -0
- package/dist/dom-generators/search.d.ts +22 -0
- package/dist/dom-generators/stepper.d.ts +23 -0
- package/dist/dom-generators/table.d.ts +25 -0
- package/dist/dom-generators/tabs.d.ts +23 -0
- package/dist/dom-generators/time-picker.d.ts +19 -0
- package/dist/dom-generators/tooltip.d.ts +19 -0
- package/dist/dom-generators/types.d.ts +155 -0
- package/dist/dom-generators/validate.d.ts +20 -0
- package/dist/runtime/bootstrap.js +59 -0
- package/dist/runtime/index.js +55 -0
- package/dist/types.d.ts +155 -0
- package/dist/types.js +552 -0
- 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 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -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
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|