Replaces the vacuous expect(true).toBe(true) sleep test with a real flyout-open assertion (role=dialog appears after trigger click) and turns the Escape-keydown smoke test into a full open→Escape→closed behavioral test. Routes the Escape event through document (matches the svelte:document binding) instead of window. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
149 lines
5.2 KiB
TypeScript
149 lines
5.2 KiB
TypeScript
import { describe, it, expect, vi, afterEach } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page as browserPage } from 'vitest/browser';
|
|
|
|
const mockPage = { url: new URL('http://localhost/admin/users') };
|
|
|
|
vi.mock('$app/state', () => ({
|
|
get page() {
|
|
return mockPage;
|
|
}
|
|
}));
|
|
|
|
afterEach(cleanup);
|
|
|
|
async function loadComponent() {
|
|
return (await import('./EntityNav.svelte')).default;
|
|
}
|
|
|
|
const baseProps = (overrides: Record<string, unknown> = {}) => ({
|
|
userCount: 5,
|
|
groupCount: 3,
|
|
tagCount: 12,
|
|
inviteCount: 1,
|
|
canManageUsers: true,
|
|
canManageTags: true,
|
|
canManagePermissions: true,
|
|
canRunMaintenance: true,
|
|
...overrides
|
|
});
|
|
|
|
describe('EntityNav', () => {
|
|
it('renders all sections when all permissions are granted', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps() });
|
|
|
|
const links = document.querySelectorAll('a[href^="/admin/"]');
|
|
// Sidebar renders: users, groups, invites, tags, system, ocr — 6 links
|
|
expect(links.length).toBeGreaterThanOrEqual(6);
|
|
});
|
|
|
|
it('hides users / invites links when canManageUsers is false', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/groups');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ canManageUsers: false }) });
|
|
|
|
const userLinks = document.querySelectorAll('a[href="/admin/users"]');
|
|
const inviteLinks = document.querySelectorAll('a[href="/admin/invites"]');
|
|
expect(userLinks.length).toBe(0);
|
|
expect(inviteLinks.length).toBe(0);
|
|
});
|
|
|
|
it('hides the groups link when canManagePermissions is false', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ canManagePermissions: false }) });
|
|
|
|
const groupLinks = document.querySelectorAll('a[href="/admin/groups"]');
|
|
expect(groupLinks.length).toBe(0);
|
|
});
|
|
|
|
it('hides the tags link when canManageTags is false', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ canManageTags: false }) });
|
|
|
|
const tagLinks = document.querySelectorAll('a[href="/admin/tags"]');
|
|
expect(tagLinks.length).toBe(0);
|
|
});
|
|
|
|
it('hides the system and ocr links when canRunMaintenance is false', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ canRunMaintenance: false }) });
|
|
|
|
const systemLinks = document.querySelectorAll('a[href="/admin/system"]');
|
|
const ocrLinks = document.querySelectorAll('a[href="/admin/ocr"]');
|
|
expect(systemLinks.length).toBe(0);
|
|
expect(ocrLinks.length).toBe(0);
|
|
});
|
|
|
|
it('does not render the flyout panel by default', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps() });
|
|
|
|
await expect.element(browserPage.getByRole('dialog')).not.toBeInTheDocument();
|
|
});
|
|
|
|
it('marks the active section with brand-mint icon color', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/groups/abc');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps() });
|
|
|
|
// At least one icon SVG should have the brand-mint class
|
|
const mintIcons = document.querySelectorAll('svg.text-brand-mint');
|
|
expect(mintIcons.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('opens the flyout dialog when a tablet section trigger is clicked', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps() });
|
|
|
|
// The first <button> in the DOM is the tablet trigger of the first section.
|
|
const triggerButton = document.querySelector('button') as HTMLButtonElement;
|
|
expect(triggerButton).not.toBeNull();
|
|
triggerButton.click();
|
|
|
|
await vi.waitFor(() => {
|
|
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
|
|
});
|
|
});
|
|
|
|
it('Escape closes the open flyout', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps() });
|
|
|
|
// Open the flyout first.
|
|
(document.querySelector('button') as HTMLButtonElement).click();
|
|
await vi.waitFor(() => {
|
|
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
|
|
});
|
|
|
|
// Then Escape should close it (handler is bound via svelte:document).
|
|
document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true }));
|
|
await vi.waitFor(() => {
|
|
expect(document.querySelector('[role="dialog"]')).toBeNull();
|
|
});
|
|
});
|
|
|
|
it('renders the user count badge on the users link', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ userCount: 42 }) });
|
|
|
|
expect(document.body.textContent).toContain('42');
|
|
});
|
|
|
|
it('renders the invite count badge on the invites link', async () => {
|
|
mockPage.url = new URL('http://localhost/admin/users');
|
|
const EntityNav = await loadComponent();
|
|
render(EntityNav, { props: baseProps({ inviteCount: 7 }) });
|
|
|
|
expect(document.body.textContent).toContain('7');
|
|
});
|
|
});
|