import { expect, test, type Page } from "@playwright/test"; import "dotenv/config"; import { prisma } from "@/lib/db"; import { SEED_TAG, SEED_VORLAGE, anmelden, kundeWaehlen, variablenFuellen, vorlageWaehlen } from "./helfer"; /** * Zwei Sitzungen an einem Lauf (#033). * * Das ist das Abnahmekriterium, das sich nur hier pruefen laesst: **hakt die * eine ab, sieht die andere es ohne neu zu laden.** Mit dem Polling aus #013 war * das richtig, aber bis zu zehn Sekunden langsam; seit dem Strom kommt es * sofort. * * Und der zweite Ablauf prueft den Rueckfall. Ein Rueckfall, der nie gefahren * wird, ist keiner — dieses Projekt hat genau dafuer schon bezahlt (#010: eine * Oberflaeche, die serverseitig richtig aussah und nie hydrierte). */ const STEMPEL = Date.now(); /** Legt einen Lauf an, beginnt ihn und gibt seine Adresse zurueck. */ async function laufVorbereiten(seite: Page, titel: string): Promise { await seite.goto("/laeufe/starten"); await vorlageWaehlen(seite, SEED_VORLAGE); const geraet = await prisma.geraet.findUniqueOrThrow({ where: { tagCode: SEED_TAG }, select: { id: true, kundeId: true }, }); await kundeWaehlen(seite, geraet.kundeId); await seite.selectOption('select[name="geraetId"]', geraet.id); await seite.locator("details summary", { hasText: "Titel, Ticket" }).click(); await seite.getByLabel("Titel", { exact: true }).fill(titel); await variablenFuellen(seite); await seite.getByRole("button", { name: "Lauf anlegen" }).click(); await seite.waitForURL(/\/laeufe\/(?!starten)[^/]+$/, { timeout: 30_000 }); await seite.getByRole("button", { name: "Lauf beginnen" }).click(); await expect(seite.getByRole("button", { name: "Lauf abschließen" })).toBeVisible(); return new URL(seite.url()).pathname; } /** * Den ersten Abschnitt aufklappen und Kopf **und** Bereich zurueckgeben. * * Gezaehlt wird spaeter im Bereich, nicht auf der Seite: die Vorlage hat 34 * Schritte in sieben Abschnitten, und `0 von 34` steht in keinem Kopf. */ async function ersterAbschnitt(seite: Page) { const bereich = seite .locator("section") .filter({ has: seite.locator("button[aria-expanded]") }) .first(); const kopf = bereich.locator("button[aria-expanded]").first(); if ((await kopf.getAttribute("aria-expanded")) === "false") await kopf.click(); return { kopf, bereich }; } test.describe("Live-Aktualisierung", () => { test("die zweite Sitzung sieht den Haken der ersten, ohne neu zu laden", async ({ page, browser }) => { test.slow(); await anmelden(page); const pfad = await laufVorbereiten(page, `ZZ E2E Live ${STEMPEL}`); // Zweite Sitzung: eigener Kontext, eigenes Cookie, eigener Strom. const zweiter = await browser.newContext(); const zuschauer = await zweiter.newPage(); try { await anmelden(zuschauer); await zuschauer.goto(pfad); const { kopf: kopfA, bereich: bereichA } = await ersterAbschnitt(page); const { kopf: kopfB } = await ersterAbschnitt(zuschauer); const schritte = await bereichA.getByRole("button", { name: "erledigt", exact: true }).count(); expect(schritte).toBeGreaterThan(0); await expect(kopfB).toContainText(`0 von ${schritte}`); /* * Jetzt der eigentliche Nachweis. Gewartet wird **nur** auf den Zaehler im * Kopf des Zuschauers — kein `reload()`, kein Klick. Kommt er dort an, hat * der Strom funktioniert. */ const start = Date.now(); await bereichA.getByRole("button", { name: "erledigt", exact: true }).first().click(); await expect(kopfA).toContainText(`1 von ${schritte}`); await expect(kopfB).toContainText(`1 von ${schritte}`, { timeout: 8_000 }); const dauer = Date.now() - start; /* * **Warum 5000 und nicht 1000.** Das Kriterium nennt „unter einer * Sekunde“, und gemessen sind es hier gewoehnlich zwei- bis * dreihundert Millisekunden. Eine Schranke von 1000 ms waere aber auf * einer belasteten Maschine ein Test, der gelegentlich ohne Fehler im Code * rot wird — und ein flatternder Test wird irgendwann ignoriert. 5000 ms * beweisen das Entscheidende: **es wurde nicht gepollt**, denn das Polling * fragt alle zehn Sekunden. */ expect(dauer).toBeLessThan(5_000); console.log(`Der Haken war nach ${dauer} ms beim Zuschauer.`); } finally { await zweiter.close(); } }); test("ohne EventSource kommt die Änderung über das Polling an", async ({ page, browser }) => { test.slow(); await anmelden(page); const pfad = await laufVorbereiten(page, `ZZ E2E Rückfall ${STEMPEL}`); const zweiter = await browser.newContext(); const zuschauer = await zweiter.newPage(); try { /* * `EventSource` wegnehmen, **bevor** die Seite laedt. Genau diesen Zustand * hat ein Browser hinter einem Proxy, der Stroeme puffert: der Strom kommt * nie an, und ohne Rueckfall sieht die Seite eine Stunde lang unveraendert * aus. */ await zuschauer.addInitScript(() => { // @ts-expect-error – genau das ist der Punkt des Ablaufs. delete window.EventSource; }); await anmelden(zuschauer); await zuschauer.goto(pfad); expect(await zuschauer.evaluate(() => typeof EventSource)).toBe("undefined"); const { kopf: kopfA, bereich: bereichA } = await ersterAbschnitt(page); const { kopf: kopfB } = await ersterAbschnitt(zuschauer); const schritte = await bereichA.getByRole("button", { name: "erledigt", exact: true }).count(); await expect(kopfB).toContainText(`0 von ${schritte}`); await bereichA.getByRole("button", { name: "erledigt", exact: true }).first().click(); await expect(kopfA).toContainText(`1 von ${schritte}`); // Das Polling fragt alle zehn Sekunden – entsprechend mehr Luft. await expect(kopfB).toContainText(`1 von ${schritte}`, { timeout: 25_000 }); } finally { await zweiter.close(); } }); });