@adia-ai/web-components 0.8.19 → 0.8.21
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/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- package/traits/view-transition/view-transition.test.js +0 -268
|
@@ -1,272 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* spinner-ui tests — covers SPEC-001 § Verification.
|
|
3
|
-
*
|
|
4
|
-
* The component is a CSS-only animation (no stamped DOM, no JS timers).
|
|
5
|
-
* Tests fall in two categories:
|
|
6
|
-
*
|
|
7
|
-
* 1. Behavioral — props reflect to host attributes, defaults apply,
|
|
8
|
-
* ARIA wiring is in place, element is not a focus target.
|
|
9
|
-
* 2. CSS source — happy-dom doesn't resolve @scope rules through
|
|
10
|
-
* getComputedStyle(), so the size-ladder, tone-mapping, and
|
|
11
|
-
* reduced-motion fallback are validated against the CSS source
|
|
12
|
-
* directly (same recipe as tag-ui / description-list / text).
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
16
|
-
import { readFileSync } from 'node:fs';
|
|
17
|
-
import { fileURLToPath } from 'node:url';
|
|
18
|
-
import { dirname, resolve } from 'node:path';
|
|
19
|
-
|
|
20
|
-
const HERE = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const SPINNER_CSS = readFileSync(resolve(HERE, 'spinner.css'), 'utf8');
|
|
22
|
-
|
|
23
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
24
|
-
|
|
25
|
-
beforeAll(async () => {
|
|
26
|
-
await import('./spinner.js');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
function mount(html) {
|
|
30
|
-
const wrap = document.createElement('div');
|
|
31
|
-
wrap.innerHTML = html;
|
|
32
|
-
document.body.appendChild(wrap);
|
|
33
|
-
return wrap.firstElementChild;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
// ── 1. Defaults + property reflection ──────────────────────────────────
|
|
37
|
-
|
|
38
|
-
describe('spinner-ui — defaults', () => {
|
|
39
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
40
|
-
|
|
41
|
-
it('renders without props using documented defaults', async () => {
|
|
42
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
43
|
-
await tick();
|
|
44
|
-
expect(s.size).toBe('md');
|
|
45
|
-
expect(s.variant).toBe('arc');
|
|
46
|
-
expect(s.tone).toBe('current');
|
|
47
|
-
expect(s.paused).toBe(false);
|
|
48
|
-
expect(s.label).toBe('Loading');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('sets role="progressbar" on the host', async () => {
|
|
52
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
53
|
-
await tick();
|
|
54
|
-
expect(s.getAttribute('role')).toBe('progressbar');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('sets aria-busy="true" on the host', async () => {
|
|
58
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
59
|
-
await tick();
|
|
60
|
-
expect(s.getAttribute('aria-busy')).toBe('true');
|
|
61
|
-
});
|
|
62
|
-
|
|
63
|
-
it('sets aria-valuetext to the default label', async () => {
|
|
64
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
65
|
-
await tick();
|
|
66
|
-
expect(s.getAttribute('aria-valuetext')).toBe('Loading');
|
|
67
|
-
});
|
|
68
|
-
});
|
|
69
|
-
|
|
70
|
-
// ── 2. Reflected props ─────────────────────────────────────────────────
|
|
71
|
-
|
|
72
|
-
describe('spinner-ui — reflected attributes', () => {
|
|
73
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
74
|
-
|
|
75
|
-
it('reflects size="sm" / "lg" to the host attribute', async () => {
|
|
76
|
-
const sm = mount('<spinner-ui size="sm"></spinner-ui>');
|
|
77
|
-
await tick();
|
|
78
|
-
expect(sm.getAttribute('size')).toBe('sm');
|
|
79
|
-
|
|
80
|
-
const lg = mount('<spinner-ui size="lg"></spinner-ui>');
|
|
81
|
-
await tick();
|
|
82
|
-
expect(lg.getAttribute('size')).toBe('lg');
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
it('reflects variant="ring" / "dots" to the host attribute', async () => {
|
|
86
|
-
const ring = mount('<spinner-ui variant="ring"></spinner-ui>');
|
|
87
|
-
await tick();
|
|
88
|
-
expect(ring.getAttribute('variant')).toBe('ring');
|
|
89
|
-
|
|
90
|
-
const dots = mount('<spinner-ui variant="dots"></spinner-ui>');
|
|
91
|
-
await tick();
|
|
92
|
-
expect(dots.getAttribute('variant')).toBe('dots');
|
|
93
|
-
});
|
|
94
|
-
|
|
95
|
-
it('reflects tone="subtle" / "accent" / "inverse" to the host attribute', async () => {
|
|
96
|
-
const subtle = mount('<spinner-ui tone="subtle"></spinner-ui>');
|
|
97
|
-
const accent = mount('<spinner-ui tone="accent"></spinner-ui>');
|
|
98
|
-
const inverse = mount('<spinner-ui tone="inverse"></spinner-ui>');
|
|
99
|
-
await tick();
|
|
100
|
-
expect(subtle.getAttribute('tone')).toBe('subtle');
|
|
101
|
-
expect(accent.getAttribute('tone')).toBe('accent');
|
|
102
|
-
expect(inverse.getAttribute('tone')).toBe('inverse');
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('reflects [paused] as a boolean attribute', async () => {
|
|
106
|
-
const s = mount('<spinner-ui paused></spinner-ui>');
|
|
107
|
-
await tick();
|
|
108
|
-
expect(s.hasAttribute('paused')).toBe(true);
|
|
109
|
-
expect(s.paused).toBe(true);
|
|
110
|
-
});
|
|
111
|
-
});
|
|
112
|
-
|
|
113
|
-
// ── 3. Accessibility wiring ────────────────────────────────────────────
|
|
114
|
-
|
|
115
|
-
describe('spinner-ui — accessibility', () => {
|
|
116
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
117
|
-
|
|
118
|
-
it('mirrors a custom [label] to aria-valuetext', async () => {
|
|
119
|
-
const s = mount('<spinner-ui label="Saving"></spinner-ui>');
|
|
120
|
-
await tick();
|
|
121
|
-
expect(s.getAttribute('aria-valuetext')).toBe('Saving');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
it('updates aria-valuetext when label is set programmatically', async () => {
|
|
125
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
126
|
-
await tick();
|
|
127
|
-
s.label = 'Uploading';
|
|
128
|
-
await tick();
|
|
129
|
-
expect(s.getAttribute('aria-valuetext')).toBe('Uploading');
|
|
130
|
-
});
|
|
131
|
-
|
|
132
|
-
it('is NOT a focus target (no tabindex set, body tabIndex chain skips it)', async () => {
|
|
133
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
134
|
-
await tick();
|
|
135
|
-
expect(s.hasAttribute('tabindex')).toBe(false);
|
|
136
|
-
// tabIndex defaults to -1 for non-focusable elements in happy-dom
|
|
137
|
-
expect(s.tabIndex).toBeLessThan(0);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('does not stamp any internal DOM (static template = null)', async () => {
|
|
141
|
-
const s = mount('<spinner-ui></spinner-ui>');
|
|
142
|
-
await tick();
|
|
143
|
-
// The visual is entirely pseudo-element-driven; the host has no
|
|
144
|
-
// element children of its own.
|
|
145
|
-
expect(s.children.length).toBe(0);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('preserves user-supplied role / aria-busy when set in markup', async () => {
|
|
149
|
-
const s = mount('<spinner-ui role="status" aria-busy="false"></spinner-ui>');
|
|
150
|
-
await tick();
|
|
151
|
-
// connected() only sets the default ARIA when absent
|
|
152
|
-
expect(s.getAttribute('role')).toBe('status');
|
|
153
|
-
expect(s.getAttribute('aria-busy')).toBe('false');
|
|
154
|
-
});
|
|
155
|
-
});
|
|
156
|
-
|
|
157
|
-
// ── 4. CSS source contract ─────────────────────────────────────────────
|
|
158
|
-
// happy-dom doesn't resolve @scope rules through getComputedStyle, so we
|
|
159
|
-
// validate the CSS source shape directly. Same recipe as tag-ui.
|
|
160
|
-
|
|
161
|
-
describe('spinner-ui — CSS source contract', () => {
|
|
162
|
-
it('opens with the canonical @scope (spinner-ui) block', () => {
|
|
163
|
-
expect(SPINNER_CSS).toMatch(/^@scope \(spinner-ui\)/);
|
|
164
|
-
});
|
|
165
|
-
|
|
166
|
-
it('declares both the :where(:scope) token block and a :scope base block', () => {
|
|
167
|
-
expect(SPINNER_CSS).toMatch(/:where\(:scope\)\s*\{/);
|
|
168
|
-
expect(SPINNER_CSS).toMatch(/^\s*:scope\s*\{/m);
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
it('declares all 7 component tokens', () => {
|
|
172
|
-
for (const tok of [
|
|
173
|
-
'--spinner-size',
|
|
174
|
-
'--spinner-color',
|
|
175
|
-
'--spinner-stroke',
|
|
176
|
-
'--spinner-duration',
|
|
177
|
-
'--spinner-track-opacity',
|
|
178
|
-
'--spinner-dot-size',
|
|
179
|
-
'--spinner-dot-gap',
|
|
180
|
-
]) {
|
|
181
|
-
expect(SPINNER_CSS).toContain(tok);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it('size="sm" / "md" / "lg" all set --spinner-size', () => {
|
|
186
|
-
// Size variants re-point the active --spinner-size token; consumers
|
|
187
|
-
// override the same un-suffixed token on the element (or @layer overrides).
|
|
188
|
-
expect(SPINNER_CSS).toMatch(/:scope\[size="sm"\][^}]*--spinner-size:\s*0\.875rem/);
|
|
189
|
-
expect(SPINNER_CSS).toMatch(/:scope\[size="md"\][^}]*--spinner-size:\s*1rem/);
|
|
190
|
-
expect(SPINNER_CSS).toMatch(/:scope\[size="lg"\][^}]*--spinner-size:\s*1\.25rem/);
|
|
191
|
-
});
|
|
192
|
-
|
|
193
|
-
it('tone="subtle" / "accent" / "inverse" set --spinner-color via semantic tokens', () => {
|
|
194
|
-
// subtle reads the Material on-surface-variant role since the
|
|
195
|
-
// static-token migration (material-color-bridge-contract.md).
|
|
196
|
-
expect(SPINNER_CSS).toMatch(/:scope\[tone="subtle"\][^}]*--spinner-color:\s*var\(--md-sys-color-neutral-on-surface-variant\)/);
|
|
197
|
-
expect(SPINNER_CSS).toMatch(/:scope\[tone="accent"\][^}]*--spinner-color:\s*var\(--a-primary-strong\)/);
|
|
198
|
-
expect(SPINNER_CSS).toMatch(/:scope\[tone="inverse"\][^}]*--spinner-color:\s*var\(--a-chrome-light\)/);
|
|
199
|
-
});
|
|
200
|
-
|
|
201
|
-
it('arc variant uses a rotating quarter-circle border on ::before', () => {
|
|
202
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="arc"\]::before/);
|
|
203
|
-
// The arc colors three borders transparent + the top currentColor
|
|
204
|
-
expect(SPINNER_CSS).toMatch(/border-color:\s*currentColor\s+transparent\s+transparent\s+transparent/);
|
|
205
|
-
expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-rotate/);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('ring variant uses a full ring with one rotating colored segment', () => {
|
|
209
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="ring"\]::before/);
|
|
210
|
-
expect(SPINNER_CSS).toMatch(/border-top-color:\s*currentColor/);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('dots variant uses 3 stamped [data-spinner-dot] children with bounce keyframes', () => {
|
|
214
|
-
// Pre-fix: ::before painted dot1 + a box-shadow middle dot, ::after
|
|
215
|
-
// painted dot3. Spacing was uneven (box-shadow doesn't participate
|
|
216
|
-
// in flex-gap math) and the middle dot couldn't animate. Post-fix
|
|
217
|
-
// (spinner.class.js #syncDots): three real <span data-spinner-dot=N>
|
|
218
|
-
// children stamped on connect; flex-gap distributes the row evenly
|
|
219
|
-
// and each child carries an independent animation-delay so the
|
|
220
|
-
// bounce reads as a left-to-right wave.
|
|
221
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot\]/);
|
|
222
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="1"\]/);
|
|
223
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="2"\]/);
|
|
224
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="dots"\]\s*>\s*\[data-spinner-dot="3"\]/);
|
|
225
|
-
expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-bounce/);
|
|
226
|
-
});
|
|
227
|
-
|
|
228
|
-
it('knight variant uses a sliding ::before thumb with knight keyframes', () => {
|
|
229
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="knight"\]\s*\{/);
|
|
230
|
-
expect(SPINNER_CSS).toMatch(/:scope\[variant="knight"\]::before/);
|
|
231
|
-
expect(SPINNER_CSS).toMatch(/animation:\s*spinner-ui-knight/);
|
|
232
|
-
// Keyframe math: translateX((1/ratio - 1) * 100%) sweeps the thumb
|
|
233
|
-
// from x=0 to x=(track - thumb) px, expressed in thumb-relative %.
|
|
234
|
-
expect(SPINNER_CSS).toMatch(/@keyframes\s+spinner-ui-knight/);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
it('knight stamps NO data-spinner-dot children (pseudo-element paint only)', async () => {
|
|
238
|
-
const k = mount('<spinner-ui variant="knight"></spinner-ui>');
|
|
239
|
-
await tick();
|
|
240
|
-
expect(k.querySelectorAll(':scope > [data-spinner-dot]').length).toBe(0);
|
|
241
|
-
});
|
|
242
|
-
|
|
243
|
-
it('dots stamps exactly 3 data-spinner-dot children on connect', async () => {
|
|
244
|
-
const d = mount('<spinner-ui variant="dots"></spinner-ui>');
|
|
245
|
-
await tick();
|
|
246
|
-
const dots = d.querySelectorAll(':scope > [data-spinner-dot]');
|
|
247
|
-
expect(dots.length).toBe(3);
|
|
248
|
-
expect(dots[0].getAttribute('data-spinner-dot')).toBe('1');
|
|
249
|
-
expect(dots[1].getAttribute('data-spinner-dot')).toBe('2');
|
|
250
|
-
expect(dots[2].getAttribute('data-spinner-dot')).toBe('3');
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('paused state freezes the animation via animation-play-state', () => {
|
|
254
|
-
expect(SPINNER_CSS).toMatch(/:scope\[paused\][^}]*animation-play-state:\s*paused/);
|
|
255
|
-
});
|
|
256
|
-
|
|
257
|
-
it('reduced-motion media query replaces the animation with a static ellipsis', () => {
|
|
258
|
-
expect(SPINNER_CSS).toMatch(/@media\s*\(prefers-reduced-motion:\s*reduce\)/);
|
|
259
|
-
expect(SPINNER_CSS).toMatch(/content:\s*"…"/);
|
|
260
|
-
});
|
|
261
|
-
|
|
262
|
-
it('uses semantic --a-* tokens for color tones; no raw hex / rgb / oklch in the file', () => {
|
|
263
|
-
// Lints adjacent to the token contract: no raw colors in component CSS.
|
|
264
|
-
expect(SPINNER_CSS).not.toMatch(/#[0-9a-fA-F]{3,8}\b/);
|
|
265
|
-
expect(SPINNER_CSS).not.toMatch(/\brgb\s*\(/);
|
|
266
|
-
expect(SPINNER_CSS).not.toMatch(/\boklch\s*\(/);
|
|
267
|
-
});
|
|
268
|
-
|
|
269
|
-
it('declares a linear rotation timing function (load-bearing for smooth spin)', () => {
|
|
270
|
-
expect(SPINNER_CSS).toMatch(/spinner-ui-rotate[^;]*linear/);
|
|
271
|
-
});
|
|
272
|
-
});
|
|
@@ -1,108 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* stat-ui — focused unit tests for the v0.6.18 `loading` boolean prop
|
|
3
|
-
* (FB-12 P2 resolution).
|
|
4
|
-
*
|
|
5
|
-
* Pre-v0.6.18: stat-ui rendered empty/zero values during data fetch with no
|
|
6
|
-
* visual indication. Consumers were forced to hand-roll skeleton-card
|
|
7
|
-
* workarounds with their own `@keyframes` CSS — duplicating skeleton-ui.
|
|
8
|
-
*
|
|
9
|
-
* v0.6.18 adds `loading: Boolean`. When set:
|
|
10
|
-
* - value + change slots render <skeleton-ui> shimmer placeholders
|
|
11
|
-
* - aria-busy="true" on the host
|
|
12
|
-
* - label + icon (static metadata) are preserved
|
|
13
|
-
* - toggling off restores text content + clears aria-busy
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
17
|
-
import '../../core/element.js';
|
|
18
|
-
import './stat.js';
|
|
19
|
-
// skeleton-ui is referenced by stat-ui at runtime when [loading]; load it so
|
|
20
|
-
// the element gets defined (otherwise the inner <skeleton-ui> stays an
|
|
21
|
-
// HTMLUnknownElement, which is still observable in the DOM but the assertions
|
|
22
|
-
// below want to confirm registration via tagName.)
|
|
23
|
-
import '../skeleton/skeleton.js';
|
|
24
|
-
|
|
25
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
26
|
-
|
|
27
|
-
function mount(html) {
|
|
28
|
-
const wrap = document.createElement('div');
|
|
29
|
-
wrap.innerHTML = html;
|
|
30
|
-
document.body.appendChild(wrap);
|
|
31
|
-
return wrap.firstElementChild;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
describe('stat-ui — v0.6.18 loading prop (FB-12 P2)', () => {
|
|
35
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
36
|
-
|
|
37
|
-
it('defaults loading to false; no aria-busy on host', async () => {
|
|
38
|
-
const el = mount('<stat-ui label="Total" value="1,234"></stat-ui>');
|
|
39
|
-
await tick();
|
|
40
|
-
expect(el.loading).toBe(false);
|
|
41
|
-
expect(el.hasAttribute('loading')).toBe(false);
|
|
42
|
-
expect(el.getAttribute('aria-busy')).toBeNull();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('reflects [loading] attribute to the property', () => {
|
|
46
|
-
const el = mount('<stat-ui label="Total" loading></stat-ui>');
|
|
47
|
-
expect(el.loading).toBe(true);
|
|
48
|
-
expect(el.hasAttribute('loading')).toBe(true);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('sets aria-busy="true" on host when [loading]', async () => {
|
|
52
|
-
const el = mount('<stat-ui label="Total" loading></stat-ui>');
|
|
53
|
-
await tick();
|
|
54
|
-
expect(el.getAttribute('aria-busy')).toBe('true');
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('renders a <skeleton-ui> inside the value slot when [loading]', async () => {
|
|
58
|
-
const el = mount('<stat-ui label="Total" loading></stat-ui>');
|
|
59
|
-
await tick();
|
|
60
|
-
const valueSlot = el.querySelector(':scope > [slot="value"]');
|
|
61
|
-
expect(valueSlot).not.toBeNull();
|
|
62
|
-
const sk = valueSlot.querySelector('skeleton-ui');
|
|
63
|
-
expect(sk).not.toBeNull();
|
|
64
|
-
expect(sk.tagName.toLowerCase()).toBe('skeleton-ui');
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('renders a <skeleton-ui> inside the change slot when [loading]', async () => {
|
|
68
|
-
const el = mount('<stat-ui label="Total" loading></stat-ui>');
|
|
69
|
-
await tick();
|
|
70
|
-
const changeSlot = el.querySelector(':scope > [slot="change"]');
|
|
71
|
-
expect(changeSlot).not.toBeNull();
|
|
72
|
-
expect(changeSlot.hidden).toBe(false);
|
|
73
|
-
const sk = changeSlot.querySelector('skeleton-ui');
|
|
74
|
-
expect(sk).not.toBeNull();
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
it('preserves label text content when [loading] (label is static metadata)', async () => {
|
|
78
|
-
const el = mount('<stat-ui label="Total Users" loading></stat-ui>');
|
|
79
|
-
await tick();
|
|
80
|
-
const labelSlot = el.querySelector(':scope > [slot="label"]');
|
|
81
|
-
expect(labelSlot.textContent).toBe('Total Users');
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('restores value + change text + clears aria-busy when loading toggles off', async () => {
|
|
85
|
-
const el = mount('<stat-ui label="Total" loading></stat-ui>');
|
|
86
|
-
await tick();
|
|
87
|
-
// Now flip off + add real values
|
|
88
|
-
el.removeAttribute('loading');
|
|
89
|
-
el.setAttribute('value', '1,234');
|
|
90
|
-
el.setAttribute('change', '+12%');
|
|
91
|
-
await tick();
|
|
92
|
-
expect(el.getAttribute('aria-busy')).toBeNull();
|
|
93
|
-
expect(el.querySelector(':scope > [slot="value"]').textContent).toBe('1,234');
|
|
94
|
-
expect(el.querySelector(':scope > [slot="change"]').textContent).toBe('+12%');
|
|
95
|
-
// skeleton-ui children should be gone (textContent setter clobbers innerHTML)
|
|
96
|
-
expect(el.querySelector(':scope > [slot="value"] skeleton-ui')).toBeNull();
|
|
97
|
-
expect(el.querySelector(':scope > [slot="change"] skeleton-ui')).toBeNull();
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('keeps icon visible when [loading] if icon prop is set', async () => {
|
|
101
|
-
const el = mount('<stat-ui label="Total" icon="users" loading></stat-ui>');
|
|
102
|
-
await tick();
|
|
103
|
-
const iconSlot = el.querySelector(':scope > [slot="icon"]');
|
|
104
|
-
expect(iconSlot).not.toBeNull();
|
|
105
|
-
expect(iconSlot.hidden).toBe(false);
|
|
106
|
-
expect(iconSlot.getAttribute('name')).toBe('users');
|
|
107
|
-
});
|
|
108
|
-
});
|
|
@@ -1,129 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../core/element.js';
|
|
3
|
-
import './step-progress.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
describe('step-progress-ui', () => {
|
|
15
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
16
|
-
|
|
17
|
-
it('registers as a custom element', () => {
|
|
18
|
-
expect(customElements.get('step-progress-ui')).toBeDefined();
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('sets role=progressbar + aria-value{min,max,now}', () => {
|
|
22
|
-
const el = mount('<step-progress-ui value="3" total="10"></step-progress-ui>');
|
|
23
|
-
expect(el.getAttribute('role')).toBe('progressbar');
|
|
24
|
-
expect(el.getAttribute('aria-valuemin')).toBe('0');
|
|
25
|
-
expect(el.getAttribute('aria-valuemax')).toBe('10');
|
|
26
|
-
expect(el.getAttribute('aria-valuenow')).toBe('3');
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('auto-mints a caption span + track div', () => {
|
|
30
|
-
const el = mount('<step-progress-ui value="2" total="5" caption="Step 2 of 5"></step-progress-ui>');
|
|
31
|
-
expect(el.querySelector(':scope > [slot="caption"]')).not.toBeNull();
|
|
32
|
-
expect(el.querySelector(':scope > [slot="track"]')).not.toBeNull();
|
|
33
|
-
});
|
|
34
|
-
|
|
35
|
-
it('renders one <span> per step in the track', () => {
|
|
36
|
-
const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
|
|
37
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
38
|
-
expect(dashes.length).toBe(5);
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
it('marks the first `value` dashes as [data-active]', () => {
|
|
42
|
-
const el = mount('<step-progress-ui value="3" total="5"></step-progress-ui>');
|
|
43
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
44
|
-
expect(dashes[0].hasAttribute('data-active')).toBe(true);
|
|
45
|
-
expect(dashes[1].hasAttribute('data-active')).toBe(true);
|
|
46
|
-
expect(dashes[2].hasAttribute('data-active')).toBe(true);
|
|
47
|
-
expect(dashes[3].hasAttribute('data-active')).toBe(false);
|
|
48
|
-
expect(dashes[4].hasAttribute('data-active')).toBe(false);
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('renders the caption text in the caption slot', () => {
|
|
52
|
-
const el = mount('<step-progress-ui value="3" total="10" caption="Step 3 of 10"></step-progress-ui>');
|
|
53
|
-
expect(el.querySelector('[slot="caption"]').textContent).toBe('Step 3 of 10');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('hides the caption span when caption is empty', () => {
|
|
57
|
-
const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
|
|
58
|
-
expect(el.querySelector('[slot="caption"]').hasAttribute('hidden')).toBe(true);
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('updates the dash + aria when value changes', async () => {
|
|
62
|
-
const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
|
|
63
|
-
el.value = 4;
|
|
64
|
-
await tick();
|
|
65
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
66
|
-
expect(el.getAttribute('aria-valuenow')).toBe('4');
|
|
67
|
-
expect(dashes[3].hasAttribute('data-active')).toBe(true);
|
|
68
|
-
expect(dashes[4].hasAttribute('data-active')).toBe(false);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('grows the dash count when total increases', async () => {
|
|
72
|
-
const el = mount('<step-progress-ui value="2" total="5"></step-progress-ui>');
|
|
73
|
-
expect(el.querySelectorAll('[slot="track"] > span').length).toBe(5);
|
|
74
|
-
el.total = 8;
|
|
75
|
-
await tick();
|
|
76
|
-
expect(el.querySelectorAll('[slot="track"] > span').length).toBe(8);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it('shrinks the dash count when total decreases', async () => {
|
|
80
|
-
const el = mount('<step-progress-ui value="2" total="8"></step-progress-ui>');
|
|
81
|
-
expect(el.querySelectorAll('[slot="track"] > span').length).toBe(8);
|
|
82
|
-
el.total = 4;
|
|
83
|
-
await tick();
|
|
84
|
-
expect(el.querySelectorAll('[slot="track"] > span').length).toBe(4);
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('clamps value to [0, total]', () => {
|
|
88
|
-
const el = mount('<step-progress-ui value="20" total="5"></step-progress-ui>');
|
|
89
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
90
|
-
expect(el.getAttribute('aria-valuenow')).toBe('5');
|
|
91
|
-
for (const dash of dashes) {
|
|
92
|
-
expect(dash.hasAttribute('data-active')).toBe(true);
|
|
93
|
-
}
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('handles value=0 (empty state) — no dashes active', () => {
|
|
97
|
-
const el = mount('<step-progress-ui value="0" total="5"></step-progress-ui>');
|
|
98
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
99
|
-
expect(el.getAttribute('aria-valuenow')).toBe('0');
|
|
100
|
-
for (const dash of dashes) {
|
|
101
|
-
expect(dash.hasAttribute('data-active')).toBe(false);
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('handles value=total (complete state) — all dashes active', () => {
|
|
106
|
-
const el = mount('<step-progress-ui value="5" total="5" caption="All steps complete"></step-progress-ui>');
|
|
107
|
-
const dashes = el.querySelectorAll(':scope > [slot="track"] > span');
|
|
108
|
-
for (const dash of dashes) {
|
|
109
|
-
expect(dash.hasAttribute('data-active')).toBe(true);
|
|
110
|
-
}
|
|
111
|
-
expect(el.querySelector('[slot="caption"]').textContent).toBe('All steps complete');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('survives disconnect without throwing', () => {
|
|
115
|
-
const el = mount('<step-progress-ui value="3" total="10"></step-progress-ui>');
|
|
116
|
-
expect(() => el.remove()).not.toThrow();
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// Regression (audit-wrapper-trap arm 2): WRAPPED consumer [slot="caption"] +
|
|
120
|
-
// [slot="track"] (e.g. via `.map()`/conditional interpolation) must be seen by
|
|
121
|
-
// the absence check so it doesn't MINT duplicates over the consumer's slots.
|
|
122
|
-
it('does NOT mint duplicates when WRAPPED consumer caption + track are present', async () => {
|
|
123
|
-
const el = mount(`<step-progress-ui value="2" total="5"><span style="display:contents" role="presentation"><span slot="caption" data-test="c">Custom</span><div slot="track" data-test="t"></div></span></step-progress-ui>`);
|
|
124
|
-
await tick();
|
|
125
|
-
expect(el.querySelectorAll('[slot="caption"]').length).toBe(1);
|
|
126
|
-
expect(el.querySelectorAll('[slot="track"]').length).toBe(1);
|
|
127
|
-
expect(el.querySelector('[slot="caption"]').dataset.test).toBe('c');
|
|
128
|
-
});
|
|
129
|
-
});
|