import { vi, describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import { m } from '$lib/paraglide/messages.js'; import PdfControls from './PdfControls.svelte'; afterEach(cleanup); const defaultProps = { currentPage: 1, totalPages: 3, isLoaded: true, showAnnotations: false, annotationCount: 0, onPrev: vi.fn(), onNext: vi.fn(), onZoomIn: vi.fn(), onZoomOut: vi.fn(), onToggleAnnotations: vi.fn() }; describe('PdfControls — annotation toggle visibility', () => { it('renders annotation toggle when annotationCount is greater than zero', async () => { render(PdfControls, { ...defaultProps, annotationCount: 3 }); await expect .element(page.getByRole('button', { name: m.pdf_annotations_show() })) .toBeInTheDocument(); }); it('does not render annotation toggle when annotationCount is zero', async () => { render(PdfControls, { ...defaultProps, annotationCount: 0 }); await expect .element(page.getByRole('button', { name: m.pdf_annotations_show() })) .not.toBeInTheDocument(); }); }); describe('PdfControls — annotation toggle label', () => { it('shows show-annotations label when annotations are hidden', async () => { render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const btn = page.getByRole('button', { name: m.pdf_annotations_show() }); await expect.element(btn).toBeInTheDocument(); }); it('shows hide-annotations label when annotations are visible', async () => { render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: true }); const btn = page.getByRole('button', { name: m.pdf_annotations_hide() }); await expect.element(btn).toBeInTheDocument(); }); }); describe('PdfControls — annotation toggle contrast (WCAG 2.1 AA)', () => { it('uses text-primary class on annotation toggle button when annotations are hidden', async () => { const { container } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const allButtons = container.querySelectorAll('button'); const annotationBtn = Array.from(allButtons).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(annotationBtn).not.toBeNull(); expect(annotationBtn!.className).toContain('text-primary'); expect(annotationBtn!.className).not.toContain('text-accent'); }); }); describe('PdfControls — focus rings (WCAG 2.1 §2.4.7)', () => { it('annotation toggle button has focus-visible:ring-2 focus ring', async () => { const { container } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const allButtons = container.querySelectorAll('button'); const annotationBtn = Array.from(allButtons).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(annotationBtn).not.toBeNull(); expect(annotationBtn!.className).toContain('focus-visible:ring-2'); }); it('icon-only nav/zoom buttons each have focus-visible:ring-2 focus ring', async () => { const { container } = render(PdfControls, { ...defaultProps }); const allButtons = container.querySelectorAll('button'); const iconOnlyButtons = Array.from(allButtons).filter((b) => { const label = b.getAttribute('aria-label') ?? ''; return [ m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(), m.viewer_zoom_in() ].includes(label); }); expect(iconOnlyButtons).toHaveLength(4); for (const btn of iconOnlyButtons) { expect(btn.className).toContain('focus-visible:ring-2'); } }); }); describe('PdfControls — touch targets (WCAG 2.2 §2.5.8)', () => { it('annotation toggle button has min-h-[44px] touch target', async () => { const { container } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const allButtons = container.querySelectorAll('button'); const annotationBtn = Array.from(allButtons).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(annotationBtn).not.toBeNull(); expect(annotationBtn!.className).toContain('min-h-[44px]'); }); it('annotation toggle button has min-w-[44px] touch target', async () => { const { container } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const allButtons = container.querySelectorAll('button'); const annotationBtn = Array.from(allButtons).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(annotationBtn).not.toBeNull(); expect(annotationBtn!.className).toContain('min-w-[44px]'); }); it('annotation toggle reflects pressed state via aria-pressed', async () => { const { container: c1 } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: false }); const btn1 = Array.from(c1.querySelectorAll('button')).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(btn1!.getAttribute('aria-pressed')).toBe('false'); cleanup(); const { container: c2 } = render(PdfControls, { ...defaultProps, annotationCount: 2, showAnnotations: true }); const btn2 = Array.from(c2.querySelectorAll('button')).find((b) => [m.pdf_annotations_show(), m.pdf_annotations_hide()].includes( b.getAttribute('aria-label') ?? '' ) ); expect(btn2!.getAttribute('aria-pressed')).toBe('true'); }); it('icon-only nav/zoom buttons each have min-h-[44px] touch target', async () => { const { container } = render(PdfControls, { ...defaultProps }); const allButtons = container.querySelectorAll('button'); const iconOnlyButtons = Array.from(allButtons).filter((b) => { const label = b.getAttribute('aria-label') ?? ''; return [ m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(), m.viewer_zoom_in() ].includes(label); }); expect(iconOnlyButtons).toHaveLength(4); for (const btn of iconOnlyButtons) { expect(btn.className).toContain('min-h-[44px]'); } }); it('icon-only nav/zoom buttons each have min-w-[44px] touch target', async () => { const { container } = render(PdfControls, { ...defaultProps }); const allButtons = container.querySelectorAll('button'); const iconOnlyButtons = Array.from(allButtons).filter((b) => { const label = b.getAttribute('aria-label') ?? ''; return [ m.viewer_previous_page(), m.viewer_next_page(), m.viewer_zoom_out(), m.viewer_zoom_in() ].includes(label); }); expect(iconOnlyButtons).toHaveLength(4); for (const btn of iconOnlyButtons) { expect(btn.className).toContain('min-w-[44px]'); } }); });