@adia-ai/web-modules 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 +17 -0
- package/dist/everything.min.js +15 -15
- package/form/form-popover/form-popover.a2ui.json +1 -1
- package/form/form-popover/form-popover.js +22 -9
- package/form/form-popover/form-popover.yaml +13 -11
- package/package.json +16 -1
- package/chat/chat-composer/chat-composer.test.js +0 -112
- package/chat/chat-sidebar/chat-sidebar.test.js +0 -110
- package/chat/chat-thread/chat-thread.test.js +0 -82
- package/editor/editor-canvas/editor-canvas.test.js +0 -100
- package/editor/editor-sidebar/editor-sidebar.test.js +0 -383
- package/editor/editor-toolbar/editor-toolbar.test.js +0 -99
- package/form/form-popover/form-popover.test.js +0 -95
- package/shell/admin-command/admin-command.test.js +0 -115
- package/shell/admin-shell/admin-shell.test.js +0 -185
- package/shell/admin-sidebar/admin-sidebar.test.js +0 -173
- package/shell/embed-shell/embed-shell.test.js +0 -127
- package/simple/simple-shell/simple-shell.test.js +0 -83
- package/theme/theme-panel/theme-panel.test.js +0 -431
|
@@ -1,115 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
|
2
|
-
import '../../../web-components/core/element.js';
|
|
3
|
-
import './admin-command.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// happy-dom's <dialog> support is partial — showModal/close need to be
|
|
15
|
-
// patched into noops that toggle the .open getter via a flag. The real
|
|
16
|
-
// browser does this natively.
|
|
17
|
-
function patchDialogPolyfill(dialog) {
|
|
18
|
-
let isOpen = false;
|
|
19
|
-
Object.defineProperty(dialog, 'open', {
|
|
20
|
-
get: () => isOpen,
|
|
21
|
-
set: (v) => { isOpen = !!v; },
|
|
22
|
-
configurable: true,
|
|
23
|
-
});
|
|
24
|
-
dialog.showModal = function () {
|
|
25
|
-
isOpen = true;
|
|
26
|
-
this.dispatchEvent(new Event('toggle'));
|
|
27
|
-
};
|
|
28
|
-
dialog.close = function () {
|
|
29
|
-
isOpen = false;
|
|
30
|
-
this.dispatchEvent(new Event('close'));
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
beforeEach(() => {
|
|
35
|
-
document.body.innerHTML = '';
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
describe('admin-command', () => {
|
|
39
|
-
it('registers admin-command as a custom element', () => {
|
|
40
|
-
expect(customElements.get('admin-command')).toBeDefined();
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('defaults to open=false, shortcut="both"', () => {
|
|
44
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
45
|
-
expect(cmd.open).toBe(false);
|
|
46
|
-
expect(cmd.shortcut).toBe('both');
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('exposes .show() / .hide() / .toggle() public methods', () => {
|
|
50
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
51
|
-
expect(typeof cmd.show).toBe('function');
|
|
52
|
-
expect(typeof cmd.hide).toBe('function');
|
|
53
|
-
expect(typeof cmd.toggle).toBe('function');
|
|
54
|
-
});
|
|
55
|
-
|
|
56
|
-
it('reflects [open] when .show() is called', async () => {
|
|
57
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
58
|
-
const dialog = cmd.shadowRoot?.querySelector('dialog') ?? cmd.querySelector('dialog');
|
|
59
|
-
if (dialog) patchDialogPolyfill(dialog);
|
|
60
|
-
cmd.show();
|
|
61
|
-
await tick();
|
|
62
|
-
expect(cmd.open).toBe(true);
|
|
63
|
-
expect(cmd.hasAttribute('open')).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('clears [open] when .hide() is called', async () => {
|
|
67
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
68
|
-
const dialog = cmd.shadowRoot?.querySelector('dialog') ?? cmd.querySelector('dialog');
|
|
69
|
-
if (dialog) patchDialogPolyfill(dialog);
|
|
70
|
-
cmd.show();
|
|
71
|
-
await tick();
|
|
72
|
-
cmd.hide();
|
|
73
|
-
await tick();
|
|
74
|
-
expect(cmd.open).toBe(false);
|
|
75
|
-
expect(cmd.hasAttribute('open')).toBe(false);
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('toggle() flips open state and returns the new value', async () => {
|
|
79
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
80
|
-
const dialog = cmd.shadowRoot?.querySelector('dialog') ?? cmd.querySelector('dialog');
|
|
81
|
-
if (dialog) patchDialogPolyfill(dialog);
|
|
82
|
-
const after1 = cmd.toggle();
|
|
83
|
-
await tick();
|
|
84
|
-
expect(after1).toBe(true);
|
|
85
|
-
const after2 = cmd.toggle();
|
|
86
|
-
await tick();
|
|
87
|
-
expect(after2).toBe(false);
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
it('removes Cmd+K listener on disconnect', () => {
|
|
91
|
-
const cmd = mount('<admin-command></admin-command>');
|
|
92
|
-
const removeSpy = vi.spyOn(document, 'removeEventListener');
|
|
93
|
-
cmd.remove();
|
|
94
|
-
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
95
|
-
expect(removedTypes).toContain('keydown');
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
it('honors [no-shortcut] — does not toggle on Cmd+K', async () => {
|
|
99
|
-
// Verify behavior, not listener registration (happy-dom timing makes
|
|
100
|
-
// spy-based tests unreliable for synchronous custom-element lifecycle).
|
|
101
|
-
const cmd = mount('<admin-command no-shortcut></admin-command>');
|
|
102
|
-
const dialog = cmd.querySelector(':scope > dialog');
|
|
103
|
-
if (dialog) patchDialogPolyfill(dialog);
|
|
104
|
-
const event = new KeyboardEvent('keydown', { key: 'k', metaKey: true });
|
|
105
|
-
document.dispatchEvent(event);
|
|
106
|
-
await tick();
|
|
107
|
-
expect(cmd.open).toBe(false);
|
|
108
|
-
cmd.remove();
|
|
109
|
-
});
|
|
110
|
-
|
|
111
|
-
it('accepts shortcut="cmd+k"', () => {
|
|
112
|
-
const cmd = mount('<admin-command shortcut="cmd+k"></admin-command>');
|
|
113
|
-
expect(cmd.shortcut).toBe('cmd+k');
|
|
114
|
-
});
|
|
115
|
-
});
|
|
@@ -1,185 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import { readFileSync } from 'node:fs';
|
|
3
|
-
import { fileURLToPath } from 'node:url';
|
|
4
|
-
import { dirname, resolve } from 'node:path';
|
|
5
|
-
|
|
6
|
-
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
7
|
-
const bespokeCSS = readFileSync(resolve(__dirname, 'css/admin-shell.bespoke.css'), 'utf8');
|
|
8
|
-
const collapsedCSS = readFileSync(resolve(__dirname, 'css/admin-shell.collapsed.css'), 'utf8');
|
|
9
|
-
|
|
10
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
11
|
-
function mount(html) {
|
|
12
|
-
document.body.innerHTML = html;
|
|
13
|
-
return document.body.firstElementChild;
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
beforeEach(() => {
|
|
17
|
-
document.body.innerHTML = '';
|
|
18
|
-
});
|
|
19
|
-
|
|
20
|
-
// ── Regression guards ─────────────────────────────────────────────
|
|
21
|
-
//
|
|
22
|
-
// These tests document substrate selector decisions that have been
|
|
23
|
-
// regressed and re-fixed at least once each. They run against the raw
|
|
24
|
-
// CSS file content + DOM `element.matches()` rather than computed style,
|
|
25
|
-
// because happy-dom doesn't fully resolve external stylesheet cascade.
|
|
26
|
-
// The trade-off: we don't assert that the rule WAS applied, but we DO
|
|
27
|
-
// assert that the rule's selector REACHES the element it's meant to
|
|
28
|
-
// style — which is the part that broke in v0.6.16.
|
|
29
|
-
|
|
30
|
-
describe('admin-shell.bespoke.css — selector reachability', () => {
|
|
31
|
-
it('v0.6.16 fix: admin-page rule uses descendant combinator (not >)', () => {
|
|
32
|
-
// The previous selector `admin-scroll > admin-page` broke when a
|
|
33
|
-
// router (e.g. <router-ui>) sat between <admin-scroll> and
|
|
34
|
-
// <admin-page>. The fix uses :is() + descendant combinator so any
|
|
35
|
-
// depth of intermediate wrapping still matches.
|
|
36
|
-
expect(bespokeCSS).toMatch(/:is\(admin-scroll,\s*admin-content\)\s+admin-page\s*\{/);
|
|
37
|
-
// Negative assertion: the broken direct-child form should not be
|
|
38
|
-
// present any more (catches accidental reverts).
|
|
39
|
-
expect(bespokeCSS).not.toMatch(/^admin-scroll\s*>\s*admin-page\s*,/m);
|
|
40
|
-
expect(bespokeCSS).not.toMatch(/^admin-content\s*>\s*admin-page\s*\{/m);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('admin-page rule reaches admin-page inside <router-ui>', () => {
|
|
44
|
-
// Synthesize the docs-site DOM shape that hit the v0.6.16 bug.
|
|
45
|
-
// The selector under test must match <admin-page> via descendant
|
|
46
|
-
// combinator even when <router-ui> sits between.
|
|
47
|
-
mount(`
|
|
48
|
-
<admin-shell>
|
|
49
|
-
<admin-content>
|
|
50
|
-
<admin-scroll>
|
|
51
|
-
<router-ui id="router">
|
|
52
|
-
<admin-page id="page"></admin-page>
|
|
53
|
-
</router-ui>
|
|
54
|
-
</admin-scroll>
|
|
55
|
-
</admin-content>
|
|
56
|
-
</admin-shell>
|
|
57
|
-
`);
|
|
58
|
-
const page = document.getElementById('page');
|
|
59
|
-
expect(page).toBeTruthy();
|
|
60
|
-
// Direct-child match should fail (router-ui sits between)
|
|
61
|
-
expect(page.matches('admin-scroll > admin-page')).toBe(false);
|
|
62
|
-
expect(page.matches('admin-content > admin-page')).toBe(false);
|
|
63
|
-
// Descendant match must succeed (this is what v0.6.17 introduced)
|
|
64
|
-
expect(page.matches(':is(admin-scroll, admin-content) admin-page')).toBe(true);
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('admin-page rule still reaches admin-page as direct child', () => {
|
|
68
|
-
// Canonical playground/example layout — no router wrapper.
|
|
69
|
-
// The descendant combinator must still match this case.
|
|
70
|
-
mount(`
|
|
71
|
-
<admin-shell>
|
|
72
|
-
<admin-content>
|
|
73
|
-
<admin-scroll>
|
|
74
|
-
<admin-page id="page"></admin-page>
|
|
75
|
-
</admin-scroll>
|
|
76
|
-
</admin-content>
|
|
77
|
-
</admin-shell>
|
|
78
|
-
`);
|
|
79
|
-
const page = document.getElementById('page');
|
|
80
|
-
expect(page.matches(':is(admin-scroll, admin-content) admin-page')).toBe(true);
|
|
81
|
-
});
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
describe('admin-shell.collapsed.css — vanilla HTML fallback (v0.6.17)', () => {
|
|
85
|
-
it('contains the vanilla-HTML fallback block', () => {
|
|
86
|
-
// v0.6.17 added rules covering <button class="nav-item"> etc. so
|
|
87
|
-
// text labels don't overflow the 48px rail when the consumer doesn't
|
|
88
|
-
// use AdiaUI <nav-item-ui> primitives.
|
|
89
|
-
expect(collapsedCSS).toMatch(/Vanilla-HTML fallback/);
|
|
90
|
-
// v0.6.20 (FEEDBACK-38 addendum): the heading-hide rule narrowed from a
|
|
91
|
-
// blanket `[slot="heading"]` to plain-text headings + headings without
|
|
92
|
-
// slotted children, so composed wrappers (<admin-entity-item slot="heading">)
|
|
93
|
-
// survive collapse. The rule still resolves to `display: none`.
|
|
94
|
-
expect(collapsedCSS).toMatch(/\[slot="heading"\][^{]*\{\s*display:\s*none/);
|
|
95
|
-
expect(collapsedCSS).toMatch(/\[slot="heading"\]:not\(:has\(>\s*\[slot\]\)\)/);
|
|
96
|
-
expect(collapsedCSS).toMatch(/button\.nav-item/);
|
|
97
|
-
expect(collapsedCSS).toMatch(/text-indent:\s*-9999px/);
|
|
98
|
-
});
|
|
99
|
-
|
|
100
|
-
it('preserves icon visibility inside vanilla nav buttons', () => {
|
|
101
|
-
// text-indent:-9999px on the parent must be undone on the icon
|
|
102
|
-
// child, otherwise the icon disappears with the text. Both rules
|
|
103
|
-
// must be present in the file.
|
|
104
|
-
expect(collapsedCSS).toMatch(/button\.nav-item\s*>\s*icon-ui[\s\S]*?text-indent:\s*0/);
|
|
105
|
-
});
|
|
106
|
-
|
|
107
|
-
it('fallback rules sit inside the @container sidebar query', () => {
|
|
108
|
-
// The vanilla-HTML fallback must be scoped to the collapsed state
|
|
109
|
-
// (max-width: 96px container query). Outside that scope, the
|
|
110
|
-
// text-indent: -9999px clip would also break the expanded state.
|
|
111
|
-
const containerMatch = collapsedCSS.match(/@container\s+sidebar\s*\(max-width:\s*96px\)\s*\{([\s\S]+)\}/);
|
|
112
|
-
expect(containerMatch).toBeTruthy();
|
|
113
|
-
const inside = containerMatch[1];
|
|
114
|
-
expect(inside).toMatch(/Vanilla-HTML fallback/);
|
|
115
|
-
expect(inside).toMatch(/text-indent:\s*-9999px/);
|
|
116
|
-
});
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
// ── FB-55 — runtime warn when AdminSidebar / AdminCommand unregistered ──
|
|
120
|
-
//
|
|
121
|
-
// Per-component import path (`@adia-ai/web-modules/shell/admin-shell`)
|
|
122
|
-
// registers ONLY AdminShell. <admin-sidebar> renders (CSS targets by
|
|
123
|
-
// tag) but is an unupgraded HTMLElement with no .toggle(). The
|
|
124
|
-
// optional chain `sidebar?.toggle?.()` previously swallowed the no-op
|
|
125
|
-
// silently — zero diagnostic. v0.6.35 adds a one-shot console.warn at
|
|
126
|
-
// toggle-time when the sidebar is found but not upgraded.
|
|
127
|
-
//
|
|
128
|
-
// These tests import ONLY AdminShell (not AdminSidebar), simulate the
|
|
129
|
-
// per-component-path failure mode, and assert the warn fires + dedups
|
|
130
|
-
// per element.
|
|
131
|
-
|
|
132
|
-
describe('admin-shell — FB-55 unregistered-element warn', () => {
|
|
133
|
-
it('logs warn when [data-sidebar-toggle] clicked and admin-sidebar not registered', async () => {
|
|
134
|
-
// AdminShell only — AdminSidebar deliberately NOT imported.
|
|
135
|
-
await import('./admin-shell.js');
|
|
136
|
-
const warns = [];
|
|
137
|
-
const origWarn = console.warn;
|
|
138
|
-
console.warn = (...args) => warns.push(args.join(' '));
|
|
139
|
-
try {
|
|
140
|
-
const shell = mount(
|
|
141
|
-
'<admin-shell>' +
|
|
142
|
-
'<admin-sidebar slot="leading"></admin-sidebar>' +
|
|
143
|
-
'<button data-sidebar-toggle="leading">☰</button>' +
|
|
144
|
-
'</admin-shell>'
|
|
145
|
-
);
|
|
146
|
-
// Allow the connect-time wiring to settle.
|
|
147
|
-
await new Promise((r) => queueMicrotask(r));
|
|
148
|
-
const btn = shell.querySelector('[data-sidebar-toggle]');
|
|
149
|
-
btn.click();
|
|
150
|
-
expect(warns.length).toBe(1);
|
|
151
|
-
expect(warns[0]).toMatch(/admin-sidebar.*slot="leading".*not registered/i);
|
|
152
|
-
expect(warns[0]).toMatch(/Sidebar toggle will not work/);
|
|
153
|
-
expect(warns[0]).toMatch(/cluster barrel/);
|
|
154
|
-
// Second click — dedup, no additional warn for the same offender.
|
|
155
|
-
btn.click();
|
|
156
|
-
expect(warns.length).toBe(1);
|
|
157
|
-
} finally {
|
|
158
|
-
console.warn = origWarn;
|
|
159
|
-
}
|
|
160
|
-
});
|
|
161
|
-
|
|
162
|
-
it('does NOT log when admin-sidebar IS registered (upgraded element)', async () => {
|
|
163
|
-
// Import the sidebar class — element is now upgraded with .toggle().
|
|
164
|
-
await import('../admin-sidebar/admin-sidebar.js');
|
|
165
|
-
await import('./admin-shell.js');
|
|
166
|
-
const warns = [];
|
|
167
|
-
const origWarn = console.warn;
|
|
168
|
-
console.warn = (...args) => warns.push(args.join(' '));
|
|
169
|
-
try {
|
|
170
|
-
const shell = mount(
|
|
171
|
-
'<admin-shell>' +
|
|
172
|
-
'<admin-sidebar slot="leading"></admin-sidebar>' +
|
|
173
|
-
'<button data-sidebar-toggle="leading">☰</button>' +
|
|
174
|
-
'</admin-shell>'
|
|
175
|
-
);
|
|
176
|
-
await new Promise((r) => queueMicrotask(r));
|
|
177
|
-
shell.querySelector('[data-sidebar-toggle]').click();
|
|
178
|
-
// No warn — sidebar.toggle() exists and runs cleanly.
|
|
179
|
-
const fb55Warns = warns.filter((w) => /FB-55|not registered/.test(w));
|
|
180
|
-
expect(fb55Warns.length).toBe(0);
|
|
181
|
-
} finally {
|
|
182
|
-
console.warn = origWarn;
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
});
|
|
@@ -1,173 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
2
|
-
import '../../../web-components/core/element.js';
|
|
3
|
-
import './admin-sidebar.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
// happy-dom doesn't ship a working ResizeObserver out-of-the-box, and
|
|
15
|
-
// returns zero from getBoundingClientRect (no layout engine). Patch both
|
|
16
|
-
// in beforeEach so connected() doesn't immediately mark every sidebar as
|
|
17
|
-
// collapsed (width <= 96).
|
|
18
|
-
let originalRect;
|
|
19
|
-
beforeEach(() => {
|
|
20
|
-
document.body.innerHTML = '';
|
|
21
|
-
try { localStorage.clear(); } catch {}
|
|
22
|
-
globalThis.ResizeObserver = class {
|
|
23
|
-
observe() {}
|
|
24
|
-
unobserve() {}
|
|
25
|
-
disconnect() {}
|
|
26
|
-
};
|
|
27
|
-
// Mock getBoundingClientRect to derive width from inline style (or 240 default)
|
|
28
|
-
originalRect = HTMLElement.prototype.getBoundingClientRect;
|
|
29
|
-
HTMLElement.prototype.getBoundingClientRect = function () {
|
|
30
|
-
const inline = this.style?.width || '';
|
|
31
|
-
const w = parseFloat(inline) || 240;
|
|
32
|
-
return { width: w, height: 600, top: 0, left: 0, right: w, bottom: 600, x: 0, y: 0 };
|
|
33
|
-
};
|
|
34
|
-
});
|
|
35
|
-
|
|
36
|
-
afterEach(() => {
|
|
37
|
-
if (originalRect) HTMLElement.prototype.getBoundingClientRect = originalRect;
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
describe('admin-sidebar', () => {
|
|
41
|
-
it('registers admin-sidebar as a custom element', () => {
|
|
42
|
-
expect(customElements.get('admin-sidebar')).toBeDefined();
|
|
43
|
-
});
|
|
44
|
-
|
|
45
|
-
it('defaults to collapsed=false on connect', () => {
|
|
46
|
-
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
47
|
-
expect(sb.collapsed).toBe(false);
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('reflects [collapsed] via property assignment', async () => {
|
|
51
|
-
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
52
|
-
sb.collapsed = true;
|
|
53
|
-
await tick();
|
|
54
|
-
expect(sb.hasAttribute('collapsed')).toBe(true);
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
it('reflects [resizing] via property assignment', async () => {
|
|
58
|
-
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
59
|
-
sb.resizing = true;
|
|
60
|
-
await tick();
|
|
61
|
-
expect(sb.hasAttribute('resizing')).toBe(true);
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('exposes .toggle() / .collapse() / .expand() public methods', () => {
|
|
65
|
-
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
66
|
-
expect(typeof sb.toggle).toBe('function');
|
|
67
|
-
expect(typeof sb.collapse).toBe('function');
|
|
68
|
-
expect(typeof sb.expand).toBe('function');
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('persists width to localStorage on collapse, restores on connect', async () => {
|
|
72
|
-
// First mount: set a width, collapse, persisted to localStorage
|
|
73
|
-
const sb1 = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
74
|
-
sb1.style.width = '240px';
|
|
75
|
-
// happy-dom doesn't compute layout; mock getBoundingClientRect to return our set width
|
|
76
|
-
sb1.getBoundingClientRect = () => ({ width: 240 });
|
|
77
|
-
sb1.collapse();
|
|
78
|
-
expect(sb1.collapsed).toBe(true);
|
|
79
|
-
// localStorage should hold either the floor or the previous width
|
|
80
|
-
const stored = localStorage.getItem('adia-sidebar-leading');
|
|
81
|
-
expect(stored).not.toBeNull();
|
|
82
|
-
});
|
|
83
|
-
|
|
84
|
-
it('uses [name] override for the localStorage key', () => {
|
|
85
|
-
const sb = mount('<admin-sidebar slot="leading" name="custom-id" collapsible></admin-sidebar>');
|
|
86
|
-
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
87
|
-
sb.style.width = '200px';
|
|
88
|
-
sb.collapse();
|
|
89
|
-
expect(localStorage.getItem('adia-sidebar-custom-id')).not.toBeNull();
|
|
90
|
-
expect(localStorage.getItem('adia-sidebar-leading')).toBeNull();
|
|
91
|
-
});
|
|
92
|
-
|
|
93
|
-
it('toggle() returns the new collapsed value', () => {
|
|
94
|
-
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
95
|
-
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
96
|
-
sb.style.width = '200px';
|
|
97
|
-
const result = sb.toggle();
|
|
98
|
-
expect(result).toBe(sb.collapsed);
|
|
99
|
-
});
|
|
100
|
-
|
|
101
|
-
it('dispatches sidebar-toggle event on toggle()', () => {
|
|
102
|
-
const sb = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
103
|
-
sb.getBoundingClientRect = () => ({ width: 200 });
|
|
104
|
-
sb.style.width = '200px';
|
|
105
|
-
const onToggle = vi.fn();
|
|
106
|
-
sb.addEventListener('sidebar-toggle', onToggle);
|
|
107
|
-
sb.toggle();
|
|
108
|
-
expect(onToggle).toHaveBeenCalledTimes(1);
|
|
109
|
-
expect(onToggle.mock.calls[0][0].detail).toEqual(
|
|
110
|
-
expect.objectContaining({ name: expect.any(String), expanded: expect.any(Boolean) })
|
|
111
|
-
);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('cleans up resize handlers on disconnect (no zombie listeners)', () => {
|
|
115
|
-
const sb = mount('<admin-sidebar slot="leading" resizable><div data-resize></div></admin-sidebar>');
|
|
116
|
-
const handle = sb.querySelector('[data-resize]');
|
|
117
|
-
const removeSpy = vi.spyOn(handle, 'removeEventListener');
|
|
118
|
-
sb.remove();
|
|
119
|
-
// disconnected() runs cleanup; pointerdown listener should have been removed
|
|
120
|
-
const removedTypes = removeSpy.mock.calls.map((args) => args[0]);
|
|
121
|
-
expect(removedTypes).toContain('pointerdown');
|
|
122
|
-
});
|
|
123
|
-
|
|
124
|
-
// gh#286 — a zero rect (linkedom/SSR shims have no layout engine; a
|
|
125
|
-
// browser can also connect an element before its first layout, e.g.
|
|
126
|
-
// inside a display:none ancestor) must NOT snap the sidebar collapsed.
|
|
127
|
-
describe('gh#286 — zero-rect connect does not force collapse', () => {
|
|
128
|
-
it('stays at the connect() default (expanded) when the rect is 0×0', () => {
|
|
129
|
-
HTMLElement.prototype.getBoundingClientRect = () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0 });
|
|
130
|
-
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
131
|
-
expect(sb.collapsed).toBe(false);
|
|
132
|
-
});
|
|
133
|
-
|
|
134
|
-
it('keeps a persisted expanded width instead of overriding it with a bogus collapse', () => {
|
|
135
|
-
// First mount at a real expanded width — persists 240px.
|
|
136
|
-
const sb1 = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
137
|
-
sb1.getBoundingClientRect = () => ({ width: 240 });
|
|
138
|
-
sb1.style.width = '240px';
|
|
139
|
-
sb1.collapse();
|
|
140
|
-
sb1.expand();
|
|
141
|
-
sb1.remove();
|
|
142
|
-
|
|
143
|
-
// Re-mount under a zero-rect environment (the SSR case) — the
|
|
144
|
-
// restored width from storage should survive; connected() must not
|
|
145
|
-
// overwrite it back to collapsed just because the rect read 0.
|
|
146
|
-
HTMLElement.prototype.getBoundingClientRect = () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0 });
|
|
147
|
-
const sb2 = mount('<admin-sidebar slot="leading" collapsible></admin-sidebar>');
|
|
148
|
-
expect(sb2.collapsed).toBe(false);
|
|
149
|
-
});
|
|
150
|
-
|
|
151
|
-
it('the ResizeObserver first tick self-corrects once real layout exists', () => {
|
|
152
|
-
let observedCallback;
|
|
153
|
-
globalThis.ResizeObserver = class {
|
|
154
|
-
constructor(cb) { observedCallback = cb; }
|
|
155
|
-
observe() {}
|
|
156
|
-
unobserve() {}
|
|
157
|
-
disconnect() {}
|
|
158
|
-
};
|
|
159
|
-
HTMLElement.prototype.getBoundingClientRect = () => ({ width: 0, height: 0, top: 0, left: 0, right: 0, bottom: 0, x: 0, y: 0 });
|
|
160
|
-
const sb = mount('<admin-sidebar slot="leading"></admin-sidebar>');
|
|
161
|
-
expect(sb.collapsed).toBe(false); // still unresolved right after connect
|
|
162
|
-
|
|
163
|
-
// Simulate the observer's first real tick (e.g. SSR hydration's
|
|
164
|
-
// first client paint) reporting an actually-narrow layout.
|
|
165
|
-
observedCallback([{ contentBoxSize: [{ inlineSize: 60 }] }]);
|
|
166
|
-
expect(sb.collapsed).toBe(true);
|
|
167
|
-
|
|
168
|
-
// And the inverse — a real wide layout expands it.
|
|
169
|
-
observedCallback([{ contentBoxSize: [{ inlineSize: 240 }] }]);
|
|
170
|
-
expect(sb.collapsed).toBe(false);
|
|
171
|
-
});
|
|
172
|
-
});
|
|
173
|
-
});
|
|
@@ -1,127 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import './embed-shell.js'; // registers <embed-shell>
|
|
3
|
-
|
|
4
|
-
// A minimal consumer shape: one [app] surface with two [opens] triggers, plus
|
|
5
|
-
// two named [panel] surfaces (one carrying a [close] trigger). The shell owns
|
|
6
|
-
// layout, not content — divs stand in for the real consumer surfaces.
|
|
7
|
-
function mount() {
|
|
8
|
-
document.body.innerHTML = `
|
|
9
|
-
<embed-shell>
|
|
10
|
-
<div app>
|
|
11
|
-
<button id="open-chat" opens="chat">chat</button>
|
|
12
|
-
<button id="open-settings" opens="settings">settings</button>
|
|
13
|
-
</div>
|
|
14
|
-
<div panel="chat"><button id="close" close>x</button></div>
|
|
15
|
-
<div panel="settings"></div>
|
|
16
|
-
</embed-shell>`;
|
|
17
|
-
return document.body.querySelector('embed-shell');
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
beforeEach(() => {
|
|
21
|
-
document.body.innerHTML = '';
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
// ── open / close / toggle ─────────────────────────────────────────
|
|
25
|
-
describe('embed-shell — open / close / toggle API', () => {
|
|
26
|
-
it('open(name) reflects [panel] on the shell + sets [active] on the matching surface', () => {
|
|
27
|
-
const shell = mount();
|
|
28
|
-
shell.open('chat');
|
|
29
|
-
expect(shell.getAttribute('panel')).toBe('chat');
|
|
30
|
-
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(true);
|
|
31
|
-
expect(shell.panel).toBe('chat');
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('open(name) switches the active panel — only one [active] at a time', () => {
|
|
35
|
-
const shell = mount();
|
|
36
|
-
shell.open('chat');
|
|
37
|
-
shell.open('settings');
|
|
38
|
-
expect(shell.getAttribute('panel')).toBe('settings');
|
|
39
|
-
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(false);
|
|
40
|
-
expect(shell.querySelector('[panel="settings"]').hasAttribute('active')).toBe(true);
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('open() with no matching [panel] is a no-op (consumer must provide the surface)', () => {
|
|
44
|
-
const shell = mount();
|
|
45
|
-
shell.open('nope');
|
|
46
|
-
expect(shell.hasAttribute('panel')).toBe(false);
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
it('open() ignores an empty name', () => {
|
|
50
|
-
const shell = mount();
|
|
51
|
-
shell.open('');
|
|
52
|
-
expect(shell.hasAttribute('panel')).toBe(false);
|
|
53
|
-
});
|
|
54
|
-
|
|
55
|
-
it('close() clears [panel] + [active]; .panel reads ""', () => {
|
|
56
|
-
const shell = mount();
|
|
57
|
-
shell.open('chat');
|
|
58
|
-
shell.close();
|
|
59
|
-
expect(shell.hasAttribute('panel')).toBe(false);
|
|
60
|
-
expect(shell.querySelector('[panel="chat"]').hasAttribute('active')).toBe(false);
|
|
61
|
-
expect(shell.panel).toBe('');
|
|
62
|
-
});
|
|
63
|
-
|
|
64
|
-
it('toggle() opens when closed, closes when the same panel is already open', () => {
|
|
65
|
-
const shell = mount();
|
|
66
|
-
shell.toggle('chat');
|
|
67
|
-
expect(shell.panel).toBe('chat');
|
|
68
|
-
shell.toggle('chat');
|
|
69
|
-
expect(shell.panel).toBe('');
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
it('emits embed:change with the new panel name on open, "" on close', () => {
|
|
73
|
-
const shell = mount();
|
|
74
|
-
const seen = [];
|
|
75
|
-
shell.addEventListener('embed:change', (e) => seen.push(e.detail.panel));
|
|
76
|
-
shell.open('chat');
|
|
77
|
-
shell.close();
|
|
78
|
-
expect(seen).toEqual(['chat', '']);
|
|
79
|
-
});
|
|
80
|
-
});
|
|
81
|
-
|
|
82
|
-
// ── delegated triggers + inbound events ───────────────────────────
|
|
83
|
-
describe('embed-shell — delegated triggers + events', () => {
|
|
84
|
-
it('[opens] click toggles the named panel', () => {
|
|
85
|
-
const shell = mount();
|
|
86
|
-
shell.querySelector('#open-chat').click();
|
|
87
|
-
expect(shell.panel).toBe('chat');
|
|
88
|
-
shell.querySelector('#open-chat').click(); // same trigger toggles it back off
|
|
89
|
-
expect(shell.panel).toBe('');
|
|
90
|
-
});
|
|
91
|
-
|
|
92
|
-
it('[opens] switches directly between panels', () => {
|
|
93
|
-
const shell = mount();
|
|
94
|
-
shell.querySelector('#open-chat').click();
|
|
95
|
-
shell.querySelector('#open-settings').click();
|
|
96
|
-
expect(shell.panel).toBe('settings');
|
|
97
|
-
});
|
|
98
|
-
|
|
99
|
-
it('[close] click dismisses the open panel', () => {
|
|
100
|
-
const shell = mount();
|
|
101
|
-
shell.open('chat');
|
|
102
|
-
shell.querySelector('#close').click();
|
|
103
|
-
expect(shell.panel).toBe('');
|
|
104
|
-
});
|
|
105
|
-
|
|
106
|
-
it('embed:open event opens/toggles a panel', () => {
|
|
107
|
-
const shell = mount();
|
|
108
|
-
shell.querySelector('[app]').dispatchEvent(
|
|
109
|
-
new CustomEvent('embed:open', { detail: { panel: 'settings' }, bubbles: true }),
|
|
110
|
-
);
|
|
111
|
-
expect(shell.panel).toBe('settings');
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('Escape closes the open panel', () => {
|
|
115
|
-
const shell = mount();
|
|
116
|
-
shell.open('chat');
|
|
117
|
-
shell.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
118
|
-
expect(shell.panel).toBe('');
|
|
119
|
-
});
|
|
120
|
-
|
|
121
|
-
it('Escape is a no-op when no panel is open', () => {
|
|
122
|
-
const shell = mount();
|
|
123
|
-
// Should not throw; panel stays closed.
|
|
124
|
-
shell.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
125
|
-
expect(shell.panel).toBe('');
|
|
126
|
-
});
|
|
127
|
-
});
|
|
@@ -1,83 +0,0 @@
|
|
|
1
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
2
|
-
import '../../../web-components/core/element.js';
|
|
3
|
-
import './simple-shell.js';
|
|
4
|
-
|
|
5
|
-
const tick = () => new Promise((r) => queueMicrotask(r));
|
|
6
|
-
|
|
7
|
-
function mount(html) {
|
|
8
|
-
const wrap = document.createElement('div');
|
|
9
|
-
wrap.innerHTML = html;
|
|
10
|
-
document.body.appendChild(wrap);
|
|
11
|
-
return wrap.firstElementChild;
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
beforeEach(() => {
|
|
15
|
-
document.body.innerHTML = '';
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
describe('simple-shell', () => {
|
|
19
|
-
it('registers simple-shell as a custom element', () => {
|
|
20
|
-
expect(customElements.get('simple-shell')).toBeDefined();
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
it('defaults to centered=false and full-bleed=false', () => {
|
|
24
|
-
const el = mount('<simple-shell></simple-shell>');
|
|
25
|
-
expect(el.hasAttribute('centered')).toBe(false);
|
|
26
|
-
expect(el.hasAttribute('full-bleed')).toBe(false);
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
it('honors initial [centered] attribute on connect', () => {
|
|
30
|
-
const el = mount('<simple-shell centered></simple-shell>');
|
|
31
|
-
expect(el.centered).toBe(true);
|
|
32
|
-
});
|
|
33
|
-
|
|
34
|
-
it('honors initial [full-bleed] attribute on connect', () => {
|
|
35
|
-
const el = mount('<simple-shell full-bleed></simple-shell>');
|
|
36
|
-
expect(el['full-bleed']).toBe(true);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it('reflects [centered] via property assignment', async () => {
|
|
40
|
-
const el = mount('<simple-shell></simple-shell>');
|
|
41
|
-
el.centered = true;
|
|
42
|
-
await tick();
|
|
43
|
-
expect(el.hasAttribute('centered')).toBe(true);
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it('reflects [full-bleed] via property assignment', async () => {
|
|
47
|
-
const el = mount('<simple-shell></simple-shell>');
|
|
48
|
-
el['full-bleed'] = true;
|
|
49
|
-
await tick();
|
|
50
|
-
expect(el.hasAttribute('full-bleed')).toBe(true);
|
|
51
|
-
});
|
|
52
|
-
|
|
53
|
-
it('removes [centered] when set to false', async () => {
|
|
54
|
-
const el = mount('<simple-shell centered></simple-shell>');
|
|
55
|
-
el.centered = false;
|
|
56
|
-
await tick();
|
|
57
|
-
expect(el.hasAttribute('centered')).toBe(false);
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
it('stamps no HTML (behavior-only orchestrator)', () => {
|
|
61
|
-
const el = mount('<simple-shell></simple-shell>');
|
|
62
|
-
// Empty content + nothing stamped by the host
|
|
63
|
-
expect(el.children.length).toBe(0);
|
|
64
|
-
});
|
|
65
|
-
|
|
66
|
-
it('preserves authored slotted children', () => {
|
|
67
|
-
const el = mount(`
|
|
68
|
-
<simple-shell>
|
|
69
|
-
<simple-hero><h1 slot="heading">Hello</h1></simple-hero>
|
|
70
|
-
<simple-content><p>Body</p></simple-content>
|
|
71
|
-
</simple-shell>
|
|
72
|
-
`);
|
|
73
|
-
expect(el.querySelector('simple-hero')).toBeTruthy();
|
|
74
|
-
expect(el.querySelector('simple-content')).toBeTruthy();
|
|
75
|
-
expect(el.querySelector('[slot="heading"]')).toBeTruthy();
|
|
76
|
-
});
|
|
77
|
-
|
|
78
|
-
it('supports both [centered] and [full-bleed] simultaneously', () => {
|
|
79
|
-
const el = mount('<simple-shell centered full-bleed></simple-shell>');
|
|
80
|
-
expect(el.centered).toBe(true);
|
|
81
|
-
expect(el['full-bleed']).toBe(true);
|
|
82
|
-
});
|
|
83
|
-
});
|