Personen: jede Rolle einzeln auf- und zuklappbar
Wunsch Filipe: "ich will ueberall die liste zu machen koennen und nur
aufmachen wenn ich sie sehen will."
Jede der vier Rollen klappt jetzt einzeln auf und zu. Das ERSETZT den
Sammelknopf von vorhin ("5 weitere zeigen") -- zwei Mechanismen
nebeneinander, die dasselbe verstecken, waeren eine Einladung zum
Missverstaendnis. Der Knopf oben rechts macht jetzt etwas anderes: alles
auf einmal ("Alle aufklappen" / "Alle zuklappen"), damit man bei vier
Rollen nicht viermal klicken muss.
Der entscheidende Unterschied zum Ausblenden: Die UEBERSCHRIFT mit der
Anzahl bleibt IMMER stehen, auch zugeklappt. Man sieht jederzeit, DASS es
einen Manager gibt -- nur nicht, welchen. Wer eine ganze Rolle spurlos
verschwinden laesst, haelt sie irgendwann fuer leer.
Und zugeklappt zaehlt genau eine Frage: Steckt da etwas Gesperrtes drin,
das ich sehen muesste? Deshalb steht am Manager auch zugeklappt
"1 gesperrt", in Warnfarbe.
Die Ueberschrift IST der Knopf, als echtes <button> -- ein eigener
kleiner Schalter daneben waere ein zweites Ziel fuer dieselbe Absicht,
und ein kleineres. Als Knopf-Element statt div mit Klick-Zuhoerer machen
Tastatur und Vorleseprogramme es ohne Zutun richtig.
Der Zustand wird JE ROLLE gemerkt, nicht als "alles auf/zu": Wer die
Creator zuklappt und die Scouts offen laesst, findet das nach dem Laden
genau so wieder.
30 Pruefungen. Darunter weiterhin der wichtigste: Zuklappen ist eine
ANSICHT, kein Recht -- Tili kann sich anmelden und normal arbeiten,
waehrend ihre Rolle zugeklappt ist, und der Server liefert unveraendert
alle sieben Personen.
Co-Authored-By: Claude Opus 5 <[email protected]>
This commit is contained in:
@@ -1,10 +1,16 @@
|
||||
/* ===================================================================
|
||||
Pruefung: In der Personenliste steht standardmaessig nur DogFather,
|
||||
alle anderen kommen auf Knopfdruck dazu.
|
||||
Pruefung: Die Personenliste ist je Rolle auf- und zuklappbar.
|
||||
|
||||
Der wichtigste Punkt ist der letzte: Verstecken ist eine ANSICHT, kein
|
||||
Recht. Wer eingeklappt ist, hat unveraendert Zugang -- der Server darf
|
||||
davon gar nichts mitbekommen.
|
||||
Zwei Dinge sind dabei wichtiger als alles andere:
|
||||
|
||||
1. Die UEBERSCHRIFT mit der Anzahl bleibt immer stehen, auch
|
||||
zugeklappt. Das ist der Unterschied zum Ausblenden: Man sieht
|
||||
jederzeit, DASS es einen Manager gibt -- nur nicht, welchen. Wer
|
||||
eine ganze Rolle spurlos verschwinden laesst, haelt sie irgendwann
|
||||
fuer leer.
|
||||
|
||||
2. Zuklappen ist eine ANSICHT, kein Recht. Wer zugeklappt ist, hat
|
||||
unveraendert Zugang -- der Server bekommt davon gar nichts mit.
|
||||
=================================================================== */
|
||||
import { mkdtempSync, rmSync } from "node:fs";
|
||||
import { tmpdir } from "node:os";
|
||||
@@ -24,7 +30,7 @@ express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(
|
||||
await import("./index.js");
|
||||
await new Promise((r) => setTimeout(r, 700));
|
||||
const BASIS = "http://127.0.0.1:4187";
|
||||
setTimeout(() => { console.log("ABBRUCH"); process.exit(1); }, 180_000).unref?.();
|
||||
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++; };
|
||||
@@ -42,7 +48,6 @@ function anlegen(name, rolle, code, aktiv = 1) {
|
||||
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 (?,?,?,?,?,?,?)")
|
||||
.run(name, rolle, hash, salt, 32768, aktiv, jetzt);
|
||||
return d.prepare("SELECT last_insert_rowid() AS id").get().id;
|
||||
}
|
||||
/* Genau die Aufstellung aus dem Bild vom 31.08.2026. */
|
||||
anlegen("Dogfather", "admin", "CODE-DOGI-0001");
|
||||
@@ -72,115 +77,136 @@ async function seiteFuer(kontext) {
|
||||
|
||||
const lesen = (seite) => seite.evaluate(() => ({
|
||||
namen: [...document.querySelectorAll(".person__name")].map((n) => n.textContent.replace(" (du)", "")),
|
||||
/* Die Abschnitte in der Reihenfolge, in der sie tatsächlich stehen --
|
||||
nicht in der, in der sie gemeint waren. */
|
||||
abschnitte: [...document.querySelectorAll(".gruppe")].map((g) => ({
|
||||
name: g.querySelector(".gruppe__name")?.textContent,
|
||||
zahl: g.querySelector(".gruppe__zahl")?.textContent,
|
||||
unter: g.querySelector(".gruppe__unter")?.textContent,
|
||||
auf: g.querySelector(".gruppe__kopf")?.getAttribute("aria-expanded"),
|
||||
knopf: g.querySelector(".gruppe__kopf")?.tagName,
|
||||
personen: [...g.querySelectorAll(".person__name")].map((n) => n.textContent.replace(" (du)", "")),
|
||||
})),
|
||||
knopfDa: !document.getElementById("rest-schalter")?.hidden,
|
||||
knopfText: document.getElementById("rest-schalter-text")?.textContent,
|
||||
auf: document.getElementById("rest-schalter")?.getAttribute("aria-expanded"),
|
||||
ueber: document.documentElement.scrollWidth - document.documentElement.clientWidth,
|
||||
}));
|
||||
|
||||
const klick = (seite, rolle) =>
|
||||
seite.locator(".gruppe__kopf", { hasText: new RegExp("^" + rolle) }).first().click();
|
||||
|
||||
const kontext = await browser.newContext({ viewport: { width: 1440, height: 1100 } });
|
||||
const konsole = [];
|
||||
const seite = await seiteFuer(kontext);
|
||||
seite.on("console", (m) => { if (m.type() === "error") konsole.push(m.text()); });
|
||||
seite.on("pageerror", (e) => konsole.push("pageerror: " + e.message));
|
||||
|
||||
console.log("\n1) Standardmäßig nur DogFather");
|
||||
console.log("\n1) Alle vier Überschriften stehen da – offen ist nur DogFather");
|
||||
{
|
||||
const r = await lesen(seite);
|
||||
console.log(" sichtbar:", r.namen.join(", "));
|
||||
console.log(" Knopf: ", r.knopfText);
|
||||
ok(r.namen.length === 2, `zwei Personen sichtbar (${r.namen.length})`);
|
||||
ok(r.abschnitte.length === 1 && r.abschnitte[0].name === "DogFather",
|
||||
`zugeklappt steht nur der DogFather-Abschnitt da (${r.abschnitte.map((a) => a.name).join(", ") || "keiner"})`);
|
||||
console.log(" ", r.abschnitte.map((a) => `${a.name}(${a.zahl},${a.auf === "true" ? "auf" : "zu"})`).join(" · "));
|
||||
|
||||
ok(r.abschnitte.length === 4, `vier Überschriften, auch zugeklappt (${r.abschnitte.length})`);
|
||||
ok(r.abschnitte.map((a) => a.name).join(",") === "DogFather,Manager,Scout,Creator",
|
||||
`Reihenfolge wie auf der Zugangsseite (${r.abschnitte.map((a) => a.name).join(", ")})`);
|
||||
ok(r.abschnitte.map((a) => a.zahl).join(",") === "2,1,2,2",
|
||||
`mit den Anzahlen (${r.abschnitte.map((a) => a.zahl).join(", ")})`);
|
||||
|
||||
ok(r.namen.length === 2, `aber nur zwei Personen sichtbar (${r.namen.length})`);
|
||||
ok(r.namen.includes("Dogfather") && r.namen.includes("VanVan"),
|
||||
`Dogfather und VanVan (${r.namen.join(", ")})`);
|
||||
ok(!r.namen.includes("Tili"), "kein Creator");
|
||||
ok(!r.namen.includes("Patrick"), "kein Scout");
|
||||
ok(r.knopfDa, "der Knopf ist da");
|
||||
/* Die ZAHL gehoert auf den Knopf -- "Alle zeigen" sagt nicht, wie
|
||||
viele man gerade nicht sieht. */
|
||||
ok(/5 weitere zeigen/.test(r.knopfText || ""), `mit der Anzahl (${r.knopfText})`);
|
||||
ok(/1 gesperrt/.test(r.knopfText || ""), "und dem Hinweis auf die gesperrte Person");
|
||||
ok(r.auf === "false", `aria-expanded=false (${r.auf})`);
|
||||
ok(r.abschnitte[0].auf === "true", `DogFather ist offen (${r.abschnitte[0].auf})`);
|
||||
ok(r.abschnitte.slice(1).every((a) => a.auf === "false"), "die anderen drei sind zu");
|
||||
|
||||
/* Als echtes <button>, damit Tastatur und Vorleseprogramme es ohne
|
||||
Zutun richtig machen. */
|
||||
ok(r.abschnitte.every((a) => a.knopf === "BUTTON"), "jede Überschrift ist ein echter Knopf");
|
||||
|
||||
/* Zugeklappt muss trotzdem auffallen, wenn dort etwas Gesperrtes liegt. */
|
||||
const manager = r.abschnitte.find((a) => a.name === "Manager");
|
||||
ok(/1 gesperrt/.test(manager?.unter || ""),
|
||||
`bei Manager steht die gesperrte Person dran ("${manager?.unter}")`);
|
||||
|
||||
ok(r.knopfDa && /Alle aufklappen/.test(r.knopfText || ""),
|
||||
`der Sammelknopf bietet alles an (${r.knopfText})`);
|
||||
}
|
||||
|
||||
console.log("\n2) Auf Knopfdruck kommen alle dazu");
|
||||
console.log("\n2) Jede Rolle einzeln auf und zu");
|
||||
{
|
||||
await klick(seite, "Scout");
|
||||
await seite.waitForTimeout(250);
|
||||
let r = await lesen(seite);
|
||||
console.log(" ", r.abschnitte.map((a) => `${a.name}=${a.auf === "true" ? "auf" : "zu"}`).join(" · "));
|
||||
ok(r.namen.includes("Patrick") && r.namen.includes("BananaStift"), "die Scouts stehen da");
|
||||
ok(!r.namen.includes("Tili"), "die Creator weiterhin nicht");
|
||||
ok(r.abschnitte.find((a) => a.name === "Creator").auf === "false", "Creator bleibt unberührt zu");
|
||||
|
||||
await klick(seite, "Scout");
|
||||
await seite.waitForTimeout(250);
|
||||
r = await lesen(seite);
|
||||
ok(!r.namen.includes("Patrick"), "dieselbe Überschrift klappt wieder zu");
|
||||
|
||||
/* "Überall" heisst überall -- auch DogFather. */
|
||||
await klick(seite, "DogFather");
|
||||
await seite.waitForTimeout(250);
|
||||
r = await lesen(seite);
|
||||
ok(r.namen.length === 0, `alles zu, keine Person sichtbar (${r.namen.length})`);
|
||||
ok(r.abschnitte.length === 4, "die vier Überschriften stehen trotzdem noch da");
|
||||
}
|
||||
|
||||
console.log("\n3) Der Sammelknopf klappt alles auf und wieder zu");
|
||||
{
|
||||
await seite.click("#rest-schalter");
|
||||
await seite.waitForTimeout(250);
|
||||
const r = await lesen(seite);
|
||||
console.log(" sichtbar:", r.namen.join(", "));
|
||||
ok(r.namen.length === 7, `alle sieben (${r.namen.length})`);
|
||||
ok(r.namen.includes("BanaStift"), "auch die gesperrte Person");
|
||||
ok(/Nur DogFather zeigen/.test(r.knopfText || ""), `Knopf bietet das Zuklappen an (${r.knopfText})`);
|
||||
ok(r.auf === "true", `aria-expanded=true (${r.auf})`);
|
||||
ok(/Alle zuklappen/.test(r.knopfText || ""), `Knopf bietet jetzt das Zuklappen an (${r.knopfText})`);
|
||||
ok(r.ueber <= 0, `nichts steht über (${r.ueber}px)`);
|
||||
|
||||
console.log(" Abschnitte:", r.abschnitte.map((a) => `${a.name}(${a.zahl})`).join(" → "));
|
||||
ok(r.abschnitte.length === 4, `vier Abschnitte (${r.abschnitte.length})`);
|
||||
ok(r.abschnitte.map((a) => a.name).join(",") === "DogFather,Manager,Scout,Creator",
|
||||
`Reihenfolge wie auf der Zugangsseite (${r.abschnitte.map((a) => a.name).join(", ")})`);
|
||||
ok(r.abschnitte.map((a) => a.zahl).join(",") === "2,1,2,2",
|
||||
`Anzahl je Rolle (${r.abschnitte.map((a) => a.zahl).join(", ")})`);
|
||||
|
||||
/* Der eigentliche Fehler von vorher: Der GESPERRTE Manager stand ganz
|
||||
unten unter den Creators, weil "aktiv" vor der Rolle sortierte. */
|
||||
/* Der Fehler von vorher: Der GESPERRTE Manager stand ganz unten unter
|
||||
den Creators, weil "aktiv" vor der Rolle sortierte. */
|
||||
const manager = r.abschnitte.find((a) => a.name === "Manager");
|
||||
ok(manager?.personen.includes("BanaStift"),
|
||||
`der gesperrte Manager steht bei den Managern (${manager?.personen.join(", ")})`);
|
||||
const creator = r.abschnitte.find((a) => a.name === "Creator");
|
||||
ok(!creator?.personen.includes("BanaStift"), "und nicht mehr bei den Creators");
|
||||
ok(creator?.personen.join(",") === "SpongBobSchwammKopf,Tili",
|
||||
`innerhalb der Rolle nach Namen (${creator?.personen.join(", ")})`);
|
||||
|
||||
/* Dieselben Zusatztexte wie auf der Zugangsseite -- wer sich eben
|
||||
angemeldet hat, findet hier dieselbe Sprache wieder. */
|
||||
ok(r.abschnitte[2]?.unter === "Eigene Pipeline & Kontakte",
|
||||
`Zusatztext wörtlich von der Zugangsseite ("${r.abschnitte[2]?.unter}")`);
|
||||
}
|
||||
|
||||
console.log("\n3) Der Zustand überlebt das Neuladen");
|
||||
console.log("\n4) Der Zustand überlebt das Neuladen – je Rolle einzeln");
|
||||
{
|
||||
/* Ein Knopf, den man nach jedem Laden neu druecken muss, ist keine
|
||||
Einstellung, sondern eine Zumutung. */
|
||||
await seite.reload({ waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(600);
|
||||
const r = await lesen(seite);
|
||||
ok(r.namen.length === 7, `nach dem Neuladen weiterhin alle (${r.namen.length})`);
|
||||
|
||||
await seite.click("#rest-schalter");
|
||||
/* Nur die Creator zuklappen. Es muss die EINZELNE Einstellung
|
||||
überleben, nicht bloss "alles auf" oder "alles zu". */
|
||||
await klick(seite, "Creator");
|
||||
await seite.waitForTimeout(250);
|
||||
await seite.reload({ waitUntil: "networkidle" });
|
||||
await seite.waitForTimeout(600);
|
||||
const z = await lesen(seite);
|
||||
ok(z.namen.length === 2, `und zugeklappt bleibt zugeklappt (${z.namen.length})`);
|
||||
await seite.waitForTimeout(700);
|
||||
const r = await lesen(seite);
|
||||
console.log(" ", r.abschnitte.map((a) => `${a.name}=${a.auf === "true" ? "auf" : "zu"}`).join(" · "));
|
||||
ok(!r.namen.includes("Tili"), "die zugeklappten Creator bleiben zu");
|
||||
ok(r.namen.includes("Patrick"), "die offenen Scouts bleiben offen");
|
||||
ok(r.abschnitte.find((a) => a.name === "Creator").auf === "false",
|
||||
"und der Zustand steht auch im aria-expanded");
|
||||
}
|
||||
|
||||
console.log("\n4) Verstecken ist eine Ansicht, kein Recht");
|
||||
console.log("\n5) Zuklappen ist eine Ansicht, kein Recht");
|
||||
{
|
||||
/* Der springende Punkt. Wer eingeklappt ist, muss unveraendert
|
||||
arbeiten koennen -- sonst waere aus einer Anzeigeeinstellung
|
||||
versehentlich eine Sperre geworden. */
|
||||
/* Der springende Punkt. Wer zugeklappt ist, muss unverändert arbeiten
|
||||
können -- sonst wäre aus einer Anzeigeeinstellung versehentlich eine
|
||||
Sperre geworden. */
|
||||
const a = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "creator", code: "CODE-TILI-0001" }), redirect: "manual" });
|
||||
ok(a.ok, `Tili kann sich anmelden, obwohl sie ausgeblendet ist (${a.status})`);
|
||||
ok(a.ok, `Tili kann sich anmelden, obwohl zugeklappt (${a.status})`);
|
||||
|
||||
const keks = (a.headers.getSetCookie?.() || []).map((c) => c.split(";")[0]).join("; ");
|
||||
const ich = await (await fetch(BASIS + "/workspace/api/ich", { headers: { cookie: keks } })).json();
|
||||
ok(ich.name === "Tili", `und ist ganz normal angemeldet (${ich.name})`);
|
||||
|
||||
/* Und der Server liefert weiterhin ALLE -- das Kuerzen passiert nur
|
||||
in der Anzeige. Waere es serverseitig, wuerde die naechste
|
||||
Auswertung stillschweigend Personen uebersehen. */
|
||||
/* Und der Server liefert weiterhin ALLE -- das Kürzen passiert nur in
|
||||
der Anzeige. Wäre es serverseitig, würde die nächste Auswertung
|
||||
stillschweigend Personen übersehen. */
|
||||
const dogiA = await fetch(BASIS + "/workspace/api/anmelden", {
|
||||
method: "POST", headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ rolle: "admin", code: "CODE-DOGI-0001" }), redirect: "manual" });
|
||||
@@ -196,13 +222,17 @@ await seite.waitForTimeout(300);
|
||||
await seite.screenshot({ path: "pruef-personen.png", fullPage: true });
|
||||
await kontext.close();
|
||||
|
||||
console.log("\n5) Auf dem Handy genauso");
|
||||
console.log("\n6) Auf dem Handy genauso");
|
||||
{
|
||||
const k = await browser.newContext({ viewport: { width: 390, height: 844 } });
|
||||
const s = await seiteFuer(k);
|
||||
const r = await lesen(s);
|
||||
ok(r.knopfDa, "der Knopf ist auch dort da");
|
||||
ok(r.ueber <= 0, `nichts steht über (${r.ueber}px)`);
|
||||
ok(r.abschnitte.length === 4, `vier Überschriften (${r.abschnitte.length})`);
|
||||
await klick(s, "Creator");
|
||||
await s.waitForTimeout(250);
|
||||
const n = await lesen(s);
|
||||
ok(n.namen.includes("Tili"), "auch dort lässt sich eine Rolle aufklappen");
|
||||
ok(n.ueber <= 0, `nichts steht über (${n.ueber}px)`);
|
||||
await k.close();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user