Sie war eine Wand: siebzehn identische Kaesten, auf jedem stand "OEFFNEN".
Wenn alles gleich aussieht, ist alles gleich wichtig -- also nichts. Und
"OEFFNEN" ist keine Information; dass eine Kachel sich oeffnen laesst,
weiss man.
Drei Aenderungen, jede mit einem Grund:
1. GRUPPEN statt einer Liste. "Taeglich" (was man sowieso jeden Tag
aufmacht), "Rund um den Creator" (die Betreuungsakte), "Team & System"
(was den Laden am Laufen haelt). Das sind drei verschiedene Absichten.
2. ZAHLEN STATT "OEFFNEN". Auf der Kachel steht jetzt, wo Arbeit liegt --
"Aufgaben 4", rot markiert, weil etwas ueberfaellig ist. Gespeist aus
DERSELBEN Quelle wie die Hinweisliste darueber, kein zweiter Zaehler:
Zwei verschiedene Wahrheiten uebereinander auf einem Bildschirm waeren
schlimmer als gar keine Zahl.
3. ZEICHEN UND FARBE. Siebzehn eigene Linienzeichen (eigene Pfade, keine
Fremdbibliothek -- kleiner als jede Schriftart und keine zusaetzliche
Lieferkette) und acht Farbtoene.
Warum acht Toene und nicht siebzehn: Ich hatte zuerst eine eigene Farbe
je Kachel gebaut und das Ergebnis pruefen lassen. Zwei Paare hatten einen
Abstand von 1.4 und 5.6 -- also praktisch dieselbe Farbe. Das sieht nach
Zufall aus, nicht nach Absicht. Die acht jetzt verwendeten sind gegen
genau diesen Hintergrund gerechnet und bestehen alle Pruefungen
(Helligkeitsband, Farbstaerke, Farbfehlsichtigkeit dE 8.4,
Normalsicht 19.3, Kontrast). Verteilt so, dass in einer Zeile nie
zweimal derselbe Ton steht. Die Farbe stuetzt nur -- WAS eine Kachel
ist, sagen Zeichen und Name.
Dazu:
- Begruessung nach Tageszeit statt immer "Willkommen".
- Sechs Nullen nebeneinander sind Rauschen: Ist wirklich nichts offen,
steht dort ein Satz und der Platz gehoert den Bereichen.
- Die Hinweisliste stand mit sieben Zeilen im Weg. Jetzt vier sichtbar,
Rest auf Knopfdruck -- derselbe Helfer wie im Protokoll und im Verlauf.
- Gestaffelter Einlauf, aber in einer no-preference-Abfrage: Bei
"weniger Bewegung" entsteht gar keine Animation, nicht nur eine ohne
Dauer. Geprueft mit reducedMotion.
Beim Ansehen des ersten Bildes fielen abgeschnittene Untertitel auf
("Stammdaten, Ziele, 90-..."). Abgeschnittener Text ist der haeufigste
stille Fehler in einer Kachel, weil er nach Absicht aussieht. Texte
gekuerzt -- und der Test misst jetzt die tatsaechliche Textbreite gegen
die verfuegbare, damit es nicht wiederkommt.
40 Pruefungen, Computer und Handy, alle vier Rollen: Ein Creator sieht
"Mein Profil" statt der Liste aller Creator, keine Personenverwaltung,
keine Pipeline, keine Automationen -- und trotzdem drei saubere Gruppen.
Co-Authored-By: Claude Opus 5 <[email protected]>
213 lines
11 KiB
JavaScript
213 lines
11 KiB
JavaScript
/* Browserpruefung der Startseite: Gruppen, Zeichen, Farbtoene, Zahlen auf
|
|
den Kacheln -- und was jede Rolle davon zu sehen bekommt. */
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-start-"));
|
|
process.env.WORKSPACE_DB = join(ordner, "workspace.db");
|
|
process.env.PORT = "4186";
|
|
process.env.SITE_ACCESS_SECRET = "lokaler-test";
|
|
process.env.SITE_ACCESS_CODE_DOGI = "DOGI-TEST-1";
|
|
process.env.SITE_ACCESS_CODE_VANVAN = "VANVAN-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:4186";
|
|
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 200_000).unref?.();
|
|
|
|
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");
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const jetzt = new Date().toISOString();
|
|
const heute = jetzt.slice(0, 10);
|
|
const gestern = new Date(Date.now() - 86400_000).toISOString().slice(0, 10);
|
|
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 idAnna = anlegen("Tili", "creator", "CODE-ANNA-0001");
|
|
d.prepare("INSERT INTO betreuung (creator_id, betreuer_id, seit) VALUES (?,?,?)").run(idAnna, idScout, jetzt);
|
|
|
|
/* Arbeit an mehreren Stellen -- damit auf den Kacheln wirklich Zahlen
|
|
stehen und man sieht, ob sie an der richtigen landen. */
|
|
for (const [status, frist] of [["offen", gestern], ["offen", gestern], ["offen", heute],
|
|
["arbeit", null], ["review", null]]) {
|
|
d.prepare("INSERT INTO aufgaben (titel, status, creator_id, frist, erstellt, erstellt_von) VALUES ('Test',?,?,?,?,?)")
|
|
.run(status, idAnna, frist, jetzt, idDogi);
|
|
}
|
|
d.prepare("INSERT INTO termine (titel, beginn, creator_id, erledigt, erstellt, erstellt_von) VALUES ('Heute',?,?,0,?,?)")
|
|
.run(heute + "T18:00", idAnna, jetzt, idDogi);
|
|
d.prepare("INSERT INTO eintraege (bereich, art, titel, datum, dringlichkeit, status, creator_id, erstellt, erstellt_von) VALUES ('technik','problem','Ton weg',?,'hoch','offen',?,?,?)")
|
|
.run(heute, idAnna, jetzt, idDogi);
|
|
d.close();
|
|
|
|
const { chromium } = await import(
|
|
"file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
const browser = await chromium.launch();
|
|
|
|
async function anmelden(kontext, rolle, code) {
|
|
const seite = await kontext.newPage();
|
|
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 });
|
|
await seite.waitForTimeout(900);
|
|
return seite;
|
|
}
|
|
|
|
const lesen = (seite) => seite.evaluate(() => ({
|
|
gruss: document.getElementById("gruss")?.textContent,
|
|
gruppen: [...document.querySelectorAll(".gruppe")].map((g) => ({
|
|
name: g.querySelector(".gruppe__name")?.textContent,
|
|
kacheln: g.querySelectorAll(".kachel").length,
|
|
})),
|
|
kacheln: [...document.querySelectorAll(".kachel")].map((k) => ({
|
|
name: k.querySelector(".kachel__name")?.textContent,
|
|
unter: k.querySelector(".kachel__unter")?.textContent,
|
|
ton: k.dataset.ton,
|
|
farbe: getComputedStyle(k.querySelector(".kachel__svg")).stroke,
|
|
pfade: k.querySelectorAll(".kachel__svg path").length,
|
|
zahl: k.querySelector(".kachel__zahl")?.textContent || null,
|
|
warn: k.querySelector(".kachel__zahl")?.dataset.warn === "ja",
|
|
ziel: k.querySelector(".kachel__link")?.getAttribute("href"),
|
|
})),
|
|
/* Abgeschnittener Text ist der haeufigste stille Fehler in einer
|
|
Kachel -- er sieht auf den ersten Blick nach Absicht aus. Gemessen
|
|
wird die tatsaechliche Textbreite gegen die verfuegbare. */
|
|
abgeschnitten: [...document.querySelectorAll(".kachel__name, .kachel__unter")]
|
|
.filter((e) => e.scrollWidth > e.clientWidth + 1)
|
|
.map((e) => e.textContent),
|
|
hinweise: [...document.querySelectorAll("#dran .dran__punkt")]
|
|
.filter((z) => z.offsetParent !== null).length,
|
|
hinweiseGesamt: document.querySelectorAll("#dran .dran__punkt").length,
|
|
zahlenLeer: !document.getElementById("zahlen-leer")?.hidden,
|
|
zahlen: [...document.querySelectorAll(".zahl")].length,
|
|
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
|
ueberstehend: [...document.querySelectorAll("main *")]
|
|
.filter((e) => e.getBoundingClientRect().right > window.innerWidth + 1)
|
|
.map((e) => String(e.className).slice(0, 40)).slice(0, 4),
|
|
}));
|
|
|
|
for (const [breite, wie] of [[1440, "Computer"], [390, "Handy"]]) {
|
|
console.log(`\n=== DogFather · ${wie}`);
|
|
const kontext = await browser.newContext({ viewport: { width: breite, height: 1200 } });
|
|
const konsole = [];
|
|
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
|
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
|
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
|
await seite.reload({ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(900);
|
|
|
|
const r = await lesen(seite);
|
|
console.log(" Gruß: ", r.gruss);
|
|
console.log(" Gruppen:", r.gruppen.map((g) => `${g.name} (${g.kacheln})`).join(" · "));
|
|
console.log(" Zahlen: ", r.kacheln.filter((k) => k.zahl).map((k) => `${k.name}=${k.zahl}${k.warn ? "!" : ""}`).join(" · "));
|
|
|
|
ok(r.gruppen.length === 3, `drei Gruppen (${r.gruppen.length})`);
|
|
ok(r.kacheln.length === 17, `siebzehn Kacheln (${r.kacheln.length})`);
|
|
ok(/^(Guten Morgen|Guten Tag|Guten Abend|Noch wach), Filipe$/.test(r.gruss || ""),
|
|
`Begrüßung nach Tageszeit ("${r.gruss}")`);
|
|
|
|
/* Jede Kachel muss ein Zeichen haben -- eine ohne waere ein leeres
|
|
Kaestchen und faellt sofort auf. */
|
|
ok(r.kacheln.every((k) => k.pfade > 0),
|
|
`jede Kachel hat ein Zeichen (${r.kacheln.filter((k) => !k.pfade).map((k) => k.name).join(", ") || "alle"})`);
|
|
ok(r.kacheln.every((k) => k.unter && k.unter.length > 4), "jede Kachel sagt, was drin ist");
|
|
ok(r.kacheln.every((k) => k.ziel && k.ziel !== "#"), "jede Kachel führt irgendwohin");
|
|
|
|
/* Acht verschiedene Farben, und nirgends steht "ÖFFNEN" mehr. */
|
|
const toene = new Set(r.kacheln.map((k) => k.farbe));
|
|
ok(toene.size === 8, `acht verschiedene Farbtöne im Einsatz (${toene.size})`);
|
|
const oeffnen = await seite.evaluate(() => document.body.textContent.includes("ÖFFNEN"));
|
|
ok(!oeffnen, "das Wort ÖFFNEN kommt nicht mehr vor");
|
|
|
|
/* Die Zahlen müssen auf der RICHTIGEN Kachel stehen. */
|
|
const auf = (n) => r.kacheln.find((k) => k.name === n);
|
|
ok(auf("Aufgaben")?.zahl === "4",
|
|
`Aufgaben trägt 4 (2 überfällig + 1 heute + 1 Review) (${auf("Aufgaben")?.zahl})`);
|
|
ok(auf("Aufgaben")?.warn === true, "und ist als Warnung markiert");
|
|
ok(auf("Kalender")?.zahl === "1", `Kalender trägt 1 (${auf("Kalender")?.zahl})`);
|
|
ok(auf("LIVE-Analyse")?.zahl === "1", `der dringende Bereichspunkt (${auf("LIVE-Analyse")?.zahl})`);
|
|
ok(!auf("Wissens-Bibliothek")?.zahl, "wo nichts liegt, steht keine Zahl");
|
|
|
|
ok(r.abgeschnitten.length === 0,
|
|
`kein Text wird abgeschnitten (${r.abgeschnitten.join(" | ") || "keiner"})`);
|
|
ok(r.hinweise <= 4, `höchstens vier Hinweise sichtbar (${r.hinweise} von ${r.hinweiseGesamt})`);
|
|
ok(r.zahlen === 6, `die Aufgabenzahlen stehen (${r.zahlen})`);
|
|
ok(!r.zahlenLeer, "und nicht der Leer-Satz");
|
|
ok(r.ueber <= 0, `nichts steht über (${r.ueber}px, ${r.ueberstehend.join(", ")})`);
|
|
ok(konsole.length === 0, `keine Fehler in der Konsole${konsole.length ? " -> " + konsole.join(" | ") : ""}`);
|
|
|
|
await seite.screenshot({ path: breite === 1440 ? "pruef-start-computer.png" : "pruef-start-handy.png", fullPage: true });
|
|
await kontext.close();
|
|
}
|
|
|
|
console.log("\n=== Creator");
|
|
{
|
|
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
|
const seite = await anmelden(kontext, "creator", "CODE-ANNA-0001");
|
|
const r = await lesen(seite);
|
|
console.log(" Gruppen:", r.gruppen.map((g) => `${g.name} (${g.kacheln})`).join(" · "));
|
|
const namen = r.kacheln.map((k) => k.name);
|
|
ok(namen.includes("Mein Profil"), "sieht sein eigenes Profil");
|
|
ok(!namen.includes("Creator-Profile"), "und nicht die Liste aller Creator");
|
|
ok(!namen.includes("Personen & Zugänge"), "keine Personenverwaltung");
|
|
ok(!namen.includes("Scout-Pipeline"), "keine Scout-Pipeline");
|
|
ok(!namen.includes("Automationen"), "keine Automationen");
|
|
ok(r.gruppen.length === 3, `trotzdem drei Gruppen (${r.gruppen.length})`);
|
|
ok(r.kacheln.every((k) => k.pfade > 0), "alle Zeichen da");
|
|
await seite.screenshot({ path: "pruef-start-creator.png", fullPage: true });
|
|
await kontext.close();
|
|
}
|
|
|
|
console.log("\n=== Scout");
|
|
{
|
|
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1200 } });
|
|
const seite = await anmelden(kontext, "scout", "CODE-SCOU-0001");
|
|
const r = await lesen(seite);
|
|
const namen = r.kacheln.map((k) => k.name);
|
|
ok(namen.includes("Scout-Pipeline"), "sieht seine Pipeline");
|
|
ok(!namen.includes("Personen & Zugänge"), "keine Personenverwaltung");
|
|
ok(!namen.includes("Automationen"), "keine Automationen");
|
|
ok(namen.includes("Creator-Profile"), "aber die Creator-Profile");
|
|
await kontext.close();
|
|
}
|
|
|
|
console.log("\n=== Weniger Bewegung: keine Animation");
|
|
{
|
|
const kontext = await browser.newContext({
|
|
viewport: { width: 1440, height: 1000 }, reducedMotion: "reduce" });
|
|
const seite = await anmelden(kontext, "admin", "CODE-DOGI-0001");
|
|
const bewegung = await seite.evaluate(() => {
|
|
const k = document.querySelector(".kachel");
|
|
const s = getComputedStyle(k);
|
|
return { animation: s.animationName, sichtbar: s.opacity };
|
|
});
|
|
ok(bewegung.animation === "none", `keine Animation (${bewegung.animation})`);
|
|
ok(Number(bewegung.sichtbar) === 1, `und trotzdem voll sichtbar (${bewegung.sichtbar})`);
|
|
await kontext.close();
|
|
}
|
|
|
|
await browser.close();
|
|
console.log(`\n${fehler === 0 ? "Alles in Ordnung." : fehler + " Prüfung(en) fehlgeschlagen."}`);
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ }
|
|
process.exit(fehler === 0 ? 0 : 1);
|