From 6538c9e59abfd1b3e402451cef4033e36fe04680 Mon Sep 17 00:00:00 2001 From: Marcel Date: Wed, 27 May 2026 11:49:35 +0200 Subject: [PATCH] feat(frontend): add accessible DocumentDate render component MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Wraps formatDocumentDate with the accessible presentation layer: a non-color UNKNOWN cue (decorative calendar-with-question icon, aria-hidden, since the visible "Datum unbekannt" text is the textual cue — WCAG 1.4.1), and the verbatim meta_date_raw shown as a VISIBLE secondary "Originaltext: …" line for UNKNOWN/SEASON/APPROX (WCAG 1.4.13, not tooltip-only). raw is rendered via Svelte default escaping, never {@html} (CWE-79); a component test asserts an angle-bracket raw value stays inert. Browser test is CI-only. Refs #666 Co-Authored-By: Claude Opus 4.7 --- frontend/src/lib/document/DocumentDate.svelte | 60 +++++++++++++++++++ .../lib/document/DocumentDate.svelte.test.ts | 35 +++++++++++ 2 files changed, 95 insertions(+) create mode 100644 frontend/src/lib/document/DocumentDate.svelte create mode 100644 frontend/src/lib/document/DocumentDate.svelte.test.ts diff --git a/frontend/src/lib/document/DocumentDate.svelte b/frontend/src/lib/document/DocumentDate.svelte new file mode 100644 index 00000000..a3539c31 --- /dev/null +++ b/frontend/src/lib/document/DocumentDate.svelte @@ -0,0 +1,60 @@ + + + + + {#if isUnknown} + + + {/if} + {label} + + {#if showRawLine} + + {m.date_original_label()} {raw} + {/if} + diff --git a/frontend/src/lib/document/DocumentDate.svelte.test.ts b/frontend/src/lib/document/DocumentDate.svelte.test.ts new file mode 100644 index 00000000..fa842b7b --- /dev/null +++ b/frontend/src/lib/document/DocumentDate.svelte.test.ts @@ -0,0 +1,35 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import DocumentDate from './DocumentDate.svelte'; + +// Browser-project (Playwright) tests — CI only. + +afterEach(cleanup); + +describe('DocumentDate', () => { + it('renders a DAY date as a full long date', async () => { + render(DocumentDate, { props: { iso: '1943-12-24', precision: 'DAY' } }); + await expect.element(page.getByText('24. Dezember 1943')).toBeInTheDocument(); + }); + + it('renders MONTH precision as month + year, never a day', async () => { + render(DocumentDate, { props: { iso: '1916-06-01', precision: 'MONTH', raw: 'Juni 1916' } }); + await expect.element(page.getByText('Juni 1916')).toBeInTheDocument(); + }); + + it('shows the verbatim raw cell as a visible secondary line for UNKNOWN (not tooltip-only)', async () => { + render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: 'Sommer?' } }); + // Real, visible text — not hidden behind a title attribute. + await expect.element(page.getByText('Datum unbekannt')).toBeInTheDocument(); + await expect.element(page.getByText(/Sommer\?/)).toBeVisible(); + }); + + it('renders a malicious raw value as inert escaped text (no element injected)', async () => { + const malicious = ''; + render(DocumentDate, { props: { iso: null, precision: 'UNKNOWN', raw: malicious } }); + // The payload appears as literal text, and no is created in the DOM. + await expect.element(page.getByText(/