TranscriptionEditView: fix 4 failing tests: - textarea → [role="textbox"] selector (editor is contenteditable, not <textarea>) - button clicks → dispatchEvent(MouseEvent) for reliable Svelte 5 onclick with TipTap - mentionedPersons test: init block with @mention token so deserialize() creates a mention node; use userEvent.type + vi.waitFor (real timers) instead of fill + fake timers, which prevents TipTap onUpdate from firing the debounce timer EntityNavSection: anchor link click → add capture-phase preventDefault before clicking to stop iframe navigation while allowing Svelte onclick handler to run Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
147 lines
6.1 KiB
TypeScript
147 lines
6.1 KiB
TypeScript
import { afterEach, describe, it, expect } from 'vitest';
|
|
import { cleanup, render } from 'vitest-browser-svelte';
|
|
import { page } from 'vitest/browser';
|
|
import { createRawSnippet } from 'svelte';
|
|
import EntityNavSection from './EntityNavSection.svelte';
|
|
|
|
afterEach(cleanup);
|
|
|
|
const testIcon = createRawSnippet(() => ({
|
|
render: () => `<svg aria-label="test-icon" aria-hidden="true"></svg>`,
|
|
setup: () => {}
|
|
}));
|
|
|
|
const baseProps = {
|
|
href: '/admin/users',
|
|
label: 'Benutzer',
|
|
icon: testIcon
|
|
};
|
|
|
|
describe('EntityNavSection — sidebar variant (default)', () => {
|
|
it('tablet button has border-brand-mint class when isActive=true', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: true });
|
|
const button = document.querySelector('button[data-flyout-trigger]')!;
|
|
expect(button.className).toContain('border-brand-mint');
|
|
});
|
|
|
|
it('tablet button has border-transparent class when isActive=false', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false });
|
|
const button = document.querySelector('button[data-flyout-trigger]')!;
|
|
expect(button.className).toContain('border-transparent');
|
|
});
|
|
|
|
it('renders count span when count is provided', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, count: 42 });
|
|
// Sidebar renders two elements (tablet button + desktop link), each with a count span
|
|
const countSpans = document.querySelectorAll('span');
|
|
const countTexts = Array.from(countSpans).filter((s) => s.textContent?.trim() === '42');
|
|
expect(countTexts.length).toBeGreaterThanOrEqual(1);
|
|
});
|
|
|
|
it('does not render count span when count is undefined', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false });
|
|
// No numeric count element — the label text is present but no count span
|
|
const spans = document.querySelectorAll('button[data-flyout-trigger] span');
|
|
expect(spans.length).toBe(0);
|
|
});
|
|
|
|
it('desktop link has hidden and lg:flex classes', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false });
|
|
const link = document.querySelector('a[href="/admin/users"]')!;
|
|
expect(link.className).toContain('hidden');
|
|
expect(link.className).toContain('lg:flex');
|
|
});
|
|
|
|
it('desktop link has aria-current=page when isActive=true', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: true });
|
|
await expect
|
|
.element(page.getByRole('link', { name: 'Benutzer' }))
|
|
.toHaveAttribute('aria-current', 'page');
|
|
});
|
|
|
|
it('desktop link does not have aria-current when isActive=false', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false });
|
|
await expect
|
|
.element(page.getByRole('link', { name: 'Benutzer' }))
|
|
.not.toHaveAttribute('aria-current');
|
|
});
|
|
|
|
it('renders the icon in the tablet button', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false });
|
|
const button = document.querySelector('button[data-flyout-trigger]')!;
|
|
expect(button.querySelector('svg')).not.toBeNull();
|
|
});
|
|
|
|
it('renders count in desktop link when count is provided', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, count: 7 });
|
|
const link = document.querySelector('a[href="/admin/users"]')!;
|
|
expect(link.textContent).toContain('7');
|
|
});
|
|
});
|
|
|
|
describe('EntityNavSection — topBorder prop', () => {
|
|
it('tablet button has border-l-transparent (not border-transparent) when topBorder=true and inactive', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, topBorder: true });
|
|
const button = document.querySelector('button[data-flyout-trigger]')!;
|
|
expect(button.className).toContain('border-l-transparent');
|
|
expect(button.className).not.toContain('border-transparent hover:bg-white/5');
|
|
});
|
|
|
|
it('tablet button still has border-brand-mint when topBorder=true and isActive=true', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: true, topBorder: true });
|
|
const button = document.querySelector('button[data-flyout-trigger]')!;
|
|
expect(button.className).toContain('border-brand-mint');
|
|
});
|
|
});
|
|
|
|
describe('EntityNavSection — flyout variant', () => {
|
|
it('renders a single anchor element (no button) in flyout variant', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, variant: 'flyout' });
|
|
expect(document.querySelector('button[data-flyout-trigger]')).toBeNull();
|
|
expect(document.querySelector('a[href="/admin/users"]')).not.toBeNull();
|
|
});
|
|
|
|
it('flyout link has border-brand-mint class when isActive=true', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: true, variant: 'flyout' });
|
|
const link = document.querySelector('a[href="/admin/users"]')!;
|
|
expect(link.className).toContain('border-brand-mint');
|
|
});
|
|
|
|
it('flyout link has border-transparent class when isActive=false', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, variant: 'flyout' });
|
|
const link = document.querySelector('a[href="/admin/users"]')!;
|
|
expect(link.className).toContain('border-transparent');
|
|
});
|
|
|
|
it('flyout link shows count when count=42', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: false, variant: 'flyout', count: 42 });
|
|
await expect.element(page.getByText('42')).toBeInTheDocument();
|
|
});
|
|
|
|
it('flyout link has aria-current=page when isActive=true', async () => {
|
|
render(EntityNavSection, { ...baseProps, isActive: true, variant: 'flyout' });
|
|
const link = document.querySelector('a[href="/admin/users"]')!;
|
|
expect(link.getAttribute('aria-current')).toBe('page');
|
|
});
|
|
|
|
it('flyout link calls onFlyoutClick when clicked', async () => {
|
|
let called = false;
|
|
render(EntityNavSection, {
|
|
...baseProps,
|
|
isActive: false,
|
|
variant: 'flyout',
|
|
onFlyoutClick: () => {
|
|
called = true;
|
|
}
|
|
});
|
|
const link = document.querySelector<HTMLAnchorElement>('a[href="/admin/users"]')!;
|
|
// Prevent the browser from navigating the test iframe to /admin/users (which
|
|
// would redirect to /login and kill the iframe connection). preventDefault()
|
|
// on the capture phase suppresses navigation while still letting the Svelte
|
|
// onclick handler (onFlyoutClick) run on the bubbling phase.
|
|
link.addEventListener('click', (e) => e.preventDefault(), { capture: true, once: true });
|
|
link.click();
|
|
expect(called).toBe(true);
|
|
});
|
|
});
|