Blockierendes Skript im <head> setzt Darstellung, Kontrast und Bewegung vor dem ersten Zeichnen (gleiche Logik wie resolveAppearance, für alle Eingaben getestet), theme-color je Satz, Wahl je Gerät in localStorage mit Rückfall bei gesperrtem Speicher; Abgleich über Tabs, Systemeinstellungen und Seitenwechsel an einer Stelle. Profil-Karte „Darstellung“, Sonnenmodus im Benutzermenü; Dunkel bleibt bis A4 hinter NEXT_PUBLIC_APPEARANCE_DARK. next-themes entfernt. ARCHITECTURE §9: 'unsafe-inline' der Frontend-CSP als getragenes Risiko mit Begründung. Refs #26 Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
65 lines
2.4 KiB
TypeScript
65 lines
2.4 KiB
TypeScript
import { useMutation } from "@tanstack/react-query";
|
|
import { act, render, screen, waitFor } from "@testing-library/react";
|
|
import userEvent from "@testing-library/user-event";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import { readFileSync } from "node:fs";
|
|
import path from "node:path";
|
|
import { Providers } from "./providers";
|
|
|
|
vi.mock("@/lib/auth/auth-context", () => ({
|
|
AuthProvider: ({ children }: { children: React.ReactNode }) => <>{children}</>,
|
|
}));
|
|
|
|
function Probe({ mutationFn }: { mutationFn: () => Promise<string> }) {
|
|
const mutation = useMutation({ mutationFn });
|
|
return <button onClick={() => mutation.mutate()}>Speichern</button>;
|
|
}
|
|
|
|
describe("Providers", () => {
|
|
afterEach(() => {
|
|
vi.restoreAllMocks();
|
|
// TanStack's onlineManager tracks the browser events, so restore it too.
|
|
act(() => {
|
|
window.dispatchEvent(new Event("online"));
|
|
});
|
|
});
|
|
|
|
it("runs mutations while navigator.onLine is false", async () => {
|
|
vi.spyOn(window.navigator, "onLine", "get").mockReturnValue(false);
|
|
const mutationFn = vi.fn().mockResolvedValue("ok");
|
|
render(
|
|
<Providers>
|
|
<Probe mutationFn={mutationFn} />
|
|
</Providers>,
|
|
);
|
|
// onlineManager (listening once mounted) only learns about airplane mode through the window event.
|
|
act(() => {
|
|
window.dispatchEvent(new Event("offline"));
|
|
});
|
|
await userEvent.click(screen.getByRole("button", { name: "Speichern" }));
|
|
await waitFor(() => expect(mutationFn).toHaveBeenCalledTimes(1));
|
|
});
|
|
|
|
it("does not apply the motion level itself any more (the inline script does)", () => {
|
|
document.documentElement.removeAttribute("data-motion");
|
|
window.localStorage.setItem("apicore.motion", "off");
|
|
render(
|
|
<Providers>
|
|
<p>x</p>
|
|
</Providers>,
|
|
);
|
|
expect(document.documentElement).not.toHaveAttribute("data-motion");
|
|
window.localStorage.clear();
|
|
});
|
|
});
|
|
|
|
describe("theme handling", () => {
|
|
it("the toaster follows resolvedTheme and next-themes is not imported", () => {
|
|
const toaster = readFileSync(path.resolve(__dirname, "../components/ui/sonner.tsx"), "utf8");
|
|
expect(toaster).toContain("resolvedTheme");
|
|
expect(toaster).not.toContain("next-themes");
|
|
const pkg = JSON.parse(readFileSync(path.resolve(__dirname, "../../package.json"), "utf8"));
|
|
expect(pkg.dependencies["next-themes"]).toBeUndefined();
|
|
});
|
|
});
|