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]>
351 lines
17 KiB
JavaScript
351 lines
17 KiB
JavaScript
/* Prueft die Unterweisungen mit beidseitiger Bestaetigung.
|
|
|
|
Das Wichtigste steht am Ende: der VOLLZUG. Aus RunOne (01.09.2026)
|
|
stammt die Lehre, dass ein Weg, den man nicht rueckgaengig machen
|
|
kann, tagelang live sein und NIE gelaufen sein kann -- dort gab es
|
|
null festgeschriebene Buchungen, obwohl der Umbau seit Tagen
|
|
ausgeliefert war. Deshalb wird hier ausdruecklich durchgespielt:
|
|
bestaetigen, abschliessen, und dann eine Aenderung VERSUCHEN. Die
|
|
Ablehnung ist der eigentliche Nachweis.
|
|
|
|
Und die Gegenprobe dazu: Ohne den Trigger MUSS die Aenderung
|
|
durchgehen. Eine Sperre, die man nicht scheitern sehen hat, ist keine
|
|
Sperre. */
|
|
import { mkdtempSync, rmSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { createHash } from "node:crypto";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-schul-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4205";
|
|
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:4205";
|
|
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 250_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();
|
|
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 idScout = anlegen("Patrick", "scout", "CODE-SCOU-0001");
|
|
const idTili = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
const idFremd = anlegen("Fremd", "creator", "CODE-FREM-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idTili, idScout, jetzt);
|
|
|
|
/* Ein echtes Dokument in der Bibliothek -- ohne Datei keine Unterweisung. */
|
|
const wOrdner = join(ordner, "wissen");
|
|
mkdirSync(wOrdner, { recursive: true });
|
|
const dateiName = randomBytes(8).toString("hex") + ".pdf";
|
|
const inhalt = "%PDF-1.4\nRegeln und Richtlinien\n%%EOF\n";
|
|
writeFileSync(join(wOrdner, dateiName), inhalt);
|
|
const echterHash = createHash("sha256").update(Buffer.from(inhalt)).digest("hex");
|
|
d.prepare(`INSERT INTO wissen
|
|
(titel, beschreibung, kategorie, stufe, geraet, name_datei, name_original,
|
|
groesse, wichtig, veroeffentlicht, hochgeladen, erstellt_von)
|
|
VALUES (?,?,?,?,?,?,?,?,0,?,?,?)`)
|
|
.run("Community-Richtlinien", "Was erlaubt ist und was nicht", "tiktok-regeln",
|
|
"einsteiger", "egal", dateiName, "Richtlinien.pdf", inhalt.length,
|
|
tagText(), jetzt, idDogi);
|
|
const wissenId = d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
|
d.close();
|
|
|
|
const keks = async (rolle, code) => {
|
|
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ rolle, code }) });
|
|
return (a.headers.getSetCookie?.() || []).map((k) => k.split(";")[0]).join("; ");
|
|
};
|
|
const kDogi = await keks("admin", "CODE-DOGI-0001");
|
|
const kScout = await keks("scout", "CODE-SCOU-0001");
|
|
const kTili = await keks("creator", "CODE-ANNA-0001");
|
|
const kFremd = await keks("creator", "CODE-FREM-0001");
|
|
const ruf = (pfad, k, o = {}) => fetch(BASIS + pfad, { ...o, headers: { Cookie: k, ...(o.headers || {}) } });
|
|
const post = (pfad, k, koerper) => ruf(pfad, k, {
|
|
method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(koerper || {}) });
|
|
|
|
/* ---------- Anlegen -------------------------------------------------------- */
|
|
console.log("\n=== Anlegen");
|
|
let schulungId;
|
|
{
|
|
const a = await post("/workspace/api/schulung", kDogi,
|
|
{ titel: "Community-Richtlinien durchgehen", wissen_id: wissenId });
|
|
ok(a.ok, `DogFather legt eine Unterweisung an (${a.status})`);
|
|
|
|
/* Ohne Dokument geht nichts -- man kann nichts bestaetigen, was man
|
|
nicht gelesen hat. */
|
|
const ohne = await post("/workspace/api/schulung", kDogi, { titel: "Ohne Datei" });
|
|
ok(ohne.status === 400, `ohne Dokument wird abgelehnt (${ohne.status})`);
|
|
|
|
const nichtErlaubt = await post("/workspace/api/schulung", kScout,
|
|
{ titel: "Von einem Scout", wissen_id: wissenId });
|
|
ok(nichtErlaubt.status === 403, `ein Scout darf keine anlegen (${nichtErlaubt.status})`);
|
|
|
|
const liste = await (await ruf("/workspace/api/schulung", kTili)).json();
|
|
ok(liste.schulungen.length === 1, `sie steht in der Liste (${liste.schulungen.length})`);
|
|
ok(liste.schulungen[0].datei === "Richtlinien.pdf",
|
|
`mit dem Dokument (${liste.schulungen[0].datei})`);
|
|
schulungId = liste.schulungen[0].id;
|
|
}
|
|
|
|
/* ---------- Eine Seite reicht NICHT ---------------------------------------- */
|
|
console.log("\n=== Erst beide zusammen ergeben einen Nachweis");
|
|
{
|
|
const a = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kTili, {});
|
|
const r = await a.json();
|
|
ok(a.ok, `der Creator bestaetigt (${a.status})`);
|
|
ok(!!r.stand.creator_am, "sein Zeitpunkt steht");
|
|
ok(!r.stand.betreuer_am, "der der Betreuung noch nicht");
|
|
ok(r.stand.abgeschlossen === 0,
|
|
`und es ist NICHT abgeschlossen (${r.stand.abgeschlossen})`);
|
|
|
|
/* Zweimal bestaetigen darf nicht gehen -- sonst liesse sich ein Datum
|
|
nachtraeglich verschieben. */
|
|
const nochmal = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kTili, {});
|
|
ok(nochmal.status === 409, `zweimal bestaetigen geht nicht (${nochmal.status})`);
|
|
|
|
/* Der Fingerabdruck der Datei muss festgehalten sein. */
|
|
ok(r.stand.datei_hash === echterHash,
|
|
`der Fingerabdruck der Datei ist festgehalten (${r.stand.datei_hash?.slice(0, 16)}…)`);
|
|
}
|
|
|
|
/* ---------- Der Betreuer schliesst ab -------------------------------------- */
|
|
console.log("\n=== Abschliessen");
|
|
{
|
|
const a = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kScout,
|
|
{ creator_id: idTili });
|
|
const r = await a.json();
|
|
ok(a.ok, `die Betreuung bestaetigt (${a.status})`);
|
|
ok(r.stand.abgeschlossen === 1, `jetzt ist es abgeschlossen (${r.stand.abgeschlossen})`);
|
|
ok(!!r.stand.abgeschlossen_am, `mit Zeitpunkt (${r.stand.abgeschlossen_am})`);
|
|
ok(r.stand.betreuer_id === idScout, `und dem Namen der Betreuung (${r.stand.betreuer_id})`);
|
|
/* Beide IPs -- ohne die waere es eine Behauptung, kein Nachweis. */
|
|
ok(!!r.stand.creator_ip && !!r.stand.betreuer_ip, "beide Zugriffe sind vermerkt");
|
|
}
|
|
|
|
/* ---------- DER VOLLZUG: es laesst sich nicht mehr aendern ---------------- */
|
|
console.log("\n=== Unveraenderlich -- und zwar wirklich");
|
|
{
|
|
/* 1. Ueber die Schnittstelle. */
|
|
const nochmal = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kTili, {});
|
|
ok(nochmal.status === 409, `erneut bestaetigen wird abgelehnt (${nochmal.status})`);
|
|
|
|
const weg = await ruf(`/workspace/api/schulung/${schulungId}/bestaetigen`, kTili,
|
|
{ method: "DELETE" });
|
|
ok(weg.status === 409, `zuruecknehmen wird abgelehnt (${weg.status})`);
|
|
|
|
/* 2. AN DER SCHNITTSTELLE VORBEI, direkt auf der Datenbank. Das ist
|
|
der eigentliche Nachweis: Ein Schutz, der nur im Code steht, ist
|
|
beim naechsten neuen Weg zur Datenbank wieder weg. */
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
let aenderung = null, loeschung = null;
|
|
try {
|
|
p.prepare("UPDATE schulung_bestaetigt SET creator_am = '2020-01-01' WHERE schulung_id = ?")
|
|
.run(schulungId);
|
|
aenderung = "DURCHGEGANGEN";
|
|
} catch (f) { aenderung = f.message; }
|
|
try {
|
|
p.prepare("DELETE FROM schulung_bestaetigt WHERE schulung_id = ?").run(schulungId);
|
|
loeschung = "DURCHGEGANGEN";
|
|
} catch (f) { loeschung = f.message; }
|
|
p.close();
|
|
|
|
ok(/nicht geaendert/.test(aenderung),
|
|
`die DATENBANK lehnt die Aenderung ab (${aenderung.slice(0, 60)})`);
|
|
ok(/nicht geloescht/.test(loeschung),
|
|
`und das Loeschen ebenfalls (${loeschung.slice(0, 60)})`);
|
|
|
|
/* Und der Datensatz steht unveraendert da. */
|
|
const p2 = new DatabaseSync(process.env.WORKSPACE_DB, { readOnly: true });
|
|
const z = p2.prepare("SELECT * FROM schulung_bestaetigt WHERE schulung_id = ?").get(schulungId);
|
|
p2.close();
|
|
ok(z && z.abgeschlossen === 1 && !String(z.creator_am).startsWith("2020"),
|
|
`der Nachweis steht unveraendert (${z?.creator_am?.slice(0, 10)})`);
|
|
}
|
|
|
|
/* ---------- GEGENPROBE: ohne Trigger geht es durch ------------------------ */
|
|
console.log("\n=== Gegenprobe -- kann die Sperre ueberhaupt scheitern?");
|
|
{
|
|
/* Eine Sperre, die man nicht hat scheitern sehen, ist keine Sperre.
|
|
Hier wird der Trigger entfernt, dieselbe Aenderung versucht -- sie
|
|
MUSS jetzt durchgehen -- und danach alles zurueckgesetzt. */
|
|
const p = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
p.exec("DROP TRIGGER schulung_nicht_aendern");
|
|
let ohne = null;
|
|
try {
|
|
p.prepare("UPDATE schulung_bestaetigt SET creator_ip = 'probe' WHERE schulung_id = ?")
|
|
.run(schulungId);
|
|
ohne = "durchgegangen";
|
|
} catch (f) { ohne = f.message; }
|
|
/* Zurueck in den richtigen Zustand. */
|
|
p.exec(`CREATE TRIGGER schulung_nicht_aendern
|
|
BEFORE UPDATE ON schulung_bestaetigt
|
|
FOR EACH ROW WHEN OLD.abgeschlossen = 1
|
|
BEGIN SELECT RAISE(ABORT, 'Abgeschlossene Bestaetigung kann nicht geaendert werden'); END`);
|
|
p.close();
|
|
ok(ohne === "durchgegangen",
|
|
`ohne Trigger geht dieselbe Aenderung durch (${ohne}) -- die Sperre wirkt also wirklich`);
|
|
}
|
|
|
|
/* ---------- Wer darf was --------------------------------------------------- */
|
|
console.log("\n=== Wer darf was");
|
|
{
|
|
/* Ein Creator bestaetigt nur fuer sich. */
|
|
const fremd = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kFremd,
|
|
{ creator_id: idTili });
|
|
/* Der Server ignoriert creator_id bei Creatorn und nimmt die eigene --
|
|
es entsteht also ein Eintrag fuer den Fremden selbst, nicht fuer
|
|
Tili. Genau das soll passieren. */
|
|
ok(fremd.ok, `ein anderer Creator legt damit nur seinen EIGENEN Stand an (${fremd.status})`);
|
|
const r = await fremd.json();
|
|
ok(r.stand.creator_id === idFremd,
|
|
`und zwar wirklich seinen (${r.stand.creator_id} = ${idFremd})`);
|
|
|
|
/* Ein Scout nicht fuer einen fremden Creator. */
|
|
const nichtSeiner = await post(`/workspace/api/schulung/${schulungId}/bestaetigen`, kScout,
|
|
{ creator_id: idFremd });
|
|
ok(nichtSeiner.status === 404,
|
|
`ein Scout nicht fuer einen fremden Creator (${nichtSeiner.status})`);
|
|
|
|
/* Ein Creator sieht nur seinen eigenen Stand. */
|
|
const sicht = await (await ruf("/workspace/api/schulung", kTili)).json();
|
|
ok(sicht.stand.every((s) => s.creator_id === idTili),
|
|
`ein Creator sieht nur seinen Stand (${sicht.stand.length} Eintraege)`);
|
|
const scoutSicht = await (await ruf("/workspace/api/schulung", kScout)).json();
|
|
ok(scoutSicht.stand.every((s) => s.creator_id === idTili),
|
|
`ein Scout nur den seiner Creator (${scoutSicht.stand.length})`);
|
|
}
|
|
|
|
/* ---------- Wird die Datei ausgetauscht, faellt es auf -------------------- */
|
|
console.log("\n=== Ausgetauschte Datei");
|
|
{
|
|
writeFileSync(join(wOrdner, dateiName), "%PDF-1.4\nGANZ ANDERE REGELN\n%%EOF\n");
|
|
const a = await (await ruf("/workspace/api/schulung", kDogi)).json();
|
|
const meiner = a.stand.find((s) => s.creator_id === idTili);
|
|
ok(meiner?.datei_stimmt === false,
|
|
`die Seite meldet, dass die Datei nicht mehr die bestaetigte ist (${meiner?.datei_stimmt})`);
|
|
/* Zurueck, damit die restliche Pruefung sauber laeuft. */
|
|
writeFileSync(join(wOrdner, dateiName), inhalt);
|
|
const b = await (await ruf("/workspace/api/schulung", kDogi)).json();
|
|
ok(b.stand.find((s) => s.creator_id === idTili)?.datei_stimmt === true,
|
|
"und wieder in Ordnung, wenn die Datei stimmt");
|
|
}
|
|
|
|
/* ---------- Im Browser --------------------------------------------------- */
|
|
console.log("\n=== Die Seite");
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
{
|
|
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="admin"]');
|
|
await seite.fill("#code", "CODE-DOGI-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=schutz", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1100);
|
|
|
|
/* Ein Betreuer sieht den Stand EINES Creators, und die Auswahl steht
|
|
auf dem ersten alphabetisch. Fuer die Pruefung wird gezielt Tili
|
|
gewaehlt -- der ist der einzige mit einem abgeschlossenen Nachweis.
|
|
Ein erster Anlauf hat das uebersehen und Fremds halben Stand
|
|
gemessen. */
|
|
await seite.selectOption("#schulung-creator", String(idTili));
|
|
await seite.waitForTimeout(700);
|
|
|
|
const r = await seite.evaluate(() => ({
|
|
da: !document.getElementById("schulung")?.hidden,
|
|
anzahl: document.querySelectorAll(".unterweisung").length,
|
|
stufen: [...document.querySelectorAll(".unterweisung")].map((u) => u.dataset.stufe),
|
|
unterschriften: document.querySelectorAll(".unterschrift").length,
|
|
gesetzt: document.querySelectorAll('.unterschrift[data-da="ja"]').length,
|
|
fest: document.querySelectorAll(".unterweisung__fest").length,
|
|
formular: !!document.querySelector(".schulung__neu"),
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
}));
|
|
ok(r.da, "der Unterweisungsblock steht im Bereich Schutz & Regeln");
|
|
ok(r.anzahl === 1, `eine Unterweisung (${r.anzahl})`);
|
|
ok(r.stufen[0] === "fertig", `sie ist abgeschlossen (${r.stufen[0]})`);
|
|
/* Beide Unterschriften sichtbar UND beide als gesetzt markiert --
|
|
ein Nachweis, bei dem nur einer unterschrieben hat, ist keiner. */
|
|
ok(r.unterschriften === 2, `beide Unterschriften stehen da (${r.unterschriften})`);
|
|
ok(r.gesetzt === 2, `und beide sind gesetzt (${r.gesetzt})`);
|
|
ok(r.fest === 1, "mit dem Hinweis, dass es unveraenderlich ist");
|
|
ok(r.formular, "DogFather sieht das Formular zum Hinzufuegen");
|
|
ok(r.ueber <= 0, `nichts steht ueber (${r.ueber}px)`);
|
|
ok(konsole.length === 0,
|
|
"keine Fehler in der Konsole" + (konsole.length ? " -> " + konsole.join(" | ") : ""));
|
|
|
|
await seite.screenshot({ path: "pruef-schulung.png", fullPage: false });
|
|
await kontext.close();
|
|
}
|
|
|
|
/* Ein Creator sieht denselben Block -- ohne das Formular zum Anlegen. */
|
|
{
|
|
const kontext = await browser.newContext({ viewport: { width: 1280, height: 900 } });
|
|
const seite = await kontext.newPage();
|
|
await seite.goto(BASIS + "/workspace/", { waitUntil: "domcontentloaded" });
|
|
await seite.click('.rolle[data-rolle="creator"]');
|
|
await seite.fill("#code", "CODE-ANNA-0001");
|
|
await seite.click("button[type=submit]");
|
|
await seite.waitForURL("**/start.html", { timeout: 15000 });
|
|
await seite.goto(BASIS + "/workspace/bereich.html?b=schutz", { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1000);
|
|
const r = await seite.evaluate(() => ({
|
|
da: !document.getElementById("schulung")?.hidden,
|
|
formular: !!document.querySelector(".schulung__neu"),
|
|
wahl: !!document.getElementById("schulung-creator"),
|
|
}));
|
|
ok(r.da, "ein Creator sieht seine Unterweisungen");
|
|
ok(!r.formular, "aber nicht das Formular zum Anlegen");
|
|
ok(!r.wahl, "und keine Creator-Auswahl -- es geht ja nur um ihn");
|
|
await 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);
|