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,269 @@
|
||||
/* UI-Integrationstest (jsdom) gegen einen echten Server-Subprozess.
|
||||
*
|
||||
* Lauf: node tests/test_ui.js (jsdom muss installierbar sein, z.B.
|
||||
* NODE_PATH=/pfad/zu/node_modules node tests/test_ui.js)
|
||||
*
|
||||
* Der Test laeuft im offenen Modus (frische DB ohne Benutzer).
|
||||
*/
|
||||
const path = require("path");
|
||||
const BASE = path.join(__dirname, "..");
|
||||
|
||||
let jsdom;
|
||||
try {
|
||||
jsdom = require("jsdom");
|
||||
} catch (_) {
|
||||
console.error("jsdom fehlt. Installieren: npm install jsdom@22.1.0 (z.B. in /tmp/tp_test) und mit NODE_PATH=<dort>/node_modules starten.");
|
||||
process.exit(2);
|
||||
}
|
||||
const { JSDOM } = jsdom;
|
||||
|
||||
const { spawn } = require("child_process");
|
||||
const fs = require("fs");
|
||||
const http = require("http");
|
||||
|
||||
const STATE = "/tmp/tp_state_ui";
|
||||
const PORT = 8233;
|
||||
const BASE_URL = `http://127.0.0.1:${PORT}`;
|
||||
|
||||
let pass = 0, fail = 0;
|
||||
let jsErrors = [];
|
||||
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));
|
||||
|
||||
async function waitFor(dom, fn, timeout = 4000) {
|
||||
const t0 = Date.now();
|
||||
while (Date.now() - t0 < timeout) {
|
||||
if (fn(dom)) return true;
|
||||
await sleep(80);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
function apiGet(p) {
|
||||
return new Promise((resolve, reject) => {
|
||||
http.get(BASE_URL + p, res => {
|
||||
let d = "";
|
||||
res.on("data", c => d += c);
|
||||
res.on("end", () => resolve(JSON.parse(d)));
|
||||
}).on("error", reject);
|
||||
});
|
||||
}
|
||||
|
||||
function cleanState() {
|
||||
fs.rmSync(STATE, { recursive: true, force: true });
|
||||
fs.mkdirSync(STATE, { recursive: true });
|
||||
}
|
||||
|
||||
async function main() {
|
||||
cleanState();
|
||||
const server = spawn("python3", [path.join(BASE, "server.py")], {
|
||||
env: { ...process.env, TRAINING_STATE_DIR: STATE, PORT: String(PORT) },
|
||||
cwd: BASE, stdio: "ignore",
|
||||
});
|
||||
let window;
|
||||
try {
|
||||
for (let i = 0; i < 60; i++) {
|
||||
try { await apiGet("/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");
|
||||
|
||||
const dom = new JSDOM(html, { runScripts: "dangerously", url: BASE_URL + "/" });
|
||||
window = dom.window;
|
||||
window.errors = [];
|
||||
jsErrors = window.errors;
|
||||
window.addEventListener("error", e => window.errors.push(e.message || String(e)));
|
||||
window.fetch = (url, opts) => fetch(BASE_URL + url, opts);
|
||||
window.eval(appJs);
|
||||
|
||||
check("init: 3 Aktivitaets-Karten", await waitFor(dom, d =>
|
||||
d.window.document.querySelectorAll(".act-card").length === 3));
|
||||
|
||||
// --- Ersteinrichtungs-Wizard abschliessen (Einzelmodus) ----------------
|
||||
const gate = () => window.document.querySelector("#setup-gate");
|
||||
await waitFor(dom, d => !d.window.document.querySelector("#setup-gate").hidden);
|
||||
check("Setup: Sprache kommt zuerst", await waitFor(dom, d =>
|
||||
gate().querySelector(".wizard-title")
|
||||
&& gate().querySelector(".wizard-title").textContent === "Sprache festlegen"));
|
||||
check("Setup: Fortschrittsleiste oben",
|
||||
gate().querySelectorAll(".wp-step").length === 3
|
||||
&& gate().querySelectorAll(".wp-step.is-current").length === 1
|
||||
&& gate().querySelectorAll(".wp-step.is-done").length === 0);
|
||||
Array.from(gate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Weiter").click();
|
||||
await waitFor(dom, d => gate().querySelectorAll(".setup-option").length === 2);
|
||||
check("Setup: Fortschritt aktualisiert",
|
||||
gate().querySelectorAll(".wp-step.is-done").length === 1
|
||||
&& gate().querySelectorAll(".wp-step.is-current").length === 1);
|
||||
const setupOptions = () => Array.from(gate().querySelectorAll(".setup-option"));
|
||||
check("Setup: zwei gleichwertige Optionen",
|
||||
setupOptions().some(b => b.textContent.includes("Mehrbenutzermodus"))
|
||||
&& setupOptions().some(b => b.textContent.includes("Einzelmodus")));
|
||||
setupOptions().find(b => b.textContent.includes("Einzelmodus")).click();
|
||||
Array.from(gate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Weiter").click();
|
||||
await waitFor(dom, d => gate().querySelector(".accent-row"));
|
||||
Array.from(gate().querySelectorAll(".wizard-nav .btn"))
|
||||
.find(b => b.textContent === "Einrichtung abschließen").click();
|
||||
check("Setup: Gate geschlossen nach Wizard", await waitFor(dom, d =>
|
||||
d.window.document.querySelector("#setup-gate").hidden));
|
||||
|
||||
const gymCard = () => window.document.querySelectorAll(".act-card")[0];
|
||||
gymCard().querySelector(".btn-primary").click();
|
||||
check("gym: Session gestartet", await waitFor(dom, d =>
|
||||
d.window.document.querySelectorAll(".act-card")[0].querySelectorAll(".set-row").length > 0));
|
||||
|
||||
const firstBlock = () => gymCard().querySelector(".ex-block");
|
||||
const chk1 = () => firstBlock().querySelectorAll(".set-check")[0];
|
||||
check("Set-Button mit Label 'Set' + Box",
|
||||
chk1().querySelector(".set-check-label").textContent === "Set"
|
||||
&& !!chk1().querySelector(".set-check-box"));
|
||||
check("Set-Button: Box leer (nicht abgehakt)", chk1().querySelector(".set-check-box").textContent === "");
|
||||
firstBlock().querySelectorAll(".set-check")[2].click();
|
||||
check("Set-Kette: 1..3 aktiv mit Haken", await waitFor(dom, d => {
|
||||
const c = d.window.document.querySelectorAll(".act-card")[0].querySelectorAll(".set-check");
|
||||
return c[0].classList.contains("is-done") && c[2].classList.contains("is-done")
|
||||
&& !c[3].classList.contains("is-done")
|
||||
&& c[0].querySelector(".set-check-box").textContent === "✓";
|
||||
}));
|
||||
const wIn = () => firstBlock().querySelectorAll(".set-row")[0].querySelector(".field-input");
|
||||
wIn().value = "18,5";
|
||||
wIn().dispatchEvent(new window.Event("input", { bubbles: true }));
|
||||
const wBefore = wIn().value;
|
||||
chk1().click();
|
||||
check("Set-Klick veraendert Gewicht nicht", wIn().value === wBefore);
|
||||
firstBlock().querySelectorAll(".set-check")[2].click();
|
||||
|
||||
const picker = () => gymCard().querySelector(".ex-picker");
|
||||
const pullEx = "Pull/Latzug / Klimmzug-Maschine";
|
||||
picker().value = pullEx;
|
||||
picker().dispatchEvent(new window.Event("change", { bubbles: true }));
|
||||
check("Uebung aus anderem Tag hinzugefuegt", await waitFor(dom, d => {
|
||||
const blocks = d.window.document.querySelectorAll(".act-card")[0].querySelectorAll(".ex-block");
|
||||
const last = blocks[blocks.length - 1];
|
||||
return last.querySelector(".ex-name").textContent === "Latzug / Klimmzug-Maschine";
|
||||
}));
|
||||
const blocks = () => gymCard().querySelectorAll(".ex-block");
|
||||
const addedBlock = blocks()[blocks().length - 1];
|
||||
const rmBtn = addedBlock.querySelector(".ex-remove");
|
||||
check("Entfernen-Knopf fuer hinzugefuegte Uebung", !!rmBtn);
|
||||
rmBtn.click();
|
||||
check("Uebung entfernt", await waitFor(dom, d =>
|
||||
!Array.from(d.window.document.querySelectorAll(".act-card")[0].querySelectorAll(".ex-block"))
|
||||
.some(b => b.querySelector(".ex-name").textContent === "Latzug / Klimmzug-Maschine")));
|
||||
|
||||
gymCard().querySelector(".session-head .btn-primary").click();
|
||||
check("abschliessen: Karte zurueck auf Start", await waitFor(dom, d => {
|
||||
const c = d.window.document.querySelectorAll(".act-card")[0];
|
||||
return !!c.querySelector(".start-box");
|
||||
}));
|
||||
let done = await apiGet("/api/sessions?status=done");
|
||||
check("Verlauf (API): 1 abgeschlossene Session", done.length === 1);
|
||||
|
||||
window.document.querySelector('.tab[data-view="history"]').click();
|
||||
await waitFor(dom, d => d.window.document.querySelectorAll(".hist-card").length === 1);
|
||||
const histCard = () => window.document.querySelector(".hist-card");
|
||||
check("Verlauf: Haken-Box zeigt Abhak-Status",
|
||||
!!histCard().querySelector(".set-done-box")
|
||||
&& histCard().querySelector(".set-done-box").textContent === "✓");
|
||||
check("Verlauf: Abgehaktes Set mit Gewicht", await waitFor(dom, d =>
|
||||
d.window.document.querySelector(".hist-sets").textContent.includes("18,5")));
|
||||
|
||||
window.document.querySelector('.tab[data-view="settings"]').click();
|
||||
await waitFor(dom, d => !!d.window.document.querySelector(".accent-row"));
|
||||
check("Einstellungen: Theme-Buttons",
|
||||
Array.from(window.document.querySelectorAll("#view-settings .day-picker .day-btn"))
|
||||
.filter(b => ["Dunkel", "System", "Hell"].includes(b.textContent)).length === 3);
|
||||
check("Einstellungen: Akzent-Swatches", window.document.querySelectorAll(".accent-swatch").length >= 5);
|
||||
check("Einstellungen: Toggles + Selects",
|
||||
window.document.querySelectorAll("#view-settings .toggle-input").length === 4
|
||||
&& window.document.querySelectorAll("#view-settings .settings-select-row").length === 2);
|
||||
|
||||
const themeBtns = () => Array.from(window.document.querySelectorAll("#view-settings .day-picker .day-btn"));
|
||||
// OLED-Toggle bei dunklem Theme -> reines Schwarz
|
||||
Array.from(window.document.querySelectorAll("#view-settings .toggle-row"))
|
||||
.find(r => r.querySelector("strong").textContent === "OLED-Dark")
|
||||
.querySelector(".toggle-input").click();
|
||||
await waitFor(dom, d => d.window.document.documentElement.dataset.theme === "oled");
|
||||
check("OLED-Toggle: dunkles Theme wird pure black",
|
||||
window.document.documentElement.dataset.theme === "oled");
|
||||
themeBtns().find(b => b.textContent === "Hell").click();
|
||||
await waitFor(dom, d => d.window.document.documentElement.dataset.theme === "light");
|
||||
check("Theme 'Hell' gesetzt", window.document.documentElement.dataset.theme === "light");
|
||||
|
||||
const swatches = () => Array.from(window.document.querySelectorAll(".accent-swatch"));
|
||||
swatches().find(s => s.getAttribute("aria-label") === "Akzent green").click();
|
||||
await waitFor(dom, d => d.window.document.documentElement.dataset.accent === "green");
|
||||
check("Akzent 'green' gesetzt", window.document.documentElement.dataset.accent === "green");
|
||||
|
||||
const toggles = () => Array.from(window.document.querySelectorAll(".toggle-row"));
|
||||
const tipRow = toggles().find(r => r.querySelector("strong").textContent === "Übungs-Hinweise");
|
||||
tipRow.querySelector(".toggle-input").click();
|
||||
await waitFor(dom, d => d.window.document.querySelector("#view-today").classList.contains("no-tips"));
|
||||
check("showTips aus: no-tips Klasse", window.document.querySelector("#view-today").classList.contains("no-tips"));
|
||||
|
||||
const resetRow = toggles().find(r => r.querySelector("strong").textContent === "Letztes Training vorbefüllen");
|
||||
resetRow.querySelector(".toggle-input").click();
|
||||
// Auf Server-Zustand warten (DOM-Check war falsch: [0] ist der OLED-Toggle)
|
||||
let arOk = false;
|
||||
for (let i = 0; i < 80; i++) {
|
||||
if ((await apiGet("/api/settings")).autoReset === true) { arOk = true; break; }
|
||||
await sleep(100);
|
||||
}
|
||||
check("Einstellung 'Letztes Training' serverseitig aktiv", arOk);
|
||||
|
||||
window.document.querySelector('.tab[data-view="today"]').click();
|
||||
gymCard().querySelector(".btn-primary").click();
|
||||
// Auto-Reset serverseitig pruefen (deterministisch statt DOM-Timing)
|
||||
let autoOk = false;
|
||||
for (let i = 0; i < 120; i++) {
|
||||
const act = await apiGet("/api/sessions?status=active");
|
||||
if (act.length && act[0].entries.length === 1
|
||||
&& act[0].entries[0].weight === "18,5") { autoOk = true; break; }
|
||||
await sleep(100);
|
||||
}
|
||||
check("Auto-Reset: 1 gefuellter Satz geladen", autoOk);
|
||||
await waitFor(dom, d => d.window.document.querySelectorAll(".act-card")[0]
|
||||
.querySelectorAll(".set-row").length > 0, 9000);
|
||||
check("Auto-Reset: Satz nicht abgehakt",
|
||||
gymCard().querySelector(".ex-block .set-check").querySelector(".set-check-box").textContent === "");
|
||||
check("Auto-Reset: 'Alles zuruecksetzen'-Knopf da",
|
||||
!!gymCard().querySelector(".session-head .btn[title*='letzte']"));
|
||||
check("Hinweiszeile ausgeblendet",
|
||||
!gymCard().querySelector(".ex-block .ex-hint"));
|
||||
|
||||
// --- Uebungen per Drag & Drop sortieren --------------------------------
|
||||
gymCard().querySelectorAll(".ex-block")[0]
|
||||
.dispatchEvent(new window.Event("dragstart", { bubbles: true }));
|
||||
const target = gymCard().querySelectorAll(".ex-block")[1];
|
||||
target.dispatchEvent(new window.Event("drop", { bubbles: true }));
|
||||
check("Uebungen: Drag & Drop sortiert",
|
||||
Array.from(gymCard().querySelectorAll(".ex-block"))[0]
|
||||
.querySelector(".ex-name").textContent !== "Bankdrücken KH (je Hand)");
|
||||
|
||||
// --- Wochen-Uebersicht ---------------------------------------------------
|
||||
window.document.querySelector('.tab[data-view="history"]').click();
|
||||
await waitFor(dom, d => d.window.document.querySelectorAll(".hist-card").length >= 1);
|
||||
Array.from(window.document.querySelectorAll(".day-picker .day-btn"))
|
||||
.find(b => b.textContent === "Woche").click();
|
||||
check("Wochen-Uebersicht: Raster mit 7 Tagen",
|
||||
await waitFor(dom, d => d.window.document.querySelectorAll(".week-day").length === 7));
|
||||
|
||||
} finally {
|
||||
server.kill("SIGTERM");
|
||||
}
|
||||
if (jsErrors && jsErrors.length) {
|
||||
console.log("JS-Fehler im Browser:", jsErrors.slice(0, 5).join(" | "));
|
||||
fail += jsErrors.length;
|
||||
}
|
||||
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