@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.
Files changed (128) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/components/card/card.a2ui.json +6 -3
  3. package/components/card/card.css +15 -1
  4. package/components/card/card.yaml +5 -3
  5. package/components/nav-group/nav-group.a2ui.json +5 -0
  6. package/components/nav-group/nav-group.class.js +14 -6
  7. package/components/nav-group/nav-group.css +5 -2
  8. package/components/nav-group/nav-group.d.ts +2 -0
  9. package/components/nav-group/nav-group.yaml +4 -0
  10. package/components/select/select.a2ui.json +3 -2
  11. package/components/select/select.class.js +14 -1
  12. package/components/select/select.css +13 -1
  13. package/components/select/select.yaml +5 -4
  14. package/components/table/table.class.js +7 -3
  15. package/components/table/table.css +7 -5
  16. package/dist/theme-provider.min.js +1 -1
  17. package/dist/web-components.min.css +1 -1
  18. package/dist/web-components.min.js +19 -19
  19. package/dist/web-components.sheet.js +1 -1
  20. package/package.json +2 -1
  21. package/components/adia-mark/adia-mark.test.js +0 -69
  22. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  23. package/components/agent-artifact/agent-artifact.test.js +0 -61
  24. package/components/alert/alert.test.js +0 -364
  25. package/components/color-picker/color-picker.test.js +0 -96
  26. package/components/combobox/combobox.test.js +0 -256
  27. package/components/command/command.test.js +0 -99
  28. package/components/date-range-picker/date-range-picker.test.js +0 -387
  29. package/components/datetime-picker/datetime-picker.test.js +0 -454
  30. package/components/demo-toggle/demo-toggle.test.js +0 -102
  31. package/components/description-list/description-list.test.js +0 -72
  32. package/components/display-field/display-field.test.js +0 -115
  33. package/components/empty-state/empty-state.test.js +0 -77
  34. package/components/field/field.test.js +0 -260
  35. package/components/fields/fields.test.js +0 -88
  36. package/components/inline-message/inline-message.test.js +0 -203
  37. package/components/input/input.test.js +0 -335
  38. package/components/integration-card/integration-card.test.js +0 -318
  39. package/components/link/link.test.js +0 -143
  40. package/components/list/list.test.js +0 -106
  41. package/components/list-window/list-window.test.js +0 -303
  42. package/components/loading-overlay/loading-overlay.test.js +0 -257
  43. package/components/menu/menu.test.js +0 -176
  44. package/components/nav/nav.test.js +0 -428
  45. package/components/nav-group/nav-group.test.js +0 -67
  46. package/components/option-card/option-card.test.js +0 -32
  47. package/components/pagination/pagination.test.js +0 -195
  48. package/components/select/select.test.js +0 -481
  49. package/components/slider/slider.test.js +0 -202
  50. package/components/spinner/spinner.test.js +0 -272
  51. package/components/stat/stat.test.js +0 -108
  52. package/components/step-progress/step-progress.test.js +0 -129
  53. package/components/swatch/swatch.test.js +0 -563
  54. package/components/table/table.test.js +0 -479
  55. package/components/table-toolbar/table-toolbar.test.js +0 -152
  56. package/components/tag/tag.test.js +0 -137
  57. package/components/tags-input/tags-input.test.js +0 -477
  58. package/components/text/text.test.js +0 -202
  59. package/components/theme-provider/theme-provider.test.js +0 -94
  60. package/components/time-picker/time-picker.test.js +0 -287
  61. package/components/timeline/timeline.test.js +0 -31
  62. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  63. package/components/tree/tree.test.js +0 -312
  64. package/core/element.test.js +0 -389
  65. package/core/icons.test.js +0 -187
  66. package/core/logical-children.test.js +0 -54
  67. package/core/markdown.test.js +0 -47
  68. package/core/responsive.test.js +0 -121
  69. package/core/template.test.js +0 -611
  70. package/traits/active-state/active-state.test.js +0 -28
  71. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  72. package/traits/announcer/announcer.test.js +0 -268
  73. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  74. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  75. package/traits/confetti/confetti.test.js +0 -46
  76. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  77. package/traits/count-up/count-up.test.js +0 -54
  78. package/traits/declarative.test.js +0 -138
  79. package/traits/dirty-state/dirty-state.test.js +0 -45
  80. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  81. package/traits/draggable/draggable.test.js +0 -60
  82. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  83. package/traits/drop-target/drop-target.test.js +0 -241
  84. package/traits/droppable/droppable.test.js +0 -54
  85. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  86. package/traits/error-shake/error-shake.test.js +0 -114
  87. package/traits/fade-presence/fade-presence.test.js +0 -20
  88. package/traits/focus-restore/focus-restore.test.js +0 -202
  89. package/traits/focus-trap/focus-trap.test.js +0 -42
  90. package/traits/focusable/focusable.test.js +0 -53
  91. package/traits/glow-focus/glow-focus.test.js +0 -31
  92. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  93. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  94. package/traits/hotkey/hotkey.test.js +0 -61
  95. package/traits/hoverable/hoverable.test.js +0 -24
  96. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  97. package/traits/input-mask/input-mask.test.js +0 -151
  98. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  99. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  100. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  101. package/traits/layout-animation/layout-animation.test.js +0 -114
  102. package/traits/long-press/long-press.test.js +0 -244
  103. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  104. package/traits/noise-texture/noise-texture.test.js +0 -20
  105. package/traits/parallax/parallax.test.js +0 -26
  106. package/traits/portal/portal.test.js +0 -30
  107. package/traits/pressable/pressable.test.js +0 -73
  108. package/traits/resettable/resettable.test.js +0 -100
  109. package/traits/resizable/resizable.test.js +0 -39
  110. package/traits/resize-observer/resize-observer.test.js +0 -38
  111. package/traits/ripple/ripple.test.js +0 -32
  112. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  113. package/traits/scale-press/scale-press.test.js +0 -39
  114. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  115. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  116. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  117. package/traits/smoke.test.js +0 -111
  118. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  119. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  120. package/traits/spring-animate/spring-animate.test.js +0 -28
  121. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  122. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  123. package/traits/tossable/tossable.test.js +0 -31
  124. package/traits/traits-host.test.js +0 -73
  125. package/traits/typeahead/typeahead.test.js +0 -38
  126. package/traits/typewriter/typewriter.test.js +0 -47
  127. package/traits/validation/validation.test.js +0 -93
  128. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,202 +0,0 @@
1
- /**
2
- * text-ui variant rendering tests — §210 (v0.5.7, FEEDBACK-17 §1).
3
- *
4
- * Verifies the 12 documented enum values in `text.yaml`/`text.d.ts`/
5
- * `text.a2ui.json` all render distinctly per the `:scope[variant=…]`
6
- * rules in `text.css`. Pre-§210 the three token-backed variants
7
- * `subsection` / `deck` / `metric` silently rendered as `body`
8
- * defaults because the matching `:scope` rules were missing — the
9
- * yaml + d.ts advertised them but the CSS didn't consume the tokens.
10
- *
11
- * Plus a guard: the 6 phantom enum entries removed in §210
12
- * (`subheading`, `h1`-`h6`) must NOT be in the .d.ts type union or
13
- * the a2ui.json enum — they had no shipped tokens and rendering them
14
- * was always body-defaults.
15
- */
16
-
17
- import { describe, it, expect, beforeEach } from 'vitest';
18
- import { readFileSync } from 'node:fs';
19
- import { fileURLToPath } from 'node:url';
20
- import { dirname, resolve } from 'node:path';
21
- import '../../core/element.js';
22
- import './text.js';
23
-
24
- const tick = () => new Promise((r) => queueMicrotask(r));
25
-
26
- function mount(html) {
27
- const wrap = document.createElement('div');
28
- wrap.innerHTML = html;
29
- document.body.appendChild(wrap);
30
- return wrap.firstElementChild;
31
- }
32
-
33
- // jsdom doesn't evaluate @scope rules in `getComputedStyle()`. We
34
- // validate the CSS by reading the rule out of text.css text and
35
- // asserting the variant-specific properties are present — coarse
36
- // but catches the §210 regression class (rule missing entirely).
37
- const HERE = dirname(fileURLToPath(import.meta.url));
38
- const TEXT_CSS = readFileSync(resolve(HERE, 'text.css'), 'utf8');
39
- const TEXT_DTS = readFileSync(resolve(HERE, 'text.d.ts'), 'utf8');
40
- const TEXT_A2UI = JSON.parse(readFileSync(resolve(HERE, 'text.a2ui.json'), 'utf8'));
41
-
42
- describe('text-ui §210 — variant enum vs CSS rule completeness', () => {
43
- beforeEach(() => { document.body.innerHTML = ''; });
44
-
45
- // ── Mounting smoke — every documented variant constructs without crash ──
46
- const documentedVariants = [
47
- 'body', 'heading', 'title', 'subsection', 'display', 'caption',
48
- 'label', 'kicker', 'deck', 'section', 'metric', 'code',
49
- ];
50
-
51
- it.each(documentedVariants)('mounts <text-ui variant="%s"> without error', async (variant) => {
52
- const t = mount(`<text-ui variant="${variant}">Sample</text-ui>`);
53
- await tick();
54
- expect(t).toBeDefined();
55
- expect(t.getAttribute('variant')).toBe(variant);
56
- });
57
-
58
- // ── CSS-side: every documented variant has a :scope rule ──
59
- it.each(documentedVariants)('text.css ships a :scope[variant="%s"] rule', (variant) => {
60
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[variant="${variant}"\\]`));
61
- });
62
-
63
- // ── Three token-backed variants restored in §210 ──
64
- it.each(['subsection', 'deck', 'metric'])(
65
- 'text.css :scope[variant="%s"] consumes role tokens (not body defaults)',
66
- (variant) => {
67
- // The rule must reference the role-specific token, not fall through
68
- // to var(--a-body-*) defaults.
69
- const ruleMatch = TEXT_CSS.match(
70
- new RegExp(`:scope\\[variant="${variant}"\\]\\s*\\{([^}]+)\\}`)
71
- );
72
- expect(ruleMatch, `rule missing for variant="${variant}"`).toBeTruthy();
73
- const rule = ruleMatch[1];
74
- expect(rule).toMatch(new RegExp(`var\\(--a-${variant}-family\\)`));
75
- expect(rule).toMatch(new RegExp(`var\\(--a-${variant}-weight\\)`));
76
- expect(rule).toMatch(new RegExp(`var\\(--a-${variant}-size\\)`));
77
- }
78
- );
79
-
80
- // ── Type-side: 6 phantom entries removed in §210 ──
81
- const removedPhantoms = ['subheading', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'];
82
-
83
- it.each(removedPhantoms)('text.d.ts variant union does NOT contain phantom "%s"', (variant) => {
84
- expect(TEXT_DTS).not.toMatch(new RegExp(`'${variant}'`));
85
- });
86
-
87
- it.each(removedPhantoms)('text.a2ui.json variant enum does NOT contain phantom "%s"', (variant) => {
88
- expect(TEXT_A2UI.properties.variant.enum).not.toContain(variant);
89
- });
90
-
91
- // ── a2ui.json enum and .d.ts union and CSS rules are mutually consistent ──
92
- it('a2ui.json variant enum matches .d.ts union and CSS rules 1:1', () => {
93
- const a2uiVariants = TEXT_A2UI.properties.variant.enum;
94
- // §247b (v0.5.10): text.d.ts was hand-authored to use a named type alias
95
- // `UITextVariant` instead of an inline union (per-variant JSDoc + ARIA-role
96
- // disclaimer). Match either the type-alias body OR an inline union; both
97
- // forms expose the same set of string-literal members.
98
- const dtsAliasMatch = TEXT_DTS.match(/export\s+type\s+UITextVariant\s*=\s*((?:[\s\S]*?))(?:;|\n\n)/);
99
- const dtsUnionMatch = TEXT_DTS.match(/variant:\s*((?:'[^']+'\s*\|?\s*)+);/);
100
- const variantsSource = dtsAliasMatch?.[1] ?? dtsUnionMatch?.[1];
101
- expect(variantsSource).toBeTruthy();
102
- const dtsVariants = [...variantsSource.matchAll(/'([^']+)'/g)].map(m => m[1]);
103
-
104
- expect(a2uiVariants.sort()).toEqual(dtsVariants.sort());
105
- expect(a2uiVariants.sort()).toEqual([...documentedVariants].sort());
106
-
107
- // Every variant has a CSS rule.
108
- for (const v of a2uiVariants) {
109
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[variant="${v}"\\]`));
110
- }
111
- });
112
- });
113
-
114
- // ─────────────────────────────────────────────────────────────────────
115
- // v0.6.18 (FB-10) — overlay attributes on top of `variant`
116
- // ─────────────────────────────────────────────────────────────────────
117
-
118
- describe('text-ui v0.6.18 — overlay props (FB-10)', () => {
119
- beforeEach(() => { document.body.innerHTML = ''; });
120
-
121
- // ── Attribute reflection: each new prop reflects to the host ──
122
- it.each([
123
- ['size', 'sm'],
124
- ['size', 'md'],
125
- ['size', 'lg'],
126
- ['color', 'subtle'],
127
- ['color', 'strong'],
128
- ['color', 'accent'],
129
- ['color', 'danger'],
130
- ['color', 'success'],
131
- ['color', 'warning'],
132
- ['color', 'default'],
133
- ['weight', 'regular'],
134
- ['weight', 'medium'],
135
- ['weight', 'semibold'],
136
- ['weight', 'bold'],
137
- ['text-align', 'start'],
138
- ['text-align', 'center'],
139
- ['text-align', 'end'],
140
- ['text-align', 'justify'],
141
- ])('<text-ui %s="%s"> reflects the attribute', async (prop, value) => {
142
- const el = mount(`<text-ui ${prop}="${value}">x</text-ui>`);
143
- await tick();
144
- expect(el.getAttribute(prop)).toBe(value);
145
- });
146
-
147
- // ── CSS-side: every documented value has a :scope[attr="value"] rule ──
148
- const sizes = ['sm', 'md', 'lg'];
149
- const colors = ['default', 'subtle', 'strong', 'accent', 'danger', 'success', 'warning'];
150
- const weights = ['regular', 'medium', 'semibold', 'bold'];
151
- const aligns = ['start', 'center', 'end', 'justify'];
152
-
153
- it.each(sizes)('text.css ships :scope[size="%s"] rule', (s) => {
154
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[size="${s}"\\]`));
155
- });
156
-
157
- it.each(colors)('text.css ships :scope[color="%s"] rule', (c) => {
158
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[color="${c}"\\]`));
159
- });
160
-
161
- it.each(weights)('text.css ships :scope[weight="%s"] rule', (w) => {
162
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[weight="${w}"\\]`));
163
- });
164
-
165
- it.each(aligns)('text.css ships :scope[text-align="%s"] rule', (a) => {
166
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[text-align="${a}"\\]`));
167
- });
168
-
169
- // ── yaml-vs-impl consistency: every prop in the a2ui enum has a CSS rule ──
170
- it('a2ui.json color enum matches CSS rules 1:1', () => {
171
- const colorEnum = TEXT_A2UI.properties.color?.enum ?? [];
172
- expect(colorEnum.sort()).toEqual([...colors].sort());
173
- for (const c of colorEnum) {
174
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[color="${c}"\\]`));
175
- }
176
- });
177
-
178
- it('a2ui.json size enum matches CSS rules 1:1', () => {
179
- const sizeEnum = TEXT_A2UI.properties.size?.enum ?? [];
180
- expect(sizeEnum.sort()).toEqual([...sizes].sort());
181
- for (const s of sizeEnum) {
182
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[size="${s}"\\]`));
183
- }
184
- });
185
-
186
- it('a2ui.json weight enum matches CSS rules 1:1', () => {
187
- const weightEnum = TEXT_A2UI.properties.weight?.enum ?? [];
188
- expect(weightEnum.sort()).toEqual([...weights].sort());
189
- for (const w of weightEnum) {
190
- expect(TEXT_CSS).toMatch(new RegExp(`:scope\\[weight="${w}"\\]`));
191
- }
192
- });
193
-
194
- // ── Permissive fallback: unknown values are no-ops (no JS error) ──
195
- it('unknown color value renders without throwing', async () => {
196
- const el = mount('<text-ui color="banana">x</text-ui>');
197
- await tick();
198
- expect(el.getAttribute('color')).toBe('banana');
199
- // The variant default wins; no CSS rule matches "banana". Test the
200
- // shape: no thrown error during mount, no console.error.
201
- });
202
- });
@@ -1,94 +0,0 @@
1
- /**
2
- * <theme-provider> tests
3
- *
4
- * The provider adopts the AdiaUI foundation (a constructable stylesheet) into
5
- * `document.adoptedStyleSheets` — once, deduped — and renders `display: contents`
6
- * + `role=presentation`. The real foundation sheet (`web-components.sheet.js`,
7
- * ~512 KB of LightningCSS output) is mocked here with a 1-rule stub so the test
8
- * exercises the ELEMENT's contract (adopt-once, dedup, transparency), not the
9
- * bundle's CSS. Byte-parity of the real sheet is covered by `check:css-bundles-fresh`.
10
- */
11
-
12
- import { describe, it, expect, beforeEach, vi } from 'vitest';
13
-
14
- // Stub the heavy foundation sheet — same default-export shape as the generated
15
- // module: a constructSheet() returning a memoized CSSStyleSheet singleton.
16
- vi.mock('../../dist/web-components.sheet.js', () => {
17
- let sheet = null;
18
- return {
19
- default: () => {
20
- if (typeof CSSStyleSheet === 'undefined') return null;
21
- if (!sheet) { sheet = new CSSStyleSheet(); sheet.replaceSync('theme-provider{display:contents}'); }
22
- return sheet;
23
- },
24
- };
25
- });
26
-
27
- // Stub the opt-in twins — the element dynamic-imports these when theme=/scale= is
28
- // set. We test the attribute contract (sync), not the adopted CSS. (Factories are
29
- // inlined because vi.mock hoists above any shared `const`.)
30
- vi.mock('../../dist/themes.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
31
- vi.mock('../../dist/verse.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
32
- vi.mock('../../dist/prose.sheet.js', () => ({ default: () => { const s = new CSSStyleSheet(); s.replaceSync(':root{}'); return s; } }));
33
-
34
- import '../../core/element.js';
35
- import './theme-provider.js'; // registers the tag + adopts the (mocked) foundation at module-eval
36
-
37
- const tick = () => new Promise((r) => queueMicrotask(r));
38
-
39
- function mount(html) {
40
- const wrap = document.createElement('div');
41
- wrap.innerHTML = html;
42
- document.body.appendChild(wrap);
43
- return wrap.firstElementChild;
44
- }
45
-
46
- describe('<theme-provider>', () => {
47
- beforeEach(() => { document.body.innerHTML = ''; });
48
-
49
- it('adopts the foundation sheet at import (module-eval side effect)', () => {
50
- expect(document.adoptedStyleSheets.length).toBeGreaterThanOrEqual(1);
51
- });
52
-
53
- it('is display:contents + role=presentation on connect', async () => {
54
- const el = mount('<theme-provider><span>hi</span></theme-provider>');
55
- await tick();
56
- expect(el.style.display).toBe('contents');
57
- expect(el.getAttribute('role')).toBe('presentation');
58
- });
59
-
60
- it('does not clobber an author-set role', async () => {
61
- const el = mount('<theme-provider role="group"><span>hi</span></theme-provider>');
62
- await tick();
63
- expect(el.getAttribute('role')).toBe('group');
64
- });
65
-
66
- it('dedupes — N instances adopt the foundation exactly once', async () => {
67
- const before = document.adoptedStyleSheets.length; // singleton already adopted at import
68
- mount('<theme-provider></theme-provider>');
69
- mount('<theme-provider></theme-provider>');
70
- mount('<theme-provider></theme-provider>');
71
- await tick();
72
- expect(document.adoptedStyleSheets.length).toBe(before);
73
- });
74
-
75
- it('reflects theme= to the [theme] attribute hook', async () => {
76
- const el = mount('<theme-provider theme="ocean"><span>x</span></theme-provider>');
77
- await tick();
78
- expect(el.getAttribute('theme')).toBe('ocean');
79
- });
80
-
81
- it('scale="verse" maps to the [verse] register attribute', async () => {
82
- const el = mount('<theme-provider scale="verse"><span>x</span></theme-provider>');
83
- await tick();
84
- expect(el.hasAttribute('verse')).toBe(true);
85
- expect(el.hasAttribute('prose')).toBe(false);
86
- });
87
-
88
- it('scale="prose" maps to the [prose] register attribute', async () => {
89
- const el = mount('<theme-provider scale="prose"><span>x</span></theme-provider>');
90
- await tick();
91
- expect(el.hasAttribute('prose')).toBe(true);
92
- expect(el.hasAttribute('verse')).toBe(false);
93
- });
94
- });
@@ -1,287 +0,0 @@
1
- /**
2
- * time-picker-ui — SPEC-043
3
- *
4
- * Coverage:
5
- * - Stamp shape (precision × hour-cycle)
6
- * - Segment text sync from value
7
- * - Arrow Up / Down increments
8
- * - Page Up / Down + Home / End
9
- * - Tab equivalents (ArrowLeft / ArrowRight)
10
- * - Digit typing + auto-advance
11
- * - Form participation (name + ISO value)
12
- * - min / max → invalid event
13
- * - disabled / readonly behavior
14
- * - precision toggle re-stamps without losing hh/mm
15
- */
16
-
17
- import { describe, it, expect, beforeEach } from 'vitest';
18
- import '../../core/element.js';
19
- import './time-picker.js';
20
-
21
- const tick = () => new Promise((r) => queueMicrotask(r));
22
-
23
- function mount(html) {
24
- const wrap = document.createElement('div');
25
- wrap.innerHTML = html;
26
- document.body.appendChild(wrap);
27
- return wrap.firstElementChild;
28
- }
29
-
30
- function segText(el, key) {
31
- return el.querySelector(`[data-segment="${key}"]`)?.textContent || '';
32
- }
33
-
34
- function fireKey(el, key) {
35
- el.dispatchEvent(new KeyboardEvent('keydown', { key, bubbles: true }));
36
- }
37
-
38
- describe('time-picker-ui', () => {
39
- beforeEach(() => { document.body.innerHTML = ''; });
40
-
41
- it('renders with empty segments when no value is set', async () => {
42
- const el = mount('<time-picker-ui></time-picker-ui>');
43
- await tick();
44
- expect(el.querySelector('[data-segment="hour"]')).not.toBeNull();
45
- expect(el.querySelector('[data-segment="minute"]')).not.toBeNull();
46
- expect(segText(el, 'hour')).toBe('');
47
- expect(segText(el, 'minute')).toBe('');
48
- });
49
-
50
- it('populates hour/minute segments from value="09:30"', async () => {
51
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
52
- await tick();
53
- expect(segText(el, 'hour')).toBe('09');
54
- expect(segText(el, 'minute')).toBe('30');
55
- });
56
-
57
- it('precision="second" stamps a seconds segment + separator', async () => {
58
- const el = mount('<time-picker-ui precision="second" value="14:30:45"></time-picker-ui>');
59
- await tick();
60
- expect(el.querySelector('[data-segment="second"]')).not.toBeNull();
61
- expect(segText(el, 'second')).toBe('45');
62
- });
63
-
64
- it('hour-cycle="h12" stamps a meridiem segment + 12h display', async () => {
65
- const el = mount('<time-picker-ui hour-cycle="h12" value="14:30"></time-picker-ui>');
66
- await tick();
67
- const meridiem = el.querySelector('[data-segment="meridiem"]');
68
- expect(meridiem).not.toBeNull();
69
- expect(segText(el, 'hour')).toBe('02');
70
- expect(segText(el, 'meridiem')).toBe('PM');
71
- });
72
-
73
- it('hour-cycle="h12" displays midnight as 12 AM', async () => {
74
- const el = mount('<time-picker-ui hour-cycle="h12" value="00:15"></time-picker-ui>');
75
- await tick();
76
- expect(segText(el, 'hour')).toBe('12');
77
- expect(segText(el, 'meridiem')).toBe('AM');
78
- });
79
-
80
- it('Arrow Up on hour segment increments by 1', async () => {
81
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
82
- await tick();
83
- const hour = el.querySelector('[data-segment="hour"]');
84
- hour.focus();
85
- fireKey(hour, 'ArrowUp');
86
- await tick();
87
- expect(el.value).toBe('10:30');
88
- });
89
-
90
- it('Arrow Down on minute segment decrements by step/60 minutes', async () => {
91
- const el = mount('<time-picker-ui value="09:30" step="60"></time-picker-ui>');
92
- await tick();
93
- const min = el.querySelector('[data-segment="minute"]');
94
- min.focus();
95
- fireKey(min, 'ArrowDown');
96
- await tick();
97
- expect(el.value).toBe('09:29');
98
- });
99
-
100
- it('step="900" increments minute by 15 on Arrow Up', async () => {
101
- const el = mount('<time-picker-ui value="09:30" step="900"></time-picker-ui>');
102
- await tick();
103
- const min = el.querySelector('[data-segment="minute"]');
104
- min.focus();
105
- fireKey(min, 'ArrowUp');
106
- await tick();
107
- expect(el.value).toBe('09:45');
108
- });
109
-
110
- it('Page Up increments by 10× the small step', async () => {
111
- const el = mount('<time-picker-ui value="09:00"></time-picker-ui>');
112
- await tick();
113
- const min = el.querySelector('[data-segment="minute"]');
114
- min.focus();
115
- fireKey(min, 'PageUp');
116
- await tick();
117
- expect(el.value).toBe('09:10');
118
- });
119
-
120
- it('Home jumps to segment min; End jumps to segment max', async () => {
121
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
122
- await tick();
123
- const hour = el.querySelector('[data-segment="hour"]');
124
- hour.focus();
125
- fireKey(hour, 'Home');
126
- await tick();
127
- expect(el.value).toBe('00:30');
128
- fireKey(hour, 'End');
129
- await tick();
130
- expect(el.value).toBe('23:30');
131
- });
132
-
133
- it('ArrowRight moves focus to next segment; ArrowLeft to previous', async () => {
134
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
135
- await tick();
136
- const hour = el.querySelector('[data-segment="hour"]');
137
- const minute = el.querySelector('[data-segment="minute"]');
138
- hour.focus();
139
- fireKey(hour, 'ArrowRight');
140
- await tick();
141
- expect(document.activeElement).toBe(minute);
142
- fireKey(minute, 'ArrowLeft');
143
- await tick();
144
- expect(document.activeElement).toBe(hour);
145
- });
146
-
147
- it('disabled prop blocks Arrow-key step', async () => {
148
- const el = mount('<time-picker-ui value="09:30" disabled></time-picker-ui>');
149
- await tick();
150
- const hour = el.querySelector('[data-segment="hour"]');
151
- fireKey(hour, 'ArrowUp');
152
- await tick();
153
- expect(el.value).toBe('09:30');
154
- });
155
-
156
- it('readonly prop blocks edits but keeps segments focusable', async () => {
157
- const el = mount('<time-picker-ui value="09:30" readonly></time-picker-ui>');
158
- await tick();
159
- const hour = el.querySelector('[data-segment="hour"]');
160
- expect(hour.getAttribute('tabindex')).toBe('0');
161
- expect(hour.contentEditable).toBe('false');
162
- hour.focus();
163
- fireKey(hour, 'ArrowUp');
164
- await tick();
165
- expect(el.value).toBe('09:30');
166
- });
167
-
168
- it('min violation fires invalid + does not mutate value', async () => {
169
- const el = mount('<time-picker-ui value="08:00" min="08:00" max="18:00"></time-picker-ui>');
170
- await tick();
171
- let captured = null;
172
- el.addEventListener('invalid', (e) => { captured = e; });
173
- const hour = el.querySelector('[data-segment="hour"]');
174
- hour.focus();
175
- fireKey(hour, 'ArrowDown');
176
- await tick();
177
- expect(captured).not.toBeNull();
178
- expect(captured.detail.reason).toBe('min');
179
- expect(el.value).toBe('08:00');
180
- });
181
-
182
- it('max violation fires invalid', async () => {
183
- const el = mount('<time-picker-ui value="18:00" min="08:00" max="18:00"></time-picker-ui>');
184
- await tick();
185
- let captured = null;
186
- el.addEventListener('invalid', (e) => { captured = e; });
187
- const hour = el.querySelector('[data-segment="hour"]');
188
- hour.focus();
189
- fireKey(hour, 'ArrowUp');
190
- await tick();
191
- expect(captured).not.toBeNull();
192
- expect(captured.detail.reason).toBe('max');
193
- expect(el.value).toBe('18:00');
194
- });
195
-
196
- it('emits change with detail.segment on Arrow step', async () => {
197
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
198
- await tick();
199
- let captured = null;
200
- el.addEventListener('change', (e) => { captured = e; });
201
- const hour = el.querySelector('[data-segment="hour"]');
202
- hour.focus();
203
- fireKey(hour, 'ArrowUp');
204
- await tick();
205
- expect(captured).not.toBeNull();
206
- expect(captured.detail).toEqual({ value: '10:30', segment: 'hour' });
207
- });
208
-
209
- it('precision="minute" → "second" re-stamps without losing hh/mm', async () => {
210
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
211
- await tick();
212
- expect(el.querySelector('[data-segment="second"]')).toBeNull();
213
- el.precision = 'second';
214
- await tick();
215
- expect(el.querySelector('[data-segment="second"]')).not.toBeNull();
216
- expect(segText(el, 'hour')).toBe('09');
217
- expect(segText(el, 'minute')).toBe('30');
218
- });
219
-
220
- it('aria-valuemax matches segment max (h23 → 23 for hour)', async () => {
221
- const el = mount('<time-picker-ui hour-cycle="h23"></time-picker-ui>');
222
- await tick();
223
- const hour = el.querySelector('[data-segment="hour"]');
224
- expect(hour.getAttribute('aria-valuemax')).toBe('23');
225
- });
226
-
227
- it('aria-valuemax for hour is 12 in h12 cycle', async () => {
228
- const el = mount('<time-picker-ui hour-cycle="h12"></time-picker-ui>');
229
- await tick();
230
- const hour = el.querySelector('[data-segment="hour"]');
231
- expect(hour.getAttribute('aria-valuemax')).toBe('12');
232
- });
233
-
234
- it('aria-valuenow + aria-valuetext are populated from value', async () => {
235
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
236
- await tick();
237
- const hour = el.querySelector('[data-segment="hour"]');
238
- const minute = el.querySelector('[data-segment="minute"]');
239
- expect(hour.getAttribute('aria-valuenow')).toBe('9');
240
- expect(minute.getAttribute('aria-valuenow')).toBe('30');
241
- expect(hour.getAttribute('aria-valuetext')).toContain('hour');
242
- expect(minute.getAttribute('aria-valuetext')).toContain('minute');
243
- });
244
-
245
- it('host role="group" + aria-label default', async () => {
246
- const el = mount('<time-picker-ui></time-picker-ui>');
247
- await tick();
248
- expect(el.getAttribute('role')).toBe('group');
249
- expect(el.hasAttribute('aria-label')).toBe(true);
250
- });
251
-
252
- it('clear() resets value to empty', async () => {
253
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
254
- await tick();
255
- el.clear();
256
- await tick();
257
- expect(el.value).toBe('');
258
- expect(segText(el, 'hour')).toBe('');
259
- });
260
-
261
- it('stepUp() increments programmatically', async () => {
262
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
263
- await tick();
264
- el.stepUp('hour');
265
- await tick();
266
- expect(el.value).toBe('10:30');
267
- });
268
-
269
- it('value setter syncs segments', async () => {
270
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
271
- await tick();
272
- el.value = '11:45';
273
- await tick();
274
- expect(segText(el, 'hour')).toBe('11');
275
- expect(segText(el, 'minute')).toBe('45');
276
- });
277
-
278
- it('focusing a segment sets [editing] + [segment="…"] on host', async () => {
279
- const el = mount('<time-picker-ui value="09:30"></time-picker-ui>');
280
- await tick();
281
- const minute = el.querySelector('[data-segment="minute"]');
282
- minute.focus();
283
- await tick();
284
- expect(el.hasAttribute('editing')).toBe(true);
285
- expect(el.getAttribute('segment')).toBe('minute');
286
- });
287
- });
@@ -1,31 +0,0 @@
1
- import { describe, it, expect, beforeEach } from 'vitest';
2
- import '../../core/element.js';
3
- import './timeline.js';
4
-
5
- const tick = () => new Promise((r) => queueMicrotask(r));
6
- function mount(html) {
7
- const w = document.createElement('div');
8
- w.innerHTML = html;
9
- document.body.appendChild(w);
10
- return w.firstElementChild;
11
- }
12
-
13
- describe('timeline-item-ui — label stamp-if-absent (wrapper trap)', () => {
14
- beforeEach(() => { document.body.innerHTML = ''; });
15
-
16
- it('stamps the label structure when the consumer provides none', async () => {
17
- const el = mount('<timeline-item-ui text="Shipped"></timeline-item-ui>');
18
- await tick();
19
- expect(el.querySelectorAll('[slot="label"]').length).toBe(1);
20
- });
21
-
22
- // Regression (audit-wrapper-trap arm 2): a WRAPPED consumer [slot="label"]
23
- // must suppress the default-structure stamp, not produce a duplicate label.
24
- it('does NOT stamp a duplicate when a WRAPPED consumer label is present', async () => {
25
- const el = mount(`<timeline-item-ui text="Shipped"><span style="display:contents" role="presentation"><span slot="label" data-test="l">Custom</span></span></timeline-item-ui>`);
26
- await tick();
27
- const labels = el.querySelectorAll('[slot="label"]');
28
- expect(labels.length).toBe(1);
29
- expect(labels[0].dataset.test).toBe('l');
30
- });
31
- });