@classic-homes/theme-svelte 1.21.0 → 1.22.1
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/dist/lib/components/Alert.svelte +8 -10
- package/dist/lib/components/DataPanel.svelte +19 -5
- package/dist/lib/components/HeaderSearch.svelte +18 -10
- package/dist/lib/components/Skeleton.svelte.d.ts +1 -1
- package/dist/lib/components/layout/AppShell.svelte +1 -1
- package/dist/lib/components/layout/DashboardLayout.svelte +15 -6
- package/dist/lib/components/layout/Footer.svelte +6 -6
- package/dist/lib/components/layout/FormPageLayout.svelte +4 -3
- package/dist/lib/components/layout/Sidebar.svelte +25 -21
- package/dist/lib/components/media/Image.svelte.d.ts +1 -1
- package/dist/lib/components/media/Lightbox.svelte +13 -9
- package/dist/lib/components/media/MediaCarousel.svelte +5 -0
- package/dist/lib/components/media/MediaGrid.svelte +4 -1
- package/dist/lib/components/notifications/NotificationDialog.svelte +8 -5
- package/dist/lib/components/notifications/NotificationItem.svelte +16 -6
- package/dist/lib/components/studio/InsertPalette.svelte +4 -1
- package/dist/lib/components/studio/StudioCanvas.svelte +23 -2
- package/dist/lib/components/studio/StudioCanvas.svelte.d.ts +5 -1
- package/dist/lib/components/studio/StudioCanvasItem.svelte +31 -4
- package/dist/lib/components/studio/StudioCanvasItem.svelte.d.ts +7 -0
- package/dist/lib/composables/useFocusTrap.svelte.js +9 -2
- package/dist/lib/notices/sanitize.js +10 -3
- package/dist/lib/notices/store.d.ts +3 -3
- package/dist/lib/notices/store.js +14 -2
- package/dist/lib/notices/types.d.ts +8 -8
- package/dist/lib/notices/types.js +5 -1
- package/dist/lib/stores/sidebar.svelte.d.ts +6 -0
- package/dist/lib/stores/sidebar.svelte.js +61 -26
- package/dist/lib/utils/media.js +4 -3
- package/dist/lib/utils/url.d.ts +10 -0
- package/dist/lib/utils/url.js +21 -0
- package/dist/lib/utils.js +14 -1
- package/package.json +5 -4
- package/dist/__tests__/FileUpload.test.js +0 -45
- package/dist/__tests__/a11y/Alert.a11y.test.js +0 -84
- package/dist/__tests__/a11y/AlertDialog.a11y.test.js +0 -126
- package/dist/__tests__/a11y/Button.a11y.test.js +0 -66
- package/dist/__tests__/a11y/Card.a11y.test.js +0 -77
- package/dist/__tests__/a11y/Combobox.a11y.test.js +0 -209
- package/dist/__tests__/a11y/Dialog.a11y.test.js +0 -103
- package/dist/__tests__/a11y/DropdownMenu.a11y.test.js +0 -132
- package/dist/__tests__/a11y/FileUpload.a11y.test.js +0 -158
- package/dist/__tests__/a11y/FilterToolbar.a11y.test.js +0 -62
- package/dist/__tests__/a11y/Icon.a11y.test.js +0 -119
- package/dist/__tests__/a11y/Input.a11y.test.js +0 -96
- package/dist/__tests__/a11y/LiveRegion.a11y.test.js +0 -102
- package/dist/__tests__/a11y/Media.a11y.test.js +0 -145
- package/dist/__tests__/a11y/PublicLayout.a11y.test.js +0 -139
- package/dist/__tests__/a11y/SegmentedControl.a11y.test.js +0 -50
- package/dist/__tests__/a11y/Switch.a11y.test.js +0 -95
- package/dist/__tests__/a11y/Tabs.a11y.test.js +0 -116
- package/dist/__tests__/a11y/Textarea.a11y.test.js +0 -111
- package/dist/__tests__/a11y/Toolbar.a11y.test.js +0 -31
- package/dist/__tests__/a11y/ViewToggle.a11y.test.js +0 -40
- package/dist/__tests__/action-header.test.js +0 -150
- package/dist/__tests__/alert-dialog-dismiss.test.js +0 -54
- package/dist/__tests__/code-field.test.js +0 -65
- package/dist/__tests__/control-text-color.test.js +0 -29
- package/dist/__tests__/data-panel-card-gestures.test.js +0 -138
- package/dist/__tests__/data-panel-viewport.test.js +0 -45
- package/dist/__tests__/dropdown-trigger.test.js +0 -39
- package/dist/__tests__/header-search.test.js +0 -90
- package/dist/__tests__/media-components.test.js +0 -206
- package/dist/__tests__/media-hls.test.js +0 -273
- package/dist/__tests__/media-matterport.test.js +0 -296
- package/dist/__tests__/media-players.test.js +0 -244
- package/dist/__tests__/media-scrubber.test.js +0 -44
- package/dist/__tests__/media-utils.test.js +0 -427
- package/dist/__tests__/nav-access.test.js +0 -145
- package/dist/__tests__/page-header.test.js +0 -36
- package/dist/__tests__/public-layout-drawer.test.js +0 -116
- package/dist/__tests__/setup.js +0 -56
- package/dist/__tests__/sidebar-collapse.test.js +0 -58
- package/dist/__tests__/sidebar-docs-parity.test.js +0 -134
- package/dist/__tests__/sidebar-expansion.test.js +0 -59
- package/dist/__tests__/sidebar-items.test.js +0 -59
- package/dist/__tests__/sidebar-mobile.test.js +0 -86
- package/dist/__tests__/studio-canvas-item-reveal.test.js +0 -50
- package/dist/__tests__/studio.test.js +0 -240
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* PublicLayout mobileNavStyle="drawer": the hamburger opens the Sidebar drawer
|
|
3
|
-
* over the page instead of expanding a dropdown inside the header (which pushed
|
|
4
|
-
* the page down and, once scrolled, sat on the header's translucent bar).
|
|
5
|
-
*/
|
|
6
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
7
|
-
import { mount, unmount, flushSync, createRawSnippet } from 'svelte';
|
|
8
|
-
import PublicLayout from '../lib/components/layout/PublicLayout.svelte';
|
|
9
|
-
const children = createRawSnippet(() => ({ render: () => '<p>Page</p>' }));
|
|
10
|
-
const navigation = [
|
|
11
|
-
{ id: 'inventory', name: 'Inventory', href: '/inventory', active: true },
|
|
12
|
-
{ id: 'pricing', name: 'Pricing', href: '/pricing' },
|
|
13
|
-
];
|
|
14
|
-
let instance = null;
|
|
15
|
-
let host;
|
|
16
|
-
function phoneWidth(matches) {
|
|
17
|
-
vi.spyOn(window, 'matchMedia').mockImplementation((query) => ({
|
|
18
|
-
matches: query.startsWith('(max-width') ? matches : false,
|
|
19
|
-
media: query,
|
|
20
|
-
onchange: null,
|
|
21
|
-
addEventListener: vi.fn(),
|
|
22
|
-
removeEventListener: vi.fn(),
|
|
23
|
-
addListener: vi.fn(),
|
|
24
|
-
removeListener: vi.fn(),
|
|
25
|
-
dispatchEvent: vi.fn(),
|
|
26
|
-
}));
|
|
27
|
-
}
|
|
28
|
-
function mountLayout(props) {
|
|
29
|
-
host = document.createElement('div');
|
|
30
|
-
document.body.appendChild(host);
|
|
31
|
-
instance = mount(PublicLayout, { target: host, props: { navigation, children, ...props } });
|
|
32
|
-
flushSync();
|
|
33
|
-
const hamburger = () => host.querySelector('button[aria-label="Open menu"]');
|
|
34
|
-
return {
|
|
35
|
-
open: () => {
|
|
36
|
-
hamburger().click();
|
|
37
|
-
flushSync();
|
|
38
|
-
},
|
|
39
|
-
drawer: () => host.querySelector('aside[aria-label="Main navigation"]'),
|
|
40
|
-
dropdown: () => host.querySelector('nav[aria-label="Mobile navigation"]'),
|
|
41
|
-
header: () => host.querySelector('header'),
|
|
42
|
-
};
|
|
43
|
-
}
|
|
44
|
-
afterEach(() => {
|
|
45
|
-
if (instance)
|
|
46
|
-
unmount(instance);
|
|
47
|
-
instance = null;
|
|
48
|
-
host?.remove();
|
|
49
|
-
vi.restoreAllMocks();
|
|
50
|
-
});
|
|
51
|
-
describe('PublicLayout drawer nav', () => {
|
|
52
|
-
it('opens the Sidebar drawer over the page on a phone', () => {
|
|
53
|
-
phoneWidth(true);
|
|
54
|
-
const layout = mountLayout({ mobileNavStyle: 'drawer' });
|
|
55
|
-
// Closed drawer is off-canvas (jsdom does not reflect `inert`)
|
|
56
|
-
expect(layout.drawer()?.className).toContain('-translate-x-full');
|
|
57
|
-
layout.open();
|
|
58
|
-
expect(layout.drawer()?.className).not.toContain('-translate-x-full');
|
|
59
|
-
expect(layout.drawer()?.textContent).toContain('Pricing');
|
|
60
|
-
// No dropdown inside the header
|
|
61
|
-
expect(layout.dropdown()?.textContent ?? '').not.toContain('Pricing');
|
|
62
|
-
// Drawer and scrim lift over the elevated header and page sheets
|
|
63
|
-
const layer = layout.drawer().parentElement;
|
|
64
|
-
expect(layer.style.getPropertyValue('--z-sidebar')).toBe('70');
|
|
65
|
-
expect(layer.querySelector('[aria-hidden="true"].fixed.inset-0')).not.toBeNull();
|
|
66
|
-
});
|
|
67
|
-
it('renders no drawer above the breakpoint', () => {
|
|
68
|
-
phoneWidth(false);
|
|
69
|
-
const layout = mountLayout({ mobileNavStyle: 'drawer' });
|
|
70
|
-
expect(layout.drawer()).toBeNull();
|
|
71
|
-
});
|
|
72
|
-
it('keeps the dropdown by default, on a solid background', () => {
|
|
73
|
-
phoneWidth(true);
|
|
74
|
-
const layout = mountLayout({});
|
|
75
|
-
layout.open();
|
|
76
|
-
expect(layout.drawer()).toBeNull();
|
|
77
|
-
expect(layout.dropdown()?.textContent).toContain('Pricing');
|
|
78
|
-
expect(layout.dropdown()?.className).toContain('bg-background');
|
|
79
|
-
expect(layout.header().className).toContain('z-50');
|
|
80
|
-
});
|
|
81
|
-
it('gives a parent with children a chevron menu in the header, and keeps the link', () => {
|
|
82
|
-
phoneWidth(false);
|
|
83
|
-
host = document.createElement('div');
|
|
84
|
-
document.body.appendChild(host);
|
|
85
|
-
instance = mount(PublicLayout, {
|
|
86
|
-
target: host,
|
|
87
|
-
props: {
|
|
88
|
-
children,
|
|
89
|
-
navigation: [
|
|
90
|
-
{
|
|
91
|
-
id: 'inventory',
|
|
92
|
-
name: 'Inventory',
|
|
93
|
-
href: '/inventory',
|
|
94
|
-
active: true,
|
|
95
|
-
children: [
|
|
96
|
-
{ id: 'inventory-community', name: 'Community', href: '/inventory?view=community' },
|
|
97
|
-
{
|
|
98
|
-
id: 'inventory-collection',
|
|
99
|
-
name: 'Collection',
|
|
100
|
-
href: '/inventory?view=collection',
|
|
101
|
-
},
|
|
102
|
-
],
|
|
103
|
-
},
|
|
104
|
-
{ id: 'pricing', name: 'Pricing', href: '/pricing' },
|
|
105
|
-
],
|
|
106
|
-
},
|
|
107
|
-
});
|
|
108
|
-
flushSync();
|
|
109
|
-
const link = host.querySelector('header a[href="/inventory"]');
|
|
110
|
-
expect(link?.getAttribute('aria-current')).toBe('page');
|
|
111
|
-
const trigger = host.querySelector('header button[aria-label="Inventory options"]');
|
|
112
|
-
expect(trigger).not.toBeNull();
|
|
113
|
-
// Only parents with children get one
|
|
114
|
-
expect(host.querySelector('header button[aria-label="Pricing options"]')).toBeNull();
|
|
115
|
-
});
|
|
116
|
-
});
|
package/dist/__tests__/setup.js
DELETED
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import '@testing-library/jest-dom/vitest';
|
|
2
|
-
import { expect, vi } from 'vitest';
|
|
3
|
-
import * as matchers from 'vitest-axe/matchers';
|
|
4
|
-
expect.extend(matchers);
|
|
5
|
-
// Mock scrollIntoView since jsdom doesn't support it
|
|
6
|
-
Element.prototype.scrollIntoView = vi.fn();
|
|
7
|
-
// jsdom has no layout engine, so scrollTo is a no-op stub
|
|
8
|
-
Element.prototype.scrollTo = vi.fn();
|
|
9
|
-
// jsdom implements no media pipeline: play/pause/load all throw "not
|
|
10
|
-
// implemented" without these stubs, which breaks any Video or Audio test.
|
|
11
|
-
HTMLMediaElement.prototype.play = vi.fn().mockResolvedValue(undefined);
|
|
12
|
-
HTMLMediaElement.prototype.pause = vi.fn();
|
|
13
|
-
HTMLMediaElement.prototype.load = vi.fn();
|
|
14
|
-
// Fullscreen and picture-in-picture are not implemented in jsdom either
|
|
15
|
-
Element.prototype.requestFullscreen = vi.fn().mockResolvedValue(undefined);
|
|
16
|
-
document.exitFullscreen = vi.fn().mockResolvedValue(undefined);
|
|
17
|
-
// Observers used for lazy loading and layout measurement
|
|
18
|
-
if (!('IntersectionObserver' in globalThis)) {
|
|
19
|
-
globalThis.IntersectionObserver = class {
|
|
20
|
-
constructor() {
|
|
21
|
-
this.observe = vi.fn();
|
|
22
|
-
this.unobserve = vi.fn();
|
|
23
|
-
this.disconnect = vi.fn();
|
|
24
|
-
this.takeRecords = vi.fn(() => []);
|
|
25
|
-
this.root = null;
|
|
26
|
-
this.rootMargin = '';
|
|
27
|
-
this.thresholds = [];
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
}
|
|
31
|
-
if (!('ResizeObserver' in globalThis)) {
|
|
32
|
-
globalThis.ResizeObserver = class {
|
|
33
|
-
constructor() {
|
|
34
|
-
this.observe = vi.fn();
|
|
35
|
-
this.unobserve = vi.fn();
|
|
36
|
-
this.disconnect = vi.fn();
|
|
37
|
-
}
|
|
38
|
-
};
|
|
39
|
-
}
|
|
40
|
-
// jsdom has no canvas backend and logs a "not implemented" notice on every
|
|
41
|
-
// getContext call. Returning null matches what a real browser does when the
|
|
42
|
-
// context type is unsupported, and the callers already handle it.
|
|
43
|
-
HTMLCanvasElement.prototype.getContext = vi.fn(() => null);
|
|
44
|
-
// matchMedia backs useReducedMotion; jsdom does not provide it
|
|
45
|
-
if (!window.matchMedia) {
|
|
46
|
-
window.matchMedia = vi.fn().mockImplementation((query) => ({
|
|
47
|
-
matches: false,
|
|
48
|
-
media: query,
|
|
49
|
-
onchange: null,
|
|
50
|
-
addEventListener: vi.fn(),
|
|
51
|
-
removeEventListener: vi.fn(),
|
|
52
|
-
addListener: vi.fn(),
|
|
53
|
-
removeListener: vi.fn(),
|
|
54
|
-
dispatchEvent: vi.fn(),
|
|
55
|
-
}));
|
|
56
|
-
}
|
|
@@ -1,58 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar Collapse Tests
|
|
3
|
-
*
|
|
4
|
-
* DashboardLayout applies its `sidebarCollapsed` default only when no open
|
|
5
|
-
* state is persisted. A first session (nothing in localStorage yet) must
|
|
6
|
-
* still let the user collapse the sidebar — the default must not re-apply
|
|
7
|
-
* on every toggle and snap it back open.
|
|
8
|
-
*/
|
|
9
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
10
|
-
import { render, screen, fireEvent } from '@testing-library/svelte';
|
|
11
|
-
import { createRawSnippet, flushSync } from 'svelte';
|
|
12
|
-
import DashboardLayout from '../lib/components/layout/DashboardLayout.svelte';
|
|
13
|
-
import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
|
|
14
|
-
let appCounter = 0;
|
|
15
|
-
function freshAppId() {
|
|
16
|
-
return `test-collapse-${++appCounter}`;
|
|
17
|
-
}
|
|
18
|
-
function renderLayout(appId) {
|
|
19
|
-
return render(DashboardLayout, {
|
|
20
|
-
props: {
|
|
21
|
-
navigation: [{ id: 'main', items: [{ id: 'home', name: 'Home', href: '/' }] }],
|
|
22
|
-
appId,
|
|
23
|
-
children: createRawSnippet(() => ({ render: () => '<p>content</p>' })),
|
|
24
|
-
},
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
describe('sidebar collapse', () => {
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
localStorage.clear();
|
|
30
|
-
sidebarStore.setMobile(false);
|
|
31
|
-
});
|
|
32
|
-
it('collapses on a first session with no persisted state', async () => {
|
|
33
|
-
const appId = freshAppId();
|
|
34
|
-
renderLayout(appId);
|
|
35
|
-
expect(sidebarStore.isOpen).toBe(true);
|
|
36
|
-
await fireEvent.click(screen.getByRole('button', { name: 'Collapse sidebar' }));
|
|
37
|
-
flushSync();
|
|
38
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
39
|
-
expect(localStorage.getItem(`classic-theme-sidebar-${appId}-open`)).toBe('false');
|
|
40
|
-
expect(screen.getByRole('button', { name: 'Expand sidebar' })).toBeInTheDocument();
|
|
41
|
-
});
|
|
42
|
-
it('keeps the collapse after a mobile round-trip in a first session', () => {
|
|
43
|
-
renderLayout(freshAppId());
|
|
44
|
-
sidebarStore.toggle();
|
|
45
|
-
flushSync();
|
|
46
|
-
sidebarStore.setMobile(true);
|
|
47
|
-
flushSync();
|
|
48
|
-
sidebarStore.setMobile(false);
|
|
49
|
-
flushSync();
|
|
50
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
51
|
-
});
|
|
52
|
-
it('marks state as persisted once the open state is written', () => {
|
|
53
|
-
sidebarStore.initialize(freshAppId());
|
|
54
|
-
expect(sidebarStore.hasPersistedState).toBe(false);
|
|
55
|
-
sidebarStore.close();
|
|
56
|
-
expect(sidebarStore.hasPersistedState).toBe(true);
|
|
57
|
-
});
|
|
58
|
-
});
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar features docs sites need (Docusaurus parity)
|
|
3
|
-
*
|
|
4
|
-
* - Items nest to any depth: a child with its own children renders as an expandable row
|
|
5
|
-
* instead of being dropped.
|
|
6
|
-
* - Search filters and expands at every depth.
|
|
7
|
-
* - `sidebarAutoCollapse`: opening a section or item closes the others at its level.
|
|
8
|
-
* - DashboardLayout renders a page `footer` below the main content.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeAll, beforeEach, vi } from 'vitest';
|
|
11
|
-
import { fireEvent, render, screen, within } from '@testing-library/svelte';
|
|
12
|
-
import { createRawSnippet } from 'svelte';
|
|
13
|
-
import DashboardLayout from '../lib/components/layout/DashboardLayout.svelte';
|
|
14
|
-
import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
|
|
15
|
-
const navigation = [
|
|
16
|
-
{
|
|
17
|
-
id: 'setup',
|
|
18
|
-
title: 'Setup',
|
|
19
|
-
collapsible: true,
|
|
20
|
-
expanded: true,
|
|
21
|
-
items: [
|
|
22
|
-
{
|
|
23
|
-
id: 'platforms',
|
|
24
|
-
name: 'Platforms',
|
|
25
|
-
expanded: true,
|
|
26
|
-
children: [
|
|
27
|
-
{ id: 'mac', name: 'macOS', href: '/setup/mac' },
|
|
28
|
-
{
|
|
29
|
-
id: 'linux',
|
|
30
|
-
name: 'Linux',
|
|
31
|
-
expanded: true,
|
|
32
|
-
children: [
|
|
33
|
-
{ id: 'debian', name: 'Debian', href: '/setup/linux/debian', active: true },
|
|
34
|
-
{
|
|
35
|
-
id: 'rhel',
|
|
36
|
-
name: 'RHEL',
|
|
37
|
-
children: [{ id: 'rhel9', name: 'RHEL 9', href: '/setup/linux/rhel/9' }],
|
|
38
|
-
},
|
|
39
|
-
],
|
|
40
|
-
},
|
|
41
|
-
],
|
|
42
|
-
},
|
|
43
|
-
{
|
|
44
|
-
id: 'tools',
|
|
45
|
-
name: 'Tools',
|
|
46
|
-
children: [{ id: 'cli', name: 'CLI', href: '/setup/cli' }],
|
|
47
|
-
},
|
|
48
|
-
],
|
|
49
|
-
},
|
|
50
|
-
{
|
|
51
|
-
id: 'reference',
|
|
52
|
-
title: 'Reference',
|
|
53
|
-
collapsible: true,
|
|
54
|
-
expanded: false,
|
|
55
|
-
items: [{ id: 'api', name: 'API', href: '/reference/api' }],
|
|
56
|
-
},
|
|
57
|
-
];
|
|
58
|
-
function renderLayout(props = {}) {
|
|
59
|
-
return render(DashboardLayout, {
|
|
60
|
-
props: {
|
|
61
|
-
appId: `test-parity-${Math.random()}`,
|
|
62
|
-
navigation,
|
|
63
|
-
children: createRawSnippet(() => ({ render: () => '<p>content</p>' })),
|
|
64
|
-
...props,
|
|
65
|
-
},
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
describe('sidebar docs parity', () => {
|
|
69
|
-
beforeAll(() => {
|
|
70
|
-
var _a;
|
|
71
|
-
// Expanding rows runs svelte's slide transition (Web Animations), which jsdom lacks
|
|
72
|
-
(_a = Element.prototype).animate ?? (_a.animate = vi.fn(() => ({
|
|
73
|
-
cancel: vi.fn(),
|
|
74
|
-
finish: vi.fn(),
|
|
75
|
-
onfinish: null,
|
|
76
|
-
oncancel: null,
|
|
77
|
-
})));
|
|
78
|
-
});
|
|
79
|
-
beforeEach(() => {
|
|
80
|
-
localStorage.clear();
|
|
81
|
-
sidebarStore.setMobile(false);
|
|
82
|
-
sidebarStore.setDefaultItems([]);
|
|
83
|
-
sidebarStore.setDefaultSections([]);
|
|
84
|
-
});
|
|
85
|
-
it('renders categories nested deeper than one level', () => {
|
|
86
|
-
renderLayout();
|
|
87
|
-
const debian = screen.getByRole('link', { name: 'Debian' });
|
|
88
|
-
expect(debian).toHaveAttribute('aria-current', 'page');
|
|
89
|
-
// RHEL is a third-level category: an expandable row, collapsed by default
|
|
90
|
-
const rhel = screen.getByRole('button', { name: 'Expand RHEL' });
|
|
91
|
-
expect(screen.queryByRole('link', { name: 'RHEL 9' })).toBeNull();
|
|
92
|
-
return fireEvent.click(rhel).then(() => {
|
|
93
|
-
expect(screen.getByRole('link', { name: 'RHEL 9' })).toHaveAttribute('href', '/setup/linux/rhel/9');
|
|
94
|
-
});
|
|
95
|
-
});
|
|
96
|
-
it('finds and reveals matches at any depth when searching', async () => {
|
|
97
|
-
renderLayout({ searchable: true });
|
|
98
|
-
const search = screen.getByRole('searchbox');
|
|
99
|
-
await fireEvent.input(search, { target: { value: 'rhel 9' } });
|
|
100
|
-
// The search input is debounced
|
|
101
|
-
await vi.waitFor(() => expect(screen.getByRole('link', { name: /RHEL 9/ })).toBeInTheDocument());
|
|
102
|
-
expect(screen.queryByRole('link', { name: /macOS/ })).toBeNull();
|
|
103
|
-
expect(screen.queryByRole('link', { name: /Debian/ })).toBeNull();
|
|
104
|
-
});
|
|
105
|
-
it('closes sibling items and sections when sidebarAutoCollapse is on', async () => {
|
|
106
|
-
renderLayout({ sidebarAutoCollapse: true });
|
|
107
|
-
expect(sidebarStore.isItemExpanded('platforms')).toBe(true);
|
|
108
|
-
await fireEvent.click(screen.getByRole('button', { name: 'Expand Tools' }));
|
|
109
|
-
expect(sidebarStore.isItemExpanded('tools')).toBe(true);
|
|
110
|
-
expect(sidebarStore.isItemExpanded('platforms')).toBe(false);
|
|
111
|
-
expect(sidebarStore.isSectionExpanded('setup')).toBe(true);
|
|
112
|
-
await fireEvent.click(screen.getByRole('button', { name: /Reference/ }));
|
|
113
|
-
expect(sidebarStore.isSectionExpanded('reference')).toBe(true);
|
|
114
|
-
expect(sidebarStore.isSectionExpanded('setup')).toBe(false);
|
|
115
|
-
});
|
|
116
|
-
it('leaves siblings open by default', async () => {
|
|
117
|
-
renderLayout();
|
|
118
|
-
await fireEvent.click(screen.getByRole('button', { name: 'Expand Tools' }));
|
|
119
|
-
expect(sidebarStore.isItemExpanded('platforms')).toBe(true);
|
|
120
|
-
expect(sidebarStore.isItemExpanded('tools')).toBe(true);
|
|
121
|
-
});
|
|
122
|
-
it('renders a page footer below the main content', () => {
|
|
123
|
-
const { container } = renderLayout({
|
|
124
|
-
footer: createRawSnippet(() => ({
|
|
125
|
-
render: () => '<footer>© Classic Homes</footer>',
|
|
126
|
-
})),
|
|
127
|
-
});
|
|
128
|
-
const main = container.querySelector('main#main-content');
|
|
129
|
-
const footer = screen.getByRole('contentinfo');
|
|
130
|
-
expect(footer).toHaveTextContent('© Classic Homes');
|
|
131
|
-
expect(main.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy();
|
|
132
|
-
expect(within(main).queryByRole('contentinfo')).toBeNull();
|
|
133
|
-
});
|
|
134
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar Role-based Expansion Tests
|
|
3
|
-
*
|
|
4
|
-
* Covers which sections start expanded for a given user. This is a UX
|
|
5
|
-
* affordance, not access control — it decides what starts open, not what is
|
|
6
|
-
* visible (that's nav-access).
|
|
7
|
-
*/
|
|
8
|
-
import { describe, it, expect } from 'vitest';
|
|
9
|
-
import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
|
|
10
|
-
function section(partial) {
|
|
11
|
-
return { title: partial.id, items: [], ...partial };
|
|
12
|
-
}
|
|
13
|
-
describe('computeInitialExpansions', () => {
|
|
14
|
-
it('expands a section for any matching role', () => {
|
|
15
|
-
const sections = [section({ id: 'admin', expandForRoles: ['admin', 'superuser'] })];
|
|
16
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual(['admin']);
|
|
17
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['superuser'])).toEqual(['admin']);
|
|
18
|
-
});
|
|
19
|
-
it('expands on a secondary role, not just the first held', () => {
|
|
20
|
-
const sections = [section({ id: 'ops', expandForRoles: ['agent'] })];
|
|
21
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['manager', 'agent'])).toEqual(['ops']);
|
|
22
|
-
});
|
|
23
|
-
it('leaves a section collapsed when no role matches', () => {
|
|
24
|
-
const sections = [section({ id: 'admin', expandForRoles: ['admin'] })];
|
|
25
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['agent'])).toEqual([]);
|
|
26
|
-
});
|
|
27
|
-
it('leaves a role-gated section collapsed for a user with no roles', () => {
|
|
28
|
-
const sections = [section({ id: 'admin', expandForRoles: ['admin'] })];
|
|
29
|
-
expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual([]);
|
|
30
|
-
expect(sidebarStore.computeInitialExpansions(sections, undefined)).toEqual([]);
|
|
31
|
-
});
|
|
32
|
-
it('expands non-collapsible sections regardless of roles', () => {
|
|
33
|
-
const sections = [section({ id: 'main', collapsible: false })];
|
|
34
|
-
expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual(['main']);
|
|
35
|
-
});
|
|
36
|
-
it('respects an explicitly collapsed section', () => {
|
|
37
|
-
const sections = [section({ id: 'main', collapsible: true, expanded: false })];
|
|
38
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual([]);
|
|
39
|
-
});
|
|
40
|
-
it('expands a collapsible section that is not explicitly collapsed', () => {
|
|
41
|
-
const sections = [section({ id: 'main', collapsible: true })];
|
|
42
|
-
expect(sidebarStore.computeInitialExpansions(sections, [])).toEqual(['main']);
|
|
43
|
-
});
|
|
44
|
-
it('prefers expandForRoles over the collapsed default', () => {
|
|
45
|
-
// expandForRoles is checked first, so it wins over `expanded: false`.
|
|
46
|
-
const sections = [
|
|
47
|
-
section({ id: 'admin', collapsible: true, expanded: false, expandForRoles: ['admin'] }),
|
|
48
|
-
];
|
|
49
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['admin'])).toEqual(['admin']);
|
|
50
|
-
});
|
|
51
|
-
it('handles a mixed navigation', () => {
|
|
52
|
-
const sections = [
|
|
53
|
-
section({ id: 'main', collapsible: false }),
|
|
54
|
-
section({ id: 'admin', expandForRoles: ['admin'] }),
|
|
55
|
-
section({ id: 'ops', expandForRoles: ['agent'] }),
|
|
56
|
-
];
|
|
57
|
-
expect(sidebarStore.computeInitialExpansions(sections, ['agent'])).toEqual(['main', 'ops']);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,59 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar item rendering
|
|
3
|
-
*
|
|
4
|
-
* - An item with an `icon` name and no custom icon renderer draws the theme
|
|
5
|
-
* Icon (it used to draw a placeholder circle). An item without an icon
|
|
6
|
-
* reserves no icon space.
|
|
7
|
-
* - Item links can shrink, so a long name truncates instead of widening the
|
|
8
|
-
* sidebar into a horizontal scroll.
|
|
9
|
-
*/
|
|
10
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
11
|
-
import { render, screen } from '@testing-library/svelte';
|
|
12
|
-
import { createRawSnippet } from 'svelte';
|
|
13
|
-
import DashboardLayout from '../lib/components/layout/DashboardLayout.svelte';
|
|
14
|
-
import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
|
|
15
|
-
const ARROW_LEFT_PATH = 'M19 12H5M12 19l-7-7 7-7';
|
|
16
|
-
function renderLayout() {
|
|
17
|
-
return render(DashboardLayout, {
|
|
18
|
-
props: {
|
|
19
|
-
appId: `test-items-${Math.random()}`,
|
|
20
|
-
navigation: [
|
|
21
|
-
{
|
|
22
|
-
id: 'main',
|
|
23
|
-
items: [
|
|
24
|
-
{ id: 'back', name: 'Back', href: '/', icon: 'arrow-left' },
|
|
25
|
-
{
|
|
26
|
-
id: 'plain',
|
|
27
|
-
name: 'A rather long page name that will not fit the sidebar',
|
|
28
|
-
href: '/plain',
|
|
29
|
-
},
|
|
30
|
-
],
|
|
31
|
-
},
|
|
32
|
-
],
|
|
33
|
-
children: createRawSnippet(() => ({ render: () => '<p>content</p>' })),
|
|
34
|
-
},
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
describe('sidebar items', () => {
|
|
38
|
-
beforeEach(() => {
|
|
39
|
-
localStorage.clear();
|
|
40
|
-
sidebarStore.setMobile(false);
|
|
41
|
-
});
|
|
42
|
-
it('draws the named theme icon when no icon renderer is given', () => {
|
|
43
|
-
renderLayout();
|
|
44
|
-
const link = screen.getByRole('link', { name: 'Back' });
|
|
45
|
-
expect(link.querySelector('svg circle')).toBeNull();
|
|
46
|
-
expect(link.querySelector('svg path')?.getAttribute('d')).toBe(ARROW_LEFT_PATH);
|
|
47
|
-
});
|
|
48
|
-
it('reserves no icon space for an item without an icon', () => {
|
|
49
|
-
renderLayout();
|
|
50
|
-
const link = screen.getByRole('link', { name: /A rather long page name/ });
|
|
51
|
-
expect(link.querySelector('svg')).toBeNull();
|
|
52
|
-
expect(link.querySelector('span.h-5.w-5')).toBeNull();
|
|
53
|
-
});
|
|
54
|
-
it('lets item links shrink so long names truncate', () => {
|
|
55
|
-
renderLayout();
|
|
56
|
-
const link = screen.getByRole('link', { name: /A rather long page name/ });
|
|
57
|
-
expect(link.classList.contains('min-w-0')).toBe(true);
|
|
58
|
-
});
|
|
59
|
-
});
|
|
@@ -1,86 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Sidebar Mobile Semantics Tests
|
|
3
|
-
*
|
|
4
|
-
* Covers the mobile drawer rules: persisted desktop open-state must never
|
|
5
|
-
* open the drawer on a phone, drawer toggles never overwrite the desktop
|
|
6
|
-
* preference, and leaving mobile restores the desktop preference.
|
|
7
|
-
*
|
|
8
|
-
* Uses the shared singleton (module state), so each test re-initializes with
|
|
9
|
-
* a fresh appId to get an isolated localStorage namespace.
|
|
10
|
-
*/
|
|
11
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
12
|
-
import { sidebarStore } from '../lib/stores/sidebar.svelte.js';
|
|
13
|
-
let appCounter = 0;
|
|
14
|
-
function freshAppId() {
|
|
15
|
-
return `test-mobile-${++appCounter}`;
|
|
16
|
-
}
|
|
17
|
-
describe('sidebar mobile semantics', () => {
|
|
18
|
-
beforeEach(() => {
|
|
19
|
-
localStorage.clear();
|
|
20
|
-
// Reset to a known desktop baseline.
|
|
21
|
-
sidebarStore.setMobile(false);
|
|
22
|
-
sidebarStore.open();
|
|
23
|
-
});
|
|
24
|
-
it('does not open the drawer from persisted state when initializing on mobile', () => {
|
|
25
|
-
const appId = freshAppId();
|
|
26
|
-
localStorage.setItem(`classic-theme-sidebar-${appId}-open`, 'true');
|
|
27
|
-
sidebarStore.setMobile(true);
|
|
28
|
-
sidebarStore.initialize(appId);
|
|
29
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
30
|
-
});
|
|
31
|
-
it('applies persisted open state when initializing on desktop', () => {
|
|
32
|
-
const appId = freshAppId();
|
|
33
|
-
localStorage.setItem(`classic-theme-sidebar-${appId}-open`, 'false');
|
|
34
|
-
sidebarStore.initialize(appId);
|
|
35
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
36
|
-
});
|
|
37
|
-
it('restores the persisted preference when leaving mobile, even if init ran while mobile', () => {
|
|
38
|
-
const appId = freshAppId();
|
|
39
|
-
localStorage.setItem(`classic-theme-sidebar-${appId}-open`, 'true');
|
|
40
|
-
sidebarStore.setMobile(true);
|
|
41
|
-
sidebarStore.initialize(appId);
|
|
42
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
43
|
-
sidebarStore.setMobile(false);
|
|
44
|
-
expect(sidebarStore.isOpen).toBe(true);
|
|
45
|
-
});
|
|
46
|
-
it('entering mobile closes the drawer and stashes the desktop state', () => {
|
|
47
|
-
sidebarStore.initialize(freshAppId());
|
|
48
|
-
sidebarStore.open();
|
|
49
|
-
sidebarStore.setMobile(true);
|
|
50
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
51
|
-
sidebarStore.setMobile(false);
|
|
52
|
-
expect(sidebarStore.isOpen).toBe(true);
|
|
53
|
-
});
|
|
54
|
-
it('mobile drawer toggles do not persist or overwrite the desktop preference', () => {
|
|
55
|
-
const appId = freshAppId();
|
|
56
|
-
sidebarStore.initialize(appId);
|
|
57
|
-
sidebarStore.open(); // persists 'true' as desktop preference
|
|
58
|
-
sidebarStore.setMobile(true);
|
|
59
|
-
sidebarStore.toggle(); // open drawer
|
|
60
|
-
sidebarStore.close(); // close drawer
|
|
61
|
-
expect(localStorage.getItem(`classic-theme-sidebar-${appId}-open`)).toBe('true');
|
|
62
|
-
sidebarStore.setMobile(false);
|
|
63
|
-
expect(sidebarStore.isOpen).toBe(true);
|
|
64
|
-
});
|
|
65
|
-
it('a collapse made on desktop survives a mobile round-trip', () => {
|
|
66
|
-
sidebarStore.initialize(freshAppId());
|
|
67
|
-
sidebarStore.close();
|
|
68
|
-
sidebarStore.setMobile(true);
|
|
69
|
-
sidebarStore.toggle(); // user opens the drawer
|
|
70
|
-
sidebarStore.setMobile(false);
|
|
71
|
-
expect(sidebarStore.isOpen).toBe(false);
|
|
72
|
-
});
|
|
73
|
-
it('does not animate expansion until the frames after initialize', async () => {
|
|
74
|
-
const appId = freshAppId();
|
|
75
|
-
localStorage.setItem(`classic-theme-sidebar-${appId}-sections`, JSON.stringify(['a']));
|
|
76
|
-
// A fresh store instance would start false; the singleton may already be settled
|
|
77
|
-
// from earlier tests, so check the reset-on-initialize path via a new appId.
|
|
78
|
-
sidebarStore.animate = false;
|
|
79
|
-
sidebarStore.initialize(appId);
|
|
80
|
-
expect(sidebarStore.isSectionExpanded('a')).toBe(true);
|
|
81
|
-
expect(sidebarStore.animate).toBe(false);
|
|
82
|
-
await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve)));
|
|
83
|
-
await new Promise((resolve) => setTimeout(resolve, 0));
|
|
84
|
-
expect(sidebarStore.animate).toBe(true);
|
|
85
|
-
});
|
|
86
|
-
});
|
|
@@ -1,50 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* StudioCanvasItem and the inspector sheet: picking an item opens the
|
|
3
|
-
* inspector on phones, unless the item is edited in place.
|
|
4
|
-
*/
|
|
5
|
-
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
6
|
-
import { mount, unmount, flushSync, createRawSnippet } from 'svelte';
|
|
7
|
-
const revealInspector = vi.fn();
|
|
8
|
-
vi.mock('../lib/components/studio/context.js', () => ({
|
|
9
|
-
getStudioContext: () => ({ revealInspector }),
|
|
10
|
-
setStudioContext: () => { },
|
|
11
|
-
}));
|
|
12
|
-
const { default: StudioCanvasItem } = await import('../lib/components/studio/StudioCanvasItem.svelte');
|
|
13
|
-
let instance = null;
|
|
14
|
-
let host;
|
|
15
|
-
function render(props) {
|
|
16
|
-
host = document.createElement('div');
|
|
17
|
-
document.body.appendChild(host);
|
|
18
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
19
|
-
instance = mount(StudioCanvasItem, {
|
|
20
|
-
target: host,
|
|
21
|
-
props: {
|
|
22
|
-
label: 'Block 1',
|
|
23
|
-
children: createRawSnippet(() => ({ render: () => '<p>Body</p>' })),
|
|
24
|
-
...props,
|
|
25
|
-
},
|
|
26
|
-
});
|
|
27
|
-
flushSync();
|
|
28
|
-
return host;
|
|
29
|
-
}
|
|
30
|
-
afterEach(() => {
|
|
31
|
-
if (instance)
|
|
32
|
-
unmount(instance);
|
|
33
|
-
instance = null;
|
|
34
|
-
host?.remove();
|
|
35
|
-
revealInspector.mockClear();
|
|
36
|
-
});
|
|
37
|
-
describe('StudioCanvasItem revealOnSelect', () => {
|
|
38
|
-
it('reveals the inspector when picked, by default', () => {
|
|
39
|
-
const el = render({ onSelect: vi.fn() });
|
|
40
|
-
el.querySelector('p').click();
|
|
41
|
-
expect(revealInspector).toHaveBeenCalledTimes(1);
|
|
42
|
-
});
|
|
43
|
-
it('selects without revealing the inspector when edited in place', () => {
|
|
44
|
-
const onSelect = vi.fn();
|
|
45
|
-
const el = render({ onSelect, revealOnSelect: false });
|
|
46
|
-
el.querySelector('p').click();
|
|
47
|
-
expect(onSelect).toHaveBeenCalledTimes(1);
|
|
48
|
-
expect(revealInspector).not.toHaveBeenCalled();
|
|
49
|
-
});
|
|
50
|
-
});
|