Files
familienarchiv/frontend/src/lib/geschichte/JourneyInterlude.svelte.spec.ts
Marcel f004b1f2a6 fix(a11y): add role="note" to JourneyInterlude so aria-label is announced
Without a landmark or widget role, aria-label on a generic <div> is
silently ignored by most screen readers (ARIA spec). Adding role="note"
gives the element an ARIA role that accepts an accessible name, making
the interlude label actually announced.

Also adds a test asserting role="note" and the matching aria-label are
both present on the same element.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-09 09:09:30 +02:00

54 lines
1.8 KiB
TypeScript

import { 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';
const { default: JourneyInterlude } = await import('./JourneyInterlude.svelte');
afterEach(cleanup);
declare global {
interface Window {
__xss_interlude?: number;
}
}
describe('JourneyInterlude', () => {
it('renders the note text as plaintext', async () => {
render(JourneyInterlude, { props: { note: 'Eine kurze Pause auf der Reise.' } });
await expect.element(page.getByText('Eine kurze Pause auf der Reise.')).toBeVisible();
});
it('has aria-label from i18n (journey_interlude_aria_label)', async () => {
render(JourneyInterlude, { props: { note: 'Notiz' } });
const el = document.querySelector(`[aria-label="${m.journey_interlude_aria_label()}"]`);
expect(el).not.toBeNull();
});
it('has role="note" so the aria-label is announced by screen readers', async () => {
render(JourneyInterlude, { props: { note: 'Notiz' } });
const el = document.querySelector('[role="note"]');
expect(el).not.toBeNull();
expect(el?.getAttribute('aria-label')).toBe(m.journey_interlude_aria_label());
});
it('renders the section-break glyph ❦', async () => {
render(JourneyInterlude, { props: { note: 'Notiz' } });
expect(document.body.textContent).toContain('❦');
});
it('XSS: note is rendered as plaintext — injected payload does not execute', async () => {
// Interlude uses Svelte text interpolation ({note}), NOT {@html}.
render(JourneyInterlude, {
props: { note: '<img src=x onerror="window.__xss_interlude=1">' }
});
expect(window.__xss_interlude).toBeUndefined();
expect(document.body.textContent).toContain('<img src=x onerror=');
});
});