feat(frontend): Oberfläche mit RRULE-Editor und Logo-Auswahl
- Vite, React 18, TypeScript und Tailwind mit dunklem Standard-Theme über CSS-Variablen, heller Modus umschaltbar und in localStorage gemerkt - Anmeldung, erzwungener Passwortwechsel, Layout mit Seitenleiste - API-Client mit stiller Token-Erneuerung, TanStack Query mit Fehler-Toasts - Seiten Recurrences (inkl. Detail-Drawer), Transactions, Merchants, Settings - Geführter RRULE-Editor mit Vorlagen, Expertenmodus, deutschem Klartext und Live-Vorschau der nächsten Termine vom preview-Endpunkt - Firmen-Kachelgrid mit Logo, Markenfarbe und Logo-Auswahldialog samt Upload - Beträge durchgängig in de-DE, Eingabe in beiden Schreibweisen - 49 Tests, tsc --noEmit und eslint sauber, Produktionsbundle baut Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_014e7t8UpmoVNMtWivY5LiSH
This commit is contained in:
@@ -0,0 +1,23 @@
|
||||
/** Gemeinsame Einrichtung aller Frontend-Tests. */
|
||||
|
||||
import "@testing-library/jest-dom/vitest";
|
||||
import { cleanup } from "@testing-library/react";
|
||||
import { afterEach, vi } from "vitest";
|
||||
|
||||
afterEach(() => {
|
||||
cleanup();
|
||||
vi.restoreAllMocks();
|
||||
});
|
||||
|
||||
// jsdom kennt matchMedia nicht; einzelne Komponenten fragen es ab.
|
||||
Object.defineProperty(window, "matchMedia", {
|
||||
writable: true,
|
||||
value: (query: string) => ({
|
||||
matches: false,
|
||||
media: query,
|
||||
onchange: null,
|
||||
addEventListener: () => {},
|
||||
removeEventListener: () => {},
|
||||
dispatchEvent: () => false,
|
||||
}),
|
||||
});
|
||||
@@ -0,0 +1,58 @@
|
||||
/** Hilfsmittel zum Rendern von Komponenten in Tests. */
|
||||
|
||||
import { type ReactElement, type ReactNode } from "react";
|
||||
|
||||
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||
import { render } from "@testing-library/react";
|
||||
import { MemoryRouter } from "react-router-dom";
|
||||
|
||||
/** Frischer Client je Test, ohne Wiederholungen und ohne Konsolenausgabe. */
|
||||
export function createTestQueryClient(): QueryClient {
|
||||
return new QueryClient({
|
||||
defaultOptions: {
|
||||
queries: { retry: false, gcTime: 0, staleTime: 0 },
|
||||
mutations: { retry: false },
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function renderWithProviders(ui: ReactElement, route = "/") {
|
||||
const client = createTestQueryClient();
|
||||
|
||||
function Rahmen({ children }: { children: ReactNode }) {
|
||||
return (
|
||||
<QueryClientProvider client={client}>
|
||||
<MemoryRouter
|
||||
initialEntries={[route]}
|
||||
future={{ v7_startTransition: true, v7_relativeSplatPath: true }}
|
||||
>
|
||||
{children}
|
||||
</MemoryRouter>
|
||||
</QueryClientProvider>
|
||||
);
|
||||
}
|
||||
|
||||
return { client, ...render(ui, { wrapper: Rahmen }) };
|
||||
}
|
||||
|
||||
/** Antwortet auf `fetch` anhand einer Zuordnung von Pfadfragment zu Nutzlast. */
|
||||
export function mockFetch(routen: Record<string, unknown>): ReturnType<typeof vi.fn> {
|
||||
const nachbildung = vi.fn(async (eingabe: RequestInfo | URL) => {
|
||||
const url = typeof eingabe === "string" ? eingabe : eingabe.toString();
|
||||
const treffer = Object.keys(routen).find((muster) => url.includes(muster));
|
||||
|
||||
if (treffer === undefined) {
|
||||
return new Response(JSON.stringify({ detail: "Not Found", code: "not_found" }), {
|
||||
status: 404,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify(routen[treffer]), {
|
||||
status: 200,
|
||||
headers: { "content-type": "application/json" },
|
||||
});
|
||||
});
|
||||
|
||||
vi.stubGlobal("fetch", nachbildung);
|
||||
return nachbildung;
|
||||
}
|
||||
Reference in New Issue
Block a user