/* ===================================================================== pruef-angebot.mjs — Angebot rausschicken und im Portal zusagen Zwei Seiten, ein Vorgang. Geprüft wird jeweils, WAS an den Server geht — ein Dialog, der gut aussieht und den falschen Preis sendet, besteht jede Sichtprüfung. Der heikelste Punkt: Wechselt man das Paket, müssen Preis UND Leistungstext neu geladen werden. Ein stehengebliebener Text vom vorigen Paket ginge unbemerkt hinaus — im Angebot stünde dann etwas, das zum Preis nicht passt. ===================================================================== */ 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(4361, r)); const ergebnisse = []; const pruefe = (n, ok, z) => { ergebnisse.push({ n, ok }); console.log((ok ? " ok " : " FEHL ") + n + (z !== undefined ? " -> " + z : "")); }; const LEIST_WEBSITE = "**Aufbau**\n· Startseite gestalten\n· Unterseiten anlegen\n\n" + "**Was ich von dir brauche**\n· Texte\n· Bilder"; const LEIST_SHOP = "**Shop**\n· Produkte anlegen\n· Bezahlung einrichten\n\n" + "**Was ich von dir brauche**\n· Produktdaten"; const ANFRAGE = { id: "t1", nummer: "A-2608-0007", name: "Anna Muster", email: "anna@example.com", paket: "website", status: "neu", sprache: "de", archiviert: 0, kunde_id: null, details: "Bitte modern.", erstellt_am: new Date().toISOString(), }; /* ---------- TEIL 1: Verwaltung ---------- */ for (const [wo, angaben] of [ ["Computer", { viewport: { width: 1440, height: 950 } }], ["Handy", { ...devices["iPhone 14 Pro"] }], ]) { const browser = await chromium.launch(); const seite = await (await browser.newContext(angaben)).newPage(); const protokoll = []; seite.on("pageerror", (e) => protokoll.push(e.message)); let gesendet = null, vorschauen = []; await seite.route("**/webdesign/admin/**", (route) => { const u = new URL(route.request().url()); if (u.pathname.endsWith("/angebot-vorschau")) { const paket = u.searchParams.get("paket") || "website"; vorschauen.push(paket); return route.fulfill({ json: { ok: true, paket, titel: "Website für Anna Muster", preisEuro: paket === "shop" ? 2990 : 1490, anzahlungEuro: paket === "shop" ? 897 : 447, werktage: paket === "shop" ? 30 : 20, gueltigBis: "2026-09-21", leistungen: paket === "shop" ? LEIST_SHOP : LEIST_WEBSITE, sprache: "de", kunde: { name: "Anna Muster" }, schonUnterwegs: null } }); } if (u.pathname.endsWith("/angebot")) { gesendet = route.request().postDataJSON(); return route.fulfill({ json: { ok: true, id: "ag1", nummer: "AG-2608-0001", preisText: "2.990,00 €", anzahlungText: "897,00 €", gueltigBis: "2026-09-21", hinweis: "Das Angebot liegt im Portal des Kunden." } }); } if (u.pathname.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: {}, meldungen: [], wartetAufAnzahlung: [] } }); } if (u.pathname.includes("/admin/anfragen/t1")) { return route.fulfill({ json: { ok: true, anfrage: ANFRAGE, notizen: [] } }); } if (u.pathname.includes("/admin/anfragen")) return route.fulfill({ json: { ok: true, anfragen: [ANFRAGE] } }); 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:4361/webdesign/verwaltung.html"); await seite.waitForTimeout(700); console.log("\n=== Verwaltung · " + wo + " ==="); await seite.click('[data-tab="anfragen"]'); await seite.waitForTimeout(500); await seite.locator(".vw-karte").first().click(); await seite.waitForTimeout(900); pruefe(wo + ": der Angebotsbereich ist da", await seite.locator("#vw-angebot").isVisible()); pruefe(wo + ": die Vorschau wurde geholt", vorschauen.length === 1, vorschauen.join(",")); pruefe(wo + ": das Paket ist aus der Anfrage vorbelegt", (await seite.locator("#vw-ag-paket").inputValue()) === "website", await seite.locator("#vw-ag-paket").inputValue()); pruefe(wo + ": der Preis ist vorbelegt", (await seite.locator("#vw-ag-preis").inputValue()) === "1490", await seite.locator("#vw-ag-preis").inputValue()); pruefe(wo + ": die Anzahlung wird angezeigt", /447,00/.test(await seite.locator("#vw-ag-anzahlung").innerText()), await seite.locator("#vw-ag-anzahlung").innerText()); pruefe(wo + ": Laufzeit und Ablauf stehen da", /20 Werktage/.test(await seite.locator("#vw-ag-gueltig").innerText()), await seite.locator("#vw-ag-gueltig").innerText()); /* Der Leistungstext ist vorbelegt -- das ist der Kern von "so wenig wie moeglich schreiben". */ const leist = await seite.locator("#vw-ag-leistungen").inputValue(); pruefe(wo + ": der Leistungsumfang ist vorbelegt", leist.includes("Startseite"), leist.slice(0, 50)); pruefe(wo + ": und ist eingeklappt, statt die Seite zu fluten", !(await seite.locator("#vw-ag-leistungen").isVisible())); /* ---- Paketwechsel: der heikle Punkt ---- */ await seite.selectOption("#vw-ag-paket", "shop"); await seite.waitForTimeout(700); pruefe(wo + ": ein Paketwechsel laedt neu", vorschauen.length === 2, vorschauen.join(",")); pruefe(wo + ": der Preis passt sich an", (await seite.locator("#vw-ag-preis").inputValue()) === "2990", await seite.locator("#vw-ag-preis").inputValue()); const leist2 = await seite.locator("#vw-ag-leistungen").inputValue(); pruefe(wo + ": und der Leistungstext wechselt MIT", leist2.includes("Produkte anlegen") && !leist2.includes("Startseite"), leist2.slice(0, 60)); await seite.click("#vw-ag-senden"); await seite.waitForTimeout(900); pruefe(wo + ": das Angebot geht hinaus", !!gesendet, JSON.stringify(gesendet).slice(0, 90)); pruefe(wo + ": mit dem gewechselten Paket", gesendet && gesendet.paket === "shop", gesendet && gesendet.paket); pruefe(wo + ": mit dem passenden Preis", gesendet && gesendet.preisEuro === 2990, gesendet && gesendet.preisEuro); pruefe(wo + ": und dem passenden Leistungstext", gesendet && /Produkte anlegen/.test(gesendet.leistungen || ""), (gesendet && gesendet.leistungen || "").slice(0, 40)); const m = await seite.locator("#vw-meldung").innerText().catch(() => ""); pruefe(wo + ": die Meldung nennt Nummer und Betraege", /AG-2608-0001/.test(m) && /2.990,00/.test(m) && /897,00/.test(m), m); pruefe(wo + ": kein Skriptfehler", protokoll.length === 0, protokoll.join(" | ")); await browser.close(); } /* ---------- TEIL 2: Portal ---------- */ const ANGEBOT = { id: "ag1", nummer: "AG-2608-0001", paket: "shop", titel: "Onlineshop für Anna Muster", preis_cent: 299000, anzahlung_cent: 89700, waehrung: "EUR", laufzeit_werktage: 30, leistungen_text: LEIST_SHOP, hinweis_text: null, status: "gesendet", gueltig_bis: "2026-09-21", preis_text: "2.990,00 €", anzahlung_text: "897,00 €", annehmbar: true, abgelaufen: false, }; for (const [wo, angaben] of [ ["Computer", { viewport: { width: 1440, height: 950 } }], ["Handy", { ...devices["iPhone 14 Pro"] }], ]) { const browser = await chromium.launch(); const seite = await (await browser.newContext(angaben)).newPage(); const protokoll = []; seite.on("pageerror", (e) => protokoll.push(e.message)); let zugesagt = null, gefragt = null; /* Nur den ERSTEN Dialog festhalten. Es erscheinen zwei: die Rueckfrage vor der Zusage und danach die Bestaetigung. Die erste Fassung ueberschrieb die Rueckfrage mit der Bestaetigung und pruefte damit den falschen Text. */ seite.on("dialog", (d) => { if (gefragt === null) gefragt = d.message(); d.accept(); }); await seite.route("**/webdesign/portal/**", (route) => { const u = new URL(route.request().url()).pathname; if (u.endsWith("/zusagen")) { zugesagt = u; return route.fulfill({ json: { ok: true, projektNummer: "P-2608-0001", anzahlungText: "897,00 €", hinweis: "Danke! Dein Projekt ist angelegt." } }); } if (u.endsWith("/uebersicht")) { return route.fulfill({ json: { ok: true, kunde: { name: "Anna Muster", email: "anna@example.com", sprache: "de" }, angebote: [ANGEBOT], projekte: [], abos: [], offeneZahlungen: [], dateien: [], nachrichten: [] } }); } /* Das Postfach braucht seine eigene Antwort. Ohne sie fiel es in diesen Auffang, bekam ein blankes {ok:true} -- und riss frueher die ganze Seite mit. Seit dem 24.08.2026 haelt es das aus; die richtige Antwort steht hier trotzdem, damit der Test den Normalfall prueft und nicht den Notfall. */ if (u.endsWith("/postfach")) return route.fulfill({ json: { ok: true, nachrichten: [] } }); return route.fulfill({ json: { ok: true, nachrichten: [], dateien: [] } }); }); await seite.addInitScript(() => { sessionStorage.setItem("wd-portal-token", "t"); sessionStorage.setItem("wd-sitzung-aktiv", "1"); }); await seite.goto("http://127.0.0.1:4361/webdesign/portal.html"); await seite.waitForTimeout(900); console.log("\n=== Portal · " + wo + " ==="); const karte = seite.locator("[data-angebot]").first(); const daIst = await karte.count(); pruefe(wo + ": die Angebotskarte erscheint", daIst === 1, daIst); if (daIst) { const txt = await karte.innerText(); pruefe(wo + ": der Preis steht gross da", /2\.990,00/.test(txt), txt.split("\n").slice(0, 4).join(" | ")); pruefe(wo + ": die Anzahlung wird genannt", /897,00/.test(txt)); pruefe(wo + ": die Dauer wird genannt", /30 Werktagen/.test(txt), txt.match(/.*Werktagen.*/) || ""); pruefe(wo + ": der Leistungsumfang ist lesbar aufbereitet", (await karte.locator(".po-ag-gruppe").count()) >= 1 && (await karte.locator(".po-ag-liste li").count()) >= 2, "Gruppen " + (await karte.locator(".po-ag-gruppe").count()) + ", Punkte " + (await karte.locator(".po-ag-liste li").count())); pruefe(wo + ": die Gueltigkeit steht dabei", /21\.09\.2026|2026-09-21/.test(txt), txt); /* Vor der Zusage MUSS gefragt werden -- hier entsteht ein Vertrag. */ await seite.locator("[data-zusagen]").first().click(); await seite.waitForTimeout(900); pruefe(wo + ": vor der Zusage wird gefragt", !!gefragt, String(gefragt).slice(0, 70)); pruefe(wo + ": die Frage nennt die Folgen", /verbindlich/.test(gefragt || "") && /Anzahlung/.test(gefragt || ""), gefragt); pruefe(wo + ": die Zusage geht hinaus", !!zugesagt, String(zugesagt)); } 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;