@adia-ai/web-components 0.8.19 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- package/traits/view-transition/view-transition.test.js +0 -268
|
@@ -1,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
|
-
});
|