Files
dogfather-universe/pruef-angebot.mjs
T
DogFatherGitandClaude Opus 5 8726110035 Angebote in der Oberflaeche: schicken und im Portal zusagen
VERWALTUNG
Der Angebotsbereich steht in der Anfrage-Detailansicht ueber dem
direkten Annehmen. Der uebliche Weg ist: Angebot raus, Kunde sagt zu,
Projekt entsteht -- direkt anzunehmen ist der Sonderfall (man hat schon
telefoniert). Stuende der Sonderfall oben, waere er der naheliegende
Griff, und man verschenkte den Beleg, den eine Zusage im Portal erzeugt.

Vorbelegt sind Paket, Preis, Anzahlung, Laufzeit, Ablaufdatum und der
komplette Leistungstext. Zu tun bleibt: Paket bestaetigen, Preis
bestaetigen. Der Leistungstext ist eingeklappt statt die Seite zu
fluten, aber aenderbar.

Ein Paketwechsel laedt ALLES neu -- auch den Leistungstext. Ein
stehengebliebener Text vom vorigen Paket waere der schlimmste Fall: Er
ginge unbemerkt hinaus, und im Angebot staende etwas, das zum Preis
nicht passt. Genau das prueft der Test.

PORTAL
Die Angebotskarte steht ganz oben, vor allen Projekten: Ein Angebot ist
das Einzige im Portal, das eine ENTSCHEIDUNG verlangt -- alles andere
ist Auskunft. Der eingefrorene Leistungstext wird lesbar aufbereitet
(Zwischenueberschriften, Punkte), nicht als Textblock hingeworfen.

Vor der Zusage wird gefragt, und die Frage nennt die Folgen ('verbindlich',
'Anzahlung'). Nicht aus Foermlichkeit: Hier entsteht ein Vertrag, und ein
Knopf, der beim Danebentippen einen Auftrag ausloest, waere eine Falle.
Beim Ablehnen wird NICHT gefragt -- das ist folgenlos.

ZWEI FEHLER GEFUNDEN, BEIDE IM NORMALFALL
- Die Angebotskarten standen im Zweig 'es gibt Projekte'. Ein Kunde mit
  offenem Angebot hat aber typischerweise noch KEIN Projekt -- genau der
  Normalfall -- und saeh damit nichts. Der Server lieferte das Angebot,
  die Seite zeichnete es nur nie. Nur im Browsertest sichtbar.
- Eine Antwort ohne 'nachrichten' liess das Postfach werfen, und weil der
  Fehler den Aufbau abbrach, erschien danach GAR NICHTS mehr -- auch nicht
  das Angebot, das ueber allem stehen sollte. Dieselbe Sorte wie zuvor im
  Cockpit: Ein halber Server ist ein realistischer Fall.

Neun Sprachschluessel in fuenf Sprachen ergaenzt.

Geprueft mit 54 Pruefungen auf Computer und Handy, die messen, was
tatsaechlich an den Server geht. Alle bestehenden weiter gruen.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-24 12:23:44 +02:00

242 lines
12 KiB
JavaScript

/* =====================================================================
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: "[email protected]",
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: "[email protected]", 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;