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
+84
-17
No files matched your search
+31
-3
@@ -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;
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ const Contact = () => {
|
||||
<div className="container mx-auto px-6">
|
||||
<div className="flex items-center justify-between">
|
||||
<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
|
||||
</span>
|
||||
</Link>
|
||||
@@ -184,6 +184,8 @@ const Contact = () => {
|
||||
Wir verwenden Ihre Angaben nur, um Ihre Anfrage zu beantworten. Mehr dazu in unserer{" "}
|
||||
<a
|
||||
href="https://project.webklar.com/datenschutz.html"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="text-foreground underline underline-offset-4 hover:no-underline"
|
||||
>
|
||||
Datenschutzerklärung
|
||||
|
||||
@@ -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 (
|
||||
<div className="flex min-h-[70vh] items-center justify-center bg-background px-6 text-foreground">
|
||||
<div className="text-center">
|
||||
|
||||
@@ -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,7 +358,6 @@ 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);
|
||||
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(
|
||||
<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