Files
dogfather-universe/server/pruef-sicht.mjs
T
DogFatherGitandClaude Opus 5 d7cf598b00 "Heute" war zwei Stunden lang gestern -- und vier Fehler auf Handy und PC
Auftrag: "einen grossen Check machen, ob alles klappt, auf dem Handy und
PC." Dafuer ein neuer Rundgang (pruef-grosscheck.mjs), der stumpf ueber
alles geht: 19 Workspace-Seiten mal 4 Rollen mal 2 Bildschirmgroessen
plus 33 oeffentliche Seiten, zweimal. 208 Seiten, 50 774 Elemente.

Solche Rundgaenge finden andere Fehler als gezielte Pruefungen: nicht
den falsch gerechneten Wert, sondern die Seite, die bei genau einer
Rolle ueberlaeuft.

=== DER WICHTIGSTE FUND: "heute" war in UTC gerechnet ===

Der Check lief um 01:10 Uhr. Ortszeit war der 2. September, in UTC noch
der 1. -- und in diesem Fenster rechnete die Anwendung an ZWOELF Stellen
"heute" als toISOString(), also in UTC. Server und Benutzer stehen beide
auf Europe/Berlin.

Was das im Alltag bedeutete, jede Nacht zwischen 0 und 2 Uhr:
  * eine heute faellige Aufgabe galt noch nicht als faellig
  * eine um Mitternacht ueberfaellig gewordene erschien erst um 2 Uhr
  * der Filter "Heute faellig" zeigte den Vortag
  * Datumsfelder schlugen gestern vor
  * der Kalender begann seine Vorgabe einen Tag zu frueh

Also genau dann, wenn nach einem Stream gearbeitet wird.

kalender.js machte es die ganze Zeit RICHTIG -- samt Begruendung, warum
die ARITHMETIK trotzdem in UTC laufen muss (UTC-Mittag ueberlebt die
Zeitumstellung; wer lokal rechnet, verliert am 27. Oktober einen Tag).
Diese Trennung gilt jetzt ueberall, aus je einer Quelle:
  RECHNEN mit Datumsangaben  -> UTC-Mittag, unveraendert
  WELCHER TAG IST HEUTE      -> Ortszeit (heuteLokal/tagLokal im Server,
                                window.heuteLokal in kopf.js)

WIE ES AUFFIEL, und das ist die eigentliche Lehre: Zuerst schlugen zwei
Pruefungen fehl -- und die Ursache lag in IHNEN, sie rechneten selbst in
UTC (36 Stellen in 17 Dateien). Nach deren Reparatur schlugen sie WIEDER
fehl, und erst da zeigten sie auf die Anwendung. Wer beim ersten Mal
aufgehoert haette ("ist ja nur die Pruefung"), haette den echten Fehler
nie gesehen.

Nachtrag desselben Musters: pruef-uebersicht legte den Termin weiterhin
in UTC an, waehrend die Erwartung schon auf Ortszeit stand. Wer eine
Datumsrechnung umstellt, muss BEIDE Seiten umstellen -- die, die
schreibt, und die, die prueft.

=== VIER FEHLER AUF HANDY UND PC ===

1. Ein langer Creator-Name ("SpongBobSchwammKopf") schob die Startseite
   auf dem Handy um 48 Pixel aus dem Bild -- ein Wort ohne Trennstelle,
   und die Seite liess sich seitlich wegschieben. Trifft echte Namen:
   Creator heissen selten "Tim".

2. Die klebende Speicherleiste verdeckte auf dem Handy ein Textfeld.
   Beim Tippen sieht man die eigene Zeile nicht. Behoben mit
   scroll-margin-bottom (WCAG 2.2, 2.4.11 "Focus Not Obscured").

3./4. Zwei Beschriftungen waren mit 9,6 px (Uebersicht: "ueberfaellig",
   "dringend", "offen") und 9,9 px (Kalender: "heute") zu klein. Fuers
   Handy gab es laengst eine Ausnahme -- nur der Rechner war vergessen
   worden. Ausgerechnet die Woerter, die den Zahlen ihre Bedeutung geben.

=== WAS KEINE FEHLER WAREN ===

Der erste Durchgang meldete 19 Maengel, die keine waren. Alle einzeln im
Quelltext nachgeprueft und dem Rundgang beigebracht:
  * Kacheln und Kopfzeilen "abgeschnitten" -- das Wasserzeichen ragt
    ABSICHTLICH ueber den Rand (steht so im Quelltext)
  * "verdeckt: wahl2__echt" -- das echte <select> liegt absichtlich
    unsichtbar unter seinem Knopf
  * "zurueck-knopf__text abgeschnitten" -- das uebliche Muster fuer
    "nur fuer Vorleseprogramme"
  * drei "zu kleine" Verweise -- WCAG 2.5.8 nimmt Verweise im Fliesstext
    AUSDRUECKLICH aus. Eine Pruefung, die ihre eigene Messlatte nicht
    kennt, misst nichts.

Beim vierten Punkt haette ich fast an der falschen Stelle repariert.

Und statt die Sticky-Meldung abzuschalten (dann faende sie auch echte
Ueberdeckungen nie mehr), wurde sie GENAUER: Ueberdeckt etwas Klebendes
ein Eingabefeld, ist das nur in Ordnung, wenn das Feld genug
scroll-margin-bottom hat, um darunter hervorzukommen. Aus einer vagen
Meldung wird eine pruefbare Zusage.

Vier Gegenproben belegen, dass der Rundgang ueberhaupt etwas finden
kann: ein zu breites Element, ein winziger Knopf, ein winziger Verweis
AUSSERHALB eines Satzes und ein wirklich abgeschnittenes Wort werden
alle gemeldet. Ohne diesen Nachweis waere "alles in Ordnung" wertlos.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-09-02 01:51:28 +02:00

391 lines
18 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/* Prueft "die Sicht eines anderen" — DogFathers Umschalter.
Wunsch vom 01.09.2026: "ich will alles von den Scouts und Managern
einsehen koennen, auswaehlen, was und von wem ich sehen will. Und das
soll ich als DogFather-Rolle ueberall haben, auf jeder Seite.
NUR WIR BEIDE SOLLEN DIESE OPTION HABEN."
DAS HIER IST EINE RECHTEPRUEFUNG, keine Bedienpruefung. Wenn etwas
davon nicht stimmt, kann ein Manager oder Scout durch Anhaengen von
?sicht=<Nummer> in fremde Bereiche sehen. Deshalb steht der Missbrauch
VOR der Funktion:
1. Ein Scout haengt ?sicht=<DogFather> an -> muss ignoriert werden
2. Ein Manager haengt ?sicht=<Creator> an -> muss ignoriert werden
3. Ein Creator haengt ?sicht=<Scout> an -> muss ignoriert werden
4. DogFather haengt eine erfundene Nummer an -> eigene Sicht
5. DogFather waehlt einen Scout -> sieht dessen Liste
Und, genauso wichtig: SCHREIBEN passiert immer im eigenen Namen. Ein
Eintrag, der unter fremdem Namen im Protokoll landet, waere schlimmer
als gar keine Funktion. */
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
const ordner = mkdtempSync(join(tmpdir(), "ws-sicht-"));
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
process.env.PORT = "4215";
process.env.SITE_ACCESS_SECRET = "lokaler-test";
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00";
const express = (await import("express")).default;
const ec = express.response.cookie;
express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); };
await import("./index.js");
await new Promise((r) => setTimeout(r, 700));
const BASIS = "http://127.0.0.1:4215";
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 300_000).unref?.();
/* ORTSZEIT, nicht UTC. Die Anwendung rechnet "heute" nach der Uhr des
Benutzers; toISOString() liefert dagegen UTC und damit zwischen
Mitternacht und 2 Uhr den VORTAG. In diesem Fenster schlug diese
Pruefung fehl, ohne dass etwas kaputt war. */
const tagText = (d = new Date()) =>
`${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, "0")}`
+ `-${String(d.getDate()).padStart(2, "0")}`;
let fehler = 0;
const ok = (b, t) => { console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle: "admin", code: "x" }) }).catch(() => {});
const { DatabaseSync } = await import("node:sqlite");
const { scryptSync, randomBytes } = await import("node:crypto");
for (let i = 0; i < 60; i++) {
try {
const p = new DatabaseSync(process.env.WORKSPACE_DB);
try { p.prepare("SELECT 1 FROM personen LIMIT 1").get(); break; } finally { p.close(); }
} catch { await new Promise((r) => setTimeout(r, 250)); }
}
const d = new DatabaseSync(process.env.WORKSPACE_DB);
const jetzt = new Date().toISOString();
const heute = tagText();
function anlegen(name, rolle, code) {
const salt = randomBytes(16).toString("hex");
const hash = scryptSync(code, salt, 64, { N: 32768, r: 8, p: 1, maxmem: 96 * 1024 * 1024 }).toString("hex");
d.prepare("INSERT INTO personen (name, rolle, code_hash, code_salt, code_n, aktiv, erstellt) VALUES (?,?,?,?,?,1,?)")
.run(name, rolle, hash, salt, 32768, jetzt);
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
}
const idDogi = anlegen("Filipe", "admin", "CODE-DOGI-0001");
const idMgr = anlegen("Mara", "manager", "CODE-MANA-0001");
const idPat = anlegen("Patrick", "scout", "CODE-SCOU-0001");
const idBen = anlegen("Ben", "scout", "CODE-SCOB-0001");
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
const idLuna = anlegen("Luna", "creator", "CODE-LUNA-0001");
/* Patrick betreut Tili, Ben betreut Luna -- so haben die beiden Scouts
wirklich VERSCHIEDENE Arbeitsplaetze. Ohne das koennte die Pruefung
nicht unterscheiden, ob umgeschaltet wurde oder ob beide dasselbe
sehen. */
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idPat, jetzt);
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idLuna, idBen, jetzt);
/* Je zwei Aufgaben mit eindeutigen Titeln -- daran laesst sich ablesen,
WESSEN Liste gerade kommt. */
const neueAufgabe = (titel, creator, verant) =>
d.prepare(`INSERT INTO aufgaben (titel, beschreibung, status, prioritaet,
creator_id, verantwortlich_id, erstellt, erstellt_von) VALUES (?,?,?,?,?,?,?,?)`)
.run(titel, "", "offen", "mittel", creator, verant, jetzt, idDogi);
neueAufgabe("PATRICK-SACHE-1", idTili, idPat);
neueAufgabe("PATRICK-SACHE-2", idTili, idPat);
neueAufgabe("BEN-SACHE-1", idLuna, idBen);
neueAufgabe("NUR-DOGI", null, idDogi);
d.close();
/* ---------------------------------------------------------------- */
async function anmelden(rolle, code) {
const a = await fetch(BASIS + "/workspace/api/anmelden", {
method: "POST", headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rolle, code }),
});
const k = a.headers.getSetCookie?.() || [];
return k.map((x) => x.split(";")[0]).join("; ");
}
const holen = (pfad, keks) =>
fetch(BASIS + pfad, { headers: { cookie: keks } });
const titel = async (pfad, keks) => {
const a = await holen(pfad, keks);
if (!a.ok) return { status: a.status, liste: [] };
const j = await a.json();
return { status: a.status, liste: (j.aufgaben || []).map((x) => x.titel).sort() };
};
const kDogi = await anmelden("admin", "CODE-DOGI-0001");
const kMgr = await anmelden("manager", "CODE-MANA-0001");
const kPat = await anmelden("scout", "CODE-SCOU-0001");
const kTili = await anmelden("creator", "CODE-ANNA-0001");
/* ================================================================
1. ZUERST DER MISSBRAUCH
================================================================ */
console.log("\n=== Niemand ausser DogFather darf umschalten");
{
const ohne = await titel("/workspace/api/aufgaben", kPat);
const mit = await titel(`/workspace/api/aufgaben?sicht=${idBen}`, kPat);
ok(JSON.stringify(ohne.liste) === JSON.stringify(mit.liste),
`ein Scout aendert mit ?sicht= nichts (${mit.liste.length} Aufgaben, wie ohne)`);
ok(!mit.liste.includes("BEN-SACHE-1"),
" und sieht die Sachen des anderen Scouts NICHT");
}
{
const ohne = await titel("/workspace/api/aufgaben", kMgr);
const mit = await titel(`/workspace/api/aufgaben?sicht=${idTili}`, kMgr);
ok(JSON.stringify(ohne.liste) === JSON.stringify(mit.liste),
`ein Manager aendert mit ?sicht= nichts (${mit.liste.length} Aufgaben)`);
}
{
const ohne = await titel("/workspace/api/aufgaben", kTili);
const mit = await titel(`/workspace/api/aufgaben?sicht=${idPat}`, kTili);
ok(JSON.stringify(ohne.liste) === JSON.stringify(mit.liste),
`ein Creator aendert mit ?sicht= nichts (${mit.liste.length} Aufgaben)`);
ok(!mit.liste.includes("NUR-DOGI"),
" und kommt so nicht an fremde Aufgaben");
}
/* ================================================================
2. DOGFATHER SCHALTET UM
================================================================ */
console.log("\n=== DogFather sieht durch fremde Augen");
const alles = await titel("/workspace/api/aufgaben", kDogi);
ok(alles.liste.length === 4, `ohne Auswahl sieht er alles (${alles.liste.length} Aufgaben)`);
const alsPat = await titel(`/workspace/api/aufgaben?sicht=${idPat}`, kDogi);
ok(alsPat.liste.length === 2 &&
alsPat.liste.every((t) => t.startsWith("PATRICK")),
`als Patrick genau dessen zwei (${alsPat.liste.join(", ")})`);
ok(!alsPat.liste.includes("BEN-SACHE-1") && !alsPat.liste.includes("NUR-DOGI"),
" und wirklich NUR dessen – nicht mehr");
const alsBen = await titel(`/workspace/api/aufgaben?sicht=${idBen}`, kDogi);
ok(alsBen.liste.length === 1 && alsBen.liste[0] === "BEN-SACHE-1",
`als Ben dessen eine (${alsBen.liste.join(", ")})`);
ok(JSON.stringify(alsPat.liste) !== JSON.stringify(alsBen.liste),
" zwei Scouts liefern wirklich Verschiedenes – es wird also umgeschaltet");
const alsTili = await titel(`/workspace/api/aufgaben?sicht=${idTili}`, kDogi);
ok(alsTili.liste.length === 2,
`als Creator dessen Brett (${alsTili.liste.length})`);
/* ================================================================
3. WAS BEI UNSINN PASSIERT
================================================================ */
console.log("\n=== Unsinnige Auswahl");
for (const [wert, was] of [
["999999", "eine Nummer, die es nicht gibt"],
["0", "die Null"],
["-5", "eine negative Zahl"],
["abc", "Buchstaben"],
["1%20OR%201=1", "ein Einschleusversuch"],
]) {
const r = await titel(`/workspace/api/aufgaben?sicht=${wert}`, kDogi);
ok(r.status === 200 && r.liste.length === 4,
`${was} faellt auf die eigene Sicht zurueck (${r.status}, ${r.liste.length} Aufgaben)`);
}
/* Eine ABGESCHALTETE Person darf keine Sicht liefern -- sonst waere das
Abschalten eines Kontos halbherzig. */
{
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
d2.prepare("UPDATE personen SET aktiv = 0 WHERE id = ?").run(idBen);
d2.close();
const r = await titel(`/workspace/api/aufgaben?sicht=${idBen}`, kDogi);
ok(r.liste.length === 4,
`eine abgeschaltete Person liefert keine Sicht mehr (${r.liste.length} Aufgaben = alles)`);
const d3 = new DatabaseSync(process.env.WORKSPACE_DB);
d3.prepare("UPDATE personen SET aktiv = 1 WHERE id = ?").run(idBen);
d3.close();
}
/* ================================================================
4. GESCHRIEBEN WIRD IMMER IM EIGENEN NAMEN
================================================================ */
console.log("\n=== Schreiben bleibt im eigenen Namen");
{
const a = await fetch(`${BASIS}/workspace/api/aufgaben?sicht=${idPat}`, {
method: "POST",
headers: { cookie: kDogi, "Content-Type": "application/json" },
body: JSON.stringify({ titel: "WER-HAT-DAS-ANGELEGT", prioritaet: "mittel" }),
});
ok(a.ok, `anlegen geht auch bei fremder Sicht (${a.status})`);
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
const z = d2.prepare("SELECT erstellt_von FROM aufgaben WHERE titel = ?")
.get("WER-HAT-DAS-ANGELEGT");
d2.close();
ok(z?.erstellt_von === idDogi,
`und steht unter DogFather, nicht unter dem angesehenen Scout `
+ `(${z?.erstellt_von} statt ${idPat})`);
}
/* ================================================================
5. DIE ANDEREN BEREICHE ZIEHEN MIT
================================================================ */
console.log("\n=== Es gilt nicht nur fuer Aufgaben");
for (const [pfad, name, feld] of [
["/workspace/api/hinweise", "Was ist dran", "hinweise"],
["/workspace/api/termine?von=" + heute + "&tage=7", "Kalender", "termine"],
["/workspace/api/dateien", "Dateien", "dateien"],
["/workspace/api/leads", "Scout-Pipeline", "leads"],
]) {
const a = await holen(`${pfad}${pfad.includes("?") ? "&" : "?"}sicht=${idPat}`, kDogi);
const b = await holen(pfad, kDogi);
ok(a.status === 200 && b.status === 200, `${name}: beide Abfragen gehen durch`);
}
/* Der Kalender ist der klarste Beleg: Patrick sieht nur seine Termine. */
{
const d2 = new DatabaseSync(process.env.WORKSPACE_DB);
d2.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,?)`)
.run("PATRICK-TERMIN", "call", heute + "T10:00", 30, idTili, jetzt, idPat);
d2.prepare(`INSERT INTO termine (titel, art, beginn, dauer_min, creator_id, erstellt, erstellt_von)
VALUES (?,?,?,?,?,?,?)`)
.run("BEN-TERMIN", "call", heute + "T12:00", 30, idLuna, jetzt, idBen);
d2.close();
const j = async (k, s) => {
const a = await holen(`/workspace/api/termine?von=${heute}&tage=2${s ? "&sicht=" + s : ""}`, k);
return ((await a.json()).termine || []).map((t) => t.titel).sort();
};
const alleT = await j(kDogi);
const patT = await j(kDogi, idPat);
ok(alleT.length === 2, `DogFather sieht beide Termine (${alleT.join(", ")})`);
ok(patT.length === 1 && patT[0] === "PATRICK-TERMIN",
`als Patrick nur dessen (${patT.join(", ")})`);
}
/* ================================================================
6. IM BROWSER: der Umschalter steht da -- und nur bei DogFather
================================================================ */
console.log("\n=== Der Umschalter im Seitenkopf");
const { chromium } = await import(
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
const browser = await chromium.launch();
async function browserAls(rolle, code) {
const kontext = await browser.newContext({ viewport: { width: 1280, height: 1000 } });
const konsole = [];
const seite = await kontext.newPage();
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
await seite.click(`.rolle[data-rolle="${rolle}"]`);
await seite.fill("#code", code);
await seite.click("button[type=submit]");
await seite.waitForURL("**/start.html", { timeout: 15000 });
return { kontext, seite, konsole };
}
const bDogi = await browserAls("admin", "CODE-DOGI-0001");
/* AUF JEDER SEITE -- das war der ausdrueckliche Wunsch. Geprueft wird
deshalb nicht eine Seite, sondern alle. */
const SEITEN = ["start.html", "aufgaben.html", "kalender.html", "dateien.html",
"bereich.html?b=live", "content.html", "calls.html", "scouting.html",
"report.html", "personen.html", "wissen.html", "uebersicht.html",
"profil.html", "startcheck.html", "steckbrief.html", "automation.html"];
let fehlend = [];
for (const s of SEITEN) {
await bDogi.seite.goto(BASIS + "/workspace/" + s, { waitUntil: "networkidle" });
await bDogi.seite.waitForTimeout(900);
const da = await bDogi.seite.evaluate(() => {
const f = document.getElementById("sicht-wahl");
if (!f) return false;
const r = f.getBoundingClientRect();
return r.width > 0 && r.height > 0;
});
if (!da) fehlend.push(s);
}
ok(fehlend.length === 0,
`der Umschalter steht auf allen ${SEITEN.length} Seiten`
+ (fehlend.length ? ` – FEHLT auf: ${fehlend.join(", ")}` : ""));
const wahl = await bDogi.seite.evaluate(() => {
const s = document.getElementById("sicht-feld");
return {
gruppen: [...s.querySelectorAll("optgroup")].map((g) => g.label),
anzahl: s.querySelectorAll("option").length,
erste: s.querySelector("option")?.textContent,
ichDrin: [...s.querySelectorAll("option")].some((o) => o.textContent === "Filipe"),
};
});
ok(wahl.gruppen.join(", ") === "Manager, Scouts, Creator",
`nach Rollen gruppiert: ${wahl.gruppen.join(", ")}`);
ok(/meine Sicht/i.test(wahl.erste || ""),
`der Rueckweg steht ganz oben ("${wahl.erste}")`);
ok(!wahl.ichDrin, "DogFather selbst steht nicht in der Liste – das waere derselbe Zustand");
/* Umschalten im Browser und pruefen, dass die Seite danach wirklich
die andere Liste zeigt. */
await bDogi.seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" });
await bDogi.seite.waitForTimeout(1100);
const vorher = await bDogi.seite.evaluate(() =>
document.querySelectorAll(".karte").length);
await bDogi.seite.selectOption("#sicht-feld", String(idPat));
await bDogi.seite.waitForTimeout(1800);
const nachher = await bDogi.seite.evaluate(() => ({
karten: document.querySelectorAll(".karte").length,
titel: [...document.querySelectorAll(".karte__titel")].map((e) => e.textContent).sort(),
rahmen: getComputedStyle(document.body, "::after").borderTopWidth,
hinweis: !!document.querySelector('.sicht[data-fremd="ja"]'),
weg: !!document.querySelector(".sicht__weg"),
}));
ok(nachher.karten < vorher,
`nach dem Umschalten weniger Aufgaben (${vorher} -> ${nachher.karten})`);
ok(nachher.titel.every((t) => t.startsWith("PATRICK")),
`und zwar genau Patricks (${nachher.titel.join(", ")})`);
ok(nachher.hinweis, "der Umschalter zeigt deutlich, dass eine fremde Sicht laeuft");
ok(nachher.weg, " und bietet den Weg zurueck an");
ok(parseFloat(nachher.rahmen) >= 1,
`ein Rahmen um die Seite erinnert daran (${nachher.rahmen}) – `
+ "der gefaehrlichste Fall ist, dass man es vergisst");
/* Und er BLEIBT beim Seitenwechsel -- sonst muesste man ihn auf jeder
Seite neu setzen, und genau das war der Wunsch ("ueberall"). */
await bDogi.seite.goto(BASIS + "/workspace/kalender.html", { waitUntil: "networkidle" });
await bDogi.seite.waitForTimeout(1200);
const bleibt = await bDogi.seite.evaluate(() => ({
wert: document.getElementById("sicht-feld")?.value,
fremd: !!document.querySelector('.sicht[data-fremd="ja"]'),
}));
ok(bleibt.wert === String(idPat) && bleibt.fremd,
`die Auswahl ueberlebt den Seitenwechsel (${bleibt.wert})`);
/* Zurueck -- und wirklich zurueck. */
await bDogi.seite.click(".sicht__weg");
await bDogi.seite.waitForTimeout(1600);
const zurueck = await bDogi.seite.evaluate(() => ({
wert: document.getElementById("sicht-feld")?.value,
fremd: !!document.querySelector('.sicht[data-fremd="ja"]'),
}));
ok(zurueck.wert === "" && !zurueck.fremd, '"zurück zu mir" stellt die eigene Sicht wieder her');
ok(bDogi.konsole.length === 0,
`keine Fehler in der Konsole${bDogi.konsole.length ? " -> " + bDogi.konsole.join(" | ") : ""}`);
await bDogi.kontext.close();
/* GEGENPROBE IM BROWSER: Die anderen drei Rollen bekommen ihn nicht
einmal zu sehen. */
for (const [rolle, code, name] of [
["manager", "CODE-MANA-0001", "Manager"],
["scout", "CODE-SCOU-0001", "Scout"],
["creator", "CODE-ANNA-0001", "Creator"],
]) {
const b = await browserAls(rolle, code);
await b.seite.goto(BASIS + "/workspace/aufgaben.html", { waitUntil: "networkidle" });
await b.seite.waitForTimeout(900);
const da = await b.seite.evaluate(() => !!document.getElementById("sicht-wahl"));
ok(!da, `Gegenprobe: ein ${name} sieht den Umschalter nicht`);
await b.kontext.close();
}
await browser.close();
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
console.log(fehler ? `\n${fehler} Prüfung(en) fehlgeschlagen.` : "\nAlles in Ordnung.");
process.exit(fehler ? 1 : 0);