/* ===================================================================== pruef-annahme.mjs — Annehmen und Ablehnen in der Verwaltung Geprüft wird nicht, ob Knöpfe da sind, sondern WAS sie senden. Ein Knopf, der sich färbt und nichts abschickt, besteht jede Sichtprüfung. Der wichtigste Fall ist der, den man beim Bauen vergisst: Der erste Entwurf rief nach dem Annehmen ein `datumKurz()` auf, das es gar nicht gab. Der Fehler wäre erst NACHDEM alles angelegt war aufgetreten — man hätte einen fertigen Kunden, ein fertiges Projekt und auf dem Schirm eine rote Meldung. Deshalb wird hier auch das Protokoll mitgelesen. ===================================================================== */ import { chromium, devices } from "playwright"; import { createServer } from "node:http"; import { readFileSync, existsSync } from "node:fs"; import { join, extname } from "node:path"; const W = process.cwd(); const T = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".png": "image/png", ".jpg": "image/jpeg", ".svg": "image/svg+xml", ".webp": "image/webp", ".ico": "image/x-icon", ".json": "application/json" }; const srv = createServer((q, r) => { const p = join(W, decodeURIComponent(q.url.split("?")[0])); if (existsSync(p) && !p.endsWith("/")) { r.writeHead(200, { "content-type": T[extname(p)] || "text/plain" }); return r.end(readFileSync(p)); } r.writeHead(404).end(""); }); await new Promise((r) => srv.listen(4351, r)); const ergebnisse = []; function pruefe(name, ok, zusatz) { ergebnisse.push({ name, ok }); console.log((ok ? " ok " : " FEHL ") + name + (zusatz !== undefined ? " -> " + zusatz : "")); } const ANFRAGE = { id: "t1", nummer: "A-2608-0007", name: "Anna Muster", email: "anna@example.com", telefon: "+352 691 000000", kontaktweg: "email", paket: "website", ziel: "Mehr Anfragen", zielgruppe: "Privatkunden", details: "Bitte modern und schlicht.", budget: "1000-2500", termin: "Mitte Oktober", status: "neu", sprache: "de", archiviert: 0, kunde_id: null, erstellt_am: new Date().toISOString(), }; const VORSCHAU = { ok: true, paket: "website", titelVorschlag: "Website für Anna Muster", terminVorschlag: "2026-09-21", werktage: 20, laufend: false, schritte: 18, kunde: null, kundeNeu: true, bereitsAngenommen: null, }; for (const [wo, angaben] of [ ["Computer", { viewport: { width: 1440, height: 950 } }], ["Handy", { ...devices["iPhone 14 Pro"] }], ]) { const browser = await chromium.launch(); const kontext = await browser.newContext(angaben); const seite = await kontext.newPage(); const protokoll = []; seite.on("pageerror", (e) => protokoll.push(e.message)); let gesendet = null, abgelehnt = null, vorschauGeholt = 0, absageGeholt = []; await seite.route("**/webdesign/admin/**", (route) => { const u = new URL(route.request().url()); const pf = u.pathname; if (pf.endsWith("/annahme-vorschau")) { vorschauGeholt++; return route.fulfill({ json: VORSCHAU }); } if (pf.endsWith("/absage-vorschau")) { absageGeholt.push(u.searchParams.get("grund")); return route.fulfill({ json: { ok: true, sprache: "de", grund: u.searchParams.get("grund"), text: "Hallo Anna Muster,\n\nvielen Dank für deine Anfrage.\n\nGrund: " + u.searchParams.get("grund") + "\n\nViele Grüße\nDogfather Webdesign" } }); } if (pf.endsWith("/annehmen")) { gesendet = route.request().postDataJSON(); return route.fulfill({ json: { ok: true, projektId: "p9", projektNummer: "P-2608-0003", kundeId: "k9", kundeNeu: true, termin: gesendet.termin || "2026-09-21", werktage: 20, schritte: 18, einladungsLink: null, hinweis: "Der Kunde hatte bereits einen Zugang — er sieht das Projekt sofort." } }); } if (pf.endsWith("/ablehnen")) { abgelehnt = route.request().postDataJSON(); return route.fulfill({ json: { ok: true, grund: abgelehnt.grund, sprache: "de", text: "…", imPostfach: true } }); } if (pf.includes("/admin/anfragen/t1")) { return route.fulfill({ json: { ok: true, anfrage: ANFRAGE, notizen: [] } }); } if (pf.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE] } }); if (pf.endsWith("/admin/uebersicht")) { return route.fulfill({ json: { ok: true, beiMir: { anfragen: { n: 1 }, nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 0 }, beiKunde: { projekte: 0, aufgaben: 0, zahlungen: { n: 0, summe: 0 }, zahlungenText: "0,00 €" }, geld: { monat: { n: 0, brutto: 0, netto: 0, bruttoText: "0,00 €", nettoText: "0,00 €" }, gesamt: { n: 0, bruttoText: "0,00 €" }, gebuehrenText: "0,00 €" }, projekte: [], verlauf: [], bestand: {} } }); } /* Ein Kunden- oder Projekt-Anlegen darf hier NICHT mehr vorkommen. Vorher waren das zwei getrennte Aufrufe; taucht einer wieder auf, ist der alte Weg zurueck. */ if (pf.endsWith("/admin/kunden") && route.request().method() === "POST") { gesendet = { FEHLER: "alter Weg: Kunde einzeln angelegt" }; return route.fulfill({ json: { ok: true, id: "k9", einladungsLink: "x" } }); } if (pf.endsWith("/admin/projekte") && route.request().method() === "POST") { gesendet = { FEHLER: "alter Weg: Projekt einzeln angelegt" }; return route.fulfill({ json: { ok: true } }); } return route.fulfill({ json: { ok: true, kunden: [], projekte: [], zahlungen: [], verlaeufe: [] } }); }); await seite.route("**/auth/**", (r) => r.fulfill({ json: { ok: true, user: { name: "Test" } } })); await seite.addInitScript(() => { sessionStorage.setItem("wd-vw-token", "t"); sessionStorage.setItem("wd-sitzung-aktiv", "1"); }); await seite.goto("http://127.0.0.1:4351/webdesign/verwaltung.html"); await seite.waitForTimeout(700); console.log("\n=== " + wo + " ==="); await seite.click('[data-tab="anfragen"]'); await seite.waitForTimeout(500); await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(800); /* ---- 1. Die Vorschau wird geladen und gezeigt ---- */ pruefe(wo + ": die Vorschau wird beim Oeffnen geholt", vorschauGeholt === 1, vorschauGeholt); pruefe(wo + ": der vorgeschlagene Titel steht da", (await seite.locator("#vw-an-titel").innerText()).includes("Anna Muster"), await seite.locator("#vw-an-titel").innerText()); pruefe(wo + ": die Zahl der Arbeitsschritte steht da", (await seite.locator("#vw-an-schritte").innerText()).includes("18"), await seite.locator("#vw-an-schritte").innerText()); pruefe(wo + ": das Terminfeld ist vorbelegt", (await seite.locator("#vw-an-termin").inputValue()) === "2026-09-21", await seite.locator("#vw-an-termin").inputValue()); /* Der Hinweis muss erklaeren, WORAUS die Zahl entsteht -- ein Datum ohne Begruendung wirkt geraten. */ pruefe(wo + ": der Hinweis nennt Werktage und abgezogene Feiertage", /20 Werktage/.test(await seite.locator("#vw-an-termin-hilfe").innerText()) && /Feiertage/.test(await seite.locator("#vw-an-termin-hilfe").innerText()), await seite.locator("#vw-an-termin-hilfe").innerText()); /* ---- 2. Die 30 % werden mitgerechnet ---- */ await seite.fill("#vw-ue-preis", "2000"); await seite.waitForTimeout(200); pruefe(wo + ": die Anzahlung wird waehrend des Tippens gerechnet", (await seite.locator("#vw-ue-anzahlung").innerText()).includes("600,00"), await seite.locator("#vw-ue-anzahlung").innerText()); /* ---- 3. Annehmen sendet EINEN Aufruf mit allem ---- */ await seite.fill("#vw-an-termin", "2026-10-15"); await seite.click("#vw-uebernehmen"); await seite.waitForTimeout(900); pruefe(wo + ": es geht genau EIN Aufruf hinaus, nicht der alte Zweischritt", gesendet && !gesendet.FEHLER, JSON.stringify(gesendet)); pruefe(wo + ": der eigene Termin wird mitgeschickt", gesendet && gesendet.termin === "2026-10-15", gesendet && gesendet.termin); pruefe(wo + ": der Preis wird mitgeschickt", gesendet && gesendet.preisEuro === 2000, gesendet && gesendet.preisEuro); /* ---- 4. Die Rueckmeldung ist verstaendlich ---- */ const meldung = await seite.locator("#vw-meldung").innerText().catch(() => ""); pruefe(wo + ": die Meldung nennt die Projektnummer", meldung.includes("P-2608-0003"), meldung); pruefe(wo + ": sie nennt die Zahl der Arbeitsschritte", meldung.includes("18"), meldung); pruefe(wo + ": sie nennt den Liefertermin lesbar (15.10.2026)", meldung.includes("15.10.2026"), meldung); /* Genau hier steckte der Fehler des ersten Entwurfs: datumKurz() gab es nicht, und der Fehler trat erst NACH dem Anlegen auf. */ pruefe(wo + ": dabei entstand KEIN Skriptfehler", protokoll.length === 0, protokoll.join(" | ")); /* ---- 5. Ablehnen ---- */ await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(700); pruefe(wo + ": das Ablehnen-Formular ist zunaechst zu", !(await seite.locator("#vw-ablehnen-form").isVisible())); await seite.click("#vw-ablehnen-auf"); await seite.waitForTimeout(600); pruefe(wo + ": es klappt auf", await seite.locator("#vw-ablehnen-form").isVisible()); pruefe(wo + ": der Absagetext wird geladen", (await seite.locator("#vw-ab-text").inputValue()).includes("Anna Muster"), (await seite.locator("#vw-ab-text").inputValue()).slice(0, 50)); /* Der Text darf NICHT im Browser bearbeitbar sein -- er ist eine geschaeftliche Aeusserung und entsteht auf dem Server. */ pruefe(wo + ": der Text ist nur zum Lesen", await seite.locator("#vw-ab-text").evaluate((e) => e.readOnly)); await seite.selectOption("#vw-ab-grund", "budget"); await seite.waitForTimeout(500); pruefe(wo + ": ein anderer Grund laedt einen anderen Text", absageGeholt.includes("budget"), absageGeholt.join(",")); await seite.click("#vw-ab-still"); await seite.waitForTimeout(700); pruefe(wo + ": beim internen Ablehnen wird senden=false geschickt", abgelehnt && abgelehnt.senden === false && abgelehnt.grund === "budget", JSON.stringify(abgelehnt)); const m2 = await seite.locator("#vw-meldung").innerText().catch(() => ""); pruefe(wo + ": die Meldung sagt, dass nichts hinausging", /nur intern/.test(m2), m2); pruefe(wo + ": bis zum Schluss kein Skriptfehler", protokoll.length === 0, protokoll.join(" | ")); await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(700); await seite.screenshot({ path: "bild-annahme-" + wo.toLowerCase() + ".png", fullPage: true }); await browser.close(); } srv.close(); const f = ergebnisse.filter((e) => !e.ok).length; console.log(`\n===== ${ergebnisse.length - f} bestanden, ${f} fehlgeschlagen =====`); process.exitCode = f ? 1 : 0;