import { test, expect, type Page } from "@playwright/test"; // These records exist only in Playwright network mocks. They are never seeded // into an application database, filesystem store, or production response. const capabilities = { readOrders: true, readCustomers: true, readCouriers: true, readInvoices: false, writeOrders: false, writeCustomers: false, writeCouriers: false, writeInvoices: false, completeStops: false, exportOrders: true, }; const user = { username: "browser-test", roles: ["VIEWER"], headquartersId: 1, capabilities, }; const system = { connected: true, database: "legacy-mysql", readOnly: true, schemaChanges: false, message: "Testfixture: bestehende Datenbank verbunden", legacyUrl: "https://legacy.example.test/votian", maxResults: 500, capabilities, unavailableModules: ["Rechnungserstellung und Zahlungsstatus"], }; const customers = [ { id: 11, number: "C-TEST", name: "Testkunde Nordlicht", street: "Teststraße 1", postalCode: "20095", city: "Hamburg", contact: "Testkontakt", phone: "", email: "", paymentDays: 14, active: true, }, ]; const couriers = [ { id: 22, number: "K-TEST", name: "Testkurier Hafen", phone: "", vehicle: "Transporter", plate: "HH-TEST", status: "AVAILABLE", active: true, }, ]; const today = new Date().toISOString().slice(0, 10); const orders = [ { id: 880001, number: "880001", version: 2, status: "DISPOSITION", legacyStatus: 9, legacyStatusLabel: "Disposition", customerId: 11, courierId: null, service: "Direktfahrt", vehicle: "Transporter", pickupAt: `${today}T09:00:00`, deliveryDeadline: null, reference: "TEST-REFERENZ", costCenter: "Testkostenstelle", notes: "Nur Browser-Testfixture", packages: null, weight: 12.5, distanceKm: null, netPrice: 42.5, courierPrice: 25, vatRate: 19, recipientName: "", deliveredAt: null, createdAt: `${today}T08:00:00`, updatedAt: `${today}T08:10:00`, cancelMode: null, invoiceId: null, stops: [ { id: 1, sequence: 1, type: "PICKUP", direction: 1, name: "Abholung Testlager", street: "Teststraße 1", postalCode: "20095", city: "Hamburg", contact: "", phone: "", note: "", completedAt: null, }, { id: 2, sequence: 2, type: "DELIVERY", direction: 2, name: "Zustellung Testbüro", street: "Hafenweg 2", postalCode: "20457", city: "Hamburg", contact: "", phone: "", note: "", completedAt: null, }, ], history: [], }, ]; const loginPassword = "browser-only fixture +%&=$"; async function mockApi( page: Page, overrides: Record = {}, options: { authenticated?: boolean } = {}, ) { // Model the session on the server, independently of browser storage. A page // reload preserves it; logout and expiry invalidate it for every endpoint. let authenticated = options.authenticated ?? true; let csrfVersion = 0; const csrfToken = () => `browser-csrf-${csrfVersion}`; const currentUser = overrides["/me"] ?? user; await page.route("**/api/**", async (route) => { const request = route.request(); const url = new URL(request.url()); const path = url.pathname.replace("/api", ""); const respond = (status: number, body: unknown) => route.fulfill({ status, contentType: "application/json", body: JSON.stringify(body), }); // The application must leave the Authorization header available to the // reverse proxy. These tests run without proxy credentials. expect(request.headers().authorization).toBeUndefined(); if (path === "/csrf" && request.method() === "GET") return respond(200, { token: csrfToken(), headerName: "X-CSRF-TOKEN", parameterName: "_csrf", }); if (request.method() !== "GET") { if (request.headers()["x-csrf-token"] !== csrfToken()) return respond(403, { message: "Invalid CSRF token", code: "CSRF_INVALID" }); if (path === "/login" && request.method() === "POST") { expect(request.headers()["content-type"]).toContain( "application/x-www-form-urlencoded", ); const credentials = new URLSearchParams(request.postData() ?? ""); if ( credentials.get("username") !== user.username || credentials.get("password") !== loginPassword ) return respond(401, { message: "Anmeldung fehlgeschlagen. Bitte Zugangsdaten prüfen.", }); authenticated = true; csrfVersion += 1; return respond(200, currentUser); } if (path === "/logout" && request.method() === "POST") { authenticated = false; csrfVersion += 1; return route.fulfill({ status: 204 }); } throw new Error(`Unexpected mutation: ${request.method()} ${path}`); } if (!authenticated && path !== "/system") return respond(401, { message: "Anmeldung erforderlich." }); let body: unknown = { "/me": currentUser, "/system": system, "/customers": customers, "/couriers": couriers, "/orders": orders, "/orders/880001": orders[0], "/orders/880001/history": [], ...overrides, }[path]; if (path === "/orders" && url.searchParams.get("q")) { const q = url.searchParams.get("q")!.toLowerCase(); body = orders.filter((o) => JSON.stringify(o).toLowerCase().includes(q)); } if (body === undefined) return route.fulfill({ status: 404, contentType: "application/json", body: JSON.stringify({ message: "Unknown test endpoint: " + path }), }); await route.fulfill({ status: 200, contentType: "application/json", headers: { "X-Total-Count": "1", "X-Result-Limit": "500" }, body: JSON.stringify(body), }); }); return { rotateCsrf: () => { csrfVersion += 1; }, expire: () => { authenticated = false; csrfVersion += 1; }, }; } async function expectNoStoredCredentials(page: Page) { expect( await page.evaluate(() => ({ session: Object.entries(sessionStorage), local: Object.entries(localStorage), })), ).toEqual({ session: [], local: [] }); } test("bundled application is available; real API fails closed without a database", async ({ request, }) => { expect((await request.get("/api/health")).status()).toBe(200); const systemResponse = await request.get("/api/system"); expect(systemResponse.status()).toBe(200); const state = await systemResponse.json(); expect(state.connected).toBe(false); expect(state.schemaChanges).toBe(false); expect(state.readOnly).toBe(true); expect((await request.get("/api/orders")).status()).toBe(401); expect((await request.post("/api/orders", { data: {} })).status()).toBe(403); const csrfResponse = await request.get("/api/csrf"); expect(csrfResponse.status()).toBe(200); const csrf = await csrfResponse.json(); expect( ( await request.post("/api/orders", { data: {}, headers: { [csrf.headerName]: csrf.token }, }) ).status(), ).toBe(401); expect((await request.get("/stadtbote-logo.png")).status()).toBe(200); }); test("real session cookie is HttpOnly and honors HTTPS forwarded by the proxy", async ({ playwright, baseURL, }) => { for (const secure of [false, true]) { const context = await playwright.request.newContext({ baseURL, extraHTTPHeaders: secure ? { "X-Forwarded-Proto": "https" } : {}, }); try { const response = await context.get("/api/csrf"); expect(response.status()).toBe(200); expect(await response.json()).toMatchObject({ token: expect.any(String), headerName: "X-CSRF-TOKEN", }); const sessionCookie = (await context.storageState()).cookies.find( (cookie) => cookie.name === "VOTIANSESSION", ); expect(sessionCookie).toMatchObject({ httpOnly: true, sameSite: "Lax", secure, path: "/", }); } finally { await context.dispose(); } } }); test("real backend ignores proxy Basic authentication and returns its own JSON 401", async ({ request, }) => { const headers = { Authorization: `Basic ${Buffer.from("proxy-fixture:unused").toString("base64")}`, }; expect((await request.get("/api/health", { headers })).status()).toBe(200); const response = await request.get("/api/me", { headers }); expect(response.status()).toBe(401); expect(response.headers()["content-type"]).toContain("application/json"); expect(response.headers()["www-authenticate"]).toBeUndefined(); }); test("offline login reports connection issue and exposes no fake work data", async ({ page, }) => { await page.goto("/"); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expect( page .getByText( /nicht erreichbar|keine.*verbindung|verbindung.*fehlgeschlagen/i, ) .first(), ).toBeVisible({ timeout: 10_000 }); await expect(page.getByText("880001")).toHaveCount(0); expect((await page.request.get("/api/csrf")).status()).toBe(200); expect( (await page.context().cookies()).find((cookie) => cookie.name === "VOTIANSESSION"), ).toMatchObject({ httpOnly: true }); expect(await page.evaluate(() => document.cookie)).not.toContain("VOTIANSESSION"); }); test("login sends form credentials with CSRF and keeps them out of browser storage", async ({ page, }) => { await mockApi(page, {}, { authenticated: false }); const initialSessionCheck = page.waitForResponse("**/api/me"); await page.goto("/"); expect((await initialSessionCheck).status()).toBe(401); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await page.getByLabel("VoTiAn Benutzerkonto").fill(user.username); await page.getByLabel("Passwort", { exact: true }).fill(loginPassword); const loginRequest = page.waitForRequest("**/api/login"); await page.getByRole("button", { name: "Anmelden", exact: true }).click(); const submitted = await loginRequest; expect(submitted.method()).toBe("POST"); expect(new URLSearchParams(submitted.postData() ?? "").get("username")).toBe( user.username, ); expect(new URLSearchParams(submitted.postData() ?? "").get("password")).toBe( loginPassword, ); expect(submitted.headers()["x-csrf-token"]).toBe("browser-csrf-0"); expect(submitted.headers().authorization).toBeUndefined(); expect(new URL(submitted.url()).search).toBe(""); await expect(page.getByText("880001").first()).toBeVisible(); await expectNoStoredCredentials(page); // Login rotates the server CSRF token. Logout must use the new token. const logoutRequest = page.waitForRequest("**/api/logout"); await page.getByRole("button", { name: "Abmelden", exact: true }).click(); expect((await logoutRequest).headers()["x-csrf-token"]).toBe("browser-csrf-1"); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expectNoStoredCredentials(page); }); test("incorrect login remains anonymous without retaining credentials", async ({ page, }) => { await mockApi(page, {}, { authenticated: false }); await page.goto("/"); await page.getByLabel("VoTiAn Benutzerkonto").fill(user.username); await page.getByLabel("Passwort", { exact: true }).fill("incorrect fixture"); await page.getByRole("button", { name: "Anmelden", exact: true }).click(); await expect(page.getByRole("alert")).toContainText("Anmeldung fehlgeschlagen"); await expect(page.getByText("880001")).toHaveCount(0); await expectNoStoredCredentials(page); await page.reload(); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); }); test("a CSRF token changed by another session is refreshed before retrying logout", async ({ page, }) => { const session = await mockApi(page, {}, { authenticated: false }); await page.goto("/"); await page.getByLabel("VoTiAn Benutzerkonto").fill(user.username); await page.getByLabel("Passwort", { exact: true }).fill(loginPassword); const refreshedToken = page.waitForResponse(async (response) => new URL(response.url()).pathname === "/api/csrf" && (await response.json()).token === "browser-csrf-1", ); await page.getByRole("button", { name: "Anmelden", exact: true }).click(); await refreshedToken; await expect(page.getByText("880001").first()).toBeVisible(); session.rotateCsrf(); const logoutTokens: string[] = []; page.on("request", (request) => { if (new URL(request.url()).pathname === "/api/logout") logoutTokens.push(request.headers()["x-csrf-token"]); }); await page.getByRole("button", { name: "Abmelden", exact: true }).click(); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); expect(logoutTokens).toEqual(["browser-csrf-1", "browser-csrf-2"]); await expect(page.getByText("880001")).toHaveCount(0); }); test("a non-CSRF logout failure is shown once and leaves the session active", async ({ page, }) => { await mockApi(page); let logoutAttempts = 0; await page.route("**/api/logout", async (route) => { logoutAttempts += 1; return route.fulfill({ status: 403, contentType: "application/json", body: JSON.stringify({ message: "Abmeldung nicht erlaubt." }), }); }); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); await page.getByRole("button", { name: "Abmelden", exact: true }).click(); await expect(page.getByRole("alert")).toContainText("Abmeldung fehlgeschlagen"); expect(logoutAttempts).toBe(1); await expect(page.getByText("880001").first()).toBeVisible(); await page.reload(); await expect(page.getByText("880001").first()).toBeVisible(); }); test("existing server session is restored on load and reload without browser storage", async ({ page, }) => { await mockApi(page); const initialSessionCheck = page.waitForResponse("**/api/me"); await page.goto("/"); expect((await initialSessionCheck).status()).toBe(200); await expect(page.getByText("880001").first()).toBeVisible(); await expectNoStoredCredentials(page); const restoredSessionCheck = page.waitForResponse("**/api/me"); await page.reload(); expect((await restoredSessionCheck).status()).toBe(200); await expect(page.getByText("880001").first()).toBeVisible(); await expectNoStoredCredentials(page); }); test("existing account opens order list, details and module navigation with read-only capabilities", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await mockApi(page); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); await expect(page.getByText("Testkunde Nordlicht").first()).toBeVisible(); await page.getByText("880001").first().click(); await expect(page.getByRole("dialog")).toBeVisible(); await expect(page.getByText("Abholung Testlager").first()).toBeVisible(); await page.getByRole("button", { name: "Details schließen", exact: true }).click(); for (const name of [ "Disposition", "Touren", "Kunden", "Kuriere", "Abrechnung", "Auswertung", ]) { await page.getByRole("button", { name, exact: true }).click(); await expect(page.locator("main")).toBeVisible(); } expect(errors).toEqual([]); await page.screenshot({ path: "work/test-results/auswertung-testfixture.png", fullPage: true, }); }); for (const customerAccess of ["empty", "denied", "loaded"] as const) { test(`order customer names remain available with ${customerAccess} customer master data`, async ({ page }) => { const order = { ...orders[0], customerName: " Testzahler Westwind " }; let customerRequests = 0; page.on("request", (request) => { if (new URL(request.url()).pathname === "/api/customers") customerRequests++; }); await mockApi(page, { "/system": { ...system, capabilities: { ...capabilities, readCustomers: customerAccess !== "denied" }, }, "/customers": customerAccess === "loaded" ? customers : [], "/orders": [order], "/orders/880001": order, }); await page.goto("/"); await expect(page.getByRole("row", { name: /880001/ })).toContainText("Testzahler Westwind"); await page.getByText("880001").first().click(); const dialog = page.getByRole("dialog"); await expect(dialog.getByText("KUNDE / ZAHLER", { exact: true })).toBeVisible(); await expect(dialog.getByRole("heading", { name: "Testzahler Westwind", exact: true })).toBeVisible(); await page.getByRole("button", { name: "Details schließen", exact: true }).click(); for (const module of ["Disposition", "Touren", "Auswertung"]) { await page.getByRole("button", { name: module, exact: true }).click(); await expect(page.locator("main").getByText("Testzahler Westwind", { exact: true })).toBeVisible(); await expect(page.locator("main").getByText("Testkunde Nordlicht", { exact: true })).toHaveCount(0); } if (customerAccess === "denied") expect(customerRequests).toBe(0); else expect(customerRequests).toBeGreaterThan(0); }); } test("orders and reports show safe customer fallbacks for missing identities and names", async ({ page }) => { const variants = [ { customerId: null, customerName: null, label: "Kunde nicht zugeordnet" }, { customerId: 0, customerName: "", label: "Kunde nicht zugeordnet" }, { customerId: undefined, customerName: undefined, label: "Kunde nicht zugeordnet" }, { customerId: 44, customerName: " ", label: "Kunde #44" }, { customerId: 11, customerName: " ", label: "Testkunde Nordlicht" }, ]; const fixture = variants.map(({ label: _label, ...customer }, index) => ({ ...orders[0], ...customer, id: 880001 + index, number: String(880001 + index), })); await mockApi(page, { "/orders": fixture }); await page.goto("/"); for (const [index, { label }] of variants.entries()) { const orderNumber = fixture[index].number; const row = page.getByRole("row").filter({ has: page.getByText(orderNumber, { exact: true }) }); await expect(row).toContainText(label); await row.getByText(orderNumber, { exact: true }).click(); await expect(page.getByRole("dialog").getByRole("heading", { name: label, exact: true })).toBeVisible(); await page.getByRole("button", { name: "Details schließen", exact: true }).click(); } await page.getByRole("button", { name: "Auswertung", exact: true }).click(); await expect(page.locator("main").getByText("Kunde nicht zugeordnet", { exact: true })).toHaveCount(3); await expect(page.locator("main").getByText("Kunde #44", { exact: true })).toBeVisible(); await expect(page.locator("main").getByText("Testkunde Nordlicht", { exact: true })).toBeVisible(); await expect(page.locator("main")).not.toContainText(/Kunde #(null|undefined|0)/); }); test("customer reports aggregate by customer ID and use a name from any matching order", async ({ page }) => { await mockApi(page, { "/customers": [], "/orders": [ { ...orders[0], customerName: null }, { ...orders[0], id: 880002, number: "880002", customerName: "Testzahler Westwind" }, ], }); await page.goto("/"); await page.getByRole("button", { name: "Auswertung", exact: true }).click(); const row = page.getByRole("row").filter({ hasText: "Testzahler Westwind" }); await expect(row).toHaveCount(1); await expect(row.getByRole("cell").nth(1)).toHaveText("2"); await expect(row.getByRole("cell").nth(2)).toHaveText("85,00 €"); }); test("order history loads on demand and shows newest events with safe multiline details", async ({ page, }) => { await mockApi(page); const detail = "Alt: 10 €\nNeu: 42,50 €\n"; const history = [ { id: 1, at: null, action: "Importiert", detail: "Altdaten", actor: "" }, { id: 2, at: `${today}T08:20:00`, action: "Angelegt", detail: "Auftrag erfasst", actor: "dispo.alt", }, { id: 3, at: `${today}T09:20:00`, action: "Geändert", detail, actor: "dispo.neu", }, { id: 4, at: `${today}T09:20:00`, action: "Geprüft", detail: "Preis geprüft", actor: "dispo.neu", }, ]; let release!: () => void; const pending = new Promise((resolve) => { release = resolve; }); let requests = 0; await page.route("**/api/orders/880001/history", async (route) => { requests++; await pending; await route.fulfill({ json: history }); }); await page.goto("/"); await page.getByText("880001").first().click(); const dialog = page.getByRole("dialog"); await expect(dialog.getByRole("button", { name: "Verlauf", exact: true })).toBeVisible(); expect(requests).toBe(0); await dialog.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(dialog.getByRole("status")).toHaveText("Historie wird geladen …"); await expect(dialog.getByText("Keine Historie vorhanden")).toHaveCount(0); release(); await expect(dialog.getByRole("button", { name: "Verlauf 4", exact: true })).toBeVisible(); await expect(dialog.locator(".history-item strong")).toHaveText([ "Geprüft", "Geändert", "Angelegt", "Importiert", ]); const changed = dialog.locator(".history-item").filter({ hasText: "Geändert" }); expect(await changed.locator("p").textContent()).toBe(detail); await expect(changed.locator("p")).toHaveCSS("white-space", "pre-wrap"); await expect(changed.locator("small")).toContainText("09:20 Uhr · dispo.neu"); await expect(dialog.locator(".history-item").last()).toContainText("Zeitpunkt unbekannt"); await expect(dialog.locator("script")).toHaveCount(0); expect(requests).toBe(1); }); test("order history distinguishes unavailable data from empty history and retries", async ({ page }) => { await mockApi(page); let attempts = 0; await page.route("**/api/orders/880001/history", async (route) => { attempts++; if (attempts === 1) return route.fulfill({ status: 503, json: { message: "Protokolldatenbank nicht erreichbar." }, }); return route.fulfill({ json: [] }); }); await page.goto("/"); await page.getByText("880001").first().click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(dialog.getByRole("alert")).toContainText("Protokolldatenbank nicht erreichbar."); await expect(dialog.getByText("Keine Historie vorhanden")).toHaveCount(0); await expect(dialog.getByRole("button", { name: "Verlauf", exact: true })).toBeVisible(); await dialog.getByRole("button", { name: "Erneut versuchen", exact: true }).click(); await expect(dialog.getByText("Keine Historie vorhanden", { exact: true })).toBeVisible(); await expect(dialog.getByRole("button", { name: "Verlauf 0", exact: true })).toBeVisible(); await expect(dialog.getByRole("alert")).toHaveCount(0); expect(attempts).toBe(2); }); test("order history is refreshed with the selected order even when its version is unchanged", async ({ page }) => { await mockApi(page); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); await page.clock.install(); await page.getByLabel("Automatisch aktualisieren").check(); let requests = 0; await page.route("**/api/orders/880001/history", async (route) => { requests++; return route.fulfill({ json: [{ id: requests, at: `${today}T09:00:00`, action: `Stand ${requests}`, detail: "", actor: "dispo" }], }); }); await page.getByText("880001").first().click(); const dialog = page.getByRole("dialog"); await dialog.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(dialog.locator(".history-item strong")).toHaveText(["Stand 1"]); await page.clock.fastForward(30_000); await expect(dialog.locator(".history-item strong")).toHaveText(["Stand 2"]); expect(requests).toBe(2); }); test("a late history response cannot replace the history of another order", async ({ page }) => { const secondOrder = { ...orders[0], id: 880002, number: "880002" }; await mockApi(page, { "/orders": [orders[0], secondOrder], "/orders/880002/history": [ { id: 2, at: null, action: "Zweiter Auftrag", detail: "Aktuelle Historie", actor: "dispo" }, ], }); let release!: () => void; const pending = new Promise((resolve) => { release = resolve; }); let responded!: () => void; const responseFinished = new Promise((resolve) => { responded = resolve; }); await page.route("**/api/orders/880001/history", async (route) => { await pending; await route.fulfill({ json: [{ id: 1, at: null, action: "Veraltete Historie", detail: "Erster Auftrag", actor: "dispo" }], }); responded(); }); await page.goto("/"); await page.getByText("880001").first().click(); await page.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(page.getByRole("dialog").getByRole("status")).toBeVisible(); await page.getByRole("button", { name: "Details schließen", exact: true }).click(); await page.getByText("880002").first().click(); await page.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(page.getByRole("dialog").locator(".history-item strong")).toHaveText(["Zweiter Auftrag"]); release(); await responseFinished; await expect(page.getByRole("dialog").locator(".history-item strong")).toHaveText(["Zweiter Auftrag"]); await expect(page.getByText("Veraltete Historie")).toHaveCount(0); }); test("session expiry while loading order history clears the drawer and account data", async ({ page }) => { const session = await mockApi(page); await page.goto("/"); await page.getByText("880001").first().click(); session.expire(); await page.getByRole("button", { name: "Verlauf", exact: true }).click(); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expect(page.getByRole("dialog")).toHaveCount(0); await expect(page.getByText("880001")).toHaveCount(0); await expect(page.getByText("Testkunde Nordlicht")).toHaveCount(0); await expectNoStoredCredentials(page); }); test("missing module rights do not trigger forbidden API calls", async ({ page, }) => { const limited = { ...capabilities, readCustomers: false, readCouriers: false, }; const requested: string[] = []; page.on("request", (request) => { if (request.url().includes("/api/")) requested.push(new URL(request.url()).pathname); }); await mockApi(page, { "/me": { ...user, capabilities: limited }, "/system": { ...system, capabilities: limited }, }); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); expect(requested).not.toContain("/api/customers"); expect(requested).not.toContain("/api/couriers"); }); test("search sends a server query instead of searching only a limited local list", async ({ page, }) => { await mockApi(page); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); const request = page.waitForRequest( (request) => new URL(request.url()).pathname === "/api/orders" && new URL(request.url()).searchParams.get("q") === "TEST-REFERENZ", ); await page .getByRole("textbox", { name: "Aufträge suchen" }) .fill("TEST-REFERENZ"); await request; await expect(page.getByText("880001").first()).toBeVisible(); }); test("nullable legacy fields do not break order lists or reports", async ({ page, }) => { const errors: string[] = []; page.on("pageerror", (error) => errors.push(error.message)); await mockApi(page, { "/orders": [ { ...orders[0], pickupAt: null, netPrice: null, courierPrice: null, weight: null, vatRate: null, }, ], }); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); await page.getByRole("button", { name: "Auswertung", exact: true }).click(); await expect(page.locator("main")).toBeVisible(); expect(errors).toEqual([]); }); test("logout invalidates the server session and removes previous account data", async ({ page, }) => { await mockApi(page); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); const logoutRequest = page.waitForRequest("**/api/logout"); await page.getByRole("button", { name: "Abmelden", exact: true }).click(); const submitted = await logoutRequest; expect(submitted.method()).toBe("POST"); expect(submitted.headers()["x-csrf-token"]).toBe("browser-csrf-0"); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expect(page.getByText("880001")).toHaveCount(0); await expect(page.getByText("Testkunde Nordlicht")).toHaveCount(0); await expectNoStoredCredentials(page); const sessionCheck = page.waitForResponse("**/api/me"); await page.reload(); expect((await sessionCheck).status()).toBe(401); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expect(page.getByText("880001")).toHaveCount(0); }); test("expired session clears work data and requests a fresh login", async ({ page }) => { const session = await mockApi(page); await page.goto("/"); await expect(page.getByText("880001").first()).toBeVisible(); await expect(page.getByText("Testkunde Nordlicht").first()).toBeVisible(); session.expire(); await page.getByRole("button", { name: "Aktualisieren", exact: true }).click(); await expect(page.getByRole("heading", { name: /anmelden/i })).toBeVisible(); await expect(page.getByText(/Sitzung ist abgelaufen/)).toBeVisible(); await expect(page.getByText("880001")).toHaveCount(0); await expect(page.getByText("Testkunde Nordlicht")).toHaveCount(0); await expect(page.getByRole("dialog")).toHaveCount(0); await expectNoStoredCredentials(page); });