From b234db0472e2ce217c76822965f699570def0757 Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 24 Apr 2026 21:28:25 +0200 Subject: [PATCH] feat(richtlinien): add /hilfe/transkription page with RichtlinienRuleCard Co-Authored-By: Claude Sonnet 4.6 --- .../lib/components/RichtlinienRuleCard.svelte | 30 +++++ .../RichtlinienRuleCard.svelte.spec.ts | 49 +++++++ .../routes/hilfe/transkription/+page.svelte | 124 ++++++++++++++++++ .../src/routes/hilfe/transkription/+page.ts | 1 + .../hilfe/transkription/page.svelte.spec.ts | 72 ++++++++++ 5 files changed, 276 insertions(+) create mode 100644 frontend/src/lib/components/RichtlinienRuleCard.svelte create mode 100644 frontend/src/lib/components/RichtlinienRuleCard.svelte.spec.ts create mode 100644 frontend/src/routes/hilfe/transkription/+page.svelte create mode 100644 frontend/src/routes/hilfe/transkription/+page.ts create mode 100644 frontend/src/routes/hilfe/transkription/page.svelte.spec.ts diff --git a/frontend/src/lib/components/RichtlinienRuleCard.svelte b/frontend/src/lib/components/RichtlinienRuleCard.svelte new file mode 100644 index 00000000..cbf9b163 --- /dev/null +++ b/frontend/src/lib/components/RichtlinienRuleCard.svelte @@ -0,0 +1,30 @@ + + +
+
+ +

{title}

+
+

{body}

+ + {#if beispielOutput !== undefined} +
+

+ {beispielLabel} +

+

+ → {beispielOutput} +

+
+ {/if} +
diff --git a/frontend/src/lib/components/RichtlinienRuleCard.svelte.spec.ts b/frontend/src/lib/components/RichtlinienRuleCard.svelte.spec.ts new file mode 100644 index 00000000..55852c1b --- /dev/null +++ b/frontend/src/lib/components/RichtlinienRuleCard.svelte.spec.ts @@ -0,0 +1,49 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import RichtlinienRuleCard from './RichtlinienRuleCard.svelte'; + +afterEach(cleanup); + +const defaultProps = { + icon: '✍', + title: 'Unleserliche Wörter', + body: 'Schreiben Sie [unleserlich].', + beispielOutput: '[unleserlich]' +}; + +describe('RichtlinienRuleCard', () => { + it('renders an h3 with the title', async () => { + render(RichtlinienRuleCard, { props: defaultProps }); + await expect + .element(page.getByRole('heading', { level: 3 })) + .toHaveTextContent('Unleserliche Wörter'); + }); + + it('renders the body text', async () => { + render(RichtlinienRuleCard, { props: defaultProps }); + await expect.element(page.getByText('Schreiben Sie [unleserlich].')).toBeInTheDocument(); + }); + + it('renders icon in a span with aria-hidden="true"', async () => { + render(RichtlinienRuleCard, { props: defaultProps }); + const iconSpan = document.querySelector('span[aria-hidden="true"]'); + expect(iconSpan).not.toBeNull(); + expect(iconSpan!.textContent).toContain('✍'); + }); + + it('renders beispielOutput in monospace with → arrow', async () => { + render(RichtlinienRuleCard, { props: defaultProps }); + const mono = document.querySelector('code, [class*="font-mono"]'); + expect(mono).not.toBeNull(); + expect(mono!.textContent).toContain('[unleserlich]'); + await expect.element(page.getByText(/→/)).toBeInTheDocument(); + }); + + it('does not render beispiel section when beispielOutput is absent', async () => { + render(RichtlinienRuleCard, { + props: { icon: '✍', title: 'Test', body: 'Body' } + }); + expect(document.querySelector('code, [class*="font-mono"]')).toBeNull(); + }); +}); diff --git a/frontend/src/routes/hilfe/transkription/+page.svelte b/frontend/src/routes/hilfe/transkription/+page.svelte new file mode 100644 index 00000000..af42eb9c --- /dev/null +++ b/frontend/src/routes/hilfe/transkription/+page.svelte @@ -0,0 +1,124 @@ + + + + {m.richtlinien_title()} — Familienarchiv + + +
+ +

{m.richtlinien_title()}

+ + +

{m.richtlinien_intro()}

+ + + + + +

+ {m.richtlinien_rules_label()} +

+
+ {#each rules as rule (rule.title)} + + {/each} +
+ + +

+ {m.richtlinien_klaerung_label()} +

+

+ {m.richtlinien_klaerung_intro()} +

+
+ {#each klaerungChips as chip (chip)} + {chip} + {/each} +
+ + +
+

{m.richtlinien_closing_title()}

+

{m.richtlinien_closing_body()}

+
+
+ + diff --git a/frontend/src/routes/hilfe/transkription/+page.ts b/frontend/src/routes/hilfe/transkription/+page.ts new file mode 100644 index 00000000..189f71e2 --- /dev/null +++ b/frontend/src/routes/hilfe/transkription/+page.ts @@ -0,0 +1 @@ +export const prerender = true; diff --git a/frontend/src/routes/hilfe/transkription/page.svelte.spec.ts b/frontend/src/routes/hilfe/transkription/page.svelte.spec.ts new file mode 100644 index 00000000..259b9f5c --- /dev/null +++ b/frontend/src/routes/hilfe/transkription/page.svelte.spec.ts @@ -0,0 +1,72 @@ +import { describe, it, expect, afterEach } from 'vitest'; +import { cleanup, render } from 'vitest-browser-svelte'; +import { page } from 'vitest/browser'; +import Page from './+page.svelte'; + +afterEach(cleanup); + +describe('Richtlinien page — structure', () => { + it('renders h1 with richtlinien title', async () => { + render(Page); + await expect + .element(page.getByRole('heading', { level: 1 })) + .toHaveTextContent('Transkriptions-Richtlinien'); + }); + + it('renders intro paragraph', async () => { + render(Page); + await expect.element(page.getByText(/Damit alle Briefe einheitlich/)).toBeInTheDocument(); + }); + + it('renders Wikipedia external link with security attributes and new-tab annotation', async () => { + render(Page); + const wikiLink = page.getByRole('link', { name: /Wikipedia/ }); + await expect.element(wikiLink).toBeInTheDocument(); + await expect.element(wikiLink).toHaveAttribute('target', '_blank'); + await expect.element(wikiLink).toHaveAttribute('rel', 'noopener noreferrer'); + await expect.element(wikiLink).toHaveAttribute('referrerpolicy', 'no-referrer'); + // visible annotation (not sr-only) + const link = document.querySelector('a[href*="wikipedia"]') as HTMLAnchorElement; + expect(link.textContent).toContain('öffnet in neuem Tab'); + }); + + it('renders Regeln h2 section', async () => { + render(Page); + await expect + .element(page.getByRole('heading', { level: 2, name: /Regeln für die Transkription/ })) + .toBeInTheDocument(); + }); + + it('renders Noch in Klärung h2 section', async () => { + render(Page); + await expect + .element(page.getByRole('heading', { level: 2, name: /Noch in Klärung/ })) + .toBeInTheDocument(); + }); + + it('renders closing invitation card', async () => { + render(Page); + await expect.element(page.getByText(/Fehlt eine Regel/)).toBeInTheDocument(); + }); +}); + +describe('Richtlinien page — rule cards', () => { + it('renders five rule card titles', async () => { + render(Page); + await expect.element(page.getByText('Nicht lesbare Wörter')).toBeInTheDocument(); + await expect.element(page.getByText('Durchgestrichene Wörter')).toBeInTheDocument(); + await expect.element(page.getByText(/Das lange s/)).toBeInTheDocument(); + await expect.element(page.getByText('Unsichere Namen')).toBeInTheDocument(); + await expect.element(page.getByText(/Dialekt/)).toBeInTheDocument(); + }); +}); + +describe('Richtlinien page — Noch in Klärung chips', () => { + it('renders four clarification chips', async () => { + render(Page); + await expect.element(page.getByText('Abkürzungen')).toBeInTheDocument(); + await expect.element(page.getByText('Datumsformate')).toBeInTheDocument(); + await expect.element(page.getByText(/Zeilenumbrüche/)).toBeInTheDocument(); + await expect.element(page.getByText(/Groß-\/Kleinschreibung/)).toBeInTheDocument(); + }); +});