/* ===================================================================== pruef-abbruch.mjs — Auftrag abbrechen, und meins gegen seins ZWEI DINGE, DIE MAN NUR MESSEN KANN 1. Der Abbruch schickt Geld-Angaben an den Server. Ein Dialog, der hübsch aussieht und den falschen Betrag sendet, besteht jede Sichtprüfung — geprüft wird deshalb, WAS hinausgeht. 2. Die Farbunterscheidung zwischen "meins" und "beim Kunden" wird an den tatsächlich berechneten Farben gemessen. Eine CSS-Regel, die im Quelltext steht, kann von einer anderen überstimmt werden — dreimal in diesem Projekt schon passiert. ===================================================================== */ 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(4359, r)); const ergebnisse = []; const pruefe = (n, ok, z) => { ergebnisse.push({ n, ok }); console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : "")); }; const PROJEKT = { id: "p1", nummer: "P-2608-0001", titel: "Website für Musterbau", paket: "website", status: "design", wartet_auf: "dogfather", naechster_schritt: "Entwurf", preis_cent: 200000, anzahlung_cent: 60000, anzahlung_bezahlt: 1, waehrung: "EUR", kunde_id: "k1", kunde_name: "Anna Muster", termin_am: "2026-10-15", }; const UEBERSICHT = { ok: true, beiMir: { anfragen: { n: 3, aeltestes: new Date().toISOString() }, nachrichten: { n: 0 }, wuensche: { n: 0 }, widerrufe: { n: 0 }, projekte: 2 }, beiKunde: { projekte: 1, aufgaben: 4, zahlungen: { n: 2, summe: 120000, aeltestes: new Date().toISOString() }, zahlungenText: "1.200,00 €" }, geld: { monat: { n: 3, brutto: 250000, netto: 241000, bruttoText: "2.500,00 €", nettoText: "2.410,00 €" }, gesamt: { n: 11, bruttoText: "9.800,00 €" }, gebuehrenText: "90,00 €" }, projekte: [], verlauf: [], bestand: {}, meldungen: [], meldungenOffen: 0, wartetAufAnzahlung: [], }; 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, vorschauGeholt = 0; seite.on("dialog", (d) => d.accept()); await seite.route("**/webdesign/admin/**", (route) => { const u = new URL(route.request().url()).pathname; if (u.endsWith("/abbruch-vorschau")) { vorschauGeholt++; return route.fulfill({ json: { ok: true, projekt: { id: "p1", nummer: PROJEKT.nummer, uhrLaeuft: true, schonAbgebrochen: false }, rechnung: { prozent: 25, fertig: 5, gesamt: 20, bezahltCent: 60000, bezahltText: "600,00 €", behaltenCent: 50000, behaltenText: "500,00 €", erstattungCent: 10000, erstattungText: "100,00 €" } } }); } if (u.endsWith("/abbrechen")) { gesendet = route.request().postDataJSON(); return route.fulfill({ json: { ok: true, grund: gesendet.grund, erstattungCent: Math.round((gesendet.erstattungEuro || 0) * 100), hinweis: "Zu erstatten: 100,00 €. Die Rückzahlung löst die Seite bewusst nicht selbst aus." } }); } if (u.endsWith("/admin/uebersicht")) return route.fulfill({ json: UEBERSICHT }); if (u.includes("/admin/projekte/p1/alles")) { return route.fulfill({ json: { ok: true, projekt: PROJEKT, aufgaben: [], fortschritt: { erledigt: 5, gesamt: 20, prozent: 25, inArbeit: "", beimKunden: "" }, aenderungen: [], nachrichten: [] } }); } if (u.includes("/admin/projekte")) return route.fulfill({ json: { ok: true, projekte: [PROJEKT] } }); return route.fulfill({ json: { ok: true, anfragen: [], kunden: [], 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:4359/webdesign/verwaltung.html"); await seite.waitForTimeout(800); console.log("\n=== " + wo + " ==="); /* ---- 1. Meins gegen seins: gemessen, nicht behauptet ---- */ const farben = await seite.evaluate(() => { const meins = document.querySelector("#vw-ub-beimir .vw-ub-kachel"); const seins = document.querySelector("#vw-ub-beikunde .vw-ub-kachel"); const st1 = getComputedStyle(meins), st2 = getComputedStyle(seins); return { meinZahl: st1.color, seinZahl: getComputedStyle(seins.querySelector(".vw-ub-zahl")).color, meinZahlEcht: getComputedStyle(meins.querySelector(".vw-ub-zahl")).color, meinKante: st1.borderLeftColor, seinKante: st2.borderLeftColor, meinBreite: st1.borderLeftWidth, seinBreite: st2.borderLeftWidth, }; }); pruefe(wo + ": die Zahlen haben unterschiedliche Farben", farben.meinZahlEcht !== farben.seinZahl, "meins " + farben.meinZahlEcht + " vs. beim Kunden " + farben.seinZahl); pruefe(wo + ": auch die Kanten unterscheiden sich", farben.meinKante !== farben.seinKante, farben.meinKante + " vs. " + farben.seinKante); /* Farbe allein trägt die Aussage nicht -- es muss auch eine sichtbare Kante geben, die man ohne Farbunterscheidung wahrnimmt. */ pruefe(wo + ": beide tragen eine sichtbare Kante, nicht nur Farbe", parseFloat(farben.meinBreite) >= 3 && parseFloat(farben.seinBreite) >= 3, farben.meinBreite + " / " + farben.seinBreite); /* ---- 2. Der Abbruch-Bereich ---- */ await seite.click('[data-tab="projekte"]'); await seite.waitForTimeout(500); await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(700); pruefe(wo + ": es gibt einen Abbruch-Knopf", await seite.locator("#vw-abbruch-auf").isVisible()); pruefe(wo + ": das Formular ist zunaechst zu", !(await seite.locator("#vw-abbruch-form").isVisible())); pruefe(wo + ": beim Oeffnen der Projektseite wurde NICHT schon gerechnet", vorschauGeholt === 0, vorschauGeholt); await seite.click("#vw-abbruch-auf"); await seite.waitForTimeout(700); pruefe(wo + ": es klappt auf", await seite.locator("#vw-abbruch-form").isVisible()); pruefe(wo + ": erst jetzt wird gerechnet", vorschauGeholt === 1, vorschauGeholt); const rechnung = await seite.locator("#vw-abbruch-rechnung").innerText(); pruefe(wo + ": der Fortschritt steht da", /5 von 20/.test(rechnung), rechnung.replace(/\n/g, " | ")); pruefe(wo + ": das Bezahlte steht da", /600,00/.test(rechnung), rechnung.replace(/\n/g, " | ")); pruefe(wo + ": der Erstattungsvorschlag steht da", /100,00/.test(rechnung), rechnung.replace(/\n/g, " | ")); pruefe(wo + ": und ist ins Feld uebernommen", (await seite.locator("#vw-ab-erstattung").inputValue()) === "100.00", await seite.locator("#vw-ab-erstattung").inputValue()); /* ---- 3. Der eingetragene Betrag muss gelten ---- */ await seite.fill("#vw-ab-erstattung", "250"); await seite.selectOption("#vw-ab-grund2", "keine_zahlung"); await seite.selectOption("#vw-ab-wer", "kunde"); await seite.fill("#vw-ab-notiz", "Dreimal erinnert, keine Reaktion."); await seite.click("#vw-abbruch-los"); await seite.waitForTimeout(900); pruefe(wo + ": der Abbruch geht hinaus", !!gesendet, JSON.stringify(gesendet)); pruefe(wo + ": mit dem GEAENDERTEN Betrag, nicht dem Vorschlag", gesendet && gesendet.erstattungEuro === 250, gesendet && gesendet.erstattungEuro); pruefe(wo + ": mit dem gewaehlten Grund", gesendet && gesendet.grund === "keine_zahlung", gesendet && gesendet.grund); pruefe(wo + ": und der Angabe, wer abbricht", gesendet && gesendet.wer === "kunde", gesendet && gesendet.wer); pruefe(wo + ": die Notiz kommt mit", gesendet && /Dreimal erinnert/.test(gesendet.notiz || ""), gesendet && gesendet.notiz); const m = await seite.locator("#vw-meldung").innerText().catch(() => ""); pruefe(wo + ": der Hinweis sagt, dass die Rueckzahlung von Hand erfolgt", /nicht selbst/.test(m), m); /* ---- 4. Ein bereits abgebrochenes Projekt bekommt keinen Knopf ---- */ PROJEKT.status = "abgebrochen"; PROJEKT.abbruch_am = new Date().toISOString(); PROJEKT.abbruch_grund = "keine_zahlung"; PROJEKT.abbruch_wer = "kunde"; PROJEKT.abbruch_erstattung_cent = 25000; await seite.click('[data-tab="projekte"]'); await seite.waitForTimeout(500); await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(700); pruefe(wo + ": ein abgebrochenes Projekt hat keinen Abbruch-Knopf mehr", (await seite.locator("#vw-abbruch-auf").count()) === 0); const info = await seite.locator(".vw-abbruch-info").innerText().catch(() => ""); pruefe(wo + ": stattdessen steht da, was passiert ist", /nicht gezahlt/.test(info) && /250,00/.test(info), info.replace(/\n/g, " | ")); PROJEKT.status = "design"; delete PROJEKT.abbruch_am; pruefe(wo + ": kein Skriptfehler", protokoll.length === 0, protokoll.join(" | ")); 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;