@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
- /**
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
- });