@adia-ai/web-components 0.8.18 → 0.8.20

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (126) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/components/nav-group/nav-group.a2ui.json +5 -0
  3. package/components/nav-group/nav-group.class.js +14 -6
  4. package/components/nav-group/nav-group.css +5 -2
  5. package/components/nav-group/nav-group.d.ts +2 -0
  6. package/components/nav-group/nav-group.yaml +4 -0
  7. package/components/select/select.a2ui.json +6 -1
  8. package/components/select/select.class.js +34 -7
  9. package/components/select/select.d.ts +2 -0
  10. package/components/select/select.yaml +16 -1
  11. package/dist/host.min.css +1 -1
  12. package/dist/host.sheet.js +1 -1
  13. package/dist/theme-provider.min.js +1 -1
  14. package/dist/web-components.min.css +1 -1
  15. package/dist/web-components.min.js +18 -18
  16. package/dist/web-components.sheet.js +1 -1
  17. package/package.json +2 -1
  18. package/styles/colors/semantics/core.css +10 -4
  19. package/components/adia-mark/adia-mark.test.js +0 -69
  20. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  21. package/components/agent-artifact/agent-artifact.test.js +0 -61
  22. package/components/alert/alert.test.js +0 -364
  23. package/components/color-picker/color-picker.test.js +0 -96
  24. package/components/combobox/combobox.test.js +0 -256
  25. package/components/command/command.test.js +0 -99
  26. package/components/date-range-picker/date-range-picker.test.js +0 -387
  27. package/components/datetime-picker/datetime-picker.test.js +0 -454
  28. package/components/demo-toggle/demo-toggle.test.js +0 -102
  29. package/components/description-list/description-list.test.js +0 -72
  30. package/components/display-field/display-field.test.js +0 -115
  31. package/components/empty-state/empty-state.test.js +0 -77
  32. package/components/field/field.test.js +0 -260
  33. package/components/fields/fields.test.js +0 -88
  34. package/components/inline-message/inline-message.test.js +0 -203
  35. package/components/input/input.test.js +0 -335
  36. package/components/integration-card/integration-card.test.js +0 -318
  37. package/components/link/link.test.js +0 -143
  38. package/components/list/list.test.js +0 -106
  39. package/components/list-window/list-window.test.js +0 -303
  40. package/components/loading-overlay/loading-overlay.test.js +0 -257
  41. package/components/menu/menu.test.js +0 -176
  42. package/components/nav/nav.test.js +0 -428
  43. package/components/nav-group/nav-group.test.js +0 -67
  44. package/components/option-card/option-card.test.js +0 -32
  45. package/components/pagination/pagination.test.js +0 -195
  46. package/components/select/select.test.js +0 -453
  47. package/components/slider/slider.test.js +0 -202
  48. package/components/spinner/spinner.test.js +0 -272
  49. package/components/stat/stat.test.js +0 -108
  50. package/components/step-progress/step-progress.test.js +0 -129
  51. package/components/swatch/swatch.test.js +0 -563
  52. package/components/table/table.test.js +0 -479
  53. package/components/table-toolbar/table-toolbar.test.js +0 -152
  54. package/components/tag/tag.test.js +0 -137
  55. package/components/tags-input/tags-input.test.js +0 -477
  56. package/components/text/text.test.js +0 -202
  57. package/components/theme-provider/theme-provider.test.js +0 -94
  58. package/components/time-picker/time-picker.test.js +0 -287
  59. package/components/timeline/timeline.test.js +0 -31
  60. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  61. package/components/tree/tree.test.js +0 -312
  62. package/core/element.test.js +0 -389
  63. package/core/icons.test.js +0 -187
  64. package/core/logical-children.test.js +0 -54
  65. package/core/markdown.test.js +0 -47
  66. package/core/responsive.test.js +0 -121
  67. package/core/template.test.js +0 -611
  68. package/traits/active-state/active-state.test.js +0 -28
  69. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  70. package/traits/announcer/announcer.test.js +0 -268
  71. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  72. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  73. package/traits/confetti/confetti.test.js +0 -46
  74. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  75. package/traits/count-up/count-up.test.js +0 -54
  76. package/traits/declarative.test.js +0 -138
  77. package/traits/dirty-state/dirty-state.test.js +0 -45
  78. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  79. package/traits/draggable/draggable.test.js +0 -60
  80. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  81. package/traits/drop-target/drop-target.test.js +0 -241
  82. package/traits/droppable/droppable.test.js +0 -54
  83. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  84. package/traits/error-shake/error-shake.test.js +0 -114
  85. package/traits/fade-presence/fade-presence.test.js +0 -20
  86. package/traits/focus-restore/focus-restore.test.js +0 -202
  87. package/traits/focus-trap/focus-trap.test.js +0 -42
  88. package/traits/focusable/focusable.test.js +0 -53
  89. package/traits/glow-focus/glow-focus.test.js +0 -31
  90. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  91. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  92. package/traits/hotkey/hotkey.test.js +0 -61
  93. package/traits/hoverable/hoverable.test.js +0 -24
  94. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  95. package/traits/input-mask/input-mask.test.js +0 -151
  96. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  97. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  98. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  99. package/traits/layout-animation/layout-animation.test.js +0 -114
  100. package/traits/long-press/long-press.test.js +0 -244
  101. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  102. package/traits/noise-texture/noise-texture.test.js +0 -20
  103. package/traits/parallax/parallax.test.js +0 -26
  104. package/traits/portal/portal.test.js +0 -30
  105. package/traits/pressable/pressable.test.js +0 -73
  106. package/traits/resettable/resettable.test.js +0 -100
  107. package/traits/resizable/resizable.test.js +0 -39
  108. package/traits/resize-observer/resize-observer.test.js +0 -38
  109. package/traits/ripple/ripple.test.js +0 -32
  110. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  111. package/traits/scale-press/scale-press.test.js +0 -39
  112. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  113. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  114. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  115. package/traits/smoke.test.js +0 -111
  116. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  117. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  118. package/traits/spring-animate/spring-animate.test.js +0 -28
  119. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  120. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  121. package/traits/tossable/tossable.test.js +0 -31
  122. package/traits/traits-host.test.js +0 -73
  123. package/traits/typeahead/typeahead.test.js +0 -38
  124. package/traits/typewriter/typewriter.test.js +0 -47
  125. package/traits/validation/validation.test.js +0 -93
  126. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,202 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import '../../core/element.js';
3
- import './slider.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('slider-ui', () => {
15
- beforeEach(() => { document.body.innerHTML = ''; });
16
-
17
- // §330 (v0.5.16) — slider iOS-style pill redesign. Thumb position no
18
- // longer set via thumb.style.left; instead the host gets a CSS custom
19
- // property `--slider-pct` (fraction 0.0–1.0) and CSS calc() composes
20
- // the full geometry: `fill_width(p) = t + p·(W−t)`, where t = thumb
21
- // width and W = track width. Test asserts the CSS variable carries
22
- // the expected fraction (50% → 0.5).
23
- const pct = (s) => s.style.getPropertyValue('--slider-pct').trim();
24
-
25
- it('renders host --slider-pct for initial value', async () => {
26
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
27
- await tick();
28
- expect(pct(s)).toBe('0.5');
29
- });
30
-
31
- // Property reactivity contract — locks in the behavior that consumer
32
- // feedback (FEEDBACK-adia-packages.md §5, 2026-05-12) incorrectly
33
- // claimed was broken. UIElement's signal-backed property setters
34
- // trigger the host's render effect on every property change; slider-ui's
35
- // render() reads this.value and writes --slider-pct on the host.
36
- it('updates --slider-pct when .value is set programmatically', async () => {
37
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
38
- await tick();
39
- expect(pct(s)).toBe('0.5');
40
-
41
- s.value = 75;
42
- await tick();
43
- expect(pct(s)).toBe('0.75');
44
-
45
- s.value = 10;
46
- await tick();
47
- expect(pct(s)).toBe('0.1');
48
- });
49
-
50
- it('updates --slider-pct when attribute is set imperatively', async () => {
51
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
52
- await tick();
53
-
54
- s.setAttribute('value', '25');
55
- await tick();
56
- expect(pct(s)).toBe('0.25');
57
- });
58
-
59
- it('updates [slot="value"] readout text when .value changes', async () => {
60
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
61
- await tick();
62
- const readout = s.querySelector('[slot="value"]');
63
- expect(readout.textContent).toBe('50');
64
-
65
- s.value = 88;
66
- await tick();
67
- expect(readout.textContent).toBe('88');
68
- });
69
-
70
- it('emits CustomEvent("change") with detail.value on keyboard step', async () => {
71
- const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
72
- await tick();
73
-
74
- let captured = null;
75
- s.addEventListener('change', (e) => { captured = e; });
76
-
77
- s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
78
- await tick();
79
-
80
- expect(captured).not.toBeNull();
81
- expect(captured).toBeInstanceOf(CustomEvent);
82
- expect(captured.detail).toEqual({ value: 51 });
83
- });
84
-
85
- it('emits CustomEvent("input") with detail.value on internal value change', async () => {
86
- const s = mount('<slider-ui value="50" min="0" max="100" step="1"></slider-ui>');
87
- await tick();
88
-
89
- let captured = null;
90
- s.addEventListener('input', (e) => { captured = e; });
91
-
92
- s.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowUp', bubbles: true }));
93
- await tick();
94
-
95
- expect(captured).not.toBeNull();
96
- expect(captured.detail).toEqual({ value: 51 });
97
- });
98
-
99
- it('reflects value to [aria-valuenow] for screen readers', async () => {
100
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
101
- await tick();
102
- expect(s.getAttribute('aria-valuenow')).toBe('50');
103
-
104
- s.value = 80;
105
- await tick();
106
- expect(s.getAttribute('aria-valuenow')).toBe('80');
107
- });
108
-
109
- // §FB-45 (color-app, 2026-05-22) — template engine snapshot trap.
110
- // connected() stamped label/suffix once; render() didn't re-sync them.
111
- // Template engine writes {{p:N}} first, then resolves to the real value —
112
- // simulated here by setAttribute('label', placeholder) then setting the
113
- // real value. Both spans must reflect the final value, not the placeholder.
114
- it('re-syncs [slot="label"] when label attribute changes after connection', async () => {
115
- const s = mount('<slider-ui value="50" min="0" max="100"></slider-ui>');
116
- await tick();
117
- // No label initially — span absent
118
- expect(s.querySelector('[slot="label"]')).toBeNull();
119
-
120
- // Simulate template engine writing placeholder then real value
121
- s.setAttribute('label', '{{p:7}}');
122
- await tick();
123
- s.setAttribute('label', 'Volume');
124
- await tick();
125
-
126
- const labelEl = s.querySelector('[slot="label"]');
127
- expect(labelEl).not.toBeNull();
128
- expect(labelEl.textContent).toBe('Volume');
129
- expect(s.getAttribute('aria-label')).toBe('Volume');
130
- });
131
-
132
- it('re-syncs [slot="suffix"] when suffix attribute changes after connection', async () => {
133
- const s = mount('<slider-ui value="50" min="0" max="100" label="Volume" suffix="%"></slider-ui>');
134
- await tick();
135
- const suffixEl = s.querySelector('[slot="suffix"]');
136
- expect(suffixEl).not.toBeNull();
137
- expect(suffixEl.textContent).toBe('%');
138
-
139
- // Simulate placeholder → resolved suffix
140
- s.setAttribute('suffix', '{{p:8}}');
141
- await tick();
142
- s.setAttribute('suffix', 'rem');
143
- await tick();
144
-
145
- expect(s.querySelector('[slot="suffix"]').textContent).toBe('rem');
146
- });
147
-
148
- // ─── Dual-thumb mode ────────────────────────────────────────────────
149
- // v1.9.x: dual-thumb range slider lives on slider-ui[dual] + [lower-value]/[upper-value].
150
- // Fill spans BETWEEN the two thumb centers; thumbs are positioned via
151
- // --slider-pct-lower / --slider-pct-upper CSS variables.
152
- const pctLower = (s) => s.style.getPropertyValue('--slider-pct-lower').trim();
153
- const pctUpper = (s) => s.style.getPropertyValue('--slider-pct-upper').trim();
154
-
155
- it('dual mode: writes --slider-pct-lower and --slider-pct-upper from lower-value / upper-value', async () => {
156
- const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
157
- await tick();
158
- expect(pctLower(s)).toBe('0.2');
159
- expect(pctUpper(s)).toBe('0.8');
160
- });
161
-
162
- it('dual mode: stamps two thumb elements with data-thumb="lower" and data-thumb="upper"', async () => {
163
- const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100"></slider-ui>');
164
- await tick();
165
- const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
166
- const upperThumb = s.querySelector('[slot="thumb"][data-thumb="upper"]');
167
- expect(lowerThumb).not.toBeNull();
168
- expect(upperThumb).not.toBeNull();
169
- });
170
-
171
- it('dual mode: change event detail carries {lower, upper}', async () => {
172
- const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
173
- await tick();
174
- let captured = null;
175
- s.addEventListener('change', (e) => { captured = e; });
176
- // Focus the lower thumb explicitly so the keyboard handler targets it
177
- const lowerThumb = s.querySelector('[slot="thumb"][data-thumb="lower"]');
178
- lowerThumb.focus();
179
- lowerThumb.dispatchEvent(new KeyboardEvent('keydown', { key: 'ArrowRight', bubbles: true }));
180
- await tick();
181
- expect(captured).not.toBeNull();
182
- expect(captured.detail).toEqual({ lower: 21, upper: 80 });
183
- });
184
-
185
- it('dual mode: lower setter clamps DOWN to upper when set too high', async () => {
186
- const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
187
- await tick();
188
- s.lowerValue = 90; // > upperValue (80) → constraint clamps lower DOWN to upper
189
- await tick();
190
- expect(s.lowerValue).toBe(80);
191
- expect(s.upperValue).toBe(80); // upper unchanged
192
- });
193
-
194
- it('dual mode: upper setter clamps UP to lower when set too low', async () => {
195
- const s = mount('<slider-ui dual lower-value="20" upper-value="80" min="0" max="100" step="1"></slider-ui>');
196
- await tick();
197
- s.upperValue = 10; // < lowerValue (20) → constraint clamps upper UP to lower
198
- await tick();
199
- expect(s.upperValue).toBe(20);
200
- expect(s.lowerValue).toBe(20); // lower unchanged
201
- });
202
- });
@@ -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
- });