@adia-ai/web-components 0.8.19 → 0.8.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/components/card/card.a2ui.json +6 -3
- package/components/card/card.css +15 -1
- package/components/card/card.yaml +5 -3
- package/components/nav-group/nav-group.a2ui.json +5 -0
- package/components/nav-group/nav-group.class.js +14 -6
- package/components/nav-group/nav-group.css +5 -2
- package/components/nav-group/nav-group.d.ts +2 -0
- package/components/nav-group/nav-group.yaml +4 -0
- package/components/select/select.a2ui.json +3 -2
- package/components/select/select.class.js +14 -1
- package/components/select/select.css +13 -1
- package/components/select/select.yaml +5 -4
- package/components/table/table.class.js +7 -3
- package/components/table/table.css +7 -5
- package/dist/theme-provider.min.js +1 -1
- package/dist/web-components.min.css +1 -1
- package/dist/web-components.min.js +19 -19
- package/dist/web-components.sheet.js +1 -1
- package/package.json +2 -1
- package/components/adia-mark/adia-mark.test.js +0 -69
- package/components/adia-wordmark/adia-wordmark.test.js +0 -69
- package/components/agent-artifact/agent-artifact.test.js +0 -61
- package/components/alert/alert.test.js +0 -364
- package/components/color-picker/color-picker.test.js +0 -96
- package/components/combobox/combobox.test.js +0 -256
- package/components/command/command.test.js +0 -99
- package/components/date-range-picker/date-range-picker.test.js +0 -387
- package/components/datetime-picker/datetime-picker.test.js +0 -454
- package/components/demo-toggle/demo-toggle.test.js +0 -102
- package/components/description-list/description-list.test.js +0 -72
- package/components/display-field/display-field.test.js +0 -115
- package/components/empty-state/empty-state.test.js +0 -77
- package/components/field/field.test.js +0 -260
- package/components/fields/fields.test.js +0 -88
- package/components/inline-message/inline-message.test.js +0 -203
- package/components/input/input.test.js +0 -335
- package/components/integration-card/integration-card.test.js +0 -318
- package/components/link/link.test.js +0 -143
- package/components/list/list.test.js +0 -106
- package/components/list-window/list-window.test.js +0 -303
- package/components/loading-overlay/loading-overlay.test.js +0 -257
- package/components/menu/menu.test.js +0 -176
- package/components/nav/nav.test.js +0 -428
- package/components/nav-group/nav-group.test.js +0 -67
- package/components/option-card/option-card.test.js +0 -32
- package/components/pagination/pagination.test.js +0 -195
- package/components/select/select.test.js +0 -481
- package/components/slider/slider.test.js +0 -202
- package/components/spinner/spinner.test.js +0 -272
- package/components/stat/stat.test.js +0 -108
- package/components/step-progress/step-progress.test.js +0 -129
- package/components/swatch/swatch.test.js +0 -563
- package/components/table/table.test.js +0 -479
- package/components/table-toolbar/table-toolbar.test.js +0 -152
- package/components/tag/tag.test.js +0 -137
- package/components/tags-input/tags-input.test.js +0 -477
- package/components/text/text.test.js +0 -202
- package/components/theme-provider/theme-provider.test.js +0 -94
- package/components/time-picker/time-picker.test.js +0 -287
- package/components/timeline/timeline.test.js +0 -31
- package/components/toggle-scheme/toggle-scheme.test.js +0 -110
- package/components/tree/tree.test.js +0 -312
- package/core/element.test.js +0 -389
- package/core/icons.test.js +0 -187
- package/core/logical-children.test.js +0 -54
- package/core/markdown.test.js +0 -47
- package/core/responsive.test.js +0 -121
- package/core/template.test.js +0 -611
- package/traits/active-state/active-state.test.js +0 -28
- package/traits/anchor-positioning/anchor-positioning.test.js +0 -122
- package/traits/announcer/announcer.test.js +0 -268
- package/traits/arrow-grid-nav/arrow-grid-nav.test.js +0 -375
- package/traits/attention-pulse/attention-pulse.test.js +0 -26
- package/traits/confetti/confetti.test.js +0 -46
- package/traits/confetti-burst/confetti-burst.test.js +0 -46
- package/traits/count-up/count-up.test.js +0 -54
- package/traits/declarative.test.js +0 -138
- package/traits/dirty-state/dirty-state.test.js +0 -45
- package/traits/drag-ghost/drag-ghost.test.js +0 -19
- package/traits/draggable/draggable.test.js +0 -60
- package/traits/draggable-list-item/draggable-list-item.test.js +0 -51
- package/traits/drop-target/drop-target.test.js +0 -241
- package/traits/droppable/droppable.test.js +0 -54
- package/traits/droppable-collection/droppable-collection.test.js +0 -99
- package/traits/error-shake/error-shake.test.js +0 -114
- package/traits/fade-presence/fade-presence.test.js +0 -20
- package/traits/focus-restore/focus-restore.test.js +0 -202
- package/traits/focus-trap/focus-trap.test.js +0 -42
- package/traits/focusable/focusable.test.js +0 -53
- package/traits/glow-focus/glow-focus.test.js +0 -31
- package/traits/gradient-shift/gradient-shift.test.js +0 -22
- package/traits/haptic-feedback/haptic-feedback.test.js +0 -52
- package/traits/hotkey/hotkey.test.js +0 -61
- package/traits/hoverable/hoverable.test.js +0 -24
- package/traits/inertia-drag/inertia-drag.test.js +0 -33
- package/traits/input-mask/input-mask.test.js +0 -151
- package/traits/intersection-observer/intersection-observer.test.js +0 -38
- package/traits/keyboard-nav/keyboard-nav.test.js +0 -41
- package/traits/keyboard-reorderable/keyboard-reorderable.test.js +0 -45
- package/traits/layout-animation/layout-animation.test.js +0 -114
- package/traits/long-press/long-press.test.js +0 -244
- package/traits/magnetic-hover/magnetic-hover.test.js +0 -30
- package/traits/noise-texture/noise-texture.test.js +0 -20
- package/traits/parallax/parallax.test.js +0 -26
- package/traits/portal/portal.test.js +0 -30
- package/traits/pressable/pressable.test.js +0 -73
- package/traits/resettable/resettable.test.js +0 -100
- package/traits/resizable/resizable.test.js +0 -39
- package/traits/resize-observer/resize-observer.test.js +0 -38
- package/traits/ripple/ripple.test.js +0 -32
- package/traits/roving-tabindex/roving-tabindex.test.js +0 -28
- package/traits/scale-press/scale-press.test.js +0 -39
- package/traits/scroll-lock/scroll-lock.test.js +0 -45
- package/traits/scroll-progress/scroll-progress.test.js +0 -182
- package/traits/shimmer-loading/shimmer-loading.test.js +0 -43
- package/traits/smoke.test.js +0 -111
- package/traits/snap-to-grid/snap-to-grid.test.js +0 -40
- package/traits/sound-feedback/sound-feedback.test.js +0 -26
- package/traits/spring-animate/spring-animate.test.js +0 -28
- package/traits/success-checkmark/success-checkmark.test.js +0 -120
- package/traits/tilt-hover/tilt-hover.test.js +0 -32
- package/traits/tossable/tossable.test.js +0 -31
- package/traits/traits-host.test.js +0 -73
- package/traits/typeahead/typeahead.test.js +0 -38
- package/traits/typewriter/typewriter.test.js +0 -47
- package/traits/validation/validation.test.js +0 -93
- package/traits/view-transition/view-transition.test.js +0 -268
|
@@ -1,563 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* <swatch-ui> behavioral tests.
|
|
3
|
-
*
|
|
4
|
-
* Currently scoped to FB-36 / §325 regression coverage. Future tests for
|
|
5
|
-
* #stamp() lifecycle, chrome-slot composition, auto-contrast probe,
|
|
6
|
-
* etc. land here.
|
|
7
|
-
*
|
|
8
|
-
* Test setup note: happy-dom connects custom elements synchronously
|
|
9
|
-
* during innerHTML parsing — BEFORE sibling children inside the same
|
|
10
|
-
* element are appended. Real browsers connect after the full innerHTML
|
|
11
|
-
* is parsed. To exercise the FB-36 path (children present at connect
|
|
12
|
-
* time), tests use `document.createElement` + `appendChild` to build
|
|
13
|
-
* the full subtree FIRST, then attach to the document — guaranteeing
|
|
14
|
-
* the children are in `this.childNodes` when `connectedCallback()` fires.
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it, expect, beforeAll, beforeEach } from 'vitest';
|
|
18
|
-
|
|
19
|
-
beforeAll(async () => {
|
|
20
|
-
await import('./swatch.js');
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
function buildSwatch({ label, defaultSlot, shape = 'block', labelPosition = 'overlay', color = '#3b82f6' }) {
|
|
24
|
-
const swatch = document.createElement('swatch-ui');
|
|
25
|
-
swatch.setAttribute('shape', shape);
|
|
26
|
-
swatch.setAttribute('label-position', labelPosition);
|
|
27
|
-
swatch.setAttribute('color', color);
|
|
28
|
-
if (label !== undefined) swatch.setAttribute('label', label);
|
|
29
|
-
if (defaultSlot) {
|
|
30
|
-
// Mimic indented HTML's whitespace text nodes — the FB-36 trigger.
|
|
31
|
-
swatch.appendChild(document.createTextNode('\n '));
|
|
32
|
-
const child = document.createElement('span');
|
|
33
|
-
child.id = defaultSlot.id;
|
|
34
|
-
child.textContent = defaultSlot.text;
|
|
35
|
-
swatch.appendChild(child);
|
|
36
|
-
swatch.appendChild(document.createTextNode('\n '));
|
|
37
|
-
}
|
|
38
|
-
return swatch;
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
describe('<swatch-ui> default-slot + label-attr precedence (FB-36 / §325)', () => {
|
|
42
|
-
let host;
|
|
43
|
-
|
|
44
|
-
beforeEach(() => {
|
|
45
|
-
host = document.createElement('div');
|
|
46
|
-
document.body.appendChild(host);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('preserves default-slot element when label="X" is also present (FB-36 §325 fix)', async () => {
|
|
50
|
-
const swatch = buildSwatch({
|
|
51
|
-
label: '500',
|
|
52
|
-
defaultSlot: { id: 'probe-default', text: 'richlabel' },
|
|
53
|
-
});
|
|
54
|
-
host.appendChild(swatch);
|
|
55
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
56
|
-
|
|
57
|
-
const label = swatch.querySelector('[data-label]');
|
|
58
|
-
const span = swatch.querySelector('#probe-default');
|
|
59
|
-
|
|
60
|
-
// Pre-§325 the span was wiped + label="500" took precedence.
|
|
61
|
-
// Post-§325 the default-slot wins (yaml spec: "richer content").
|
|
62
|
-
expect(span).not.toBeNull();
|
|
63
|
-
expect(label.textContent.trim()).toBe('richlabel');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('renders label-attr text when no default-slot is present', async () => {
|
|
67
|
-
const swatch = buildSwatch({ label: '500' });
|
|
68
|
-
host.appendChild(swatch);
|
|
69
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
70
|
-
const label = swatch.querySelector('[data-label]');
|
|
71
|
-
expect(label.textContent.trim()).toBe('500');
|
|
72
|
-
});
|
|
73
|
-
|
|
74
|
-
it('renders default-slot text when no label-attr is present', async () => {
|
|
75
|
-
const swatch = buildSwatch({
|
|
76
|
-
defaultSlot: { id: 'probe-only', text: 'defaultonly' },
|
|
77
|
-
});
|
|
78
|
-
host.appendChild(swatch);
|
|
79
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
80
|
-
const label = swatch.querySelector('[data-label]');
|
|
81
|
-
const span = swatch.querySelector('#probe-only');
|
|
82
|
-
expect(span).not.toBeNull();
|
|
83
|
-
expect(label.textContent.trim()).toBe('defaultonly');
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it('syncs label-attr changes when there is no default-slot (no whitespace regression)', async () => {
|
|
87
|
-
const swatch = buildSwatch({ label: '500' });
|
|
88
|
-
host.appendChild(swatch);
|
|
89
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
90
|
-
swatch.label = '600';
|
|
91
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
92
|
-
const label = swatch.querySelector('[data-label]');
|
|
93
|
-
expect(label.textContent.trim()).toBe('600');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
|
|
97
|
-
describe('<swatch-ui> late-arriving [slot="chrome"] children (FB-37 / §326)', () => {
|
|
98
|
-
let host;
|
|
99
|
-
|
|
100
|
-
beforeEach(() => {
|
|
101
|
-
host = document.createElement('div');
|
|
102
|
-
document.body.appendChild(host);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('absorbs chrome child appended AFTER connectedCallback (template-render path)', async () => {
|
|
106
|
-
// Mimic the template-engine timing: create the swatch + connect it FIRST
|
|
107
|
-
// (children empty), then append the chrome child + trigger a render.
|
|
108
|
-
const swatch = document.createElement('swatch-ui');
|
|
109
|
-
swatch.setAttribute('shape', 'block');
|
|
110
|
-
swatch.setAttribute('label-position', 'overlay');
|
|
111
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
112
|
-
swatch.setAttribute('label', '500');
|
|
113
|
-
host.appendChild(swatch);
|
|
114
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
115
|
-
|
|
116
|
-
// At this point #stamp() has run + chromeSlot was empty.
|
|
117
|
-
// Append a chrome child LATER — this is what template engines do.
|
|
118
|
-
const chrome = document.createElement('span');
|
|
119
|
-
chrome.setAttribute('slot', 'chrome');
|
|
120
|
-
chrome.id = 'late-chrome';
|
|
121
|
-
chrome.textContent = '★';
|
|
122
|
-
swatch.appendChild(chrome);
|
|
123
|
-
|
|
124
|
-
// Trigger a re-render via property mutation (template engines typically
|
|
125
|
-
// set props after appending — this nudge fires render → #syncCore →
|
|
126
|
-
// #absorbChromeSlot picks up the late chrome child). Must use a value
|
|
127
|
-
// DIFFERENT from current to actually fire the signal write.
|
|
128
|
-
swatch.label = '999';
|
|
129
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
130
|
-
|
|
131
|
-
const found = swatch.querySelector('#late-chrome');
|
|
132
|
-
expect(found).not.toBeNull();
|
|
133
|
-
// Chrome child should be a direct host sibling positioned between tile + badge.
|
|
134
|
-
const tile = swatch.querySelector('[data-tile]');
|
|
135
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
136
|
-
expect(found.previousElementSibling).toBe(tile);
|
|
137
|
-
expect(found.nextElementSibling).toBe(badge);
|
|
138
|
-
});
|
|
139
|
-
|
|
140
|
-
it('still handles chrome child appended BEFORE connect (static-HTML path)', async () => {
|
|
141
|
-
// Build swatch with chrome child present at connect time — the canonical
|
|
142
|
-
// FB-34 / §314 path should still work post-§326.
|
|
143
|
-
const swatch = document.createElement('swatch-ui');
|
|
144
|
-
swatch.setAttribute('shape', 'block');
|
|
145
|
-
swatch.setAttribute('label-position', 'overlay');
|
|
146
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
147
|
-
swatch.setAttribute('label', '500');
|
|
148
|
-
const chrome = document.createElement('span');
|
|
149
|
-
chrome.setAttribute('slot', 'chrome');
|
|
150
|
-
chrome.id = 'early-chrome';
|
|
151
|
-
chrome.textContent = '⚑';
|
|
152
|
-
swatch.appendChild(chrome);
|
|
153
|
-
host.appendChild(swatch);
|
|
154
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
155
|
-
|
|
156
|
-
const found = swatch.querySelector('#early-chrome');
|
|
157
|
-
expect(found).not.toBeNull();
|
|
158
|
-
const tile = swatch.querySelector('[data-tile]');
|
|
159
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
160
|
-
expect(found.previousElementSibling).toBe(tile);
|
|
161
|
-
expect(found.nextElementSibling).toBe(badge);
|
|
162
|
-
});
|
|
163
|
-
});
|
|
164
|
-
|
|
165
|
-
describe('<swatch-ui> chrome child nested in display:contents wrapper (FB-39 / §331)', () => {
|
|
166
|
-
let host;
|
|
167
|
-
|
|
168
|
-
beforeEach(() => {
|
|
169
|
-
host = document.createElement('div');
|
|
170
|
-
document.body.appendChild(host);
|
|
171
|
-
});
|
|
172
|
-
|
|
173
|
-
it('hoists chrome child from inside wrapper span at #stamp() time (template-interpolation path)', async () => {
|
|
174
|
-
// Simulates AdiaUI's html`` template wrapping interpolated content in
|
|
175
|
-
// <span style="display: contents"> (per core/template.js:212 `wrap()`).
|
|
176
|
-
// When consumer writes `html`<swatch-ui>${chromeFrag}</swatch-ui>``,
|
|
177
|
-
// the chromeFrag's content lands NESTED inside a wrapper span — not
|
|
178
|
-
// as direct children of <swatch-ui>. Pre-§331 the chrome was wiped;
|
|
179
|
-
// §341 (FB-43) refined to keep chrome INSIDE the wrapper + move the
|
|
180
|
-
// wrapper to canonical position (preserves template-engine `part.n`
|
|
181
|
-
// identity so parent re-renders update in-place).
|
|
182
|
-
const swatch = document.createElement('swatch-ui');
|
|
183
|
-
swatch.setAttribute('shape', 'block');
|
|
184
|
-
swatch.setAttribute('label-position', 'overlay');
|
|
185
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
186
|
-
swatch.setAttribute('label', '500');
|
|
187
|
-
|
|
188
|
-
const wrapper = document.createElement('span');
|
|
189
|
-
wrapper.style.display = 'contents';
|
|
190
|
-
const chrome = document.createElement('span');
|
|
191
|
-
chrome.setAttribute('slot', 'chrome');
|
|
192
|
-
chrome.id = 'wrapped-chrome';
|
|
193
|
-
chrome.textContent = 'P3';
|
|
194
|
-
wrapper.appendChild(chrome);
|
|
195
|
-
swatch.appendChild(wrapper);
|
|
196
|
-
|
|
197
|
-
host.appendChild(swatch);
|
|
198
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
199
|
-
|
|
200
|
-
const found = swatch.querySelector('#wrapped-chrome');
|
|
201
|
-
expect(found).not.toBeNull();
|
|
202
|
-
// §341: chrome stays inside the display:contents wrapper; wrapper is
|
|
203
|
-
// at canonical position (between tile + badge). Visually equivalent
|
|
204
|
-
// to direct-child placement because display:contents collapses the box.
|
|
205
|
-
expect(found.parentElement).toBe(wrapper);
|
|
206
|
-
expect(wrapper.parentElement).toBe(swatch);
|
|
207
|
-
const tile = swatch.querySelector('[data-tile]');
|
|
208
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
209
|
-
expect(wrapper.previousElementSibling).toBe(tile);
|
|
210
|
-
expect(wrapper.nextElementSibling).toBe(badge);
|
|
211
|
-
});
|
|
212
|
-
|
|
213
|
-
it('handles MULTIPLE chrome children nested in separate wrapper spans', async () => {
|
|
214
|
-
// Each interpolated fragment in html`` gets its own wrapper span,
|
|
215
|
-
// so multi-chrome usage (`${badge}${dot1}${dot2}`) produces multiple
|
|
216
|
-
// wrapper spans, each containing one chrome child. §341: wrappers
|
|
217
|
-
// stay intact, chrome remains inside them.
|
|
218
|
-
const swatch = document.createElement('swatch-ui');
|
|
219
|
-
swatch.setAttribute('shape', 'block');
|
|
220
|
-
swatch.setAttribute('label-position', 'overlay');
|
|
221
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
222
|
-
|
|
223
|
-
const ids = ['c1', 'c2', 'c3'];
|
|
224
|
-
for (const id of ids) {
|
|
225
|
-
const w = document.createElement('span');
|
|
226
|
-
w.style.display = 'contents';
|
|
227
|
-
const c = document.createElement('span');
|
|
228
|
-
c.setAttribute('slot', 'chrome');
|
|
229
|
-
c.id = id;
|
|
230
|
-
c.textContent = id;
|
|
231
|
-
w.appendChild(c);
|
|
232
|
-
swatch.appendChild(w);
|
|
233
|
-
}
|
|
234
|
-
host.appendChild(swatch);
|
|
235
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
236
|
-
|
|
237
|
-
for (const id of ids) {
|
|
238
|
-
const found = swatch.querySelector(`#${id}`);
|
|
239
|
-
expect(found).not.toBeNull();
|
|
240
|
-
// §341: chrome stays inside its wrapper; wrapper is a direct child of swatch.
|
|
241
|
-
expect(found.parentElement.tagName).toBe('SPAN');
|
|
242
|
-
expect(found.parentElement.style.display).toBe('contents');
|
|
243
|
-
expect(found.parentElement.parentElement).toBe(swatch);
|
|
244
|
-
}
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it('hoists chrome from late-arriving wrapper span (post-stamp template re-render)', async () => {
|
|
248
|
-
// Parent template re-render after the swatch is already stamped:
|
|
249
|
-
// a new wrapper span carrying chrome content arrives after #stamp().
|
|
250
|
-
// #absorbChromeSlot() should pick it up on next render pass.
|
|
251
|
-
const swatch = document.createElement('swatch-ui');
|
|
252
|
-
swatch.setAttribute('shape', 'block');
|
|
253
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
254
|
-
swatch.setAttribute('label', '500');
|
|
255
|
-
host.appendChild(swatch);
|
|
256
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
257
|
-
|
|
258
|
-
// Late: append a wrapper-span carrying chrome
|
|
259
|
-
const wrapper = document.createElement('span');
|
|
260
|
-
wrapper.style.display = 'contents';
|
|
261
|
-
const chrome = document.createElement('span');
|
|
262
|
-
chrome.setAttribute('slot', 'chrome');
|
|
263
|
-
chrome.id = 'late-wrapped';
|
|
264
|
-
wrapper.appendChild(chrome);
|
|
265
|
-
swatch.appendChild(wrapper);
|
|
266
|
-
|
|
267
|
-
// Nudge a render via a property change
|
|
268
|
-
swatch.label = '600';
|
|
269
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
270
|
-
|
|
271
|
-
const found = swatch.querySelector('#late-wrapped');
|
|
272
|
-
expect(found).not.toBeNull();
|
|
273
|
-
// §341: chrome stays inside wrapper; wrapper is now at canonical
|
|
274
|
-
// position (between tile and badge) after #absorbChromeSlot()'s
|
|
275
|
-
// re-positioning of the wrapper.
|
|
276
|
-
expect(found.parentElement).toBe(wrapper);
|
|
277
|
-
expect(wrapper.parentElement).toBe(swatch);
|
|
278
|
-
const tile = swatch.querySelector('[data-tile]');
|
|
279
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
280
|
-
expect(wrapper.previousElementSibling).toBe(tile);
|
|
281
|
-
expect(wrapper.nextElementSibling).toBe(badge);
|
|
282
|
-
});
|
|
283
|
-
});
|
|
284
|
-
|
|
285
|
-
describe('<swatch-ui> [auto-contrast] does not override [label-position="overlay"] position (FB-41 / §339)', () => {
|
|
286
|
-
// Structural CSS-source-string assertion: happy-dom does not resolve
|
|
287
|
-
// @scope CSS specificity reliably, so we verify the fix by reading
|
|
288
|
-
// swatch.css text and asserting the auto-contrast rules don't carry
|
|
289
|
-
// `position: relative` (which collided at equal specificity with the
|
|
290
|
-
// overlay rule's `position: absolute` via source order).
|
|
291
|
-
//
|
|
292
|
-
// Pre-§339: `:scope[shape="block"][auto-contrast] > [data-label]`
|
|
293
|
-
// declared `position: relative; z-index: 1`. Combined with overlay
|
|
294
|
-
// attribute, the later rule won → label dropped out of overlay layout.
|
|
295
|
-
//
|
|
296
|
-
// Post-§339: only `z-index: 1` remains (z-index is no-op on static
|
|
297
|
-
// elements but harmless; for the overlay case it stacks above the
|
|
298
|
-
// tile correctly via the overlay rule's `position: absolute`).
|
|
299
|
-
it('auto-contrast label rule does NOT declare position: relative (regression for FB-41)', async () => {
|
|
300
|
-
const fs = await import('node:fs');
|
|
301
|
-
const path = await import('node:path');
|
|
302
|
-
const url = await import('node:url');
|
|
303
|
-
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
304
|
-
const css = fs.readFileSync(path.join(here, 'swatch.css'), 'utf8');
|
|
305
|
-
const match = css.match(
|
|
306
|
-
/:scope\[shape="block"\]\[auto-contrast\]\s*>\s*\[data-label\]\s*\{([^}]*)\}/
|
|
307
|
-
);
|
|
308
|
-
expect(match).not.toBeNull();
|
|
309
|
-
const block = match[1];
|
|
310
|
-
expect(block).not.toMatch(/position\s*:\s*relative/);
|
|
311
|
-
expect(block).toMatch(/z-index\s*:\s*1/);
|
|
312
|
-
});
|
|
313
|
-
|
|
314
|
-
it('auto-contrast detail rule does NOT declare position: relative (regression for FB-41)', async () => {
|
|
315
|
-
const fs = await import('node:fs');
|
|
316
|
-
const path = await import('node:path');
|
|
317
|
-
const url = await import('node:url');
|
|
318
|
-
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
319
|
-
const css = fs.readFileSync(path.join(here, 'swatch.css'), 'utf8');
|
|
320
|
-
const match = css.match(
|
|
321
|
-
/:scope\[shape="block"\]\[auto-contrast\]\s*>\s*\[data-detail\]\s*\{([^}]*)\}/
|
|
322
|
-
);
|
|
323
|
-
expect(match).not.toBeNull();
|
|
324
|
-
const block = match[1];
|
|
325
|
-
expect(block).not.toMatch(/position\s*:\s*relative/);
|
|
326
|
-
expect(block).toMatch(/z-index\s*:\s*1/);
|
|
327
|
-
});
|
|
328
|
-
|
|
329
|
-
it('overlay rule still declares position: absolute (regression for FB-41)', async () => {
|
|
330
|
-
// Defensive: also assert the overlay rule's `position: absolute` is
|
|
331
|
-
// still in place — if a future refactor drops it, the §253 contract
|
|
332
|
-
// breaks regardless of the auto-contrast collision.
|
|
333
|
-
const fs = await import('node:fs');
|
|
334
|
-
const path = await import('node:path');
|
|
335
|
-
const url = await import('node:url');
|
|
336
|
-
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
337
|
-
const css = fs.readFileSync(path.join(here, 'swatch.css'), 'utf8');
|
|
338
|
-
const match = css.match(
|
|
339
|
-
/:scope\[shape="block"\]\[label-position="overlay"\]\s*>\s*\[data-label\]\s*\{([^}]*)\}/
|
|
340
|
-
);
|
|
341
|
-
expect(match).not.toBeNull();
|
|
342
|
-
const block = match[1];
|
|
343
|
-
expect(block).toMatch(/position\s*:\s*absolute/);
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
|
|
347
|
-
describe('<swatch-ui> chrome survives parent template re-render (FB-43 / §341)', () => {
|
|
348
|
-
// FB-43 (P1): claim is `template.js` mount() replaceChildren() destroys
|
|
349
|
-
// chrome children on every re-render. The real mechanism is more subtle:
|
|
350
|
-
// §331's #stamp() does `innerHTML = ''` which wipes the wrapper-span
|
|
351
|
-
// created by the parent template engine's wrap() (after collecting
|
|
352
|
-
// chrome into chromeSlot). The parent template's `part.n` still points
|
|
353
|
-
// to the now-detached wrapper-span. On parent re-render, applyValue()
|
|
354
|
-
// stamps the new chrome value INTO the detached wrapper-span — landing
|
|
355
|
-
// in memory but not in the DOM. Chrome is effectively destroyed.
|
|
356
|
-
//
|
|
357
|
-
// This empirical test reproduces the exact scenario.
|
|
358
|
-
let host;
|
|
359
|
-
let html, stamp;
|
|
360
|
-
|
|
361
|
-
beforeAll(async () => {
|
|
362
|
-
({ html, stamp } = await import('../../core/template.js'));
|
|
363
|
-
});
|
|
364
|
-
|
|
365
|
-
beforeEach(() => {
|
|
366
|
-
host = document.createElement('div');
|
|
367
|
-
document.body.appendChild(host);
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
// §341 contract: chrome is queryable from swatch + lives inside a
|
|
371
|
-
// display:contents wrapper-span (the template-engine's stable part.n).
|
|
372
|
-
// The wrapper sits at canonical chrome position (between tile + badge).
|
|
373
|
-
// Visually equivalent to direct-child placement because display:contents
|
|
374
|
-
// collapses the wrapper's box.
|
|
375
|
-
function assertChromeCanonical(swatch, chrome) {
|
|
376
|
-
expect(chrome).not.toBeNull();
|
|
377
|
-
const wrapper = chrome.parentElement;
|
|
378
|
-
expect(wrapper).not.toBeNull();
|
|
379
|
-
// The wrapper is the chrome's direct parent + a display:contents span
|
|
380
|
-
// that's a direct child of the swatch (the template-engine's
|
|
381
|
-
// wrap()-created stable reference).
|
|
382
|
-
expect(wrapper.tagName).toBe('SPAN');
|
|
383
|
-
expect(wrapper.style.display).toBe('contents');
|
|
384
|
-
expect(wrapper.parentElement).toBe(swatch);
|
|
385
|
-
// Wrapper sits between tile and badge (canonical chrome region).
|
|
386
|
-
const tile = swatch.querySelector('[data-tile]');
|
|
387
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
388
|
-
expect(tile).not.toBeNull();
|
|
389
|
-
expect(badge).not.toBeNull();
|
|
390
|
-
// Walk from tile forward: should hit wrapper before badge.
|
|
391
|
-
let cursor = tile.nextElementSibling;
|
|
392
|
-
let foundWrapper = false;
|
|
393
|
-
while (cursor && cursor !== badge) {
|
|
394
|
-
if (cursor === wrapper) { foundWrapper = true; break; }
|
|
395
|
-
cursor = cursor.nextElementSibling;
|
|
396
|
-
}
|
|
397
|
-
expect(foundWrapper).toBe(true);
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
it('FB-43: chrome interpolated via html`` template survives first render', async () => {
|
|
401
|
-
const chromeFrag = html`<span slot="chrome" id="fb43-chrome">P3</span>`;
|
|
402
|
-
const tpl = html`<swatch-ui label="500" color="#3b82f6">${chromeFrag}</swatch-ui>`;
|
|
403
|
-
stamp(tpl, host);
|
|
404
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
405
|
-
|
|
406
|
-
const found = host.querySelector('#fb43-chrome');
|
|
407
|
-
expect(found).not.toBeNull();
|
|
408
|
-
const swatch = host.querySelector('swatch-ui');
|
|
409
|
-
assertChromeCanonical(swatch, found);
|
|
410
|
-
});
|
|
411
|
-
|
|
412
|
-
it('FB-43: chrome interpolated via html`` template survives second parent re-stamp (same strings)', async () => {
|
|
413
|
-
// FB-43's core claim: re-render destroys chrome. Same template strings
|
|
414
|
-
// → stamp() cache should hit → update() runs. Chrome content updates
|
|
415
|
-
// in-place inside the preserved wrapper-span.
|
|
416
|
-
function renderOnce(chromeText) {
|
|
417
|
-
const chromeFrag = html`<span slot="chrome" id="fb43-chrome">${chromeText}</span>`;
|
|
418
|
-
return html`<swatch-ui label="500" color="#3b82f6">${chromeFrag}</swatch-ui>`;
|
|
419
|
-
}
|
|
420
|
-
stamp(renderOnce('P3'), host);
|
|
421
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
422
|
-
expect(host.querySelector('#fb43-chrome')).not.toBeNull();
|
|
423
|
-
|
|
424
|
-
// Re-stamp with same template strings (different value).
|
|
425
|
-
stamp(renderOnce('OOG'), host);
|
|
426
|
-
await new Promise((r) => setTimeout(r, 80));
|
|
427
|
-
|
|
428
|
-
const swatch = host.querySelector('swatch-ui');
|
|
429
|
-
const chrome = swatch.querySelector('[slot="chrome"]');
|
|
430
|
-
expect(chrome).not.toBeNull();
|
|
431
|
-
expect(chrome.textContent).toBe('OOG');
|
|
432
|
-
assertChromeCanonical(swatch, chrome);
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
it('FB-43: chrome survives N consecutive re-renders (regression stress test)', async () => {
|
|
436
|
-
function renderOnce(chromeText) {
|
|
437
|
-
const chromeFrag = html`<span slot="chrome" id="fb43-chrome">${chromeText}</span>`;
|
|
438
|
-
return html`<swatch-ui label="500" color="#3b82f6">${chromeFrag}</swatch-ui>`;
|
|
439
|
-
}
|
|
440
|
-
for (let i = 0; i < 5; i++) {
|
|
441
|
-
stamp(renderOnce(`label-${i}`), host);
|
|
442
|
-
await new Promise((r) => setTimeout(r, 60));
|
|
443
|
-
}
|
|
444
|
-
const swatch = host.querySelector('swatch-ui');
|
|
445
|
-
const chrome = swatch.querySelector('[slot="chrome"]');
|
|
446
|
-
expect(chrome).not.toBeNull();
|
|
447
|
-
expect(chrome.textContent).toBe('label-4');
|
|
448
|
-
assertChromeCanonical(swatch, chrome);
|
|
449
|
-
});
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
describe('<swatch-ui> chrome absorbed on late arrival (FB-44 / §-TBD)', () => {
|
|
453
|
-
// FB-44 (P1): #absorbChromeSlot() runs from #syncCore() which only fires
|
|
454
|
-
// on attribute/property changes. scan() walks element.attributes BEFORE
|
|
455
|
-
// descending to child nodes, so the parent template's update() loop
|
|
456
|
-
// assigns properties (→ render() → #syncCore() → #absorbChromeSlot())
|
|
457
|
-
// BEFORE interpolating child node parts. The once-only absorb pass sees
|
|
458
|
-
// an empty children set; chrome lands moments later at the arrival
|
|
459
|
-
// position, not the canonical post-tile region.
|
|
460
|
-
//
|
|
461
|
-
// §-TBD adds a MutationObserver on host childList that re-runs
|
|
462
|
-
// #absorbChromeSlot() when chrome (direct or wrapper-spanned) arrives.
|
|
463
|
-
let host;
|
|
464
|
-
|
|
465
|
-
beforeEach(() => {
|
|
466
|
-
host = document.createElement('div');
|
|
467
|
-
document.body.appendChild(host);
|
|
468
|
-
});
|
|
469
|
-
|
|
470
|
-
// Inline a minimal canonical-position assertion (same shape as the
|
|
471
|
-
// FB-43 helper but local to this describe block).
|
|
472
|
-
function assertChromePositionedBeforeBadge(swatch, chrome) {
|
|
473
|
-
expect(chrome).not.toBeNull();
|
|
474
|
-
const badge = swatch.querySelector('[data-badge]');
|
|
475
|
-
expect(badge).not.toBeNull();
|
|
476
|
-
// The chrome (or its wrapper-span ancestor) should be a child of swatch
|
|
477
|
-
// that appears before the badge.
|
|
478
|
-
let chromeChild = chrome;
|
|
479
|
-
while (chromeChild.parentElement !== swatch && chromeChild.parentElement) {
|
|
480
|
-
chromeChild = chromeChild.parentElement;
|
|
481
|
-
}
|
|
482
|
-
expect(chromeChild.parentElement).toBe(swatch);
|
|
483
|
-
// Walk siblings from chromeChild forward — badge must be reachable.
|
|
484
|
-
let cursor = chromeChild;
|
|
485
|
-
let foundBadge = false;
|
|
486
|
-
while (cursor) {
|
|
487
|
-
if (cursor === badge) { foundBadge = true; break; }
|
|
488
|
-
cursor = cursor.nextElementSibling;
|
|
489
|
-
}
|
|
490
|
-
expect(foundBadge).toBe(true);
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
it('FB-44: chrome appended via host.appendChild() after connect lands at canonical position', async () => {
|
|
494
|
-
const swatch = document.createElement('swatch-ui');
|
|
495
|
-
swatch.setAttribute('shape', 'block');
|
|
496
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
497
|
-
host.appendChild(swatch);
|
|
498
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
499
|
-
|
|
500
|
-
// Append chrome AFTER initial #stamp() + #syncCore() pass completed.
|
|
501
|
-
const badge = document.createElement('span');
|
|
502
|
-
badge.setAttribute('slot', 'chrome');
|
|
503
|
-
badge.id = 'fb44-late-chrome';
|
|
504
|
-
badge.textContent = 'P3';
|
|
505
|
-
swatch.appendChild(badge);
|
|
506
|
-
// Wait a microtask for the MutationObserver to fire.
|
|
507
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
508
|
-
|
|
509
|
-
const found = swatch.querySelector('#fb44-late-chrome');
|
|
510
|
-
assertChromePositionedBeforeBadge(swatch, found);
|
|
511
|
-
});
|
|
512
|
-
|
|
513
|
-
it('FB-44: chrome appended via display:contents wrapper-span after connect lands at canonical position', async () => {
|
|
514
|
-
const swatch = document.createElement('swatch-ui');
|
|
515
|
-
swatch.setAttribute('shape', 'block');
|
|
516
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
517
|
-
host.appendChild(swatch);
|
|
518
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
519
|
-
|
|
520
|
-
// Simulate the template engine's wrap() output: display:contents span
|
|
521
|
-
// carrying the chrome child as its only descendant.
|
|
522
|
-
const wrapper = document.createElement('span');
|
|
523
|
-
wrapper.style.display = 'contents';
|
|
524
|
-
const badge = document.createElement('span');
|
|
525
|
-
badge.setAttribute('slot', 'chrome');
|
|
526
|
-
badge.id = 'fb44-wrapped-chrome';
|
|
527
|
-
badge.textContent = 'OOG';
|
|
528
|
-
wrapper.appendChild(badge);
|
|
529
|
-
swatch.appendChild(wrapper);
|
|
530
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
531
|
-
|
|
532
|
-
const found = swatch.querySelector('#fb44-wrapped-chrome');
|
|
533
|
-
assertChromePositionedBeforeBadge(swatch, found);
|
|
534
|
-
});
|
|
535
|
-
|
|
536
|
-
it('FB-44: chrome observer is disconnected when host leaves the DOM (leak regression)', async () => {
|
|
537
|
-
const swatch = document.createElement('swatch-ui');
|
|
538
|
-
swatch.setAttribute('shape', 'block');
|
|
539
|
-
swatch.setAttribute('color', '#3b82f6');
|
|
540
|
-
host.appendChild(swatch);
|
|
541
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
542
|
-
|
|
543
|
-
// Remove from DOM — disconnected() should disconnect the observer.
|
|
544
|
-
swatch.remove();
|
|
545
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
546
|
-
|
|
547
|
-
// Subsequent appendChild on the now-orphaned host should not throw,
|
|
548
|
-
// and re-attaching the swatch should re-arm a fresh observer.
|
|
549
|
-
document.body.appendChild(swatch);
|
|
550
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
551
|
-
|
|
552
|
-
const badge = document.createElement('span');
|
|
553
|
-
badge.setAttribute('slot', 'chrome');
|
|
554
|
-
badge.id = 'fb44-reattach-chrome';
|
|
555
|
-
swatch.appendChild(badge);
|
|
556
|
-
await new Promise((r) => setTimeout(r, 30));
|
|
557
|
-
|
|
558
|
-
const found = swatch.querySelector('#fb44-reattach-chrome');
|
|
559
|
-
expect(found).not.toBeNull();
|
|
560
|
-
assertChromePositionedBeforeBadge(swatch, found);
|
|
561
|
-
swatch.remove();
|
|
562
|
-
});
|
|
563
|
-
});
|