import { describe, it, expect, afterEach } from 'vitest'; import { cleanup, render } from 'vitest-browser-svelte'; import { page } from 'vitest/browser'; import type { components } from '$lib/generated/api'; const { default: JourneyItemCard } = await import('./JourneyItemCard.svelte'); afterEach(cleanup); declare global { interface Window { __xss_note?: number; } } type JourneyItemView = components['schemas']['JourneyItemView']; const baseItem = (overrides: Partial = {}): JourneyItemView => ({ id: 'item1', position: 0, document: { id: 'd1', title: 'Brief an Helene', documentDate: '1923-05-15', datePrecision: 'FULL' }, ...overrides }); describe('JourneyItemCard', () => { it('renders the document title', async () => { render(JourneyItemCard, { props: { item: baseItem() } }); await expect.element(page.getByText('Brief an Helene')).toBeVisible(); }); it('renders the document date when documentDate is present', async () => { render(JourneyItemCard, { props: { item: baseItem() } }); await expect.element(page.getByText(/1923/)).toBeVisible(); }); it('whole card is a single element', async () => { render(JourneyItemCard, { props: { item: baseItem() } }); const link = document.querySelector('a'); expect(link).not.toBeNull(); expect(link?.href).toContain('/documents/d1'); }); it('link has dated aria-label when documentDate is present', async () => { render(JourneyItemCard, { props: { item: baseItem() } }); const link = document.querySelector('a'); expect(link?.getAttribute('aria-label')).toContain('Brief'); expect(link?.getAttribute('aria-label')).toContain('1923'); }); it('link has undated aria-label when documentDate is absent', async () => { render(JourneyItemCard, { props: { item: baseItem({ document: { id: 'd2', title: 'Ohne Datum', datePrecision: 'NONE' } }) } }); const link = document.querySelector('a'); expect(link?.getAttribute('aria-label')).toBe('Brief öffnen'); }); it('omits date text when documentDate is absent', async () => { render(JourneyItemCard, { props: { item: baseItem({ document: { id: 'd2', title: 'Ohne Datum', datePrecision: 'NONE' } }) } }); await expect.element(page.getByText(/1923/)).not.toBeInTheDocument(); }); it('renders ✎ glyph and note text when note is present', async () => { render(JourneyItemCard, { props: { item: baseItem({ note: 'Ein wichtiger Brief' }) } }); expect(document.body.textContent).toContain('✎'); await expect.element(page.getByText('Ein wichtiger Brief')).toBeVisible(); }); it('omits annotation block when note is blank or whitespace', async () => { render(JourneyItemCard, { props: { item: baseItem({ note: ' ' }) } }); expect(document.body.textContent).not.toContain('✎'); }); it('omits annotation block when note is absent', async () => { render(JourneyItemCard, { props: { item: baseItem({ note: undefined }) } }); expect(document.body.textContent).not.toContain('✎'); }); it('link meets 44px touch-target minimum height', async () => { render(JourneyItemCard, { props: { item: baseItem() } }); const link = document.querySelector('a'); const rect = link?.getBoundingClientRect(); expect(rect?.height).toBeGreaterThanOrEqual(44); }); it('XSS: note is rendered as plaintext — injected payload does not execute', async () => { // Note uses Svelte text interpolation ({note}), NOT {@html}. render(JourneyItemCard, { props: { item: baseItem({ note: '' }) } }); expect(window.__xss_note).toBeUndefined(); expect(document.body.textContent).toContain('