Files
votianng/tests/browser.spec.ts
T
Sven 363bc27c26 Erweitere VoTiAn NG um Auftrags-, Stamm- und Verwaltungsfunktionen
Ergänzt Auftragserfassung und -bearbeitung, Unternehmerbelege, Stornos, Lagerkorrekturen, Stammdaten, Abrechnung, CRM, Formulare, Berichte und Preisverwaltung auf Basis der aktuellen PHP-Anwendung.

Enthält Integrations- und Oberflächentests sowie dokumentierte Abgleiche und Prüfstände.
2026-10-13 20:31:31 +02:00

806 lines
31 KiB
TypeScript

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,
readStatistics: 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<string, unknown> = {},
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 });
}
if (path === "/statistics/query" && request.method() === "POST") {
if (!authenticated) return respond(401, { message: "Anmeldung erforderlich." });
const query = request.postDataJSON();
const totals = { orders: 1234, revenue: 68000, courierRevenue: 47000, missingCourierValues: 0, serviceRevenue: 1500, fuelMarkup: 2300, courierService: 500, kilometers: 26000, co2eKg: 3300, kilometersOrders: 1200, co2eKgOrders: 1100 };
return respond(200, overrides[path] ?? {
periods: [query.interval, ...query.comparisons].map((interval: {from:string;to:string}) => ({interval,totals,rows:[{...totals,period:interval.from.slice(0,7),...Object.fromEntries(query.splits.map((_:string,i:number)=>[`split${i}`,"Testauswertung"]))}]})),
splits: query.splits, dateBasis: query.dateBasis, period: query.period, completePeriod: true,
});
}
throw new Error(`Unexpected mutation: ${request.method()} ${path}`);
}
if (!authenticated && path !== "/system")
return respond(401, { message: "Anmeldung erforderlich." });
let body: unknown = {
"/statistics/options": {headquarters:[{id:1,label:"Test-Niederlassung"}],currentHeadquarters:1,groups:[],excludeFreeCancellations:false,relatedCustomer:false},
"/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"]) {
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 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 expect(page.locator("main")).not.toContainText(/Kunde #(null|undefined|0)/);
});
test("statistics use server totals beyond the order list and submit comparison filters", async ({ page }) => {
await mockApi(page, { "/orders": [orders[0]] });
await page.goto("/");
await page.getByRole("button", { name: "Auswertung", exact: true }).click();
await expect(page.getByText("Geladener Ausschnitt:")).toHaveCount(0);
await page.getByRole("button", { name: "Vergleichszeitraum", exact: true }).click();
await page.getByLabel("Kunde", {exact:true}).check();
const pending=page.waitForRequest("**/api/statistics/query");
await page.getByRole("button", { name: "Statistik berechnen", exact: true }).click();
const query=(await pending).postDataJSON();
expect(query.comparisons).toHaveLength(1);
expect(query.splits).toEqual(["HEADQUARTERS","CUSTOMER"]);
expect(query.headquarters).toEqual([1]);
const comparison=page.getByRole("heading",{name:"Zeiträume im Vergleich"}).locator("..");
await expect(comparison.getByRole("cell",{name:"1234",exact:true})).toHaveCount(2);
await expect(page.getByRole("button",{name:"Statistik als CSV"})).toBeVisible();
});
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<script>alert('Protokoll')</script>";
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<void>((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<void>((resolve) => { release = resolve; });
let responded!: () => void;
const responseFinished = new Promise<void>((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);
});