Archived
Footer-Anker von Unterseiten, 404 ohne Konsolenfehler, Datenschutz-Link im neuen Tab
ScrollToTop springt bei /#services nach dem Laden der Schriften zum Abschnitt (vorher landete man oben auf der Startseite bzw. 220 px zu weit unten). Dazu Quelltext-Test gegen 99,9. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
e6d603cd0c
commit
d78338a969
4 files changed
+85
-18
No files matched your search
+31
-3
@@ -10,11 +10,39 @@ import { AppRoutes } from "./routes";
|
|||||||
|
|
||||||
const queryClient = new QueryClient();
|
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();
|
const { pathname, hash } = useLocation();
|
||||||
useEffect(() => {
|
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]);
|
}, [pathname, hash]);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ const Contact = () => {
|
|||||||
<div className="container mx-auto px-6">
|
<div className="container mx-auto px-6">
|
||||||
<div className="flex items-center justify-between">
|
<div className="flex items-center justify-between">
|
||||||
<Link to="/" className="flex items-center gap-2 group">
|
<Link to="/" className="flex items-center gap-2 group">
|
||||||
<span className="text-xl font-display font-medium text-foreground tracking-tight">
|
<span className="text-xl font-display font-medium text-foreground tracking-normal">
|
||||||
WEBklar
|
WEBklar
|
||||||
</span>
|
</span>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -184,6 +184,8 @@ const Contact = () => {
|
|||||||
Wir verwenden Ihre Angaben nur, um Ihre Anfrage zu beantworten. Mehr dazu in unserer{" "}
|
Wir verwenden Ihre Angaben nur, um Ihre Anfrage zu beantworten. Mehr dazu in unserer{" "}
|
||||||
<a
|
<a
|
||||||
href="https://project.webklar.com/datenschutz.html"
|
href="https://project.webklar.com/datenschutz.html"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
className="text-foreground underline underline-offset-4 hover:no-underline"
|
className="text-foreground underline underline-offset-4 hover:no-underline"
|
||||||
>
|
>
|
||||||
Datenschutzerklärung
|
Datenschutzerklärung
|
||||||
|
|||||||
@@ -1,15 +1,9 @@
|
|||||||
import { Link, useLocation } from "react-router-dom";
|
import { Link } from "react-router-dom";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { usePageMeta } from "@/hooks/use-page-meta";
|
import { usePageMeta } from "@/hooks/use-page-meta";
|
||||||
|
|
||||||
const NotFound = () => {
|
const NotFound = () => {
|
||||||
const location = useLocation();
|
|
||||||
usePageMeta("Seite nicht gefunden");
|
usePageMeta("Seite nicht gefunden");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
console.error("404 Error: User attempted to access non-existent route:", location.pathname);
|
|
||||||
}, [location.pathname]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-h-[70vh] items-center justify-center bg-background px-6 text-foreground">
|
<div className="flex min-h-[70vh] items-center justify-center bg-background px-6 text-foreground">
|
||||||
<div className="text-center">
|
<div className="text-center">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { describe, it, expect, vi, afterEach } from "vitest";
|
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 { MemoryRouter } from "react-router-dom";
|
||||||
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
@@ -17,6 +17,7 @@ import KontaktAbschnitt from "@/components/Contact";
|
|||||||
import About from "@/pages/About";
|
import About from "@/pages/About";
|
||||||
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
import { TextHoverEffect } from "@/components/ui/text-hover-effect";
|
||||||
import { Toaster } from "@/components/ui/toaster";
|
import { Toaster } from "@/components/ui/toaster";
|
||||||
|
import { ScrollToTop } from "@/App";
|
||||||
|
|
||||||
// WebGL, Canvas und Netzwerk laden wir in jsdom nicht: die Effekt-Komponenten und
|
// WebGL, Canvas und Netzwerk laden wir in jsdom nicht: die Effekt-Komponenten und
|
||||||
// das Kontaktformular-Backend werden durch Attrappen ersetzt.
|
// 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"];
|
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) => {
|
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);
|
seite(pfad);
|
||||||
const footer = screen.getByRole("contentinfo");
|
const footer = screen.getByRole("contentinfo");
|
||||||
expect(within(footer).getByRole("link", { name: "Impressum" })).toHaveAttribute("href", "/impressum");
|
expect(within(footer).getByRole("link", { name: "Impressum" })).toHaveAttribute("href", "/impressum");
|
||||||
@@ -327,7 +327,6 @@ const THEMEN: Thema[] = ["hell", "dunkel"];
|
|||||||
|
|
||||||
describe("C5 404-Seite", () => {
|
describe("C5 404-Seite", () => {
|
||||||
it("zeigt Überschrift, Erklärung und Link zur Startseite auf Deutsch", () => {
|
it("zeigt Überschrift, Erklärung und Link zur Startseite auf Deutsch", () => {
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
seite("/gibt-es-nicht");
|
seite("/gibt-es-nicht");
|
||||||
expect(screen.getByRole("heading", { level: 1, name: "Seite nicht gefunden" })).toBeInTheDocument();
|
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.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", () => {
|
it("nutzt die Theme-Farben statt eines festen grauen Hintergrunds", () => {
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
const { container } = seite("/gibt-es-nicht");
|
const { container } = seite("/gibt-es-nicht");
|
||||||
expect(container.querySelector(".bg-background.text-foreground")).not.toBeNull();
|
expect(container.querySelector(".bg-background.text-foreground")).not.toBeNull();
|
||||||
expect(container.querySelector(".bg-muted")).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"])(
|
it.each(["/", "/kontakt", "/ueber-uns", "/agb", "/impressum", "/wartungs-abo", "/hosting-abo", "/gibt-es-nicht"])(
|
||||||
"%s: die Marke steht als WEBklar da",
|
"%s: die Marke steht als WEBklar da",
|
||||||
(pfad) => {
|
(pfad) => {
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
seite(pfad);
|
||||||
seite(pfad);
|
|
||||||
expect(screen.queryAllByText(/Webklar/)).toHaveLength(0);
|
expect(screen.queryAllByText(/Webklar/)).toHaveLength(0);
|
||||||
expect(screen.getAllByText("WEBklar").length).toBeGreaterThan(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«", () => {
|
it("die 404-Seite setzt den Titel »Seite nicht gefunden«", () => {
|
||||||
vi.spyOn(console, "error").mockImplementation(() => {});
|
|
||||||
seite("/gibt-es-nicht");
|
seite("/gibt-es-nicht");
|
||||||
expect(document.title).toBe("Seite nicht gefunden | WEBklar");
|
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/);
|
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(
|
||||||
|
<MemoryRouter initialEntries={["/#services"]} future={routerFuture}>
|
||||||
|
<ScrollToTop />
|
||||||
|
<section id="services" />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
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(
|
||||||
|
<MemoryRouter initialEntries={["/kontakt"]} future={routerFuture}>
|
||||||
|
<ScrollToTop />
|
||||||
|
</MemoryRouter>,
|
||||||
|
);
|
||||||
|
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*</)).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in new issue
Block a user