diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 7bc2234..ee6c9f3 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,4 +1,4 @@ -import { useLocation } from "react-router-dom"; +import { Link, useLocation } from "react-router-dom"; import { useEffect } from "react"; const NotFound = () => { @@ -9,13 +9,13 @@ const NotFound = () => { }, [location.pathname]); return ( -
+
-

404

-

Oops! Page not found

- - Return to Home - +

Seite nicht gefunden

+

Diese Seite gibt es nicht oder nicht mehr.

+ + Zur Startseite +
); diff --git a/src/test/sofort-paket.test.tsx b/src/test/sofort-paket.test.tsx index 652f2d7..f0e0b71 100644 --- a/src/test/sofort-paket.test.tsx +++ b/src/test/sofort-paket.test.tsx @@ -282,3 +282,71 @@ describe("C1 Footer auf allen Unterseiten", () => { expect(inhalt.compareDocumentPosition(footer) & Node.DOCUMENT_POSITION_FOLLOWING).toBeTruthy(); }); }); + +// ── Kontrast-Hilfen (WCAG 2.x), gelesen werden die Farb-Token aus src/index.css ── +type RGB = [number, number, number]; + +const rgbAusHsl = (h: number, s: number, l: number): RGB => { + s /= 100; + l /= 100; + const k = (n: number) => (n + h / 30) % 12; + const a = s * Math.min(l, 1 - l); + const f = (n: number) => l - a * Math.max(-1, Math.min(k(n) - 3, Math.min(9 - k(n), 1))); + return [f(0) * 255, f(8) * 255, f(4) * 255]; +}; +const leuchtdichte = (rgb: RGB) => { + const [r, g, b] = rgb.map((v) => { + const c = v / 255; + return c <= 0.03928 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4; + }); + return 0.2126 * r + 0.7152 * g + 0.0722 * b; +}; +const kontrast = (a: RGB, b: RGB) => { + const [hell, dunkel] = [leuchtdichte(a), leuchtdichte(b)].sort((x, y) => y - x); + return (hell + 0.05) / (dunkel + 0.05); +}; +/** "#120F17" oder "hsl(0 0% 6%)" als RGB. */ +const farbe = (wert: string): RGB => { + const hex = wert.trim().match(/^#([0-9a-f]{6})$/i); + if (hex) return [0, 2, 4].map((i) => parseInt(hex[1].slice(i, i + 2), 16)) as RGB; + const hsl = wert.trim().match(/^hsl\(\s*([\d.]+)(?:deg)?[\s,]+([\d.]+)%[\s,]+([\d.]+)%\s*\)$/); + if (hsl) return rgbAusHsl(Number(hsl[1]), Number(hsl[2]), Number(hsl[3])); + throw new Error(`Unbekanntes Farbformat: ${wert}`); +}; +type Thema = "hell" | "dunkel"; +/** Farb-Token ("--foreground: 0 0% 9%") eines Themes aus index.css. */ +const tokenFarbe = (thema: Thema, name: string): RGB => { + const css = lies("src/index.css"); + const block = css.match(thema === "dunkel" ? /\.dark\s*\{([^}]*)\}/ : /:root\s*\{([^}]*)\}/)?.[1] ?? ""; + const wert = block.match(new RegExp(`--${name}:\\s*([\\d.]+)\\s+([\\d.]+)%\\s+([\\d.]+)%`)); + if (!wert) throw new Error(`Token --${name} fehlt im Theme ${thema}`); + return rgbAusHsl(Number(wert[1]), Number(wert[2]), Number(wert[3])); +}; +const THEMEN: Thema[] = ["hell", "dunkel"]; + +describe("C5 404-Seite", () => { + it("zeigt Überschrift, Erklärung und Link zur Startseite auf Deutsch", () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + seite("/gibt-es-nicht"); + expect(screen.getByRole("heading", { level: 1, name: "Seite nicht gefunden" })).toBeInTheDocument(); + expect(screen.getByText("Diese Seite gibt es nicht oder nicht mehr.")).toBeInTheDocument(); + expect(screen.getByRole("link", { name: "Zur Startseite" })).toHaveAttribute("href", "/"); + expect(screen.queryByText(/Oops|Return to Home|Page not found/)).toBeNull(); + }); + + it("nutzt die Theme-Farben statt eines festen grauen Hintergrunds", () => { + vi.spyOn(console, "error").mockImplementation(() => {}); + const { container } = seite("/gibt-es-nicht"); + expect(container.querySelector(".bg-background.text-foreground")).not.toBeNull(); + expect(container.querySelector(".bg-muted")).toBeNull(); + expect(screen.getByRole("link", { name: "Zur Startseite" })).toHaveClass("text-foreground"); + }); + + it("Textfarben der Seite erreichen auf dem Hintergrund in beiden Themes mindestens 4,5:1", () => { + for (const thema of THEMEN) { + for (const name of ["foreground", "muted-foreground"]) { + expect(kontrast(tokenFarbe(thema, name), tokenFarbe(thema, "background"))).toBeGreaterThanOrEqual(4.5); + } + } + }); +});