@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
package/core/template.test.js
DELETED
|
@@ -1,611 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* core/template.js — focused tests for the §250 (v0.5.11) `?attr=${bool}`
|
|
3
|
-
* silent-failure-trap close. Surfaced by FEEDBACK-27 against v0.5.10 source.
|
|
4
|
-
*
|
|
5
|
-
* Pre-§250: `?disabled=${true}` registered a literal `?disabled` DOM
|
|
6
|
-
* attribute with no console warning. Consumer ergonomics gap — Lit-familiar
|
|
7
|
-
* developers reach for the most ergonomic syntax + ship inert bindings.
|
|
8
|
-
*
|
|
9
|
-
* Post-§250: scan-time `console.warn` fires + the bogus part is degraded to
|
|
10
|
-
* a no-op text-node, so nothing reaches the DOM. Migration path is
|
|
11
|
-
* `.attr=${bool}` (property binding) — primitives reflect properties to DOM
|
|
12
|
-
* attributes automatically.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
16
|
-
import { html, stamp, repeat } from './template.js';
|
|
17
|
-
|
|
18
|
-
describe('html template — §250 (v0.5.11) ?attr=${bool} silent-failure trap', () => {
|
|
19
|
-
let container;
|
|
20
|
-
let warnSpy;
|
|
21
|
-
|
|
22
|
-
beforeEach(() => {
|
|
23
|
-
container = document.createElement('div');
|
|
24
|
-
document.body.appendChild(container);
|
|
25
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
afterEach(() => {
|
|
29
|
-
container.remove();
|
|
30
|
-
warnSpy.mockRestore();
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('fires console.warn at scan time when ?attr=${bool} is encountered', () => {
|
|
34
|
-
const tpl = html`<div ?disabled=${true}></div>`;
|
|
35
|
-
stamp(tpl, container);
|
|
36
|
-
|
|
37
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
38
|
-
const msg = warnSpy.mock.calls[0][0];
|
|
39
|
-
expect(msg).toMatch(/Lit-style boolean attribute/);
|
|
40
|
-
expect(msg).toMatch(/\?disabled=/);
|
|
41
|
-
expect(msg).toMatch(/\.disabled=/); // migration path mentioned
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('does NOT register a literal "?attr" attribute on the DOM node', () => {
|
|
45
|
-
const tpl = html`<div ?disabled=${true} data-test="probe"></div>`;
|
|
46
|
-
stamp(tpl, container);
|
|
47
|
-
|
|
48
|
-
const node = container.querySelector('[data-test="probe"]');
|
|
49
|
-
expect(node).not.toBeNull();
|
|
50
|
-
expect(node.getAttribute('?disabled')).toBeNull();
|
|
51
|
-
expect(node.hasAttribute('?disabled')).toBe(false);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('does NOT set the matching DOM attribute either (no Lit-attr semantics)', () => {
|
|
55
|
-
// AdiaUI intentionally does not implement `?attr=${bool}` semantics.
|
|
56
|
-
// The warn + degrade-to-no-op path means truthy `?disabled` does NOT
|
|
57
|
-
// add `[disabled]` to the DOM. Consumer must use `.disabled=` for that.
|
|
58
|
-
const tpl = html`<div ?disabled=${true} data-test="probe"></div>`;
|
|
59
|
-
stamp(tpl, container);
|
|
60
|
-
|
|
61
|
-
const node = container.querySelector('[data-test="probe"]');
|
|
62
|
-
expect(node.hasAttribute('disabled')).toBe(false);
|
|
63
|
-
});
|
|
64
|
-
|
|
65
|
-
it('fires the warn even when the bound value is falsy', () => {
|
|
66
|
-
// Lit's `?attr=${false}` removes the attribute. AdiaUI's contract is
|
|
67
|
-
// "this syntax is not supported, period" — warn fires regardless of
|
|
68
|
-
// truthiness to maximize visibility of the authoring error.
|
|
69
|
-
const tpl = html`<div ?disabled=${false}></div>`;
|
|
70
|
-
stamp(tpl, container);
|
|
71
|
-
|
|
72
|
-
expect(warnSpy).toHaveBeenCalledTimes(1);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
it('does not warn on canonical .prop=${val} (no false positive)', () => {
|
|
76
|
-
// Make sure the new `?`-prefix branch doesn't accidentally fire on
|
|
77
|
-
// adjacent canonical syntaxes.
|
|
78
|
-
const tpl = html`<div .className=${'x'}></div>`;
|
|
79
|
-
stamp(tpl, container);
|
|
80
|
-
|
|
81
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('does not warn on canonical @event=${handler} (no false positive)', () => {
|
|
85
|
-
const tpl = html`<div @click=${() => {}}></div>`;
|
|
86
|
-
stamp(tpl, container);
|
|
87
|
-
|
|
88
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
89
|
-
});
|
|
90
|
-
|
|
91
|
-
it('does not warn on canonical attr=${value} (no false positive)', () => {
|
|
92
|
-
const tpl = html`<div data-id=${'42'}></div>`;
|
|
93
|
-
stamp(tpl, container);
|
|
94
|
-
|
|
95
|
-
expect(warnSpy).not.toHaveBeenCalled();
|
|
96
|
-
});
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
describe('html template — FB-40 (v0.5.16) apostrophe in HTML comments does NOT mis-classify subsequent placeholders', () => {
|
|
100
|
-
// FEEDBACK-06 §1 P0 (long resolved) made `inTag()` comment-aware:
|
|
101
|
-
// when scan() hits `<!--` outside a string context, it jumps past
|
|
102
|
-
// `-->` wholesale. FB-40 (filed 2026-05-16 post-v0.5.16 cut) reported
|
|
103
|
-
// crashes attributed to apostrophes in comments. This suite verifies
|
|
104
|
-
// empirically that the comment-aware logic still holds — the FB-40
|
|
105
|
-
// diagnosis is mistaken about the mechanism (the existing fix covers
|
|
106
|
-
// the apostrophe-in-comment case).
|
|
107
|
-
|
|
108
|
-
let container;
|
|
109
|
-
let warnSpy;
|
|
110
|
-
|
|
111
|
-
beforeEach(() => {
|
|
112
|
-
container = document.createElement('div');
|
|
113
|
-
document.body.appendChild(container);
|
|
114
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
afterEach(() => {
|
|
118
|
-
container.remove();
|
|
119
|
-
warnSpy.mockRestore();
|
|
120
|
-
});
|
|
121
|
-
|
|
122
|
-
it('@event=${handler} placeholder AFTER apostrophe-bearing comment is classified as attribute-position', () => {
|
|
123
|
-
let received = null;
|
|
124
|
-
const handler = (e) => { received = e; };
|
|
125
|
-
const tpl = html`
|
|
126
|
-
<div>
|
|
127
|
-
<!-- editor-shell's named slots -->
|
|
128
|
-
<button data-test="probe" @click=${handler}>X</button>
|
|
129
|
-
</div>
|
|
130
|
-
`;
|
|
131
|
-
stamp(tpl, container);
|
|
132
|
-
const btn = container.querySelector('[data-test="probe"]');
|
|
133
|
-
expect(btn).not.toBeNull();
|
|
134
|
-
btn.click();
|
|
135
|
-
expect(received).not.toBeNull();
|
|
136
|
-
expect(received.type).toBe('click');
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('double-quote in HTML comment does not mis-classify subsequent attribute placeholder', () => {
|
|
140
|
-
const tpl = html`
|
|
141
|
-
<div>
|
|
142
|
-
<!-- the "Stop Override" button below -->
|
|
143
|
-
<button data-test="probe" data-id=${'42'}></button>
|
|
144
|
-
</div>
|
|
145
|
-
`;
|
|
146
|
-
stamp(tpl, container);
|
|
147
|
-
const btn = container.querySelector('[data-test="probe"]');
|
|
148
|
-
expect(btn).not.toBeNull();
|
|
149
|
-
expect(btn.getAttribute('data-id')).toBe('42');
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
it('multiple apostrophes across multiple comments still keep subsequent placeholders attribute-position', () => {
|
|
153
|
-
let received = null;
|
|
154
|
-
const tpl = html`
|
|
155
|
-
<div>
|
|
156
|
-
<!-- one's first comment -->
|
|
157
|
-
<span>x</span>
|
|
158
|
-
<!-- two's second comment -->
|
|
159
|
-
<button data-test="probe" @click=${(e) => { received = e; }}></button>
|
|
160
|
-
</div>
|
|
161
|
-
`;
|
|
162
|
-
stamp(tpl, container);
|
|
163
|
-
container.querySelector('[data-test="probe"]').click();
|
|
164
|
-
expect(received).not.toBeNull();
|
|
165
|
-
});
|
|
166
|
-
|
|
167
|
-
it('apostrophe + em-dash (U+2014) in a single comment does not break parsing', () => {
|
|
168
|
-
// dts-export-drawer.ts case from FB-40's cited crashes:
|
|
169
|
-
// <!-- 2026-05-14 — migrated number inputs ... -->
|
|
170
|
-
let received = null;
|
|
171
|
-
const tpl = html`
|
|
172
|
-
<div>
|
|
173
|
-
<!-- 2026-05-14 — author's notes about migrated number inputs -->
|
|
174
|
-
<button data-test="probe" @click=${(e) => { received = e; }}></button>
|
|
175
|
-
</div>
|
|
176
|
-
`;
|
|
177
|
-
stamp(tpl, container);
|
|
178
|
-
container.querySelector('[data-test="probe"]').click();
|
|
179
|
-
expect(received).not.toBeNull();
|
|
180
|
-
});
|
|
181
|
-
});
|
|
182
|
-
|
|
183
|
-
describe('html template — FB-47 (v0.5.19) stamp() cache + repeat() keyed reuse', () => {
|
|
184
|
-
// FEEDBACK-47 claimed `stamp()` unconditionally calls `mount()` →
|
|
185
|
-
// `replaceChildren()`, defeating `repeat()` keyed reconciliation. Source
|
|
186
|
-
// inspection (template.js:94-102) shows `stamp()` has a per-container
|
|
187
|
-
// cache keyed on `result.strings`; `mount()` runs ONLY on cache miss
|
|
188
|
-
// (first stamp on this container OR new template strings). These tests
|
|
189
|
-
// verify the cache behavior empirically against the same-template
|
|
190
|
-
// re-stamp + repeat()-same-key paths.
|
|
191
|
-
|
|
192
|
-
let container;
|
|
193
|
-
|
|
194
|
-
beforeEach(() => {
|
|
195
|
-
container = document.createElement('div');
|
|
196
|
-
document.body.appendChild(container);
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
afterEach(() => {
|
|
200
|
-
container.remove();
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('stamp() with same template strings reuses the mounted instance (no replaceChildren)', () => {
|
|
204
|
-
// Build a template factory whose strings literal is reference-stable
|
|
205
|
-
// (the tagged-template caller — closure captures `strings` once).
|
|
206
|
-
const make = (value) => html`<div class="probe">${value}</div>`;
|
|
207
|
-
|
|
208
|
-
stamp(make('first'), container);
|
|
209
|
-
const firstDiv = container.querySelector('.probe');
|
|
210
|
-
expect(firstDiv).not.toBeNull();
|
|
211
|
-
expect(firstDiv.textContent).toBe('first');
|
|
212
|
-
|
|
213
|
-
// Mark the existing node so we can prove it survives the re-stamp.
|
|
214
|
-
firstDiv.dataset.marker = 'preserved';
|
|
215
|
-
|
|
216
|
-
stamp(make('second'), container);
|
|
217
|
-
const secondDiv = container.querySelector('.probe');
|
|
218
|
-
expect(secondDiv).toBe(firstDiv); // ✅ same node
|
|
219
|
-
expect(secondDiv.dataset.marker).toBe('preserved'); // ✅ no replaceChildren
|
|
220
|
-
expect(secondDiv.textContent).toBe('second'); // ✅ value updated in-place
|
|
221
|
-
});
|
|
222
|
-
|
|
223
|
-
it('repeat() with same key preserves wrapper children across re-renders', () => {
|
|
224
|
-
// FB-47's central reproduction: same-key repeat() should preserve the
|
|
225
|
-
// per-item wrapper-span AND the child element stamped into it on the
|
|
226
|
-
// prior render. Both the outer template AND the per-item template must
|
|
227
|
-
// share strings references across renders (single tagged-template-literal
|
|
228
|
-
// sites) — JS spec gives each source site its own strings array, so
|
|
229
|
-
// inline-duplicating the html`` between stamp calls would force a
|
|
230
|
-
// cache miss and defeat preservation. Real consumers naturally satisfy
|
|
231
|
-
// this by having one render function called repeatedly.
|
|
232
|
-
const items = [{ id: 'a', label: 'first' }];
|
|
233
|
-
const tplFn = (item) => html`<div class="probe" data-id=${item.id}>${item.label}</div>`;
|
|
234
|
-
const renderAll = (its) => html`${repeat(its, (x) => x.id, tplFn)}`;
|
|
235
|
-
|
|
236
|
-
stamp(renderAll(items), container);
|
|
237
|
-
const firstDiv = container.querySelector('.probe[data-id="a"]');
|
|
238
|
-
expect(firstDiv).not.toBeNull();
|
|
239
|
-
|
|
240
|
-
// Append a manually-added child that the framework should NOT destroy.
|
|
241
|
-
const probe = document.createElement('span');
|
|
242
|
-
probe.id = 'fb47-manual-child';
|
|
243
|
-
probe.textContent = 'sentinel';
|
|
244
|
-
firstDiv.appendChild(probe);
|
|
245
|
-
|
|
246
|
-
// Re-stamp with the same items array (same key 'a').
|
|
247
|
-
items[0] = { id: 'a', label: 'second' };
|
|
248
|
-
stamp(renderAll(items), container);
|
|
249
|
-
|
|
250
|
-
const secondDiv = container.querySelector('.probe[data-id="a"]');
|
|
251
|
-
expect(secondDiv).toBe(firstDiv); // ✅ wrapper reused
|
|
252
|
-
expect(secondDiv.querySelector('#fb47-manual-child')).not.toBeNull();
|
|
253
|
-
expect(secondDiv.firstChild.nodeValue).toBe('second'); // text updated in-place
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
it('Array.isArray() branch in applyValue does replace children on every re-render (the actual root cause)', () => {
|
|
257
|
-
// FB-47's empirical observation IS valid for `.map()` — the
|
|
258
|
-
// Array.isArray() branch in applyValue() (template.js:232-244) does
|
|
259
|
-
// unconditional `container.replaceChildren()` + allocates fresh wrapper
|
|
260
|
-
// spans per item. Switching consumers to repeat() avoids this. This
|
|
261
|
-
// test pins the current behavior so any future "make .map() smart"
|
|
262
|
-
// change is intentional, not accidental. Use a single render function
|
|
263
|
-
// (one tagged-template-literal site for the outer template) so the
|
|
264
|
-
// observed re-creation is isolated to the .map() Array branch, not
|
|
265
|
-
// a cache miss from inline-duplicated source sites.
|
|
266
|
-
const items = [{ id: 'a', label: 'first' }];
|
|
267
|
-
const tplFn = (item) => html`<div class="probe" data-id=${item.id}>${item.label}</div>`;
|
|
268
|
-
const renderAll = (its) => html`${its.map(tplFn)}`;
|
|
269
|
-
|
|
270
|
-
stamp(renderAll(items), container);
|
|
271
|
-
const firstDiv = container.querySelector('.probe[data-id="a"]');
|
|
272
|
-
expect(firstDiv).not.toBeNull();
|
|
273
|
-
|
|
274
|
-
items[0] = { id: 'a', label: 'second' };
|
|
275
|
-
stamp(renderAll(items), container);
|
|
276
|
-
|
|
277
|
-
const secondDiv = container.querySelector('.probe[data-id="a"]');
|
|
278
|
-
expect(secondDiv).not.toBe(firstDiv); // ✅ NEW node — .map() re-creates
|
|
279
|
-
expect(secondDiv.textContent).toBe('second');
|
|
280
|
-
});
|
|
281
|
-
});
|
|
282
|
-
|
|
283
|
-
describe('html template — FB-55 (v0.6.8) `.camelCase=${expr}` property-binding lowercase trap close', () => {
|
|
284
|
-
// FEEDBACK-55: the HTML parser lowercases attribute names inside
|
|
285
|
-
// <template>.innerHTML per HTML5 §13.2.5.32. Pre-fix, `.className=` arrived
|
|
286
|
-
// at scan() as `.classname`, then `name.slice(1)` yielded `"classname"`,
|
|
287
|
-
// and `applyValue()` wrote `p.n["classname"] = v` — an enumerable expando,
|
|
288
|
-
// never invoking the camelCase property setter. Classes never applied; no
|
|
289
|
-
// warn, no error.
|
|
290
|
-
//
|
|
291
|
-
// Fix: PROP_CASE_FIX static map (for built-in DOM camelCase props) +
|
|
292
|
-
// prototype-walk fallback (for UIElement-defined custom-element props).
|
|
293
|
-
// Backward-compatible: when no case-insensitive match exists, the
|
|
294
|
-
// original lowercase name is preserved (genuine expando consumers
|
|
295
|
-
// unaffected).
|
|
296
|
-
//
|
|
297
|
-
// RESPONSE-55 documents the trap end-to-end + reviewer-#A scope
|
|
298
|
-
// expansion to UIElement primitives.
|
|
299
|
-
|
|
300
|
-
let container;
|
|
301
|
-
let warnSpy;
|
|
302
|
-
|
|
303
|
-
beforeEach(() => {
|
|
304
|
-
container = document.createElement('div');
|
|
305
|
-
document.body.appendChild(container);
|
|
306
|
-
warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
307
|
-
});
|
|
308
|
-
|
|
309
|
-
afterEach(() => {
|
|
310
|
-
container.remove();
|
|
311
|
-
warnSpy.mockRestore();
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('.className=${expr} writes to the real className property (not lowercase expando)', () => {
|
|
315
|
-
const tpl = html`<span .className=${'foo bar'}>x</span>`;
|
|
316
|
-
stamp(tpl, container);
|
|
317
|
-
const span = container.querySelector('span');
|
|
318
|
-
expect(span.className).toBe('foo bar'); // ✅ real className set
|
|
319
|
-
expect(span.getAttribute('class')).toBe('foo bar'); // ✅ reflected to DOM attr
|
|
320
|
-
expect(span.classname).toBeUndefined(); // ✅ NO lowercase expando
|
|
321
|
-
});
|
|
322
|
-
|
|
323
|
-
it('.tabIndex=${n} writes to the real tabIndex property', () => {
|
|
324
|
-
const tpl = html`<div .tabIndex=${3}></div>`;
|
|
325
|
-
stamp(tpl, container);
|
|
326
|
-
const div = container.querySelector('div');
|
|
327
|
-
expect(div.tabIndex).toBe(3);
|
|
328
|
-
expect(div.getAttribute('tabindex')).toBe('3');
|
|
329
|
-
});
|
|
330
|
-
|
|
331
|
-
it('.innerHTML=${str} survives unchanged (already-lowercase name)', () => {
|
|
332
|
-
// .innerHTML is one of the few camelCase-looking names whose
|
|
333
|
-
// canonical form is already lowercase (well, it's `innerHTML` but
|
|
334
|
-
// the parser lowercases it to `innerhtml` AND PROP_CASE_FIX maps
|
|
335
|
-
// back to `innerHTML`). Verify it lands on the property regardless.
|
|
336
|
-
const tpl = html`<div .innerHTML=${'<span class="inner">payload</span>'}></div>`;
|
|
337
|
-
stamp(tpl, container);
|
|
338
|
-
const div = container.querySelector('div');
|
|
339
|
-
expect(div.querySelector('.inner')?.textContent).toBe('payload');
|
|
340
|
-
});
|
|
341
|
-
|
|
342
|
-
it('.lowercaseProp=${v} on a stock element preserves expando semantics (backward compat)', () => {
|
|
343
|
-
// The fallback prototype walk skips Object.prototype to avoid noisy
|
|
344
|
-
// matches. A genuinely-lowercase property name with no matching
|
|
345
|
-
// camelCase pair on the chain (e.g. `.myexpando`) lands on the
|
|
346
|
-
// element as a lowercase expando — the documented escape hatch
|
|
347
|
-
// for arbitrary data attachment. Verify no regression.
|
|
348
|
-
const tpl = html`<div .myexpando=${'attached'}></div>`;
|
|
349
|
-
stamp(tpl, container);
|
|
350
|
-
const div = container.querySelector('div');
|
|
351
|
-
expect(div.myexpando).toBe('attached');
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it('.camelCaseProp=${v} on a UIElement-style custom element invokes the canonical setter', async () => {
|
|
355
|
-
// Reviewer-addition #A from RESPONSE-55: UIElement defines
|
|
356
|
-
// properties under camelCase keys via Object.defineProperty. The
|
|
357
|
-
// pre-fix trap wrote `el['camelprop'] = v` (expando) instead of
|
|
358
|
-
// invoking the setter. Verify the fix routes correctly through
|
|
359
|
-
// the prototype-walk fallback.
|
|
360
|
-
class FB55El extends HTMLElement {
|
|
361
|
-
constructor() {
|
|
362
|
-
super();
|
|
363
|
-
let _camelProp = '';
|
|
364
|
-
Object.defineProperty(this, 'camelProp', {
|
|
365
|
-
get() { return _camelProp; },
|
|
366
|
-
set(v) { _camelProp = v; this.dataset.lastSet = v; },
|
|
367
|
-
configurable: true,
|
|
368
|
-
enumerable: true,
|
|
369
|
-
});
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
if (!customElements.get('fb55-el')) customElements.define('fb55-el', FB55El);
|
|
373
|
-
|
|
374
|
-
const tpl = html`<fb55-el .camelProp=${'via-setter'}></fb55-el>`;
|
|
375
|
-
stamp(tpl, container);
|
|
376
|
-
const el = container.querySelector('fb55-el');
|
|
377
|
-
expect(el.camelProp).toBe('via-setter'); // ✅ getter returns set value
|
|
378
|
-
expect(el.dataset.lastSet).toBe('via-setter'); // ✅ setter side-effect ran
|
|
379
|
-
expect(el.camelprop).toBeUndefined(); // ✅ NO lowercase expando
|
|
380
|
-
});
|
|
381
|
-
|
|
382
|
-
it('.maxChroma=${n} on a UIElement-style ce reaches the camelCase property', () => {
|
|
383
|
-
// Real-world surface: <color-picker max-chroma> and <color-input
|
|
384
|
-
// max-chroma> declare `maxChroma` as a static property. Consumers
|
|
385
|
-
// reaching for `.maxChroma=${value}` template binding must invoke
|
|
386
|
-
// the setter, not write an expando.
|
|
387
|
-
class FB55Numeric extends HTMLElement {
|
|
388
|
-
constructor() {
|
|
389
|
-
super();
|
|
390
|
-
let _maxChroma = 0;
|
|
391
|
-
Object.defineProperty(this, 'maxChroma', {
|
|
392
|
-
get() { return _maxChroma; },
|
|
393
|
-
set(v) { _maxChroma = v; },
|
|
394
|
-
configurable: true, enumerable: true,
|
|
395
|
-
});
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
if (!customElements.get('fb55-num')) customElements.define('fb55-num', FB55Numeric);
|
|
399
|
-
|
|
400
|
-
const tpl = html`<fb55-num .maxChroma=${0.42}></fb55-num>`;
|
|
401
|
-
stamp(tpl, container);
|
|
402
|
-
const el = container.querySelector('fb55-num');
|
|
403
|
-
expect(el.maxChroma).toBe(0.42);
|
|
404
|
-
});
|
|
405
|
-
|
|
406
|
-
it('updates to .className=${expr} re-route through the setter on every value change', () => {
|
|
407
|
-
// The PROP_CASE_FIX resolution is cached in parts[i].name at scan()
|
|
408
|
-
// time. Subsequent update() ticks must continue to use the resolved
|
|
409
|
-
// camelCase name. Verify by re-stamping with a new value.
|
|
410
|
-
const make = (cls) => html`<span .className=${cls}>x</span>`;
|
|
411
|
-
stamp(make('a'), container);
|
|
412
|
-
let span = container.querySelector('span');
|
|
413
|
-
expect(span.className).toBe('a');
|
|
414
|
-
|
|
415
|
-
stamp(make('b'), container);
|
|
416
|
-
span = container.querySelector('span');
|
|
417
|
-
expect(span.className).toBe('b'); // ✅ second update routes correctly
|
|
418
|
-
expect(span.getAttribute('class')).toBe('b');
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
it('warning text no longer recommends .className= as the FIRST option for class= partial interp', () => {
|
|
422
|
-
// FB-55 #2: pre-fix, the v0.5.5 §184 warn text put `.className=`
|
|
423
|
-
// first; post-fix, `class="${expr}"` (the discoverable form) leads.
|
|
424
|
-
// Both work now (FB-55 #1), but the recommended ordering matters
|
|
425
|
-
// for new consumers reading the warning cold.
|
|
426
|
-
const cls = 'foo';
|
|
427
|
-
const tpl = html`<span class="prefix ${cls}">x</span>`;
|
|
428
|
-
stamp(tpl, container);
|
|
429
|
-
expect(warnSpy).toHaveBeenCalled();
|
|
430
|
-
const msg = warnSpy.mock.calls[0][0];
|
|
431
|
-
const classFullPos = msg.indexOf('class="${expression}"');
|
|
432
|
-
const classNamePos = msg.indexOf('.className=${expression}');
|
|
433
|
-
expect(classFullPos).toBeGreaterThan(-1);
|
|
434
|
-
expect(classNamePos).toBeGreaterThan(-1);
|
|
435
|
-
expect(classFullPos).toBeLessThan(classNamePos); // ✅ class="" first, .className= second
|
|
436
|
-
});
|
|
437
|
-
|
|
438
|
-
it('warning text dropped the .classList= aspirational line (FB-55 #2)', () => {
|
|
439
|
-
// Pre-fix the warn text mentioned `.classList=${{foo: true, bar: false}}`
|
|
440
|
-
// as "if/when implemented". classList is a read-only DOMTokenList
|
|
441
|
-
// accessor — no PROP_CASE_FIX could make it assignable, so the line
|
|
442
|
-
// was misleading. Post-fix it's removed.
|
|
443
|
-
const cls = 'foo';
|
|
444
|
-
const tpl = html`<span class="prefix ${cls}">x</span>`;
|
|
445
|
-
stamp(tpl, container);
|
|
446
|
-
const msg = warnSpy.mock.calls[0][0];
|
|
447
|
-
expect(msg).not.toMatch(/\.classList=/);
|
|
448
|
-
});
|
|
449
|
-
|
|
450
|
-
it('does not regress: canonical .style=${cssText}, .title=${str}, .value=${v} still work', () => {
|
|
451
|
-
// These property names are already lowercase, so they survived
|
|
452
|
-
// the parser pre-fix. Verify PROP_CASE_FIX didn't break them.
|
|
453
|
-
const tpl = html`<input .title=${'tip'} .value=${'val'}>`;
|
|
454
|
-
stamp(tpl, container);
|
|
455
|
-
const input = container.querySelector('input');
|
|
456
|
-
expect(input.title).toBe('tip');
|
|
457
|
-
expect(input.value).toBe('val');
|
|
458
|
-
});
|
|
459
|
-
});
|
|
460
|
-
|
|
461
|
-
describe('html template — FB-57 (v0.6.8) nested SVG/MathML template namespace re-routing', () => {
|
|
462
|
-
// FEEDBACK-57: nested `${html`<svg-child/>`}` interpolations produce
|
|
463
|
-
// HTML-namespaced elements (HTMLUnknownElement) instead of SVGElement.
|
|
464
|
-
// Root cause: each nested html`` is a SEPARATE cached template, and
|
|
465
|
-
// `tpl.innerHTML = m` is parsed at document level without SVG context.
|
|
466
|
-
// The <span style="display:contents"> wrapper from wrap() doesn't fix
|
|
467
|
-
// it because display:contents is layout-level not namespace-level;
|
|
468
|
-
// SVG layout is namespace-strict → invisible-but-present output.
|
|
469
|
-
//
|
|
470
|
-
// Fix: namespace-aware mount() — when the container is inside an SVG
|
|
471
|
-
// (or MathML) subtree, recursively re-namespace the cloned fragment
|
|
472
|
-
// via createElementNS. <foreignObject> and <annotation-xml
|
|
473
|
-
// encoding=text/html> revert to HTML per HTML5 §12.2.5 foreign-content
|
|
474
|
-
// insertion mode.
|
|
475
|
-
//
|
|
476
|
-
// RESPONSE-57 documents the trap end-to-end + the implementation-site
|
|
477
|
-
// choice (mount() not getTemplate(), to keep cache coherent).
|
|
478
|
-
|
|
479
|
-
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
480
|
-
const MATHML_NS = 'http://www.w3.org/1998/Math/MathML';
|
|
481
|
-
|
|
482
|
-
let container;
|
|
483
|
-
|
|
484
|
-
beforeEach(() => {
|
|
485
|
-
container = document.createElement('div');
|
|
486
|
-
document.body.appendChild(container);
|
|
487
|
-
});
|
|
488
|
-
|
|
489
|
-
afterEach(() => {
|
|
490
|
-
container.remove();
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
it('inline <svg><circle/></svg> in a single template keeps working (regression)', () => {
|
|
494
|
-
// Pre-fix this case already worked because the HTML parser sees
|
|
495
|
-
// <svg> in tpl.innerHTML and enters SVG insertion mode for its
|
|
496
|
-
// descendants. Post-fix it must still work — the container is
|
|
497
|
-
// HTML-namespaced (<div>), so foreignContentNS() returns null,
|
|
498
|
-
// re-namespacing is skipped, and the inline parser's NS survives.
|
|
499
|
-
const tpl = html`<svg viewBox="0 0 10 10"><circle cx="5" cy="5" r="3"/></svg>`;
|
|
500
|
-
stamp(tpl, container);
|
|
501
|
-
const svg = container.querySelector('svg');
|
|
502
|
-
const circle = container.querySelector('circle');
|
|
503
|
-
expect(svg.namespaceURI).toBe(SVG_NS);
|
|
504
|
-
expect(circle.namespaceURI).toBe(SVG_NS);
|
|
505
|
-
});
|
|
506
|
-
|
|
507
|
-
it('nested ${html`<circle/>`} inside an SVG container now lands in SVG namespace', () => {
|
|
508
|
-
// The canonical fix surface. Pre-fix, the inner template's <circle>
|
|
509
|
-
// arrived as HTMLUnknownElement (NS xhtml). Post-fix, the wrap()
|
|
510
|
-
// span is HTML-namespaced (correct — it has display:contents) but
|
|
511
|
-
// its children get re-namespaced via the wrap-span's closest('svg')
|
|
512
|
-
// matching the outer SVG.
|
|
513
|
-
const svgRoot = document.createElementNS(SVG_NS, 'svg');
|
|
514
|
-
container.appendChild(svgRoot);
|
|
515
|
-
const dots = [{ x: 1 }, { x: 2 }, { x: 3 }];
|
|
516
|
-
const tpl = html`${dots.map(d => html`<circle cx=${d.x} cy="5" r="2"/>`)}`;
|
|
517
|
-
stamp(tpl, svgRoot);
|
|
518
|
-
const circles = svgRoot.querySelectorAll('circle');
|
|
519
|
-
expect(circles.length).toBe(3);
|
|
520
|
-
for (const c of circles) {
|
|
521
|
-
expect(c.namespaceURI).toBe(SVG_NS); // ✅ SVG-NS
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
it('a directly-stamped html`<circle/>` template into an SVG container is SVG-namespaced', () => {
|
|
526
|
-
// Even without array interpolation, stamping a single SVG-content
|
|
527
|
-
// template into an SVG container must re-namespace.
|
|
528
|
-
const svgRoot = document.createElementNS(SVG_NS, 'svg');
|
|
529
|
-
container.appendChild(svgRoot);
|
|
530
|
-
const tpl = html`<circle cx="5" cy="5" r="3"/>`;
|
|
531
|
-
stamp(tpl, svgRoot);
|
|
532
|
-
const circle = svgRoot.querySelector('circle');
|
|
533
|
-
expect(circle.namespaceURI).toBe(SVG_NS);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it('<foreignObject> children REVERT to HTML namespace (per HTML5 spec)', () => {
|
|
537
|
-
// <foreignObject> is the spec-defined HTML escape hatch inside SVG.
|
|
538
|
-
// Re-namespacing must stop at the foreignObject boundary; its
|
|
539
|
-
// children stay HTML-namespaced.
|
|
540
|
-
const svgRoot = document.createElementNS(SVG_NS, 'svg');
|
|
541
|
-
container.appendChild(svgRoot);
|
|
542
|
-
const tpl = html`<foreignObject><div class="html-inside-svg">html content</div></foreignObject>`;
|
|
543
|
-
stamp(tpl, svgRoot);
|
|
544
|
-
const fo = svgRoot.querySelector('foreignObject');
|
|
545
|
-
const innerDiv = svgRoot.querySelector('.html-inside-svg');
|
|
546
|
-
expect(fo.namespaceURI).toBe(SVG_NS); // ✅ foreignObject itself is SVG
|
|
547
|
-
expect(innerDiv.namespaceURI).toBe('http://www.w3.org/1999/xhtml'); // ✅ children HTML
|
|
548
|
-
});
|
|
549
|
-
|
|
550
|
-
it('MathML <math><mi/></math> inline template stays in MathML namespace via the HTML parser', () => {
|
|
551
|
-
// Like inline-SVG: the HTML parser enters MathML insertion mode for
|
|
552
|
-
// <math>, so inline children arrive correctly namespaced even
|
|
553
|
-
// pre-fix. Post-fix this remains true (no re-namespacing because
|
|
554
|
-
// the container is HTML).
|
|
555
|
-
const tpl = html`<math><mi>x</mi></math>`;
|
|
556
|
-
stamp(tpl, container);
|
|
557
|
-
const math = container.querySelector('math');
|
|
558
|
-
const mi = container.querySelector('mi');
|
|
559
|
-
// happy-dom may or may not surface MathML namespace; smoke that the
|
|
560
|
-
// elements at least exist and the math element is acknowledged.
|
|
561
|
-
expect(math).not.toBeNull();
|
|
562
|
-
expect(mi).not.toBeNull();
|
|
563
|
-
expect(math.localName).toBe('math');
|
|
564
|
-
});
|
|
565
|
-
|
|
566
|
-
it('does not re-namespace when stamping into an HTML container (no overhead in the common case)', () => {
|
|
567
|
-
// Probe the discriminator: a plain <div> container should skip the
|
|
568
|
-
// re-namespacing path entirely. Verify by stamping a template
|
|
569
|
-
// whose children are HTML elements and confirming they stay HTML.
|
|
570
|
-
const tpl = html`<p class="probe">html paragraph</p>`;
|
|
571
|
-
stamp(tpl, container);
|
|
572
|
-
const p = container.querySelector('p.probe');
|
|
573
|
-
expect(p.namespaceURI).toBe('http://www.w3.org/1999/xhtml');
|
|
574
|
-
});
|
|
575
|
-
});
|
|
576
|
-
|
|
577
|
-
describe('html template — FB-94/FB-95 (v0.7.x) ARIA-transparent wrapper spans', () => {
|
|
578
|
-
let container;
|
|
579
|
-
beforeEach(() => { container = document.createElement('div'); document.body.appendChild(container); });
|
|
580
|
-
afterEach(() => container.remove());
|
|
581
|
-
|
|
582
|
-
// The display:contents wrapper span the engine emits for interpolated parts
|
|
583
|
-
// must carry role="presentation" — display:contents removes the box from
|
|
584
|
-
// layout but the span still generates a generic a11y node, which breaks
|
|
585
|
-
// required-owned / required-parent relationships when it sits between a
|
|
586
|
-
// role-bearing parent and role-bearing children rendered via .map()/repeat()
|
|
587
|
-
// (tree→treeitem = FB-94, menu→menuitem = FB-95). Verified end-to-end with
|
|
588
|
-
// axe-core; these lock the substrate behaviour against regression.
|
|
589
|
-
const isContentsSpan = (s) => /display:\s*contents/.test(s.getAttribute('style') || '');
|
|
590
|
-
|
|
591
|
-
it('a `.map()` of templates wraps each item in a role="presentation" display:contents span', () => {
|
|
592
|
-
stamp(html`<ul>${['a', 'b', 'c'].map((x) => html`<li>${x}</li>`)}</ul>`, container);
|
|
593
|
-
const spans = [...container.querySelectorAll('span')].filter(isContentsSpan);
|
|
594
|
-
expect(spans.length).toBeGreaterThan(0);
|
|
595
|
-
spans.forEach((s) => expect(s.getAttribute('role')).toBe('presentation'));
|
|
596
|
-
});
|
|
597
|
-
|
|
598
|
-
it('a conditional `${cond ? html`` : null}` branch wraps in role="presentation"', () => {
|
|
599
|
-
stamp(html`<div>${true ? html`<span data-x>hi</span>` : null}</div>`, container);
|
|
600
|
-
const wrap = [...container.querySelectorAll('div > span')].find(isContentsSpan);
|
|
601
|
-
expect(wrap).toBeTruthy();
|
|
602
|
-
expect(wrap.getAttribute('role')).toBe('presentation');
|
|
603
|
-
});
|
|
604
|
-
|
|
605
|
-
it('`repeat()` keyed items wrap in role="presentation"', () => {
|
|
606
|
-
stamp(html`<ul>${repeat(['a', 'b'], (x) => x, (x) => html`<li>${x}</li>`)}</ul>`, container);
|
|
607
|
-
const spans = [...container.querySelectorAll('span')].filter(isContentsSpan);
|
|
608
|
-
expect(spans.length).toBeGreaterThan(0);
|
|
609
|
-
spans.forEach((s) => expect(s.getAttribute('role')).toBe('presentation'));
|
|
610
|
-
});
|
|
611
|
-
});
|
|
@@ -1,28 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { activeState } from './active-state.js';
|
|
3
|
-
import { mountHost, connectTrait, resetDOM } from '../test-helpers.js';
|
|
4
|
-
|
|
5
|
-
describe('active-state', () => {
|
|
6
|
-
beforeEach(resetDOM);
|
|
7
|
-
|
|
8
|
-
it('toggles data-active-state-active on click', () => {
|
|
9
|
-
const host = mountHost();
|
|
10
|
-
connectTrait(activeState, host);
|
|
11
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(false);
|
|
12
|
-
host.click();
|
|
13
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(true);
|
|
14
|
-
host.click();
|
|
15
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(false);
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('disconnect removes the active attribute and stops listening', () => {
|
|
19
|
-
const host = mountHost();
|
|
20
|
-
const inst = connectTrait(activeState, host);
|
|
21
|
-
host.click();
|
|
22
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(true);
|
|
23
|
-
inst.disconnect(host);
|
|
24
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(false);
|
|
25
|
-
host.click();
|
|
26
|
-
expect(host.hasAttribute('data-active-state-active')).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
});
|