@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/element.test.js
DELETED
|
@@ -1,389 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { UIElement, signal, effect } from './element.js';
|
|
3
|
-
|
|
4
|
-
// ── Shared helpers ──
|
|
5
|
-
|
|
6
|
-
let registrationCounter = 0;
|
|
7
|
-
function registerTestElement(BaseImpl) {
|
|
8
|
-
const tag = `test-el-${++registrationCounter}`;
|
|
9
|
-
customElements.define(tag, BaseImpl);
|
|
10
|
-
return tag;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
function mount(tag, attrs = {}) {
|
|
14
|
-
const el = document.createElement(tag);
|
|
15
|
-
for (const [k, v] of Object.entries(attrs)) el.setAttribute(k, v);
|
|
16
|
-
document.body.appendChild(el);
|
|
17
|
-
return el;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
// Signals flush via queueMicrotask — await one tick to let reactive
|
|
21
|
-
// effects settle after a mutation.
|
|
22
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
23
|
-
|
|
24
|
-
describe('UIElement — construction + lifecycle', () => {
|
|
25
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
26
|
-
|
|
27
|
-
it('instantiates and calls connected() on mount, disconnected() on remove', () => {
|
|
28
|
-
const events = [];
|
|
29
|
-
class El extends UIElement {
|
|
30
|
-
connected() { events.push('connected'); }
|
|
31
|
-
disconnected() { events.push('disconnected'); }
|
|
32
|
-
}
|
|
33
|
-
const tag = registerTestElement(El);
|
|
34
|
-
const el = mount(tag);
|
|
35
|
-
expect(events).toEqual(['connected']);
|
|
36
|
-
el.remove();
|
|
37
|
-
expect(events).toEqual(['connected', 'disconnected']);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
it('remount after disconnect re-runs connected()', () => {
|
|
41
|
-
const events = [];
|
|
42
|
-
class El extends UIElement {
|
|
43
|
-
connected() { events.push('connected'); }
|
|
44
|
-
disconnected() { events.push('disconnected'); }
|
|
45
|
-
}
|
|
46
|
-
const tag = registerTestElement(El);
|
|
47
|
-
const el = mount(tag);
|
|
48
|
-
el.remove();
|
|
49
|
-
document.body.appendChild(el);
|
|
50
|
-
expect(events).toEqual(['connected', 'disconnected', 'connected']);
|
|
51
|
-
});
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
describe('UIElement — properties', () => {
|
|
55
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
56
|
-
|
|
57
|
-
it('reads default value from static properties', () => {
|
|
58
|
-
class El extends UIElement {
|
|
59
|
-
static properties = { label: { type: String, default: 'hi' } };
|
|
60
|
-
}
|
|
61
|
-
const tag = registerTestElement(El);
|
|
62
|
-
const el = mount(tag);
|
|
63
|
-
expect(el.label).toBe('hi');
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
// FEEDBACK-99 (color-app, 2026-06-07): a property assigned on the instance
|
|
67
|
-
// BEFORE upgrade — template engines set `.prop=${v}` during stamp, possibly
|
|
68
|
-
// before the element is defined (see template.js property-binding path) —
|
|
69
|
-
// must survive upgrade, not be clobbered by the signal default. WHATWG
|
|
70
|
-
// "upgrade a property" pattern, here in installProps. Fixes input-ui `.value`.
|
|
71
|
-
it('reifies a property set on the instance before upgrade (lazy upgrade)', () => {
|
|
72
|
-
const tag = `test-el-${++registrationCounter}`;
|
|
73
|
-
const el = document.createElement(tag); // undefined tag → un-upgraded
|
|
74
|
-
document.body.appendChild(el);
|
|
75
|
-
el.value = 'pre-upgrade'; // own data-property, no accessor yet
|
|
76
|
-
class El extends UIElement {
|
|
77
|
-
static properties = { value: { type: String, default: '' } };
|
|
78
|
-
}
|
|
79
|
-
customElements.define(tag, El); // upgrade → installProps runs
|
|
80
|
-
expect(el.value).toBe('pre-upgrade'); // reified, not clobbered to ''
|
|
81
|
-
});
|
|
82
|
-
|
|
83
|
-
it('reflects Boolean properties to HTML attributes when reflect: true', () => {
|
|
84
|
-
class El extends UIElement {
|
|
85
|
-
static properties = { disabled: { type: Boolean, default: false, reflect: true } };
|
|
86
|
-
}
|
|
87
|
-
const tag = registerTestElement(El);
|
|
88
|
-
const el = mount(tag);
|
|
89
|
-
expect(el.hasAttribute('disabled')).toBe(false);
|
|
90
|
-
el.disabled = true;
|
|
91
|
-
expect(el.hasAttribute('disabled')).toBe(true);
|
|
92
|
-
el.disabled = false;
|
|
93
|
-
expect(el.hasAttribute('disabled')).toBe(false);
|
|
94
|
-
});
|
|
95
|
-
|
|
96
|
-
it('reflects String properties as attribute values', () => {
|
|
97
|
-
class El extends UIElement {
|
|
98
|
-
static properties = { variant: { type: String, default: 'neutral', reflect: true } };
|
|
99
|
-
}
|
|
100
|
-
const tag = registerTestElement(El);
|
|
101
|
-
const el = mount(tag);
|
|
102
|
-
el.variant = 'accent';
|
|
103
|
-
expect(el.getAttribute('variant')).toBe('accent');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('parses incoming attributes back into typed properties', () => {
|
|
107
|
-
class El extends UIElement {
|
|
108
|
-
static properties = {
|
|
109
|
-
count: { type: Number, default: 0 },
|
|
110
|
-
active: { type: Boolean, default: false },
|
|
111
|
-
};
|
|
112
|
-
}
|
|
113
|
-
const tag = registerTestElement(El);
|
|
114
|
-
const el = mount(tag, { count: '42', active: '' });
|
|
115
|
-
expect(el.count).toBe(42);
|
|
116
|
-
expect(el.active).toBe(true);
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
it('does NOT reflect when reflect: false / omitted', () => {
|
|
120
|
-
class El extends UIElement {
|
|
121
|
-
static properties = { internal: { type: String, default: 'x' } };
|
|
122
|
-
}
|
|
123
|
-
const tag = registerTestElement(El);
|
|
124
|
-
const el = mount(tag);
|
|
125
|
-
el.internal = 'y';
|
|
126
|
-
expect(el.hasAttribute('internal')).toBe(false);
|
|
127
|
-
});
|
|
128
|
-
});
|
|
129
|
-
|
|
130
|
-
describe('UIElement — reactive effect', () => {
|
|
131
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
132
|
-
|
|
133
|
-
it('calls render() on mount and when a reactive property changes', async () => {
|
|
134
|
-
let renders = 0;
|
|
135
|
-
class El extends UIElement {
|
|
136
|
-
static properties = { label: { type: String, default: 'a' } };
|
|
137
|
-
render() { renders++; void this.label; }
|
|
138
|
-
}
|
|
139
|
-
const tag = registerTestElement(El);
|
|
140
|
-
const el = mount(tag);
|
|
141
|
-
expect(renders).toBeGreaterThan(0);
|
|
142
|
-
const before = renders;
|
|
143
|
-
el.label = 'b';
|
|
144
|
-
await tick();
|
|
145
|
-
expect(renders).toBeGreaterThan(before);
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it('stops re-rendering after disconnect', async () => {
|
|
149
|
-
let renders = 0;
|
|
150
|
-
class El extends UIElement {
|
|
151
|
-
static properties = { label: { type: String, default: 'a' } };
|
|
152
|
-
render() { renders++; void this.label; }
|
|
153
|
-
}
|
|
154
|
-
const tag = registerTestElement(El);
|
|
155
|
-
const el = mount(tag);
|
|
156
|
-
el.remove();
|
|
157
|
-
const frozen = renders;
|
|
158
|
-
el.label = 'c';
|
|
159
|
-
await tick();
|
|
160
|
-
expect(renders).toBe(frozen);
|
|
161
|
-
});
|
|
162
|
-
});
|
|
163
|
-
|
|
164
|
-
describe('UIElement — subscription-leak guard (0.0.10 regression)', () => {
|
|
165
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
166
|
-
|
|
167
|
-
it('child connected() reads do NOT subscribe an outer effect', async () => {
|
|
168
|
-
// The bug: when a parent effect called appendChild(child) while running,
|
|
169
|
-
// child.connectedCallback would synchronously call connected(), and any
|
|
170
|
-
// template-literal reads of child's reactive properties would subscribe
|
|
171
|
-
// the OUTER effect to the CHILD's signals. A later change to a child
|
|
172
|
-
// prop would spuriously re-run the parent effect.
|
|
173
|
-
//
|
|
174
|
-
// Fix: connected() is now wrapped in untracked(…).
|
|
175
|
-
class Child extends UIElement {
|
|
176
|
-
static properties = { value: { type: String, default: 'initial', reflect: true } };
|
|
177
|
-
connected() {
|
|
178
|
-
// Read our own reactive prop exactly the way template-literal
|
|
179
|
-
// interpolation would — this read must not subscribe an ancestor
|
|
180
|
-
// effect.
|
|
181
|
-
void this.value;
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const tag = registerTestElement(Child);
|
|
185
|
-
|
|
186
|
-
const trigger = signal(0);
|
|
187
|
-
let outerRuns = 0;
|
|
188
|
-
let childRef = null;
|
|
189
|
-
|
|
190
|
-
const dispose = effect(() => {
|
|
191
|
-
outerRuns++;
|
|
192
|
-
void trigger.value;
|
|
193
|
-
if (!childRef) {
|
|
194
|
-
childRef = document.createElement(tag);
|
|
195
|
-
document.body.appendChild(childRef);
|
|
196
|
-
}
|
|
197
|
-
});
|
|
198
|
-
|
|
199
|
-
const outerRunsAfterMount = outerRuns;
|
|
200
|
-
expect(childRef).not.toBeNull();
|
|
201
|
-
|
|
202
|
-
// Now mutate the CHILD's signal — the outer effect MUST NOT re-run.
|
|
203
|
-
childRef.value = 'changed';
|
|
204
|
-
await tick();
|
|
205
|
-
expect(outerRuns).toBe(outerRunsAfterMount);
|
|
206
|
-
|
|
207
|
-
// Sanity check: mutating the trigger signal the outer effect DID
|
|
208
|
-
// subscribe to re-runs the effect, proving the effect is alive.
|
|
209
|
-
trigger.value = 1;
|
|
210
|
-
await tick();
|
|
211
|
-
expect(outerRuns).toBe(outerRunsAfterMount + 1);
|
|
212
|
-
|
|
213
|
-
dispose();
|
|
214
|
-
});
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
describe('UIElement — addTrait', () => {
|
|
218
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
219
|
-
|
|
220
|
-
it('applies a trait once, idempotent on repeat', () => {
|
|
221
|
-
let connects = 0;
|
|
222
|
-
const myTrait = () => ({
|
|
223
|
-
connect() { connects++; },
|
|
224
|
-
disconnect() {},
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
class El extends UIElement {}
|
|
228
|
-
const tag = registerTestElement(El);
|
|
229
|
-
const el = mount(tag);
|
|
230
|
-
el.addTrait(myTrait);
|
|
231
|
-
el.addTrait(myTrait);
|
|
232
|
-
expect(connects).toBe(1);
|
|
233
|
-
});
|
|
234
|
-
|
|
235
|
-
it('disconnects all applied traits on remove', () => {
|
|
236
|
-
let connects = 0, disconnects = 0;
|
|
237
|
-
const myTrait = () => ({
|
|
238
|
-
connect() { connects++; },
|
|
239
|
-
disconnect() { disconnects++; },
|
|
240
|
-
});
|
|
241
|
-
class El extends UIElement {
|
|
242
|
-
static traits = [myTrait];
|
|
243
|
-
}
|
|
244
|
-
const tag = registerTestElement(El);
|
|
245
|
-
const el = mount(tag);
|
|
246
|
-
expect(connects).toBe(1);
|
|
247
|
-
expect(disconnects).toBe(0);
|
|
248
|
-
el.remove();
|
|
249
|
-
expect(disconnects).toBe(1);
|
|
250
|
-
});
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
// gh#285 — SSR DOM shims (linkedom et al.) implement no ElementInternals
|
|
254
|
-
// and no document.adoptedStyleSheets. These tests simulate that absence
|
|
255
|
-
// directly (deleting the real APIs) rather than trusting a mock — the
|
|
256
|
-
// bug was an unconditional call crashing the CONSTRUCTOR, before any
|
|
257
|
-
// subclass code ran, so the only real proof is exercising construction
|
|
258
|
-
// with the API genuinely gone.
|
|
259
|
-
describe('UIElement — SSR browser-API absence (gh#285)', () => {
|
|
260
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
261
|
-
|
|
262
|
-
it('constructs without throwing when attachInternals does not exist', () => {
|
|
263
|
-
const original = HTMLElement.prototype.attachInternals;
|
|
264
|
-
delete HTMLElement.prototype.attachInternals;
|
|
265
|
-
try {
|
|
266
|
-
class El extends UIElement {}
|
|
267
|
-
const tag = registerTestElement(El);
|
|
268
|
-
expect(() => mount(tag)).not.toThrow();
|
|
269
|
-
} finally {
|
|
270
|
-
HTMLElement.prototype.attachInternals = original;
|
|
271
|
-
}
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
it('the no-op internals shim answers every call site the framework actually makes', () => {
|
|
275
|
-
const original = HTMLElement.prototype.attachInternals;
|
|
276
|
-
delete HTMLElement.prototype.attachInternals;
|
|
277
|
-
let el;
|
|
278
|
-
try {
|
|
279
|
-
class El extends UIElement {}
|
|
280
|
-
const tag = registerTestElement(El);
|
|
281
|
-
el = mount(tag);
|
|
282
|
-
} finally {
|
|
283
|
-
HTMLElement.prototype.attachInternals = original;
|
|
284
|
-
}
|
|
285
|
-
expect(() => el.internals.setFormValue('x')).not.toThrow();
|
|
286
|
-
expect(() => el.internals.setValidity({})).not.toThrow();
|
|
287
|
-
expect(el.internals.checkValidity()).toBe(true);
|
|
288
|
-
expect(el.internals.reportValidity()).toBe(true);
|
|
289
|
-
expect(el.internals.form).toBeNull();
|
|
290
|
-
expect(el.internals.labels).toEqual([]);
|
|
291
|
-
expect(el.internals.validity).toEqual({});
|
|
292
|
-
expect(el.internals.validationMessage).toBe('');
|
|
293
|
-
expect(el.internals.willValidate).toBe(false);
|
|
294
|
-
});
|
|
295
|
-
|
|
296
|
-
it('a real attachInternals is still used when the browser provides one (no regression)', () => {
|
|
297
|
-
class El extends UIElement {}
|
|
298
|
-
const tag = registerTestElement(El);
|
|
299
|
-
const el = mount(tag);
|
|
300
|
-
// happy-dom's real attachInternals — not the shim.
|
|
301
|
-
expect(el.internals).not.toBe(null);
|
|
302
|
-
expect(typeof el.internals.setFormValue).toBe('function');
|
|
303
|
-
});
|
|
304
|
-
|
|
305
|
-
it('adoptStyles no-ops instead of throwing when adoptedStyleSheets does not exist', () => {
|
|
306
|
-
const desc = Object.getOwnPropertyDescriptor(Document.prototype, 'adoptedStyleSheets');
|
|
307
|
-
delete Document.prototype.adoptedStyleSheets;
|
|
308
|
-
try {
|
|
309
|
-
class El extends UIElement {
|
|
310
|
-
static styles = ['/* fake sheet */'];
|
|
311
|
-
}
|
|
312
|
-
const tag = registerTestElement(El);
|
|
313
|
-
expect(() => mount(tag)).not.toThrow();
|
|
314
|
-
} finally {
|
|
315
|
-
if (desc) Object.defineProperty(Document.prototype, 'adoptedStyleSheets', desc);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
// gh#284 — the custom-elements spec's "upgrade an element" algorithm
|
|
321
|
-
// (§4.13.5 step 6) requires replaying attributeChangedCallback for every
|
|
322
|
-
// attribute already present on an element BEFORE connectedCallback fires
|
|
323
|
-
// on upgrade. happy-dom (this repo's own test DOM) skips that replay —
|
|
324
|
-
// confirmed directly: a bare custom element upgraded with a pre-existing
|
|
325
|
-
// observed attribute gets connectedCallback but never the matching
|
|
326
|
-
// attributeChangedCallback. linkedom (the real SSR consumer's DOM shim,
|
|
327
|
-
// per adia-ssr) is a similarly from-scratch custom-elements registry, so
|
|
328
|
-
// the same gap is expected there too. Simulating the absence directly
|
|
329
|
-
// (a real late-upgrade sequence, not a mock) rather than trusting that
|
|
330
|
-
// happy-dom already reproduces the SSR shape — it does, which is exactly
|
|
331
|
-
// how this was found.
|
|
332
|
-
describe('UIElement — SSR attribute-upgrade replay (gh#284)', () => {
|
|
333
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
334
|
-
|
|
335
|
-
// Parses markup into an UNDEFINED custom element (so its attributes land
|
|
336
|
-
// on the element the way a server-rendered / pre-parsed document would),
|
|
337
|
-
// THEN defines the tag — matching the "define after parse" shape SSR
|
|
338
|
-
// hydration and lazy client registration both produce.
|
|
339
|
-
function mountLateUpgrade(tag, BaseImpl, html) {
|
|
340
|
-
const container = document.createElement('div');
|
|
341
|
-
container.innerHTML = html;
|
|
342
|
-
document.body.appendChild(container);
|
|
343
|
-
customElements.define(tag, BaseImpl);
|
|
344
|
-
return container.querySelector(tag);
|
|
345
|
-
}
|
|
346
|
-
|
|
347
|
-
it('confirms the environment gap this fix compensates for', () => {
|
|
348
|
-
const tag = `test-plain-${++registrationCounter}`;
|
|
349
|
-
const calls = [];
|
|
350
|
-
class Plain extends HTMLElement {
|
|
351
|
-
static get observedAttributes() { return ['foo']; }
|
|
352
|
-
attributeChangedCallback(name, oldV, newV) { calls.push([name, oldV, newV]); }
|
|
353
|
-
connectedCallback() { calls.push(['connected']); }
|
|
354
|
-
}
|
|
355
|
-
mountLateUpgrade(tag, Plain, `<${tag} foo="bar"></${tag}>`);
|
|
356
|
-
expect(calls).toEqual([['connected']]); // NOT ['foo', null, 'bar'] first, per spec
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
it('a reflected property set only via pre-parsed HTML is not stuck at its class default', () => {
|
|
360
|
-
const tag = `test-late-${++registrationCounter}`;
|
|
361
|
-
class El extends UIElement {
|
|
362
|
-
static properties = { label: { type: String, default: '', reflect: true } };
|
|
363
|
-
}
|
|
364
|
-
const el = mountLateUpgrade(tag, El, `<${tag} label="Profile"></${tag}>`);
|
|
365
|
-
expect(el.label).toBe('Profile');
|
|
366
|
-
});
|
|
367
|
-
|
|
368
|
-
it('a reflected boolean attribute present-with-no-value resolves true, not the class default', () => {
|
|
369
|
-
const tag = `test-late-bool-${++registrationCounter}`;
|
|
370
|
-
class El extends UIElement {
|
|
371
|
-
static properties = { active: { type: Boolean, default: false, reflect: true } };
|
|
372
|
-
}
|
|
373
|
-
const el = mountLateUpgrade(tag, El, `<${tag} active></${tag}>`);
|
|
374
|
-
expect(el.active).toBe(true);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
it('no regression: normal define-before-parse still gets attributeChangedCallback (no extra render)', async () => {
|
|
378
|
-
let renders = 0;
|
|
379
|
-
class El extends UIElement {
|
|
380
|
-
static properties = { label: { type: String, default: '', reflect: true } };
|
|
381
|
-
render() { renders++; }
|
|
382
|
-
}
|
|
383
|
-
const tag = registerTestElement(El);
|
|
384
|
-
const el = mount(tag, { label: 'Profile' });
|
|
385
|
-
await tick();
|
|
386
|
-
expect(el.label).toBe('Profile');
|
|
387
|
-
expect(renders).toBe(1); // the connectedCallback re-sync is a no-op here, not a second render
|
|
388
|
-
});
|
|
389
|
-
});
|
package/core/icons.test.js
DELETED
|
@@ -1,187 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import {
|
|
3
|
-
installIconLoaders,
|
|
4
|
-
registerIcon,
|
|
5
|
-
hasIcon,
|
|
6
|
-
isIconName,
|
|
7
|
-
getIcon,
|
|
8
|
-
listIcons,
|
|
9
|
-
} from './icons.js';
|
|
10
|
-
|
|
11
|
-
// Tests for `installIconLoaders` + `resolveLoader` + the §140 suffix-
|
|
12
|
-
// fallback fix for FEEDBACK-06 (color-app's `installIconLoaders` →
|
|
13
|
-
// `warnMissingIcon` failures on `caret-right` / `x` / `caret-up-down`
|
|
14
|
-
// when the consumer's `modules` keys didn't match the canonical
|
|
15
|
-
// `/node_modules/@phosphor-icons/core/assets/<weight>/<name>.svg` path.
|
|
16
|
-
//
|
|
17
|
-
// `isIconName` is the SYNC probe — it calls `resolveLoader` without
|
|
18
|
-
// triggering an async load. That's what we need to assert the fallback
|
|
19
|
-
// logic finds the loader regardless of path shape.
|
|
20
|
-
|
|
21
|
-
const CARET_RIGHT_SVG = '<svg data-icon="caret-right"/>';
|
|
22
|
-
const X_SVG = '<svg data-icon="x"/>';
|
|
23
|
-
const CARET_UP_DOWN_SVG = '<svg data-icon="caret-up-down"/>';
|
|
24
|
-
|
|
25
|
-
describe('installIconLoaders + resolveLoader (§140 suffix fallback)', () => {
|
|
26
|
-
beforeEach(() => {
|
|
27
|
-
// Reset to a known state. Note: the registry Map itself is module-
|
|
28
|
-
// singleton + doesn't reset between tests, so we keep test SVG
|
|
29
|
-
// contents distinct from any real phosphor names just in case.
|
|
30
|
-
installIconLoaders({ regular: {}, thin: {}, light: {}, bold: {}, fill: {}, duotone: {} });
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('resolves by canonical phosphor path (fast path)', () => {
|
|
34
|
-
installIconLoaders({
|
|
35
|
-
regular: {
|
|
36
|
-
'/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': CARET_RIGHT_SVG,
|
|
37
|
-
'/node_modules/@phosphor-icons/core/assets/regular/x.svg': X_SVG,
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
expect(isIconName('caret-right')).toBe(true);
|
|
42
|
-
expect(isIconName('x')).toBe(true);
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('resolves by filename suffix when key has a pnpm-style prefix', () => {
|
|
46
|
-
// pnpm hoists phosphor inside `.pnpm/<pkg>@<ver>/node_modules/...`
|
|
47
|
-
installIconLoaders({
|
|
48
|
-
regular: {
|
|
49
|
-
'/node_modules/.pnpm/@phosphor-icons+core@2.1.1/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg':
|
|
50
|
-
CARET_RIGHT_SVG,
|
|
51
|
-
},
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
expect(isIconName('caret-right')).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('resolves by filename suffix when key is a custom Vite-root absolute path', () => {
|
|
58
|
-
// Custom Vite root puts node_modules at a deeper location.
|
|
59
|
-
installIconLoaders({
|
|
60
|
-
regular: {
|
|
61
|
-
'/packages/my-app/node_modules/@phosphor-icons/core/assets/regular/caret-up-down.svg':
|
|
62
|
-
CARET_UP_DOWN_SVG,
|
|
63
|
-
},
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
expect(isIconName('caret-up-down')).toBe(true);
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
it('resolves by filename suffix for an arbitrary manually-built loader map', () => {
|
|
70
|
-
// Manual loader maps may have any keys at all — only the filename matters.
|
|
71
|
-
installIconLoaders({
|
|
72
|
-
regular: {
|
|
73
|
-
'/anywhere/at/all/caret-right.svg': CARET_RIGHT_SVG,
|
|
74
|
-
'/another/path/entirely/x.svg': X_SVG,
|
|
75
|
-
},
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
expect(isIconName('caret-right')).toBe(true);
|
|
79
|
-
expect(isIconName('x')).toBe(true);
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
it('returns false on miss across both paths', () => {
|
|
83
|
-
installIconLoaders({
|
|
84
|
-
regular: {
|
|
85
|
-
'/node_modules/@phosphor-icons/core/assets/regular/caret-right.svg': CARET_RIGHT_SVG,
|
|
86
|
-
},
|
|
87
|
-
});
|
|
88
|
-
|
|
89
|
-
expect(isIconName('not-a-real-icon-name')).toBe(false);
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('resolves `-fill` back-compat suffix in name via fast path', () => {
|
|
93
|
-
const STAR_FILL_SVG = '<svg data-icon="star-fill"/>';
|
|
94
|
-
installIconLoaders({
|
|
95
|
-
regular: {},
|
|
96
|
-
fill: {
|
|
97
|
-
'/node_modules/@phosphor-icons/core/assets/fill/star-fill.svg': STAR_FILL_SVG,
|
|
98
|
-
},
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
// `name="star-fill"` with default weight=regular falls back to fill weight.
|
|
102
|
-
expect(isIconName('star-fill')).toBe(true);
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
it('resolves `-fill` back-compat suffix in name via suffix fallback', () => {
|
|
106
|
-
const STAR_FILL_SVG = '<svg data-icon="star-fill"/>';
|
|
107
|
-
installIconLoaders({
|
|
108
|
-
regular: {},
|
|
109
|
-
fill: {
|
|
110
|
-
'/anywhere/star-fill.svg': STAR_FILL_SVG,
|
|
111
|
-
},
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
expect(isIconName('star-fill')).toBe(true);
|
|
115
|
-
});
|
|
116
|
-
|
|
117
|
-
it('resolves non-regular weights (bold, fill, etc.) via suffix fallback', () => {
|
|
118
|
-
const HEART_BOLD_SVG = '<svg data-icon="heart-bold"/>';
|
|
119
|
-
installIconLoaders({
|
|
120
|
-
regular: {},
|
|
121
|
-
bold: {
|
|
122
|
-
'/anywhere/heart-bold.svg': HEART_BOLD_SVG,
|
|
123
|
-
},
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
expect(isIconName('heart', 'bold')).toBe(true);
|
|
127
|
-
});
|
|
128
|
-
|
|
129
|
-
it('isIconName returns true via alias resolution + suffix fallback', () => {
|
|
130
|
-
// `send` → `paper-plane-right` in ICON_ALIASES. Suffix fallback should
|
|
131
|
-
// still match the aliased filename.
|
|
132
|
-
const PAPER_PLANE_SVG = '<svg data-icon="paper-plane-right"/>';
|
|
133
|
-
installIconLoaders({
|
|
134
|
-
regular: {
|
|
135
|
-
'/anywhere/paper-plane-right.svg': PAPER_PLANE_SVG,
|
|
136
|
-
},
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
expect(isIconName('send')).toBe(true);
|
|
140
|
-
});
|
|
141
|
-
|
|
142
|
-
it('end-to-end via DOM re-stamp: installIconLoaders auto-loads existing <icon-ui[name]>', async () => {
|
|
143
|
-
// The §140 fix's primary symptom: `installIconLoaders` walks existing
|
|
144
|
-
// `<icon-ui[name]>` elements + calls `loadIcon` for each. Without the
|
|
145
|
-
// suffix fallback, `loadIcon` → `resolveLoader` returns null →
|
|
146
|
-
// `warnMissingIcon` fires. With the fix, the load completes.
|
|
147
|
-
const PAW_SVG = '<svg data-icon="paw"/>';
|
|
148
|
-
|
|
149
|
-
// Plant an `<icon-ui[name="paw"]>` element. We don't need it to
|
|
150
|
-
// actually upgrade; we just need `querySelectorAll('icon-ui[name]')`
|
|
151
|
-
// to find it.
|
|
152
|
-
const el = document.createElement('icon-ui');
|
|
153
|
-
el.setAttribute('name', 'paw');
|
|
154
|
-
document.body.appendChild(el);
|
|
155
|
-
|
|
156
|
-
// Install a loader map with a pnpm-style path (suffix fallback exercise).
|
|
157
|
-
installIconLoaders({
|
|
158
|
-
regular: {
|
|
159
|
-
'/some/non-canonical/prefix/paw.svg': PAW_SVG,
|
|
160
|
-
},
|
|
161
|
-
});
|
|
162
|
-
|
|
163
|
-
// `loadIcon` runs sync (loader is a string, not a function), so the
|
|
164
|
-
// registry is written by the time `installIconLoaders` returns.
|
|
165
|
-
expect(hasIcon('paw')).toBe(true);
|
|
166
|
-
|
|
167
|
-
document.body.removeChild(el);
|
|
168
|
-
});
|
|
169
|
-
});
|
|
170
|
-
|
|
171
|
-
describe('registerIcon / hasIcon — unchanged by §140', () => {
|
|
172
|
-
it('registerIcon adds an icon by name without needing a loader map', () => {
|
|
173
|
-
const CUSTOM_SVG = '<svg data-icon="custom-§140-marker"/>';
|
|
174
|
-
registerIcon('my-§140-custom-icon', CUSTOM_SVG);
|
|
175
|
-
|
|
176
|
-
expect(hasIcon('my-§140-custom-icon')).toBe(true);
|
|
177
|
-
expect(getIcon('my-§140-custom-icon')).toBe(CUSTOM_SVG);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('listIcons returns registered names', () => {
|
|
181
|
-
registerIcon('§140-listIcons-marker-one', '<svg/>');
|
|
182
|
-
registerIcon('§140-listIcons-marker-two', '<svg/>');
|
|
183
|
-
|
|
184
|
-
expect(listIcons().some((k) => k.includes('§140-listIcons-marker-one'))).toBe(true);
|
|
185
|
-
expect(listIcons().some((k) => k.includes('§140-listIcons-marker-two'))).toBe(true);
|
|
186
|
-
});
|
|
187
|
-
});
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { logicalChildren, logicalSlotted } from './logical-children.js';
|
|
3
|
-
|
|
4
|
-
function host(html) {
|
|
5
|
-
const el = document.createElement('div');
|
|
6
|
-
el.innerHTML = html;
|
|
7
|
-
return el;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
describe('logical-children', () => {
|
|
11
|
-
beforeEach(() => { document.body.innerHTML = ''; });
|
|
12
|
-
|
|
13
|
-
it('returns static direct children as-is', () => {
|
|
14
|
-
const h = host(`<button slot="primary">A</button><p>body</p><button slot="secondary">B</button>`);
|
|
15
|
-
expect(logicalChildren(h).map((e) => e.tagName)).toEqual(['BUTTON', 'P', 'BUTTON']);
|
|
16
|
-
expect(logicalSlotted(h, 'primary').length).toBe(1);
|
|
17
|
-
expect(logicalSlotted(h, 'secondary')[0].textContent).toBe('B');
|
|
18
|
-
expect(logicalSlotted(h, 'missing').length).toBe(0);
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
it('pierces a single display:contents/role=presentation wrapper', () => {
|
|
22
|
-
const h = host(`
|
|
23
|
-
<span style="display:contents" role="presentation"><button slot="primary">A</button></span>
|
|
24
|
-
<p>body</p>
|
|
25
|
-
`);
|
|
26
|
-
// Without piercing, h.querySelector(':scope > [slot="primary"]') would be null:
|
|
27
|
-
expect(h.querySelector(':scope > [slot="primary"]')).toBeNull();
|
|
28
|
-
// With the helper, it's found:
|
|
29
|
-
expect(logicalSlotted(h, 'primary').length).toBe(1);
|
|
30
|
-
expect(logicalChildren(h).map((e) => e.tagName)).toEqual(['BUTTON', 'P']);
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('pierces NESTED wrappers (the .map() shape)', () => {
|
|
34
|
-
const h = host(`
|
|
35
|
-
<span style="display:contents" role="presentation">
|
|
36
|
-
<span style="display:contents" role="presentation"><button slot="act">A</button></span>
|
|
37
|
-
<span style="display:contents" role="presentation"><button slot="act">B</button></span>
|
|
38
|
-
</span>
|
|
39
|
-
`);
|
|
40
|
-
expect(logicalSlotted(h, 'act').map((e) => e.textContent)).toEqual(['A', 'B']);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('does NOT pierce non-wrapper elements (logical = direct, wrappers aside)', () => {
|
|
44
|
-
const h = host(`<div><button slot="primary">nested-in-real-div</button></div>`);
|
|
45
|
-
// The button is inside a real <div>, not a transparent wrapper → not a logical child.
|
|
46
|
-
expect(logicalSlotted(h, 'primary').length).toBe(0);
|
|
47
|
-
expect(logicalChildren(h).map((e) => e.tagName)).toEqual(['DIV']);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('tolerates a null host', () => {
|
|
51
|
-
expect(logicalChildren(null)).toEqual([]);
|
|
52
|
-
expect(logicalSlotted(null, 'x')).toEqual([]);
|
|
53
|
-
});
|
|
54
|
-
});
|
package/core/markdown.test.js
DELETED
|
@@ -1,47 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect } from 'vitest';
|
|
2
|
-
import { renderMarkdown } from './markdown.js';
|
|
3
|
-
|
|
4
|
-
// ── inline() protect-verbatim-regions regression (site-a2ui/FINDINGS.md
|
|
5
|
-
// "No inline-flow construct" — root cause: emphasis regexes ran BEFORE
|
|
6
|
-
// code-span extraction, so any code span containing `*`/`_` mangled) ──
|
|
7
|
-
|
|
8
|
-
describe('renderMarkdown — inline code spans survive markdown-unsafe content', () => {
|
|
9
|
-
it('code span containing an asterisk is not treated as emphasis', () => {
|
|
10
|
-
const html = renderMarkdown('Use `--a-chrome-*` for chrome tokens.');
|
|
11
|
-
expect(html).toContain('<code>--a-chrome-*</code>');
|
|
12
|
-
expect(html).not.toContain('<em>');
|
|
13
|
-
expect(html).not.toContain('<strong>');
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
it('code span containing underscores is not treated as emphasis', () => {
|
|
17
|
-
const html = renderMarkdown('Call `foo_bar_baz()` to run it.');
|
|
18
|
-
expect(html).toContain('<code>foo_bar_baz()</code>');
|
|
19
|
-
expect(html).not.toContain('<em>');
|
|
20
|
-
});
|
|
21
|
-
|
|
22
|
-
it('code span containing double-asterisks is not treated as bold', () => {
|
|
23
|
-
const html = renderMarkdown('The pattern `**not-bold**` is literal.');
|
|
24
|
-
expect(html).toContain('<code>**not-bold**</code>');
|
|
25
|
-
expect(html).not.toContain('<strong>');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('bold, italic, code, and link combine correctly within one run', () => {
|
|
29
|
-
const html = renderMarkdown('This is **bold**, this is *italic*, this is `code_with_*` and this is [a link](https://example.com).');
|
|
30
|
-
expect(html).toContain('<strong>bold</strong>');
|
|
31
|
-
expect(html).toContain('<em>italic</em>');
|
|
32
|
-
expect(html).toContain('<code>code_with_*</code>');
|
|
33
|
-
expect(html).toContain('<a href="https://example.com">a link</a>');
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
it('adjacent code spans do not bleed into each other', () => {
|
|
37
|
-
const html = renderMarkdown('First `a_b` then `c*d` then plain _italic_ text.');
|
|
38
|
-
expect(html).toContain('<code>a_b</code>');
|
|
39
|
-
expect(html).toContain('<code>c*d</code>');
|
|
40
|
-
expect(html).toContain('<em>italic</em>');
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('HTML-unsafe characters inside a code span are still escaped', () => {
|
|
44
|
-
const html = renderMarkdown('Use `<script>` tags carefully.');
|
|
45
|
-
expect(html).toContain('<code><script></code>');
|
|
46
|
-
});
|
|
47
|
-
});
|