diff --git a/src/App.tsx b/src/App.tsx index 00aa157..5c5f53c 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -10,11 +10,39 @@ import { AppRoutes } from "./routes"; const queryClient = new QueryClient(); -/** Scrollt bei einem Seitenwechsel nach oben; Anker-Links (URL mit #) bleiben unberührt. */ -const ScrollToTop = () => { +/** + * Scrollt bei einem Seitenwechsel nach oben. Mit Anker (URL mit #) springt es zum Abschnitt, + * sobald er gerendert ist, z. B. beim Footer-Link „/#services“ von einer Unterseite aus. + */ +export const ScrollToTop = () => { const { pathname, hash } = useLocation(); useEffect(() => { - if (!hash) window.scrollTo(0, 0); + if (!hash) { + window.scrollTo(0, 0); + return; + } + const id = decodeURIComponent(hash.slice(1)); + let versuche = 0; + let frame = 0; + let abgebrochen = false; + const springe = () => { + const ziel = document.getElementById(id); + if (!ziel) { + if (versuche++ < 60) frame = requestAnimationFrame(springe); // höchstens ca. 1 s warten + return; + } + // Erst springen, wenn die Schriften geladen sind: Vorher ist der Text darüber höher, + // und der Abschnitt landet nach dem Nachladen ein Stück zu weit oben. + const schriften = document.fonts?.ready ?? Promise.resolve(); + schriften.then(() => { + if (!abgebrochen) ziel.scrollIntoView(); + }); + }; + springe(); + return () => { + abgebrochen = true; + cancelAnimationFrame(frame); + }; }, [pathname, hash]); return null; }; diff --git a/src/pages/Contact.tsx b/src/pages/Contact.tsx index b05a1a2..203a96e 100644 --- a/src/pages/Contact.tsx +++ b/src/pages/Contact.tsx @@ -64,7 +64,7 @@ const Contact = () => {
- + WEBklar @@ -184,6 +184,8 @@ const Contact = () => { Wir verwenden Ihre Angaben nur, um Ihre Anfrage zu beantworten. Mehr dazu in unserer{" "} Datenschutzerklärung diff --git a/src/pages/NotFound.tsx b/src/pages/NotFound.tsx index 19c58cb..b13a2b0 100644 --- a/src/pages/NotFound.tsx +++ b/src/pages/NotFound.tsx @@ -1,15 +1,9 @@ -import { Link, useLocation } from "react-router-dom"; -import { useEffect } from "react"; +import { Link } from "react-router-dom"; import { usePageMeta } from "@/hooks/use-page-meta"; const NotFound = () => { - const location = useLocation(); usePageMeta("Seite nicht gefunden"); - useEffect(() => { - console.error("404 Error: User attempted to access non-existent route:", location.pathname); - }, [location.pathname]); - return (
diff --git a/src/test/sofort-paket.test.tsx b/src/test/sofort-paket.test.tsx index d1be2b4..1428909 100644 --- a/src/test/sofort-paket.test.tsx +++ b/src/test/sofort-paket.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi, afterEach } from "vitest"; -import { render, screen, within, fireEvent, act, cleanup } from "@testing-library/react"; +import { render, screen, within, fireEvent, act, cleanup, waitFor } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; @@ -17,6 +17,7 @@ import KontaktAbschnitt from "@/components/Contact"; import About from "@/pages/About"; import { TextHoverEffect } from "@/components/ui/text-hover-effect"; import { Toaster } from "@/components/ui/toaster"; +import { ScrollToTop } from "@/App"; // WebGL, Canvas und Netzwerk laden wir in jsdom nicht: die Effekt-Komponenten und // das Kontaktformular-Backend werden durch Attrappen ersetzt. @@ -264,7 +265,6 @@ describe("C1 Footer auf allen Unterseiten", () => { const unterseiten = ["/kontakt", "/ueber-uns", "/agb", "/impressum", "/wartungs-abo", "/hosting-abo", "/gibt-es-nicht"]; it.each(unterseiten)("%s zeigt genau einen Footer mit Impressum und Datenschutz", (pfad) => { - vi.spyOn(console, "error").mockImplementation(() => {}); // die 404-Seite protokolliert den Aufruf seite(pfad); const footer = screen.getByRole("contentinfo"); expect(within(footer).getByRole("link", { name: "Impressum" })).toHaveAttribute("href", "/impressum"); @@ -327,7 +327,6 @@ 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(); @@ -336,7 +335,6 @@ describe("C5 404-Seite", () => { }); 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(); @@ -360,8 +358,7 @@ describe("C4 Schreibweise WEBklar", () => { it.each(["/", "/kontakt", "/ueber-uns", "/agb", "/impressum", "/wartungs-abo", "/hosting-abo", "/gibt-es-nicht"])( "%s: die Marke steht als WEBklar da", (pfad) => { - vi.spyOn(console, "error").mockImplementation(() => {}); - seite(pfad); + seite(pfad); expect(screen.queryAllByText(/Webklar/)).toHaveLength(0); expect(screen.getAllByText("WEBklar").length).toBeGreaterThan(0); }, @@ -446,7 +443,6 @@ describe("C3 Seitentitel und Beschreibung der Unterseiten", () => { }); it("die 404-Seite setzt den Titel »Seite nicht gefunden«", () => { - vi.spyOn(console, "error").mockImplementation(() => {}); seite("/gibt-es-nicht"); expect(document.title).toBe("Seite nicht gefunden | WEBklar"); }); @@ -671,3 +667,50 @@ describe("A1 Karten (BorderGlow) im hellen Modus lesbar", () => { expect(css).toMatch(/\.border-glow-card\s*>\s*\.edge-light\s*\{[^}]*mix-blend-mode:\s*plus-lighter/); }); }); + +describe("Nacharbeit nach der Prüfung", () => { + it("99,9 steht nirgends mehr im Quelltext", () => { + expect(vorkommen(/99[,.]9/)).toEqual([]); + }); + + it("ScrollToTop springt bei einem Anker zum Abschnitt (Footer-Link »Leistungen« von einer Unterseite)", async () => { + const springen = vi.fn(); + Element.prototype.scrollIntoView = springen; + render( + + +
+ , + ); + await waitFor(() => expect(springen).toHaveBeenCalledTimes(1)); + expect(springen.mock.instances[0]).toBe(document.getElementById("services")); + }); + + it("ScrollToTop scrollt ohne Anker wie bisher nach oben", () => { + const nachOben = vi.fn(); + vi.stubGlobal("scrollTo", nachOben); + render( + + + , + ); + expect(nachOben).toHaveBeenCalledWith(0, 0); + }); + + it("die 404-Seite schreibt nichts in die Konsole", () => { + const fehler = vi.spyOn(console, "error"); + seite("/gibt-es-nicht"); + expect(fehler).not.toHaveBeenCalled(); + }); + + it("der Datenschutz-Link am Formular öffnet einen neuen Tab, damit eine getippte Nachricht nicht verloren geht", () => { + seite("/kontakt"); + const link = screen.getByRole("link", { name: "Datenschutzerklärung" }); + expect(link).toHaveAttribute("target", "_blank"); + expect(link.getAttribute("rel")).toMatch(/noopener/); + }); + + it("der Markenname steht nicht mehr mit enger Laufweite da (B und k berührten sich)", () => { + expect(vorkommen(/tracking-tight[^"]*">\s*WEBklar\s*