CSS class string assertion was fragile — class names can change without breaking the actual layout. DOM measurement via getBoundingClientRect is the correct way to verify computed height meets WCAG 2.2 minimum. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
125 lines
3.6 KiB
TypeScript
125 lines
3.6 KiB
TypeScript
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> = {}): 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 <a> 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: '<img src=x onerror="window.__xss_note=1">'
|
|
})
|
|
}
|
|
});
|
|
|
|
expect(window.__xss_note).toBeUndefined();
|
|
expect(document.body.textContent).toContain('<img src=x onerror=');
|
|
});
|
|
});
|