import { afterEach, describe, it, expect, vi } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import EntityNav from './EntityNav.svelte'; vi.mock('$app/state', () => ({ page: { url: { pathname: '/admin/users' } } })); afterEach(cleanup); const props = { userCount: 5, groupCount: 3, tagCount: 8, inviteCount: 2, canManageUsers: true, canManageTags: true, canManagePermissions: true, canRunMaintenance: true }; describe('EntityNav — flyout', () => { it('flyout dialog is not visible initially', async () => { render(EntityNav, props); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); }); it('clicking a flyout trigger opens the dialog', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); }); it('flyout dialog has aria-modal="true"', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toHaveAttribute('aria-modal', 'true'); }); it('flyout dialog has an aria-label', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); const dialog = document.querySelector('[role="dialog"]')!; expect(dialog.getAttribute('aria-label')).toBeTruthy(); }); it('flyout contains navigation links to each entity', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); const dialog = document.querySelector('[role="dialog"]')!; const links = dialog.querySelectorAll('a[href^="/admin/"]'); expect(links.length).toBeGreaterThanOrEqual(3); }); it('pressing Escape closes the flyout', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); document.dispatchEvent(new KeyboardEvent('keydown', { key: 'Escape', bubbles: true })); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); }); it('clicking the backdrop closes the flyout', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); document.querySelector('[data-flyout-backdrop]')!.click(); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); }); it('clicking a flyout link closes the flyout', async () => { render(EntityNav, props); document.querySelector('[data-flyout-trigger]')!.click(); await expect.element(page.getByRole('dialog')).toBeInTheDocument(); const dialog = document.querySelector('[role="dialog"]')!; const link = dialog.querySelector('a[href^="/admin/"]')!; // Prevent the browser from navigating the test iframe to /admin/... (which // would redirect to /login and kill the iframe connection). preventDefault() // on the capture phase suppresses navigation while still letting the Svelte // onclick handler (closeFlyout) run on the bubbling phase. link.addEventListener('click', (e) => e.preventDefault(), { capture: true, once: true }); link.click(); await expect.element(page.getByRole('dialog')).not.toBeInTheDocument(); }); }); describe('EntityNav — OCR entry', () => { it('renders OCR link when canRunMaintenance is true', async () => { render(EntityNav, props); await expect.element(page.getByRole('link', { name: /OCR/i })).toBeInTheDocument(); }); it('does not render OCR link when canRunMaintenance is false', async () => { render(EntityNav, { ...props, canRunMaintenance: false }); const links = document.querySelectorAll('a[href="/admin/ocr"]'); expect(links.length).toBe(0); }); });