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,393 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property 1: CSS bundle contains all button class names
|
|
3
|
+
* Tag: Feature: ux4g-button-npm-package, Property 1
|
|
4
|
+
* Validates: Requirements 2.3, 2.5, 2.6
|
|
5
|
+
*
|
|
6
|
+
* Property 2: CSS class names are ux4g-prefixed
|
|
7
|
+
* Tag: Feature: ux4g-button-npm-package, Property 2
|
|
8
|
+
* Validates: Requirements 2.8
|
|
9
|
+
*
|
|
10
|
+
* Property 3: CSS bundle preserves layer ordering
|
|
11
|
+
* Tag: Feature: ux4g-button-npm-package, Property 3
|
|
12
|
+
* Validates: Requirements 2.1
|
|
13
|
+
*/
|
|
14
|
+
import * as fc from 'fast-check';
|
|
15
|
+
import * as fs from 'fs';
|
|
16
|
+
import * as path from 'path';
|
|
17
|
+
// Known set of button class names defined in the design system
|
|
18
|
+
const VARIANT_CLASS_NAMES = [
|
|
19
|
+
'ux4g-btn-primary',
|
|
20
|
+
'ux4g-btn-outline-primary',
|
|
21
|
+
'ux4g-btn-text-primary',
|
|
22
|
+
'ux4g-btn-tonal-primary',
|
|
23
|
+
'ux4g-btn-danger',
|
|
24
|
+
'ux4g-btn-outline-danger',
|
|
25
|
+
'ux4g-btn-text-danger',
|
|
26
|
+
];
|
|
27
|
+
const SIZE_CLASS_NAMES = [
|
|
28
|
+
'ux4g-btn-xl',
|
|
29
|
+
'ux4g-btn-lg',
|
|
30
|
+
'ux4g-btn-sm',
|
|
31
|
+
'ux4g-btn-xs',
|
|
32
|
+
];
|
|
33
|
+
const STATE_CLASS_NAMES = [];
|
|
34
|
+
const ALL_BUTTON_CLASS_NAMES = [
|
|
35
|
+
...VARIANT_CLASS_NAMES,
|
|
36
|
+
...SIZE_CLASS_NAMES,
|
|
37
|
+
...STATE_CLASS_NAMES,
|
|
38
|
+
];
|
|
39
|
+
// Resolve path to styles/ux4g.css relative to this test file
|
|
40
|
+
const CSS_BUNDLE_PATH = path.resolve(__dirname, '../../styles/ux4g.css');
|
|
41
|
+
/**
|
|
42
|
+
* Property 2: CSS class names are ux4g-prefixed
|
|
43
|
+
* Tag: Feature: ux4g-button-npm-package, Property 2
|
|
44
|
+
* Validates: Requirements 2.8
|
|
45
|
+
*
|
|
46
|
+
* Known vendor exceptions (non-ux4g-prefixed classes used by the design system):
|
|
47
|
+
* - .is-open — open-state modifier used by Dropdown, Combobox, Modal
|
|
48
|
+
* - .is-selected — selected-state modifier used by Dropdown, Combobox
|
|
49
|
+
* - .has-selection — selection-state modifier used by Dropdown, Combobox
|
|
50
|
+
* - .active — active-state modifier used by List
|
|
51
|
+
*/
|
|
52
|
+
describe('Property 2: CSS class names are ux4g-prefixed', () => {
|
|
53
|
+
let cssContent;
|
|
54
|
+
let classSelectors;
|
|
55
|
+
// Known vendor classes that are intentionally not ux4g-prefixed
|
|
56
|
+
const KNOWN_NON_PREFIXED = new Set([
|
|
57
|
+
// State modifiers used across multiple components
|
|
58
|
+
'.is-open',
|
|
59
|
+
'.is-selected',
|
|
60
|
+
'.has-selection',
|
|
61
|
+
'.active',
|
|
62
|
+
'.show',
|
|
63
|
+
'.is-hovered',
|
|
64
|
+
'.is-active',
|
|
65
|
+
'.is-focused',
|
|
66
|
+
'.is-placeholder',
|
|
67
|
+
'.has-value',
|
|
68
|
+
'.disabled',
|
|
69
|
+
'.is-disabled',
|
|
70
|
+
'.is-empty',
|
|
71
|
+
'.is-muted',
|
|
72
|
+
'.collapsed',
|
|
73
|
+
'.collapsing',
|
|
74
|
+
'.selected',
|
|
75
|
+
// Vendor component classes (non-prefixed legacy patterns)
|
|
76
|
+
'.card-group',
|
|
77
|
+
'.avatar-badge',
|
|
78
|
+
'.btn-link',
|
|
79
|
+
'.next',
|
|
80
|
+
'.prev',
|
|
81
|
+
'.bordered',
|
|
82
|
+
// Date/time picker vendor classes
|
|
83
|
+
'.is-today',
|
|
84
|
+
'.is-in-range',
|
|
85
|
+
'.is-range-start',
|
|
86
|
+
'.is-range-end',
|
|
87
|
+
// Journey timeline vendor classes
|
|
88
|
+
'.journey-timeline-card',
|
|
89
|
+
'.journey-timeline-title',
|
|
90
|
+
'.journey-timeline-description',
|
|
91
|
+
// Feedback vendor classes
|
|
92
|
+
'.feedback-thumb-wrapper',
|
|
93
|
+
'.feedback-thumb-down',
|
|
94
|
+
'.feedback-thumb-up',
|
|
95
|
+
'.feedback-nps-button',
|
|
96
|
+
'.feedback-emoji-button',
|
|
97
|
+
'.feedback-submitted-wrapper',
|
|
98
|
+
'.contact-wrapper',
|
|
99
|
+
// Time slot vendor classes
|
|
100
|
+
'.holiday',
|
|
101
|
+
'.weekly-off',
|
|
102
|
+
'.limited',
|
|
103
|
+
'.no-slots',
|
|
104
|
+
'.today',
|
|
105
|
+
'.success',
|
|
106
|
+
'.warning',
|
|
107
|
+
'.grid-2',
|
|
108
|
+
// Accessibility/navbar vendor classes
|
|
109
|
+
'.india-flag',
|
|
110
|
+
'.acc-top-divider',
|
|
111
|
+
'.icon-language',
|
|
112
|
+
// Misc vendor references (from svg/external links in CSS)
|
|
113
|
+
'.w3',
|
|
114
|
+
'.org',
|
|
115
|
+
'.check-purple',
|
|
116
|
+
'.border-yellow',
|
|
117
|
+
'.muted',
|
|
118
|
+
]);
|
|
119
|
+
beforeAll(() => {
|
|
120
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
121
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
cssContent = '';
|
|
125
|
+
}
|
|
126
|
+
// Parse all class selectors from the CSS content, excluding known vendor exceptions
|
|
127
|
+
const allSelectors = cssContent.match(/\.[a-zA-Z][a-zA-Z0-9_-]*/g) ?? [];
|
|
128
|
+
classSelectors = allSelectors.filter((s) => !KNOWN_NON_PREFIXED.has(s));
|
|
129
|
+
});
|
|
130
|
+
it('every class selector in the CSS bundle starts with .ux4g- (excluding known vendor exceptions)', () => {
|
|
131
|
+
if (classSelectors.length === 0) {
|
|
132
|
+
// CSS bundle is empty or has no class selectors — pass trivially
|
|
133
|
+
return;
|
|
134
|
+
}
|
|
135
|
+
fc.assert(fc.property(fc.constantFrom(...classSelectors), (selector) => {
|
|
136
|
+
return selector.startsWith('.ux4g-');
|
|
137
|
+
}), {
|
|
138
|
+
numRuns: Math.min(classSelectors.length, 20),
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
});
|
|
142
|
+
describe('Property 1: CSS bundle contains all button class names', () => {
|
|
143
|
+
let cssContent;
|
|
144
|
+
beforeAll(() => {
|
|
145
|
+
// Read the CSS bundle once before all tests
|
|
146
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
147
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
148
|
+
}
|
|
149
|
+
else {
|
|
150
|
+
cssContent = '';
|
|
151
|
+
}
|
|
152
|
+
});
|
|
153
|
+
it('styles/ux4g.css exists', () => {
|
|
154
|
+
expect(fs.existsSync(CSS_BUNDLE_PATH)).toBe(true);
|
|
155
|
+
});
|
|
156
|
+
/**
|
|
157
|
+
* Property 1: For any button class name from the known set,
|
|
158
|
+
* that class name SHALL appear as a selector in styles/ux4g.css.
|
|
159
|
+
*
|
|
160
|
+
* Validates: Requirements 2.3, 2.5, 2.6
|
|
161
|
+
*/
|
|
162
|
+
it('every button class name appears as a selector in the CSS bundle', () => {
|
|
163
|
+
fc.assert(fc.property(fc.constantFrom(...ALL_BUTTON_CLASS_NAMES), (className) => {
|
|
164
|
+
// The class should appear as a CSS selector: .className
|
|
165
|
+
const selector = `.${className}`;
|
|
166
|
+
return cssContent.includes(selector);
|
|
167
|
+
}), {
|
|
168
|
+
numRuns: 20,
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
/**
|
|
173
|
+
* Property 3: CSS bundle preserves layer ordering
|
|
174
|
+
* Tag: Feature: ux4g-button-npm-package, Property 3
|
|
175
|
+
* Validates: Requirements 2.1
|
|
176
|
+
*
|
|
177
|
+
* The layer order is: tokens → semantic → components
|
|
178
|
+
* - Token layer: CSS custom properties like `--ux4g-color-` or `--ux4g-spacing-`
|
|
179
|
+
* - Semantic layer: CSS custom properties like `--ux4g-bg-` or `--ux4g-text-`
|
|
180
|
+
* - Component layer: class selectors like `.ux4g-btn-`
|
|
181
|
+
*
|
|
182
|
+
* Character offsets are used to verify ordering in the (potentially minified) bundle.
|
|
183
|
+
* If the bundle is empty or markers are absent, the test passes trivially.
|
|
184
|
+
*/
|
|
185
|
+
describe('Property 3: CSS bundle preserves layer ordering', () => {
|
|
186
|
+
let cssContent;
|
|
187
|
+
beforeAll(() => {
|
|
188
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
189
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
190
|
+
}
|
|
191
|
+
else {
|
|
192
|
+
cssContent = '';
|
|
193
|
+
}
|
|
194
|
+
});
|
|
195
|
+
/**
|
|
196
|
+
* Property 3: token rules precede semantic rules, which precede component rules.
|
|
197
|
+
*
|
|
198
|
+
* Token layer markers: --ux4g-color- or --ux4g-spacing-
|
|
199
|
+
* Semantic layer markers: --ux4g-bg- or --ux4g-text-
|
|
200
|
+
* Component layer markers: .ux4g-btn-
|
|
201
|
+
*
|
|
202
|
+
* **Validates: Requirements 2.1**
|
|
203
|
+
*/
|
|
204
|
+
it('token layer appears before semantic layer, which appears before component layer', () => {
|
|
205
|
+
// Token layer: find the FIRST occurrence of a token custom property *definition* in :root
|
|
206
|
+
// Semantic layer: find the FIRST occurrence of a semantic custom property *definition* in :root
|
|
207
|
+
// Component layer: find the FIRST occurrence of a component class selector
|
|
208
|
+
//
|
|
209
|
+
// Note: The CSS bundle may reference semantic variables in base/reset rules before :root,
|
|
210
|
+
// so we look for the first *definition* (i.e., inside a :root block) rather than any reference.
|
|
211
|
+
// We use the :root{ marker to find where definitions start.
|
|
212
|
+
const rootIdx = cssContent.indexOf(':root{');
|
|
213
|
+
if (rootIdx === -1) {
|
|
214
|
+
// No :root block found — pass trivially
|
|
215
|
+
return;
|
|
216
|
+
}
|
|
217
|
+
const rootContent = cssContent.substring(rootIdx);
|
|
218
|
+
const tokenMarkers = ['--ux4g-color-', '--ux4g-spacing-'];
|
|
219
|
+
const semanticMarkers = ['--ux4g-bg-', '--ux4g-text-'];
|
|
220
|
+
const componentMarkers = ['.ux4g-btn-'];
|
|
221
|
+
// Find first token-layer offset within :root
|
|
222
|
+
let firstTokenOffset = -1;
|
|
223
|
+
for (const marker of tokenMarkers) {
|
|
224
|
+
const idx = rootContent.indexOf(marker);
|
|
225
|
+
if (idx !== -1 && (firstTokenOffset === -1 || idx < firstTokenOffset)) {
|
|
226
|
+
firstTokenOffset = idx;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
// Find first semantic-layer offset within :root
|
|
230
|
+
let firstSemanticOffset = -1;
|
|
231
|
+
for (const marker of semanticMarkers) {
|
|
232
|
+
const idx = rootContent.indexOf(marker);
|
|
233
|
+
if (idx !== -1 && (firstSemanticOffset === -1 || idx < firstSemanticOffset)) {
|
|
234
|
+
firstSemanticOffset = idx;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
// Find first component-layer offset in full CSS (components are outside :root)
|
|
238
|
+
let firstComponentOffset = -1;
|
|
239
|
+
for (const marker of componentMarkers) {
|
|
240
|
+
const idx = cssContent.indexOf(marker);
|
|
241
|
+
if (idx !== -1 && (firstComponentOffset === -1 || idx < firstComponentOffset)) {
|
|
242
|
+
firstComponentOffset = idx;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
// If none of the markers are found (e.g. empty/placeholder bundle), pass trivially
|
|
246
|
+
if (firstTokenOffset === -1 && firstSemanticOffset === -1 && firstComponentOffset === -1) {
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
// Within :root: token definitions must precede semantic definitions
|
|
250
|
+
if (firstTokenOffset !== -1 && firstSemanticOffset !== -1) {
|
|
251
|
+
expect(firstTokenOffset).toBeLessThan(firstSemanticOffset);
|
|
252
|
+
}
|
|
253
|
+
// Component classes must appear after the :root block (i.e., after rootIdx)
|
|
254
|
+
if (firstComponentOffset !== -1) {
|
|
255
|
+
expect(firstComponentOffset).toBeGreaterThan(rootIdx);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
});
|
|
259
|
+
const SPINNER_VARIANTS = ['primary', 'inverse', 'danger'];
|
|
260
|
+
const SPINNER_SIZES = ['xl', 'lg', 'md', 'sm', 'xs'];
|
|
261
|
+
const SPINNER_TYPES = ['full', 'split', 'partial'];
|
|
262
|
+
describe('Property 9: CSS bundle contains all spinner class patterns', () => {
|
|
263
|
+
let cssContent;
|
|
264
|
+
beforeAll(() => {
|
|
265
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
266
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
cssContent = '';
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
/**
|
|
273
|
+
* Property 9: For any SpinnerVariant × SpinnerSize × SpinnerType triple,
|
|
274
|
+
* the CSS bundle SHALL contain the base attribute selector, variant selector,
|
|
275
|
+
* size class selector, and type selector.
|
|
276
|
+
*
|
|
277
|
+
* Note: minified CSS omits quotes in attribute selectors, so we check both forms.
|
|
278
|
+
*
|
|
279
|
+
* **Validates: Requirements 2.1, 2.3, 2.4, 2.7**
|
|
280
|
+
*/
|
|
281
|
+
it('CSS bundle contains base spinner attribute selector for all triples', () => {
|
|
282
|
+
fc.assert(fc.property(fc.constantFrom(...SPINNER_VARIANTS), fc.constantFrom(...SPINNER_SIZES), fc.constantFrom(...SPINNER_TYPES), (_variant, _size, _type) => {
|
|
283
|
+
// Base selector — check both quoted and unquoted forms (minified CSS omits quotes)
|
|
284
|
+
const baseQuoted = '[class^="ux4g-spinner-"]';
|
|
285
|
+
const baseUnquoted = '[class^=ux4g-spinner-]';
|
|
286
|
+
return cssContent.includes(baseQuoted) || cssContent.includes(baseUnquoted);
|
|
287
|
+
}), { numRuns: 20 });
|
|
288
|
+
});
|
|
289
|
+
it('CSS bundle contains variant attribute selector for all spinner variants', () => {
|
|
290
|
+
fc.assert(fc.property(fc.constantFrom(...SPINNER_VARIANTS), fc.constantFrom(...SPINNER_SIZES), fc.constantFrom(...SPINNER_TYPES), (variant, _size, _type) => {
|
|
291
|
+
// Variant selector — check both quoted and unquoted forms
|
|
292
|
+
const variantQuoted = `[class*="spinner-${variant}"]`;
|
|
293
|
+
const variantUnquoted = `[class*=spinner-${variant}]`;
|
|
294
|
+
return cssContent.includes(variantQuoted) || cssContent.includes(variantUnquoted);
|
|
295
|
+
}), { numRuns: 20 });
|
|
296
|
+
});
|
|
297
|
+
it('CSS bundle contains size class selector for all spinner sizes', () => {
|
|
298
|
+
fc.assert(fc.property(fc.constantFrom(...SPINNER_VARIANTS), fc.constantFrom(...SPINNER_SIZES), fc.constantFrom(...SPINNER_TYPES), (_variant, size, _type) => {
|
|
299
|
+
// Size class selector
|
|
300
|
+
const sizeSelector = `.ux4g-spinner-${size}`;
|
|
301
|
+
return cssContent.includes(sizeSelector);
|
|
302
|
+
}), { numRuns: 20 });
|
|
303
|
+
});
|
|
304
|
+
it('CSS bundle contains type attribute selector for all spinner types', () => {
|
|
305
|
+
fc.assert(fc.property(fc.constantFrom(...SPINNER_VARIANTS), fc.constantFrom(...SPINNER_SIZES), fc.constantFrom(...SPINNER_TYPES), (_variant, _size, type) => {
|
|
306
|
+
// Type selector — check both quoted and unquoted forms
|
|
307
|
+
const typeQuoted = `[class*="-${type}"]`;
|
|
308
|
+
const typeUnquoted = `[class*=-${type}]`;
|
|
309
|
+
return cssContent.includes(typeQuoted) || cssContent.includes(typeUnquoted);
|
|
310
|
+
}), { numRuns: 20 });
|
|
311
|
+
});
|
|
312
|
+
});
|
|
313
|
+
/**
|
|
314
|
+
* Property 9: CSS bundle contains all new component selectors
|
|
315
|
+
* Tag: Feature: ux4g-phase3-components, Property 9
|
|
316
|
+
*/
|
|
317
|
+
describe('Property 9: CSS bundle contains all new Phase 3 component selectors', () => {
|
|
318
|
+
let cssContent;
|
|
319
|
+
beforeAll(() => {
|
|
320
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
321
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
322
|
+
}
|
|
323
|
+
else {
|
|
324
|
+
cssContent = '';
|
|
325
|
+
}
|
|
326
|
+
});
|
|
327
|
+
const phase3Selectors = [
|
|
328
|
+
'.ux4g-text-link-sm',
|
|
329
|
+
'.ux4g-text-link-neutral-md',
|
|
330
|
+
'.ux4g-avatar',
|
|
331
|
+
'.ux4g-img',
|
|
332
|
+
];
|
|
333
|
+
it('Feature: ux4g-phase3-components, Property 9 — CSS bundle contains all Phase 3 selectors', () => {
|
|
334
|
+
fc.assert(fc.property(fc.constantFrom(...phase3Selectors), (selector) => {
|
|
335
|
+
return cssContent.includes(selector);
|
|
336
|
+
}), { numRuns: 20 });
|
|
337
|
+
});
|
|
338
|
+
it('CSS bundle contains badge selectors (dot, icon, digit)', () => {
|
|
339
|
+
const hasDot = cssContent.includes('ux4g-badge-dot-') || cssContent.includes('[class*=ux4g-badge-dot-]');
|
|
340
|
+
const hasIcon = cssContent.includes('ux4g-badge-icon-') || cssContent.includes('[class*=ux4g-badge-icon-]');
|
|
341
|
+
const hasDigit = cssContent.includes('ux4g-badge-digit-') || cssContent.includes('[class*=ux4g-badge-digit-]');
|
|
342
|
+
expect(hasDot).toBe(true);
|
|
343
|
+
expect(hasIcon).toBe(true);
|
|
344
|
+
expect(hasDigit).toBe(true);
|
|
345
|
+
});
|
|
346
|
+
});
|
|
347
|
+
/**
|
|
348
|
+
* Property 9: CSS bundle contains all new Phase 4 component selectors
|
|
349
|
+
* Tag: Feature: ux4g-phase4-components, Property 9
|
|
350
|
+
* Validates: Requirements 2.1, 2.2, 2.3, 2.4
|
|
351
|
+
*/
|
|
352
|
+
describe('Property 9: CSS bundle contains all new Phase 4 component selectors', () => {
|
|
353
|
+
let cssContent;
|
|
354
|
+
beforeAll(() => {
|
|
355
|
+
if (fs.existsSync(CSS_BUNDLE_PATH)) {
|
|
356
|
+
cssContent = fs.readFileSync(CSS_BUNDLE_PATH, 'utf8');
|
|
357
|
+
}
|
|
358
|
+
else {
|
|
359
|
+
cssContent = '';
|
|
360
|
+
}
|
|
361
|
+
});
|
|
362
|
+
const phase4Selectors = [
|
|
363
|
+
// Chip — all 7 base classes
|
|
364
|
+
'.ux4g-filter-chip-md',
|
|
365
|
+
'.ux4g-filter-chip-sm',
|
|
366
|
+
'.ux4g-choice-chip-md',
|
|
367
|
+
'.ux4g-choice-chip-sm',
|
|
368
|
+
'.ux4g-input-chip-md',
|
|
369
|
+
'.ux4g-input-chip-sm',
|
|
370
|
+
'.ux4g-input-chip-xs',
|
|
371
|
+
// Tag — representative compound classes
|
|
372
|
+
'.ux4g-tag-tonal-neutral',
|
|
373
|
+
'.ux4g-tag-filled-primary', // vendor CSS uses 'primary' for brand/primary color in filled variant
|
|
374
|
+
'.ux4g-tag-outline-error',
|
|
375
|
+
'.ux4g-tag-text-info',
|
|
376
|
+
'.ux4g-tag-s',
|
|
377
|
+
// Divider
|
|
378
|
+
'.ux4g-divider-horizontal',
|
|
379
|
+
'.ux4g-divider-vertical',
|
|
380
|
+
// Breadcrumb
|
|
381
|
+
'.ux4g-breadcrumb',
|
|
382
|
+
'.ux4g-breadcrumb-divider',
|
|
383
|
+
'.ux4g-breadcrumb-icon',
|
|
384
|
+
];
|
|
385
|
+
/**
|
|
386
|
+
* **Validates: Requirements 2.1, 2.2, 2.3, 2.4**
|
|
387
|
+
*/
|
|
388
|
+
it('Feature: ux4g-phase4-components, Property 9 — CSS bundle contains all Phase 4 selectors', () => {
|
|
389
|
+
fc.assert(fc.property(fc.constantFrom(...phase4Selectors), (selector) => {
|
|
390
|
+
return cssContent.includes(selector);
|
|
391
|
+
}), { numRuns: 20 });
|
|
392
|
+
});
|
|
393
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-based tests for DOM Generator determinism
|
|
3
|
+
* Property 7: DOM Generator is deterministic
|
|
4
|
+
* Tag: Feature: ux4g-native-framework-experience, Property 7: DOM Generator is deterministic
|
|
5
|
+
*
|
|
6
|
+
* **Validates: Requirements 3.8**
|
|
7
|
+
*/
|
|
8
|
+
import * as fc from 'fast-check';
|
|
9
|
+
import { generateId, resetIdCounter } from '../dom-generators/id-generator';
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resetIdCounter();
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Property 7: DOM Generator is deterministic
|
|
15
|
+
* Tag: Feature: ux4g-native-framework-experience, Property 7: DOM Generator is deterministic
|
|
16
|
+
*
|
|
17
|
+
* **Validates: Requirements 3.8**
|
|
18
|
+
*/
|
|
19
|
+
describe('Property 7: DOM Generator is deterministic', () => {
|
|
20
|
+
it('ID Generator determinism: same prefix sequence after reset produces identical IDs', () => {
|
|
21
|
+
fc.assert(fc.property(fc.array(fc.string({ minLength: 1, maxLength: 20 }), { minLength: 1, maxLength: 50 }), (prefixes) => {
|
|
22
|
+
// First run: generate IDs from the prefix sequence
|
|
23
|
+
resetIdCounter();
|
|
24
|
+
const firstRun = prefixes.map((prefix) => generateId(prefix));
|
|
25
|
+
// Second run: reset and replay the same sequence
|
|
26
|
+
resetIdCounter();
|
|
27
|
+
const secondRun = prefixes.map((prefix) => generateId(prefix));
|
|
28
|
+
// Both runs must produce identical results
|
|
29
|
+
if (firstRun.length !== secondRun.length)
|
|
30
|
+
return false;
|
|
31
|
+
return firstRun.every((id, i) => id === secondRun[i]);
|
|
32
|
+
}), { numRuns: 100 });
|
|
33
|
+
});
|
|
34
|
+
it('Structural determinism: same factory function called twice after reset produces deeply equal NodeDescriptor trees', () => {
|
|
35
|
+
fc.assert(fc.property(fc.string({ minLength: 1, maxLength: 20 }), fc.array(fc.string({ minLength: 1, maxLength: 15 }), { minLength: 0, maxLength: 5 }), fc.constantFrom('div', 'span', 'button', 'ul', 'li', 'section', 'nav'), (componentPrefix, childLabels, tag) => {
|
|
36
|
+
// Factory function that builds a NodeDescriptor tree using generateId()
|
|
37
|
+
const buildTree = () => {
|
|
38
|
+
const rootId = generateId(componentPrefix);
|
|
39
|
+
const children = childLabels.map((label) => {
|
|
40
|
+
const childId = generateId(`${componentPrefix}-item`);
|
|
41
|
+
return {
|
|
42
|
+
tag: 'li',
|
|
43
|
+
props: {
|
|
44
|
+
id: childId,
|
|
45
|
+
className: `ux4g-${componentPrefix}-item`,
|
|
46
|
+
'aria-labelledby': rootId,
|
|
47
|
+
},
|
|
48
|
+
children: [label],
|
|
49
|
+
};
|
|
50
|
+
});
|
|
51
|
+
return {
|
|
52
|
+
tag,
|
|
53
|
+
props: {
|
|
54
|
+
id: rootId,
|
|
55
|
+
className: `ux4g-${componentPrefix}`,
|
|
56
|
+
role: 'list',
|
|
57
|
+
},
|
|
58
|
+
children,
|
|
59
|
+
};
|
|
60
|
+
};
|
|
61
|
+
// First call
|
|
62
|
+
resetIdCounter();
|
|
63
|
+
const tree1 = buildTree();
|
|
64
|
+
// Second call with same ID counter state
|
|
65
|
+
resetIdCounter();
|
|
66
|
+
const tree2 = buildTree();
|
|
67
|
+
// Deep equality check
|
|
68
|
+
return JSON.stringify(tree1) === JSON.stringify(tree2);
|
|
69
|
+
}), { numRuns: 100 });
|
|
70
|
+
});
|
|
71
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Property-based tests for ID Generator uniqueness and format
|
|
3
|
+
* Property 5: Generated IDs are unique and correctly formatted
|
|
4
|
+
* Tag: Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted
|
|
5
|
+
*
|
|
6
|
+
* **Validates: Requirements 3.6**
|
|
7
|
+
*/
|
|
8
|
+
import * as fc from 'fast-check';
|
|
9
|
+
import { generateId, resetIdCounter } from '../dom-generators/id-generator';
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
resetIdCounter();
|
|
12
|
+
});
|
|
13
|
+
/**
|
|
14
|
+
* Property 5.1: Uniqueness — For any sequence of generateId() calls with arbitrary prefixes,
|
|
15
|
+
* all returned IDs are unique (no duplicates in a Set).
|
|
16
|
+
*/
|
|
17
|
+
describe('Property 5.1: All generated IDs are unique', () => {
|
|
18
|
+
it('Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted', () => {
|
|
19
|
+
fc.assert(fc.property(fc.array(fc.stringMatching(/^[a-z][a-z0-9-]*$/), { minLength: 1, maxLength: 50 }), (prefixes) => {
|
|
20
|
+
resetIdCounter();
|
|
21
|
+
const ids = prefixes.map((prefix) => generateId(prefix));
|
|
22
|
+
const uniqueIds = new Set(ids);
|
|
23
|
+
return uniqueIds.size === ids.length;
|
|
24
|
+
}), { numRuns: 100 });
|
|
25
|
+
});
|
|
26
|
+
});
|
|
27
|
+
/**
|
|
28
|
+
* Property 5.2: Format — Every generated ID matches the regex pattern /^.+-\d+$/
|
|
29
|
+
* where the prefix can be anything like `ux4g-dropdown`, `ux4g-tab-panel`, etc.
|
|
30
|
+
*/
|
|
31
|
+
describe('Property 5.2: Every generated ID matches the expected format', () => {
|
|
32
|
+
it('Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted', () => {
|
|
33
|
+
const formatRegex = /^.+-\d+$/;
|
|
34
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9-]*$/), (prefix) => {
|
|
35
|
+
resetIdCounter();
|
|
36
|
+
const id = generateId(prefix);
|
|
37
|
+
return formatRegex.test(id);
|
|
38
|
+
}), { numRuns: 100 });
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
/**
|
|
42
|
+
* Property 5.3: Monotonic increment — The numeric suffix of sequential IDs from the same prefix
|
|
43
|
+
* is monotonically increasing.
|
|
44
|
+
*/
|
|
45
|
+
describe('Property 5.3: Numeric suffixes are monotonically increasing', () => {
|
|
46
|
+
it('Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted', () => {
|
|
47
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9-]*$/), fc.integer({ min: 2, max: 50 }), (prefix, count) => {
|
|
48
|
+
resetIdCounter();
|
|
49
|
+
const ids = Array.from({ length: count }, () => generateId(prefix));
|
|
50
|
+
const suffixes = ids.map((id) => {
|
|
51
|
+
const lastDash = id.lastIndexOf('-');
|
|
52
|
+
return parseInt(id.slice(lastDash + 1), 10);
|
|
53
|
+
});
|
|
54
|
+
for (let i = 1; i < suffixes.length; i++) {
|
|
55
|
+
if (suffixes[i] <= suffixes[i - 1])
|
|
56
|
+
return false;
|
|
57
|
+
}
|
|
58
|
+
return true;
|
|
59
|
+
}), { numRuns: 100 });
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
/**
|
|
63
|
+
* Property 5.4: Reset behavior — After resetIdCounter(), the counter starts from 1 again.
|
|
64
|
+
*/
|
|
65
|
+
describe('Property 5.4: Reset counter restarts from 1', () => {
|
|
66
|
+
it('Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted', () => {
|
|
67
|
+
fc.assert(fc.property(fc.stringMatching(/^[a-z][a-z0-9-]*$/), fc.integer({ min: 1, max: 20 }), (prefix, callsBefore) => {
|
|
68
|
+
resetIdCounter();
|
|
69
|
+
// Generate some IDs first
|
|
70
|
+
for (let i = 0; i < callsBefore; i++) {
|
|
71
|
+
generateId(prefix);
|
|
72
|
+
}
|
|
73
|
+
// Reset and verify next ID ends with -1
|
|
74
|
+
resetIdCounter();
|
|
75
|
+
const idAfterReset = generateId(prefix);
|
|
76
|
+
const lastDash = idAfterReset.lastIndexOf('-');
|
|
77
|
+
const suffix = parseInt(idAfterReset.slice(lastDash + 1), 10);
|
|
78
|
+
return suffix === 1;
|
|
79
|
+
}), { numRuns: 100 });
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
/**
|
|
83
|
+
* Property 5.5: Cross-prefix uniqueness — IDs generated with different prefixes still have
|
|
84
|
+
* unique numeric suffixes (since they share the same counter).
|
|
85
|
+
*/
|
|
86
|
+
describe('Property 5.5: Cross-prefix IDs have unique numeric suffixes', () => {
|
|
87
|
+
it('Feature: ux4g-native-framework-experience, Property 5: Generated IDs are unique and correctly formatted', () => {
|
|
88
|
+
fc.assert(fc.property(fc.array(fc.stringMatching(/^[a-z][a-z0-9-]*$/), { minLength: 2, maxLength: 30 }), (prefixes) => {
|
|
89
|
+
resetIdCounter();
|
|
90
|
+
const ids = prefixes.map((prefix) => generateId(prefix));
|
|
91
|
+
const suffixes = ids.map((id) => {
|
|
92
|
+
const lastDash = id.lastIndexOf('-');
|
|
93
|
+
return parseInt(id.slice(lastDash + 1), 10);
|
|
94
|
+
});
|
|
95
|
+
const uniqueSuffixes = new Set(suffixes);
|
|
96
|
+
return uniqueSuffixes.size === suffixes.length;
|
|
97
|
+
}), { numRuns: 100 });
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|