@adia-ai/web-components 0.8.18 → 0.8.20

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