@adia-ai/web-components 0.8.19 → 0.8.21

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +26 -0
  2. package/components/card/card.a2ui.json +6 -3
  3. package/components/card/card.css +15 -1
  4. package/components/card/card.yaml +5 -3
  5. package/components/nav-group/nav-group.a2ui.json +5 -0
  6. package/components/nav-group/nav-group.class.js +14 -6
  7. package/components/nav-group/nav-group.css +5 -2
  8. package/components/nav-group/nav-group.d.ts +2 -0
  9. package/components/nav-group/nav-group.yaml +4 -0
  10. package/components/select/select.a2ui.json +3 -2
  11. package/components/select/select.class.js +14 -1
  12. package/components/select/select.css +13 -1
  13. package/components/select/select.yaml +5 -4
  14. package/components/table/table.class.js +7 -3
  15. package/components/table/table.css +7 -5
  16. package/dist/theme-provider.min.js +1 -1
  17. package/dist/web-components.min.css +1 -1
  18. package/dist/web-components.min.js +19 -19
  19. package/dist/web-components.sheet.js +1 -1
  20. package/package.json +2 -1
  21. package/components/adia-mark/adia-mark.test.js +0 -69
  22. package/components/adia-wordmark/adia-wordmark.test.js +0 -69
  23. package/components/agent-artifact/agent-artifact.test.js +0 -61
  24. package/components/alert/alert.test.js +0 -364
  25. package/components/color-picker/color-picker.test.js +0 -96
  26. package/components/combobox/combobox.test.js +0 -256
  27. package/components/command/command.test.js +0 -99
  28. package/components/date-range-picker/date-range-picker.test.js +0 -387
  29. package/components/datetime-picker/datetime-picker.test.js +0 -454
  30. package/components/demo-toggle/demo-toggle.test.js +0 -102
  31. package/components/description-list/description-list.test.js +0 -72
  32. package/components/display-field/display-field.test.js +0 -115
  33. package/components/empty-state/empty-state.test.js +0 -77
  34. package/components/field/field.test.js +0 -260
  35. package/components/fields/fields.test.js +0 -88
  36. package/components/inline-message/inline-message.test.js +0 -203
  37. package/components/input/input.test.js +0 -335
  38. package/components/integration-card/integration-card.test.js +0 -318
  39. package/components/link/link.test.js +0 -143
  40. package/components/list/list.test.js +0 -106
  41. package/components/list-window/list-window.test.js +0 -303
  42. package/components/loading-overlay/loading-overlay.test.js +0 -257
  43. package/components/menu/menu.test.js +0 -176
  44. package/components/nav/nav.test.js +0 -428
  45. package/components/nav-group/nav-group.test.js +0 -67
  46. package/components/option-card/option-card.test.js +0 -32
  47. package/components/pagination/pagination.test.js +0 -195
  48. package/components/select/select.test.js +0 -481
  49. package/components/slider/slider.test.js +0 -202
  50. package/components/spinner/spinner.test.js +0 -272
  51. package/components/stat/stat.test.js +0 -108
  52. package/components/step-progress/step-progress.test.js +0 -129
  53. package/components/swatch/swatch.test.js +0 -563
  54. package/components/table/table.test.js +0 -479
  55. package/components/table-toolbar/table-toolbar.test.js +0 -152
  56. package/components/tag/tag.test.js +0 -137
  57. package/components/tags-input/tags-input.test.js +0 -477
  58. package/components/text/text.test.js +0 -202
  59. package/components/theme-provider/theme-provider.test.js +0 -94
  60. package/components/time-picker/time-picker.test.js +0 -287
  61. package/components/timeline/timeline.test.js +0 -31
  62. package/components/toggle-scheme/toggle-scheme.test.js +0 -110
  63. package/components/tree/tree.test.js +0 -312
  64. package/core/element.test.js +0 -389
  65. package/core/icons.test.js +0 -187
  66. package/core/logical-children.test.js +0 -54
  67. package/core/markdown.test.js +0 -47
  68. package/core/responsive.test.js +0 -121
  69. package/core/template.test.js +0 -611
  70. package/traits/active-state/active-state.test.js +0 -28
  71. package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
  72. package/traits/announcer/announcer.test.js +0 -268
  73. package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
  74. package/traits/attention-pulse/attention-pulse.test.js +0 -26
  75. package/traits/confetti/confetti.test.js +0 -46
  76. package/traits/confetti-burst/confetti-burst.test.js +0 -46
  77. package/traits/count-up/count-up.test.js +0 -54
  78. package/traits/declarative.test.js +0 -138
  79. package/traits/dirty-state/dirty-state.test.js +0 -45
  80. package/traits/drag-ghost/drag-ghost.test.js +0 -19
  81. package/traits/draggable/draggable.test.js +0 -60
  82. package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
  83. package/traits/drop-target/drop-target.test.js +0 -241
  84. package/traits/droppable/droppable.test.js +0 -54
  85. package/traits/droppable-collection/droppable-collection.test.js +0 -99
  86. package/traits/error-shake/error-shake.test.js +0 -114
  87. package/traits/fade-presence/fade-presence.test.js +0 -20
  88. package/traits/focus-restore/focus-restore.test.js +0 -202
  89. package/traits/focus-trap/focus-trap.test.js +0 -42
  90. package/traits/focusable/focusable.test.js +0 -53
  91. package/traits/glow-focus/glow-focus.test.js +0 -31
  92. package/traits/gradient-shift/gradient-shift.test.js +0 -22
  93. package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
  94. package/traits/hotkey/hotkey.test.js +0 -61
  95. package/traits/hoverable/hoverable.test.js +0 -24
  96. package/traits/inertia-drag/inertia-drag.test.js +0 -33
  97. package/traits/input-mask/input-mask.test.js +0 -151
  98. package/traits/intersection-observer/intersection-observer.test.js +0 -38
  99. package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
  100. package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
  101. package/traits/layout-animation/layout-animation.test.js +0 -114
  102. package/traits/long-press/long-press.test.js +0 -244
  103. package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
  104. package/traits/noise-texture/noise-texture.test.js +0 -20
  105. package/traits/parallax/parallax.test.js +0 -26
  106. package/traits/portal/portal.test.js +0 -30
  107. package/traits/pressable/pressable.test.js +0 -73
  108. package/traits/resettable/resettable.test.js +0 -100
  109. package/traits/resizable/resizable.test.js +0 -39
  110. package/traits/resize-observer/resize-observer.test.js +0 -38
  111. package/traits/ripple/ripple.test.js +0 -32
  112. package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
  113. package/traits/scale-press/scale-press.test.js +0 -39
  114. package/traits/scroll-lock/scroll-lock.test.js +0 -45
  115. package/traits/scroll-progress/scroll-progress.test.js +0 -182
  116. package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
  117. package/traits/smoke.test.js +0 -111
  118. package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
  119. package/traits/sound-feedback/sound-feedback.test.js +0 -26
  120. package/traits/spring-animate/spring-animate.test.js +0 -28
  121. package/traits/success-checkmark/success-checkmark.test.js +0 -120
  122. package/traits/tilt-hover/tilt-hover.test.js +0 -32
  123. package/traits/tossable/tossable.test.js +0 -31
  124. package/traits/traits-host.test.js +0 -73
  125. package/traits/typeahead/typeahead.test.js +0 -38
  126. package/traits/typewriter/typewriter.test.js +0 -47
  127. package/traits/validation/validation.test.js +0 -93
  128. package/traits/view-transition/view-transition.test.js +0 -268
@@ -1,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
- });