Startpunkt 02.10.2026: aktueller Stand des Trainingsplans
Historie bewusst neu aufgesetzt, ein einziger Commit als neuer Ausgangspunkt.
This commit is contained in:
commit
8fc77212cb
18 files changed
+10863
No files matched your search
@@ -0,0 +1,255 @@
|
||||
/* UI-Test Auth-Gate (jsdom) gegen einen echten Server.
|
||||
*
|
||||
* Lauf: NODE_PATH=<...>/node_modules node tests/test_ui_auth.js
|
||||
*/
|
||||
const path = require("path");
|
||||
const BASE = path.join(__dirname, "..");
|
||||
const { JSDOM } = require("jsdom");
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
|
||||
const STATE = "/tmp/tp_state_ui_auth";
|
||||
const PORT = 8237;
|
||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
function check(name, cond, detail = "") {
|
||||
if (cond) { pass++; console.log(" ok " + name); }
|
||||
else { fail++; console.log("FAIL " + name + " " + detail); }
|
||||
}
|
||||
const sleep = ms => new Promise(r => setTimeout(r, ms));
|
||||
|
||||
function api(method, p, body, token) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const data = body ? JSON.stringify(body) : null;
|
||||
const req = http.request(BASE_URL + p, {
|
||||
method,
|
||||
headers: Object.assign(data ? {
|
||||
"Content-Type": "application/json",
|
||||
"Content-Length": Buffer.byteLength(data),
|
||||
} : {}, token ? { Authorization: "Bearer " + token } : {}),
|
||||
}, res => {
|
||||
let d = "";
|
||||
res.on("data", c => d += c);
|
||||
res.on("end", () => resolve({ status: res.statusCode, body: JSON.parse(d || "{}") }));
|
||||
});
|
||||
if (data) req.write(data);
|
||||
req.end();
|
||||
req.on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadApp(html, appJs, token) {
|
||||
const dom = new JSDOM(html, { runScripts: "dangerously", url: BASE_URL + "/" });
|
||||
const { window } = dom;
|
||||
window.errors = [];
|
||||
window.addEventListener("error", e => window.errors.push(e.message || String(e)));
|
||||
window.fetch = (url, opts) => fetch(BASE_URL + url, opts);
|
||||
if (token) window.localStorage.setItem("tp_token", token);
|
||||
window.eval(appJs);
|
||||
return window;
|
||||
}
|
||||
|
||||
async function main() {
|
||||
fs.rmSync(STATE, { recursive: true, force: true });
|
||||
fs.mkdirSync(STATE, { recursive: true });
|
||||
const server = spawn("python3", [path.join(BASE, "server.py")], {
|
||||
env: { ...process.env, TRAINING_STATE_DIR: STATE, PORT: String(PORT) },
|
||||
cwd: BASE, stdio: "ignore",
|
||||
});
|
||||
try {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try { await api("GET", "/api/meta"); break; }
|
||||
catch (_) { await sleep(200); }
|
||||
}
|
||||
const html = fs.readFileSync(path.join(BASE, "index.html"), "utf8")
|
||||
.replace(/<script src="\/app.js"><\/script>/, "");
|
||||
const appJs = fs.readFileSync(path.join(BASE, "app.js"), "utf8");
|
||||
|
||||
// --- Szenario 0: Ersteinrichtungs-Wizard (offener Modus) ---------------
|
||||
const winSetup = await loadApp(html, appJs, null);
|
||||
await sleep(1000);
|
||||
const sgate = () => winSetup.document.querySelector("#setup-gate");
|
||||
check("Setup: Gate im offenen Modus sichtbar",
|
||||
!!sgate() && !sgate().hidden);
|
||||
check("Setup: Sprache kommt zuerst",
|
||||
sgate().querySelector(".wizard-title")
|
||||
&& sgate().querySelector(".wizard-title").textContent === "Sprache festlegen");
|
||||
Array.from(sgate().querySelectorAll(".day-btn"))
|
||||
.find(b => b.textContent === "English").click();
|
||||
await sleep(200);
|
||||
check("Setup: Wizard wechselt auf Englisch",
|
||||
Array.from(sgate().querySelectorAll(".wizard-nav .btn"))
|
||||
.some(b => b.textContent === "Next"));
|
||||
Array.from(sgate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Next").click();
|
||||
await sleep(200);
|
||||
const setupOptions = () => Array.from(sgate().querySelectorAll(".setup-option"));
|
||||
check("Setup: zwei gleichwertige Optionen",
|
||||
setupOptions().some(b => b.textContent.includes("Multi-user mode"))
|
||||
&& setupOptions().some(b => b.textContent.includes("Single-user mode")));
|
||||
check("Setup: Mehrbenutzer-Infotext",
|
||||
setupOptions().some(b => b.textContent.includes("administrator")
|
||||
&& b.textContent.toLowerCase().includes("single user")));
|
||||
setupOptions().find(b => b.textContent.includes("Multi-user mode")).click();
|
||||
await sleep(150);
|
||||
check("Setup: Formular nach Mehrbenutzermodus-Klick",
|
||||
!!sgate().querySelector("input#setup-user")
|
||||
&& !!sgate().querySelector("input#setup-pass"));
|
||||
// Einzelmodus waehlen -> Weiter -> Darstellung -> abschliessen
|
||||
setupOptions().find(b => b.textContent.includes("Single-user mode")).click();
|
||||
Array.from(sgate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Next").click();
|
||||
await sleep(200);
|
||||
check("Setup: Darstellung-Schritt (Theme + Akzent)",
|
||||
sgate().querySelector(".wizard-title").textContent === "Appearance"
|
||||
&& !!sgate().querySelector(".accent-row")
|
||||
&& !!sgate().querySelector(".toggle-row"));
|
||||
Array.from(sgate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Finish setup").click();
|
||||
await sleep(400);
|
||||
check("Setup: Gate zu + setupDone + Sprache persistiert",
|
||||
sgate().hidden
|
||||
&& (await api("GET", "/api/settings")).body.setupDone === true
|
||||
&& (await api("GET", "/api/settings")).body.language === "en");
|
||||
|
||||
const reg = await api("POST", "/api/register", { username: "alex", password: "geheim" });
|
||||
check("API: Register", reg.status === 200 && !!reg.body.token, reg.body);
|
||||
|
||||
// --- Szenario 1: ohne Token -> Auth-Gate, Inline-Fehler, Login in-place ---
|
||||
const winNoToken = await loadApp(html, appJs, null);
|
||||
await sleep(1200);
|
||||
const gate = () => winNoToken.document.querySelector("#auth-gate");
|
||||
check("ohne Token: Auth-Gate sichtbar",
|
||||
gate() && !gate().hidden && winNoToken.document.body.classList.contains("no-auth"));
|
||||
check("ohne Token: Login-Formular + Felder",
|
||||
!!gate().querySelector('input#auth-user')
|
||||
&& !!gate().querySelector('input#auth-pass')
|
||||
&& !!gate().querySelector(".btn-primary"));
|
||||
check("ohne Token: Login-Screen in Server-Sprache (en)",
|
||||
Array.from(gate().querySelectorAll(".btn"))
|
||||
.some(b => b.textContent === "Log in")
|
||||
&& gate().querySelector(".auth-sub").textContent.includes("Log in to see"));
|
||||
// falsches Kennwort -> Inline-Fehler
|
||||
gate().querySelector('input#auth-user').value = "alex";
|
||||
gate().querySelector('input#auth-pass').value = "falsch";
|
||||
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Log in").click();
|
||||
await sleep(500);
|
||||
const authErr = gate().querySelector(".auth-error");
|
||||
check("ohne Token: Inline-Fehler bei falschem Kennwort",
|
||||
authErr && !authErr.hidden
|
||||
&& authErr.textContent.includes("Incorrect username or password"),
|
||||
authErr && authErr.textContent);
|
||||
// korrektes Kennwort -> Login wechselt OHNE Reload in-place
|
||||
gate().querySelector('input#auth-pass').value = "geheim";
|
||||
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Log in").click();
|
||||
let loggedIn = false;
|
||||
for (let i = 0; i < 60; i++) {
|
||||
const og = winNoToken.document.querySelector("#setup-gate");
|
||||
if (og && !og.hidden && winNoToken.document.body.classList.contains("no-auth")) {
|
||||
loggedIn = true; // Onboarding-Gate erscheint -> Anmeldung war erfolgreich
|
||||
break;
|
||||
}
|
||||
if (winNoToken.document.querySelectorAll(".act-card").length === 3
|
||||
&& winNoToken.document.querySelector("#user-chip").textContent === "alex") {
|
||||
loggedIn = true;
|
||||
break;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
check("Login: wechselt ohne Reload in den eingeloggten Zustand", loggedIn);
|
||||
// Onboarding abschliessen (im selben Fenster)
|
||||
const ogate = () => winNoToken.document.querySelector("#setup-gate");
|
||||
if (ogate() && !ogate().hidden) {
|
||||
Array.from(ogate().querySelectorAll(".day-btn"))
|
||||
.find(b => b.textContent === "German").click();
|
||||
await sleep(150);
|
||||
Array.from(ogate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Weiter").click();
|
||||
await sleep(200);
|
||||
Array.from(ogate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Einrichtung abschließen").click();
|
||||
await sleep(400);
|
||||
}
|
||||
check("Login: Onboarding abgeschlossen, Heute-Ansicht sichtbar",
|
||||
ogate() && ogate().hidden
|
||||
&& winNoToken.document.querySelectorAll(".act-card").length === 3
|
||||
&& winNoToken.document.querySelector("#user-chip").textContent === "alex");
|
||||
check("Login: Abmelden-Knopf sichtbar",
|
||||
!winNoToken.document.querySelector("#logout-btn").hidden);
|
||||
check("Login: kein Auth-Gate mehr",
|
||||
winNoToken.document.querySelector("#auth-gate").hidden);
|
||||
|
||||
// --- Szenario 2: frisches Fenster mit Token -> direkt eingeloggt ------
|
||||
const winTok = await loadApp(html, appJs, reg.body.token);
|
||||
await sleep(1200);
|
||||
let tokOk = false;
|
||||
for (let i = 0; i < 50; i++) {
|
||||
if (winTok.document.querySelectorAll(".act-card").length === 3
|
||||
&& winTok.document.querySelector("#user-chip").textContent === "alex") {
|
||||
tokOk = true;
|
||||
break;
|
||||
}
|
||||
await sleep(100);
|
||||
}
|
||||
check("mit Token: Heute-Ansicht geladen (kein Onboarding mehr)", tokOk);
|
||||
check("mit Token: kein Auth-Gate", winTok.document.querySelector("#auth-gate").hidden);
|
||||
|
||||
// --- Szenario 3: Einstellungen mit Konto- und Benutzer-Bereich --------
|
||||
winTok.document.querySelector('.tab[data-view="settings"]').click();
|
||||
let secTitles = [];
|
||||
for (let i = 0; i < 40; i++) {
|
||||
secTitles = Array.from(winTok.document.querySelectorAll(".data-section h3"))
|
||||
.map(h => h.textContent);
|
||||
if (secTitles.includes("Konto") && secTitles.includes("Benutzer")) break;
|
||||
await sleep(100);
|
||||
}
|
||||
check("Einstellungen: Konto-Bereich (Kennwort aendern)",
|
||||
secTitles.includes("Konto")
|
||||
&& Array.from(winTok.document.querySelectorAll(".data-section"))
|
||||
.some(s => s.querySelector("h3").textContent === "Konto"
|
||||
&& !!s.querySelector('input[placeholder="Aktuelles Kennwort"]')
|
||||
&& !!s.querySelector('input[placeholder="Neues Kennwort"]')));
|
||||
check("Einstellungen: nur ein Benutzer -> Hinweis statt Liste", await (async () => {
|
||||
for (let i = 0; i < 40; i++) {
|
||||
if (Array.from(winTok.document.querySelectorAll(".user-row"))
|
||||
.some(r => r.textContent.includes("einzige"))) return true;
|
||||
await sleep(100);
|
||||
}
|
||||
return false;
|
||||
})());
|
||||
// Zweiten Benutzer anlegen -> Liste zeigt ihn, Admin bleibt draussen
|
||||
const createdB = await api("POST", "/api/users",
|
||||
{ username: "berta", password: "geheim" }, reg.body.token);
|
||||
check("API: Owner legt Berta an", createdB.status === 200, createdB.body);
|
||||
winTok.document.querySelector('.tab[data-view="today"]').click();
|
||||
winTok.document.querySelector('.tab[data-view="settings"]').click();
|
||||
await sleep(400);
|
||||
const rows = () => Array.from(winTok.document.querySelectorAll(".user-row"));
|
||||
check("Einstellungen: Berta in Liste, Admin nicht aufgelistet",
|
||||
rows().length === 1 && rows()[0].textContent.includes("berta")
|
||||
&& !rows().some(r => r.textContent.includes("alex")));
|
||||
check("Einstellungen: Kennwort-Knopf pro Benutzer",
|
||||
Array.from(rows()[0].querySelectorAll(".btn"))
|
||||
.some(b => b.textContent === "Kennwort ändern"));
|
||||
|
||||
// --- Szenario 4: API-Login-Faelle --------------------------------------
|
||||
check("API: Login mit falschem Passwort -> 401",
|
||||
(await api("POST", "/api/login", { username: "alex", password: "falsch" })).status === 401);
|
||||
check("API: Login korrekt -> Token",
|
||||
(await api("POST", "/api/login", { username: "alex", password: "geheim" })).status === 200);
|
||||
|
||||
const errs = winNoToken.errors.concat(winTok.errors);
|
||||
if (errs.length) {
|
||||
console.log("JS-Fehler:", errs.slice(0, 5).join(" | "));
|
||||
fail += errs.length;
|
||||
}
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
}
|
||||
console.log(`\n${pass} ok, ${fail} fail`);
|
||||
process.exit(fail ? 1 : 0);
|
||||
}
|
||||
|
||||
main().catch(e => { console.error(e); process.exit(1); });
|
||||
Reference in new issue
Block a user