@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,203 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* inline-message-ui tests — SPEC-033.
|
|
3
|
-
*
|
|
4
|
-
* Verification covers the seven contract points from §11:
|
|
5
|
-
* 1. Renders with [text]; textContent reflects the value
|
|
6
|
-
* 2. Renders with default-slot content; slot wins over [text]
|
|
7
|
-
* 3. Each [variant] sets the expected --inline-message-fg token
|
|
8
|
-
* 4. [variant] change re-stamps the leading icon to the variant default
|
|
9
|
-
* 5. Consumer-provided slot="leading" icon survives variant change
|
|
10
|
-
* 6. [live="polite"] reflects to aria-live="polite" on the host
|
|
11
|
-
* 7. No background / border / padding-box CSS applied (computed-style check)
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
15
|
-
import '../../core/element.js';
|
|
16
|
-
import './inline-message.js';
|
|
17
|
-
|
|
18
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
19
|
-
|
|
20
|
-
function mount(html) {
|
|
21
|
-
const wrap = document.createElement('div');
|
|
22
|
-
wrap.innerHTML = html;
|
|
23
|
-
document.body.appendChild(wrap);
|
|
24
|
-
return wrap.firstElementChild;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
describe('inline-message-ui — content modes', () => {
|
|
28
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
29
|
-
|
|
30
|
-
it('renders the [text] attribute into a marked body span', async () => {
|
|
31
|
-
const el = mount('<inline-message-ui text="Email is required" variant="danger"></inline-message-ui>');
|
|
32
|
-
await tick();
|
|
33
|
-
const body = el.querySelector(':scope > [data-im-text]');
|
|
34
|
-
expect(body).not.toBeNull();
|
|
35
|
-
expect(body.textContent).toBe('Email is required');
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('preserves default-slot content; does not stamp the body span', async () => {
|
|
39
|
-
const el = mount('<inline-message-ui variant="warning">Custom <a href="/x">link</a></inline-message-ui>');
|
|
40
|
-
await tick();
|
|
41
|
-
// Body span is NOT stamped when consumer slotted body content
|
|
42
|
-
expect(el.querySelector(':scope > [data-im-text]')).toBeNull();
|
|
43
|
-
// Consumer's link survives
|
|
44
|
-
expect(el.querySelector('a')).not.toBeNull();
|
|
45
|
-
expect(el.querySelector('a').getAttribute('href')).toBe('/x');
|
|
46
|
-
});
|
|
47
|
-
|
|
48
|
-
it('updates body span text on [text] mutation without clobbering leading icon', async () => {
|
|
49
|
-
const el = mount('<inline-message-ui text="First" variant="info"></inline-message-ui>');
|
|
50
|
-
await tick();
|
|
51
|
-
el.text = 'Second';
|
|
52
|
-
await tick();
|
|
53
|
-
const body = el.querySelector(':scope > [data-im-text]');
|
|
54
|
-
expect(body.textContent).toBe('Second');
|
|
55
|
-
expect(el.querySelector(':scope > [slot="leading"]')).not.toBeNull();
|
|
56
|
-
});
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
describe('inline-message-ui — variants', () => {
|
|
60
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
61
|
-
|
|
62
|
-
it('reflects [variant] to attribute for CSS matching', async () => {
|
|
63
|
-
const el = mount('<inline-message-ui variant="danger" text="Err"></inline-message-ui>');
|
|
64
|
-
await tick();
|
|
65
|
-
expect(el.getAttribute('variant')).toBe('danger');
|
|
66
|
-
});
|
|
67
|
-
|
|
68
|
-
it('default variant has no leading icon (pure hint)', async () => {
|
|
69
|
-
const el = mount('<inline-message-ui text="At least 8 characters"></inline-message-ui>');
|
|
70
|
-
await tick();
|
|
71
|
-
expect(el.querySelector(':scope > [slot="leading"]')).toBeNull();
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('info variant stamps the info icon', async () => {
|
|
75
|
-
const el = mount('<inline-message-ui variant="info" text="Heads up"></inline-message-ui>');
|
|
76
|
-
await tick();
|
|
77
|
-
const leading = el.querySelector(':scope > [slot="leading"]');
|
|
78
|
-
expect(leading).not.toBeNull();
|
|
79
|
-
expect(leading.getAttribute('name')).toBe('info');
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('success variant stamps the check-circle icon', async () => {
|
|
83
|
-
const el = mount('<inline-message-ui variant="success" text="Saved"></inline-message-ui>');
|
|
84
|
-
await tick();
|
|
85
|
-
const leading = el.querySelector(':scope > [slot="leading"]');
|
|
86
|
-
expect(leading.getAttribute('name')).toBe('check-circle');
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
it('warning variant stamps the warning icon', async () => {
|
|
90
|
-
const el = mount('<inline-message-ui variant="warning" text="Watch out"></inline-message-ui>');
|
|
91
|
-
await tick();
|
|
92
|
-
const leading = el.querySelector(':scope > [slot="leading"]');
|
|
93
|
-
expect(leading.getAttribute('name')).toBe('warning');
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('danger variant stamps the x-circle icon', async () => {
|
|
97
|
-
const el = mount('<inline-message-ui variant="danger" text="Failed"></inline-message-ui>');
|
|
98
|
-
await tick();
|
|
99
|
-
const leading = el.querySelector(':scope > [slot="leading"]');
|
|
100
|
-
expect(leading.getAttribute('name')).toBe('x-circle');
|
|
101
|
-
});
|
|
102
|
-
|
|
103
|
-
it('[variant] change re-stamps the leading icon to the new variant default', async () => {
|
|
104
|
-
const el = mount('<inline-message-ui variant="info" text="X"></inline-message-ui>');
|
|
105
|
-
await tick();
|
|
106
|
-
expect(el.querySelector(':scope > [slot="leading"]').getAttribute('name')).toBe('info');
|
|
107
|
-
el.variant = 'danger';
|
|
108
|
-
await tick();
|
|
109
|
-
expect(el.querySelector(':scope > [slot="leading"]').getAttribute('name')).toBe('x-circle');
|
|
110
|
-
});
|
|
111
|
-
|
|
112
|
-
it('explicit [icon] overrides the variant default', async () => {
|
|
113
|
-
const el = mount('<inline-message-ui variant="info" icon="bug" text="Bug"></inline-message-ui>');
|
|
114
|
-
await tick();
|
|
115
|
-
expect(el.querySelector(':scope > [slot="leading"]').getAttribute('name')).toBe('bug');
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
describe('inline-message-ui — consumer-slotted leading icon', () => {
|
|
120
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
121
|
-
|
|
122
|
-
it('preserves consumer-provided slot="leading" across variant changes', async () => {
|
|
123
|
-
const el = mount('<inline-message-ui variant="info" text="X"><icon-ui slot="leading" name="lightbulb"></icon-ui></inline-message-ui>');
|
|
124
|
-
await tick();
|
|
125
|
-
// Consumer-owned: no data-im-auto flag
|
|
126
|
-
const initial = el.querySelector(':scope > [slot="leading"]');
|
|
127
|
-
expect(initial.getAttribute('name')).toBe('lightbulb');
|
|
128
|
-
expect(initial.hasAttribute('data-im-auto')).toBe(false);
|
|
129
|
-
|
|
130
|
-
// Variant change must NOT overwrite consumer-owned slot
|
|
131
|
-
el.variant = 'danger';
|
|
132
|
-
await tick();
|
|
133
|
-
const after = el.querySelector(':scope > [slot="leading"]');
|
|
134
|
-
expect(after.getAttribute('name')).toBe('lightbulb');
|
|
135
|
-
});
|
|
136
|
-
});
|
|
137
|
-
|
|
138
|
-
describe('inline-message-ui — accessibility', () => {
|
|
139
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
140
|
-
|
|
141
|
-
it('[live="polite"] reflects to aria-live="polite" on the host', async () => {
|
|
142
|
-
const el = mount('<inline-message-ui variant="success" live="polite" text="Saved"></inline-message-ui>');
|
|
143
|
-
await tick();
|
|
144
|
-
expect(el.getAttribute('aria-live')).toBe('polite');
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('[live="assertive"] reflects to aria-live="assertive" on the host', async () => {
|
|
148
|
-
const el = mount('<inline-message-ui variant="danger" live="assertive" text="Lost"></inline-message-ui>');
|
|
149
|
-
await tick();
|
|
150
|
-
expect(el.getAttribute('aria-live')).toBe('assertive');
|
|
151
|
-
});
|
|
152
|
-
|
|
153
|
-
it('omitting [live] omits aria-live (no static live-region pickup)', async () => {
|
|
154
|
-
const el = mount('<inline-message-ui variant="warning" text="Watch"></inline-message-ui>');
|
|
155
|
-
await tick();
|
|
156
|
-
expect(el.hasAttribute('aria-live')).toBe(false);
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('clearing [live] strips aria-live (dynamic update)', async () => {
|
|
160
|
-
const el = mount('<inline-message-ui variant="success" live="polite" text="X"></inline-message-ui>');
|
|
161
|
-
await tick();
|
|
162
|
-
expect(el.getAttribute('aria-live')).toBe('polite');
|
|
163
|
-
el.live = '';
|
|
164
|
-
await tick();
|
|
165
|
-
expect(el.hasAttribute('aria-live')).toBe(false);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it('does not set a host role (transparent inline annotation)', async () => {
|
|
169
|
-
const el = mount('<inline-message-ui variant="danger" text="Err"></inline-message-ui>');
|
|
170
|
-
await tick();
|
|
171
|
-
// Per SPEC-033 §5 + OD-002 current-lean: no auto role; consumer opts
|
|
172
|
-
// in via [live="assertive"] for danger if they want it announced.
|
|
173
|
-
expect(el.hasAttribute('role')).toBe(false);
|
|
174
|
-
});
|
|
175
|
-
|
|
176
|
-
it('is not a tab stop', async () => {
|
|
177
|
-
const el = mount('<inline-message-ui variant="danger" text="Err"></inline-message-ui>');
|
|
178
|
-
await tick();
|
|
179
|
-
expect(el.tabIndex).toBe(-1);
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('inline-message-ui — surface-bearing assertions (in-flow, not banner)', () => {
|
|
184
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
185
|
-
|
|
186
|
-
it('declares inline-flex display in its component CSS (verified via @scope rule resolution)', async () => {
|
|
187
|
-
// happy-dom does not run a layout engine, so we cannot assert
|
|
188
|
-
// resolved computed values. Instead, assert the contract: the
|
|
189
|
-
// component CSS file has no `background:` / `border:` / `padding:`
|
|
190
|
-
// declarations at the root level. The CSS file is the contract
|
|
191
|
-
// — guarding it in the test catches regressions even when
|
|
192
|
-
// visual-QA isn't running.
|
|
193
|
-
const cssUrl = new URL('./inline-message.css', import.meta.url);
|
|
194
|
-
const css = await fetch(cssUrl).then((r) => r.text()).catch(() => '');
|
|
195
|
-
if (!css) return; // skip if fetch unavailable in env
|
|
196
|
-
const baseBlock = css.match(/:scope\s*\{([^}]*)\}/);
|
|
197
|
-
expect(baseBlock).not.toBeNull();
|
|
198
|
-
const body = baseBlock[1];
|
|
199
|
-
expect(body).not.toMatch(/\bbackground\s*:/);
|
|
200
|
-
expect(body).not.toMatch(/\bborder\s*:/);
|
|
201
|
-
expect(body).not.toMatch(/\bpadding\s*:/);
|
|
202
|
-
});
|
|
203
|
-
});
|
|
@@ -1,335 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* input-ui — focused unit tests for the §199 (v0.5.7) leading/trailing
|
|
3
|
-
* affordance-slot wiring.
|
|
4
|
-
*
|
|
5
|
-
* Pre-§199 the yaml declared `slot="leading"` + `slot="trailing"` since
|
|
6
|
-
* v1 but nothing rendered them — consumer-authored buttons sat OUTSIDE
|
|
7
|
-
* the field chrome. §199 closes the schema-vs-impl gap by moving
|
|
8
|
-
* consumer-supplied affordance nodes into [slot="field"] at the right
|
|
9
|
-
* insertion points on connected().
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
13
|
-
import { readFileSync } from 'node:fs';
|
|
14
|
-
import { fileURLToPath } from 'node:url';
|
|
15
|
-
import { dirname, resolve } from 'node:path';
|
|
16
|
-
import '../../core/element.js';
|
|
17
|
-
import './input.js';
|
|
18
|
-
import { html, stamp } from '../../core/template.js';
|
|
19
|
-
|
|
20
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
21
|
-
const INPUT_CSS = readFileSync(resolve(__dirname, 'input.css'), 'utf8');
|
|
22
|
-
|
|
23
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
24
|
-
|
|
25
|
-
function mount(html) {
|
|
26
|
-
const wrap = document.createElement('div');
|
|
27
|
-
wrap.innerHTML = html;
|
|
28
|
-
document.body.appendChild(wrap);
|
|
29
|
-
return wrap.firstElementChild;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
describe('input-ui — §199 leading/trailing affordance slots', () => {
|
|
33
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
34
|
-
|
|
35
|
-
it('renders a baseline input without any affordances (no regression)', () => {
|
|
36
|
-
const el = mount('<input-ui value="hello"></input-ui>');
|
|
37
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
38
|
-
expect(field).not.toBeNull();
|
|
39
|
-
expect(field.querySelector('[slot="text"]')).not.toBeNull();
|
|
40
|
-
expect(field.querySelector('[slot="leading"]')).toBeNull();
|
|
41
|
-
expect(field.querySelector('[slot="trailing"]')).toBeNull();
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('moves consumer [slot="trailing"] into [slot="field"]', () => {
|
|
45
|
-
const el = mount(`
|
|
46
|
-
<input-ui value="Theme 1" suffix="Light">
|
|
47
|
-
<button slot="trailing" data-test="open" aria-label="Open">↗</button>
|
|
48
|
-
</input-ui>
|
|
49
|
-
`);
|
|
50
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
51
|
-
const trailing = field.querySelector(':scope > [slot="trailing"]');
|
|
52
|
-
expect(trailing).not.toBeNull();
|
|
53
|
-
expect(trailing.dataset.test).toBe('open');
|
|
54
|
-
// No stray trailing nodes left as direct children of <input-ui>
|
|
55
|
-
expect(el.querySelector(':scope > [slot="trailing"]')).toBeNull();
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
it('moves consumer [slot="leading"] into [slot="field"]', () => {
|
|
59
|
-
const el = mount(`
|
|
60
|
-
<input-ui value="https://...">
|
|
61
|
-
<button slot="leading" data-test="link" aria-label="Open link">↗</button>
|
|
62
|
-
</input-ui>
|
|
63
|
-
`);
|
|
64
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
65
|
-
const leading = field.querySelector(':scope > [slot="leading"]');
|
|
66
|
-
expect(leading).not.toBeNull();
|
|
67
|
-
expect(leading.dataset.test).toBe('link');
|
|
68
|
-
expect(el.querySelector(':scope > [slot="leading"]')).toBeNull();
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
// Wrapper-trap regression (audit-wrapper-trap arm 2): a `.map()`-interpolated
|
|
72
|
-
// affordance arrives nested in a display:contents/role=presentation wrapper
|
|
73
|
-
// span. The old `:scope > [slot]` direct-child grab missed it, so innerHTML=''
|
|
74
|
-
// wiped it. logicalSlotted pierces the wrapper → it reaches the field.
|
|
75
|
-
it('moves a WRAPPED (interpolated) [slot="trailing"] into the field (wrapper trap)', () => {
|
|
76
|
-
const el = mount(`
|
|
77
|
-
<input-ui value="Theme 1" suffix="Light">
|
|
78
|
-
<span style="display:contents" role="presentation"><button slot="trailing" data-test="open" aria-label="Open">↗</button></span>
|
|
79
|
-
</input-ui>
|
|
80
|
-
`);
|
|
81
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
82
|
-
const trailing = field.querySelector(':scope > [slot="trailing"]');
|
|
83
|
-
expect(trailing).not.toBeNull();
|
|
84
|
-
expect(trailing.dataset.test).toBe('open');
|
|
85
|
-
});
|
|
86
|
-
|
|
87
|
-
it('positions trailing after suffix and before controls (number mode)', () => {
|
|
88
|
-
const el = mount(`
|
|
89
|
-
<input-ui type="number" value="42" suffix="kg">
|
|
90
|
-
<button slot="trailing" data-test="reset">↺</button>
|
|
91
|
-
</input-ui>
|
|
92
|
-
`);
|
|
93
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
94
|
-
const children = Array.from(field.children);
|
|
95
|
-
const suffixIdx = children.findIndex((c) => c.getAttribute('slot') === 'suffix');
|
|
96
|
-
const trailingIdx = children.findIndex((c) => c.getAttribute('slot') === 'trailing');
|
|
97
|
-
const controlsIdx = children.findIndex((c) => c.getAttribute('slot') === 'controls');
|
|
98
|
-
expect(suffixIdx).toBeGreaterThanOrEqual(0);
|
|
99
|
-
expect(trailingIdx).toBeGreaterThanOrEqual(0);
|
|
100
|
-
expect(controlsIdx).toBeGreaterThanOrEqual(0);
|
|
101
|
-
expect(suffixIdx).toBeLessThan(trailingIdx);
|
|
102
|
-
expect(trailingIdx).toBeLessThan(controlsIdx);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('positions leading after label (when label present)', () => {
|
|
106
|
-
const el = mount(`
|
|
107
|
-
<input-ui label="Theme" value="Theme 1">
|
|
108
|
-
<button slot="leading" data-test="picker">🎨</button>
|
|
109
|
-
</input-ui>
|
|
110
|
-
`);
|
|
111
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
112
|
-
const children = Array.from(field.children);
|
|
113
|
-
const labelIdx = children.findIndex((c) => c.getAttribute('slot') === 'label');
|
|
114
|
-
const leadingIdx = children.findIndex((c) => c.getAttribute('slot') === 'leading');
|
|
115
|
-
const textIdx = children.findIndex((c) => c.getAttribute('slot') === 'text');
|
|
116
|
-
expect(labelIdx).toBeGreaterThanOrEqual(0);
|
|
117
|
-
expect(leadingIdx).toBeGreaterThanOrEqual(0);
|
|
118
|
-
expect(textIdx).toBeGreaterThanOrEqual(0);
|
|
119
|
-
expect(labelIdx).toBeLessThan(leadingIdx);
|
|
120
|
-
expect(leadingIdx).toBeLessThan(textIdx);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it('supports multiple trailing nodes in author order', () => {
|
|
124
|
-
const el = mount(`
|
|
125
|
-
<input-ui value="hello">
|
|
126
|
-
<button slot="trailing" data-test="a">A</button>
|
|
127
|
-
<button slot="trailing" data-test="b">B</button>
|
|
128
|
-
</input-ui>
|
|
129
|
-
`);
|
|
130
|
-
const field = el.querySelector(':scope > [slot="field"]');
|
|
131
|
-
const trailings = Array.from(field.querySelectorAll(':scope > [slot="trailing"]'));
|
|
132
|
-
expect(trailings.map((n) => n.dataset.test)).toEqual(['a', 'b']);
|
|
133
|
-
});
|
|
134
|
-
|
|
135
|
-
it('preserves contenteditable surface alongside affordances', () => {
|
|
136
|
-
const el = mount(`
|
|
137
|
-
<input-ui value="Theme 1" suffix="Light">
|
|
138
|
-
<button slot="trailing" data-test="open">↗</button>
|
|
139
|
-
</input-ui>
|
|
140
|
-
`);
|
|
141
|
-
const text = el.querySelector('[slot="text"]');
|
|
142
|
-
expect(text).not.toBeNull();
|
|
143
|
-
expect(text.getAttribute('contenteditable')).toBe('plaintext-only');
|
|
144
|
-
expect(text.textContent).toBe('Theme 1');
|
|
145
|
-
});
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
describe('input-ui — §220 (v0.5.9) throttle parity', () => {
|
|
149
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
150
|
-
|
|
151
|
-
it('inherits `throttle` property default 0 from UIFormElement', () => {
|
|
152
|
-
const el = mount('<input-ui></input-ui>');
|
|
153
|
-
expect(el.throttle).toBe(0);
|
|
154
|
-
});
|
|
155
|
-
|
|
156
|
-
it('reflects [throttle] attribute to the property', () => {
|
|
157
|
-
const el = mount('<input-ui throttle="150"></input-ui>');
|
|
158
|
-
expect(el.throttle).toBe(150);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('dispatches `input` synchronously when throttle=0 (default)', async () => {
|
|
162
|
-
const el = mount('<input-ui></input-ui>');
|
|
163
|
-
await tick();
|
|
164
|
-
let count = 0;
|
|
165
|
-
// Filter for host-emitted CustomEvents (have detail.value); surface
|
|
166
|
-
// InputEvents bubble up too but have no detail property.
|
|
167
|
-
el.addEventListener('input', (e) => {
|
|
168
|
-
if (e.detail?.value !== undefined) count++;
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
// Simulate user typing — set value + dispatch DOM input on the
|
|
172
|
-
// contenteditable surface so the host's #onInput handler fires.
|
|
173
|
-
const surface = el.querySelector('[slot="text"]');
|
|
174
|
-
surface.textContent = 'a';
|
|
175
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
176
|
-
surface.textContent = 'ab';
|
|
177
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
178
|
-
surface.textContent = 'abc';
|
|
179
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
180
|
-
|
|
181
|
-
expect(count).toBe(3);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
it('collapses burst dispatches to one trailing `input` when throttle>0', async () => {
|
|
185
|
-
const el = mount('<input-ui throttle="20"></input-ui>');
|
|
186
|
-
await tick();
|
|
187
|
-
const events = [];
|
|
188
|
-
// Filter for host-emitted CustomEvents (have detail.value); surface
|
|
189
|
-
// InputEvents bubble up too but have no detail property.
|
|
190
|
-
el.addEventListener('input', (e) => {
|
|
191
|
-
if (e.detail?.value !== undefined) events.push(e.detail.value);
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
const surface = el.querySelector('[slot="text"]');
|
|
195
|
-
surface.textContent = 'a';
|
|
196
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
197
|
-
surface.textContent = 'ab';
|
|
198
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
199
|
-
surface.textContent = 'abc';
|
|
200
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
201
|
-
|
|
202
|
-
expect(events).toEqual([]);
|
|
203
|
-
|
|
204
|
-
await new Promise((r) => setTimeout(r, 35));
|
|
205
|
-
expect(events).toEqual(['abc']);
|
|
206
|
-
});
|
|
207
|
-
|
|
208
|
-
it('flushPendingInput() fires the pending dispatch synchronously', async () => {
|
|
209
|
-
const el = mount('<input-ui throttle="50"></input-ui>');
|
|
210
|
-
await tick();
|
|
211
|
-
const events = [];
|
|
212
|
-
// Filter for host-emitted CustomEvents (have detail.value); surface
|
|
213
|
-
// InputEvents bubble up too but have no detail property.
|
|
214
|
-
el.addEventListener('input', (e) => {
|
|
215
|
-
if (e.detail?.value !== undefined) events.push(e.detail.value);
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
const surface = el.querySelector('[slot="text"]');
|
|
219
|
-
surface.textContent = 'pending';
|
|
220
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
221
|
-
|
|
222
|
-
expect(events).toEqual([]);
|
|
223
|
-
el.flushPendingInput();
|
|
224
|
-
expect(events).toEqual(['pending']);
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
it('dropPendingInput() prevents the dispatch (used by disconnected)', async () => {
|
|
228
|
-
const el = mount('<input-ui throttle="20"></input-ui>');
|
|
229
|
-
await tick();
|
|
230
|
-
const events = [];
|
|
231
|
-
// Filter for host-emitted CustomEvents (have detail.value); surface
|
|
232
|
-
// InputEvents bubble up too but have no detail property.
|
|
233
|
-
el.addEventListener('input', (e) => {
|
|
234
|
-
if (e.detail?.value !== undefined) events.push(e.detail.value);
|
|
235
|
-
});
|
|
236
|
-
|
|
237
|
-
const surface = el.querySelector('[slot="text"]');
|
|
238
|
-
surface.textContent = 'gone';
|
|
239
|
-
surface.dispatchEvent(new InputEvent('input', { bubbles: true }));
|
|
240
|
-
|
|
241
|
-
el.dropPendingInput();
|
|
242
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
243
|
-
expect(events).toEqual([]);
|
|
244
|
-
});
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
// ── Placeholder caret-position regression guard ─────────────────────
|
|
248
|
-
//
|
|
249
|
-
// Per the v0.6.35 bug report: with the `::before { content: attr(data-placeholder) }`
|
|
250
|
-
// pseudo rendered in inline flow, typing-then-deleting left the caret
|
|
251
|
-
// visually at the END of the placeholder text (not at position 0) because
|
|
252
|
-
// the in-flow pseudo box pushed the caret rendering position to its right.
|
|
253
|
-
// Fix: pseudo is `position: absolute` so it doesn't occupy inline-flow
|
|
254
|
-
// space; the host carries `position: relative` as its anchor.
|
|
255
|
-
// These tests assert the CSS-source contract that prevents accidental
|
|
256
|
-
// revert to the in-flow pseudo shape.
|
|
257
|
-
|
|
258
|
-
describe('input-ui — CSS source contract: placeholder pseudo is out of flow', () => {
|
|
259
|
-
it('host [slot="text"] declares position: relative (pseudo anchor)', () => {
|
|
260
|
-
expect(INPUT_CSS).toMatch(
|
|
261
|
-
/\[slot="text"\]\s*\{[^}]*position:\s*relative/s
|
|
262
|
-
);
|
|
263
|
-
});
|
|
264
|
-
|
|
265
|
-
it('[data-empty]::before is position: absolute (not inline-flow)', () => {
|
|
266
|
-
expect(INPUT_CSS).toMatch(
|
|
267
|
-
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*position:\s*absolute/s
|
|
268
|
-
);
|
|
269
|
-
});
|
|
270
|
-
|
|
271
|
-
it('[data-empty]::before fills the host content-box (inset: 0 + padding: inherit)', () => {
|
|
272
|
-
expect(INPUT_CSS).toMatch(
|
|
273
|
-
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*inset:\s*0/s
|
|
274
|
-
);
|
|
275
|
-
expect(INPUT_CSS).toMatch(
|
|
276
|
-
/span\[slot="text"\]\[data-empty\]::before\s*\{[^}]*padding:\s*inherit/s
|
|
277
|
-
);
|
|
278
|
-
});
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
describe('input-ui — FEEDBACK-93 reactive prefix/suffix re-sync', () => {
|
|
282
|
-
const settle = () => new Promise((r) => setTimeout(r, 30));
|
|
283
|
-
|
|
284
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
285
|
-
|
|
286
|
-
function suffixOf(el) { return el.querySelector(':scope > [slot="field"] > [slot="suffix"]'); }
|
|
287
|
-
function prefixOf(el) { return el.querySelector(':scope > [slot="field"] > [slot="prefix"]'); }
|
|
288
|
-
|
|
289
|
-
it('resolves a reactive suffix binding instead of leaking the {{p:N}} marker (the bug)', async () => {
|
|
290
|
-
const container = document.createElement('div');
|
|
291
|
-
document.body.appendChild(container);
|
|
292
|
-
// Interpolated attribute → the engine writes {{p:0}} into `suffix` before
|
|
293
|
-
// connected() reads it, then resolves on the update() tick. Pre-fix the
|
|
294
|
-
// baked marker stuck; post-fix render() re-resolves it.
|
|
295
|
-
stamp(html`<input-ui type="number" suffix="${'px'}" value="2"></input-ui>`, container);
|
|
296
|
-
await settle();
|
|
297
|
-
|
|
298
|
-
const el = container.querySelector('input-ui');
|
|
299
|
-
const suffix = suffixOf(el);
|
|
300
|
-
expect(suffix).not.toBeNull();
|
|
301
|
-
expect(suffix.textContent).toBe('px');
|
|
302
|
-
expect(suffix.textContent).not.toMatch(/\{\{p:/);
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('resolves a reactive prefix binding', async () => {
|
|
306
|
-
const container = document.createElement('div');
|
|
307
|
-
document.body.appendChild(container);
|
|
308
|
-
stamp(html`<input-ui prefix="${'$'}" value="9.99"></input-ui>`, container);
|
|
309
|
-
await settle();
|
|
310
|
-
|
|
311
|
-
const el = container.querySelector('input-ui');
|
|
312
|
-
expect(prefixOf(el).textContent).toBe('$');
|
|
313
|
-
expect(prefixOf(el).textContent).not.toMatch(/\{\{p:/);
|
|
314
|
-
});
|
|
315
|
-
|
|
316
|
-
it('re-resolves when the bound value later changes', async () => {
|
|
317
|
-
const container = document.createElement('div');
|
|
318
|
-
document.body.appendChild(container);
|
|
319
|
-
stamp(html`<input-ui type="number" suffix="${'px'}" value="2"></input-ui>`, container);
|
|
320
|
-
await settle();
|
|
321
|
-
const el = container.querySelector('input-ui');
|
|
322
|
-
expect(suffixOf(el).textContent).toBe('px');
|
|
323
|
-
|
|
324
|
-
// Simulate the binding resolving to a new value on a later reactive tick.
|
|
325
|
-
el.setAttribute('suffix', 'rem');
|
|
326
|
-
await settle();
|
|
327
|
-
expect(suffixOf(el).textContent).toBe('rem');
|
|
328
|
-
});
|
|
329
|
-
|
|
330
|
-
it('literal suffix still renders (no regression)', async () => {
|
|
331
|
-
const el = mount('<input-ui suffix="%"></input-ui>');
|
|
332
|
-
await settle();
|
|
333
|
-
expect(suffixOf(el).textContent).toBe('%');
|
|
334
|
-
});
|
|
335
|
-
});
|