Files
DogFatherGitandClaude Opus 5 658febb6c5 Abbruch-Dialog und Farbunterscheidung meins/beim Kunden
ABBRECHEN IN DER OBERFLAECHE
Der Server konnte es seit gestern, die Knoepfe fehlten. Jetzt steht am
Ende der Projektansicht ein zurueckhaltender Knopf -- bewusst nicht
zwischen den anderen: Es ist die seltenste und endgueltigste Handlung an
einem Projekt, und ein gleich lauter Knopf daneben laedt zum
Verwechseln ein.

Beim Aufklappen rechnet die Seite vor: wie viele Schritte erledigt sind,
wie viel gezahlt wurde, wie viel davon verdient ist, und was sich daraus
als Erstattung ergibt. Der Betrag steht als Vorschlag im Feld und ist
aenderbar -- geprueft wird, dass der GEAENDERTE Wert hinausgeht und nicht
der vorgeschlagene, sonst waere das Feld eine Attrappe.

Gerechnet wird erst beim Aufklappen, nicht beim Oeffnen der
Projektansicht: Dazwischen kann man Punkte abgehakt haben, und die
Zahlen sollen den Stand von JETZT zeigen. Faellt die Vorschau aus, laesst
sich der Betrag von Hand eintragen -- ein ausgefallener Rechendienst darf
kein Projekt in der Liste festhalten.

Ein bereits abgebrochenes Projekt bekommt keinen Knopf mehr, sondern
einen Kasten mit Datum, Grund, wer abgebrochen hat und was zu erstatten
war.

MEINS ODER SEINS
Wunsch: 'ich will dass die kunden sachen auch in der verwaltungs seite
von kacheln eine andere farbe haben wie meine damit ich sie gut
unterscheide.'

Was bei mir liegt, bleibt im Markenblau. Was beim Kunden liegt, bekommt
Lila. Gemessen: rgb(127,208,232) gegen rgb(183,157,255).

Bewusst NICHT ueber Rot/Gruen: Die Warnstufen sind an das ALTER
vergeben und muessen frei bleiben. Eine Kachel, die gleichzeitig 'beim
Kunden' und 'seit acht Tagen ueberfaellig' faerben muesste, koennte nur
eine der beiden Aussagen zeigen -- und die Frist ist die wichtigere.

Dazu eine 3px-Kante links auf beiden Seiten. Farbe allein traegt die
Aussage nicht: Wer sie nicht unterscheiden kann, saehe sonst zwei gleich
aussehende Bloecke (WCAG 1.4.1). Die Kante ist ein Gegensatz, keine
Markierung einer Gruppe -- meins blau, seins lila.

Geprueft mit 42 Pruefungen auf Computer und Handy, die messen, was
tatsaechlich an den Server geht und welche Farben wirklich berechnet
werden. Alle bestehenden Pruefungen weiter gruen.

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

205 lines
9.8 KiB
JavaScript

/* =====================================================================
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;