import { describe, it, expect, vi, afterEach } from "vitest"; import { render, screen, within, fireEvent, act, cleanup } from "@testing-library/react"; import { MemoryRouter } from "react-router-dom"; import { existsSync, readdirSync, readFileSync, statSync } from "node:fs"; import { join } from "node:path"; import { AppRoutes } from "@/routes"; import { usePageMeta } from "@/hooks/use-page-meta"; import Header from "@/components/Header"; import Footer from "@/components/Footer"; import Hero from "@/components/Hero"; import ProblemSection from "@/components/ProblemSection"; import SolutionSection from "@/components/SolutionSection"; import Values from "@/components/Values"; import Services from "@/components/Services"; import Process from "@/components/Process"; import KontaktAbschnitt from "@/components/Contact"; import About from "@/pages/About"; import { TextHoverEffect } from "@/components/ui/text-hover-effect"; import { Toaster } from "@/components/ui/toaster"; // WebGL, Canvas und Netzwerk laden wir in jsdom nicht: die Effekt-Komponenten und // das Kontaktformular-Backend werden durch Attrappen ersetzt. vi.mock("@/components/Silk", () => ({ default: () => null })); vi.mock("@/components/PixelBlast", () => ({ default: () => null })); vi.mock("@/components/LightRays", () => ({ default: () => null })); vi.mock("@/components/ui/flickering-grid", () => ({ FlickeringGrid: () => null })); vi.mock("@/lib/appwrite", () => ({ createContactDocument: vi.fn().mockResolvedValue(undefined) })); // Dieselben Future-Flags wie der BrowserRouter in App.tsx, sonst warnt React Router in der Testausgabe. const routerFuture = { v7_startTransition: true, v7_relativeSplatPath: true }; const DATENSCHUTZ = "https://project.webklar.com/datenschutz.html"; const TELEFON = "0176 23726355"; const TELEFON_HREF = "tel:+4917623726355"; /** Rendert die komplette Routenliste (mit gemeinsamem Footer) an einem Pfad. */ const seite = (pfad: string) => render( , ); /** Rendert eine einzelne Komponente im Router. */ const zeige = (ui: React.ReactElement) => render( {ui} , ); // ── Quelltext-Scan ───────────────────────────────────────────────────────────── // vitest läuft im Projektverzeichnis const WURZEL = `${process.cwd()}/`; const dateien = (ordner: string): string[] => readdirSync(ordner).flatMap((name) => { const pfad = join(ordner, name); return statSync(pfad).isDirectory() ? dateien(pfad) : [pfad]; }); const QUELLEN = [ ...dateien(join(WURZEL, "src")).filter((p) => /\.(tsx?|css)$/.test(p) && !p.includes("/src/test/")), join(WURZEL, "index.html"), ].map((pfad) => ({ pfad: pfad.replace(WURZEL, ""), text: readFileSync(pfad, "utf8") })); /** Pfade aller Quelldateien (ohne Tests), in denen das Muster vorkommt. */ const vorkommen = (muster: RegExp) => QUELLEN.filter(({ text }) => muster.test(text)).map(({ pfad }) => pfad); const lies = (pfad: string) => readFileSync(join(WURZEL, pfad), "utf8"); const gibtEs = (pfad: string) => existsSync(join(WURZEL, pfad)); const meta = (name: string) => document.querySelector(`meta[name="${name}"]`)?.getAttribute("content"); afterEach(() => { cleanup(); vi.restoreAllMocks(); vi.unstubAllGlobals(); }); describe("B1 Hero: ehrliche Fakten statt Zahlenleiste", () => { const FAKTEN = [ ["Falkenstein", "Server in Deutschland"], ["Live-Vorschau", "im Kundenportal"], ["ab 20 €", "Hosting im Monat"], ]; it("zeigt genau drei Fakten mit Wert und Beschriftung", () => { zeige(); for (const [wert, beschriftung] of FAKTEN) { expect(screen.getByText(wert)).toBeInTheDocument(); expect(screen.getByText(beschriftung)).toBeInTheDocument(); } const raster = screen.getByText("Falkenstein").closest(".grid"); expect(raster?.children).toHaveLength(3); expect(raster).toHaveClass("grid-cols-1", "md:grid-cols-3"); }); it("die alte Zahlenleiste ist weg: kein Projektzähler, kein »Am Markt«, keine 99,9 %, keine Flagge", () => { const { container } = zeige(); expect(screen.queryByText("Am Markt")).toBeNull(); expect(screen.queryByText("Projekte")).toBeNull(); expect(screen.queryByText("Systemverfügbarkeit")).toBeNull(); expect(container).not.toHaveTextContent(/99[,.]9|🇩🇪|Serverstandort/); }); it("hat nur noch den Potenzialanalyse-Button, keinen »Projekte ansehen«", () => { zeige(); expect(screen.getAllByRole("button").map((b) => b.getAttribute("aria-label"))).toEqual([ "Kostenlose Potenzialanalyse sichern", ]); }); it("lädt nichts vom Netz und braucht keinen QueryClient", () => { const abruf = vi.fn(); vi.stubGlobal("fetch", abruf); zeige(); expect(abruf).not.toHaveBeenCalled(); expect(gibtEs("src/lib/projects.ts")).toBe(false); expect(vorkommen(/api\/projects|fetchProjects|FOUNDING_DATE/)).toEqual([]); }); }); describe("B2 Projekte-Bereich ausgeblendet", () => { it("ProjectShowcase ist gelöscht; nichts verweist auf #projects oder Unsplash", () => { expect(gibtEs("src/components/ProjectShowcase.tsx")).toBe(false); expect(vorkommen(/#projects|id="projects"|images\.unsplash\.com|ProjectShowcase/)).toEqual([]); }); it("die Startseite hat keinen Projekte-Abschnitt und keinen Nav-Link dorthin", () => { const { container } = seite("/"); expect(container.querySelector("#projects")).toBeNull(); expect(container.querySelector('a[href$="#projects"]')).toBeNull(); expect(screen.queryByRole("link", { name: "Projekte" })).toBeNull(); expect(screen.queryByText("Verbatim Labs")).toBeNull(); // die übrigen Nav-Punkte bleiben for (const name of ["Über uns", "Leistungen", "Ablauf"]) { expect(screen.getAllByRole("link", { name }).length).toBeGreaterThan(0); } }); }); describe("B3 Eine Telefonnummer überall", () => { it("die alte Nummer 0170 4969375 kommt in keiner Quelldatei mehr vor", () => { expect(vorkommen(/0170\s?4969375|491704969375/)).toEqual([]); }); it("Kontaktseite zeigt 0176 23726355 mit passendem tel:-Link", () => { seite("/kontakt"); const links = screen.getAllByRole("link", { name: TELEFON }); expect(links.length).toBeGreaterThan(0); for (const link of links) expect(link).toHaveAttribute("href", TELEFON_HREF); }); it("der Footer zeigt dieselbe Nummer", () => { zeige(