Image bauen und in die Registry schieben / build (push) Successful in 6s
- Sortieren nur ueber einen kleinen Griff (Punkt-Icon) links im Uebungskopf; das ganze Element ist nicht mehr draggable (kein versehentliches Ziehen beim Tippen/Auswaehlen) - Beim Session-Start wird die Uebungs-Reihenfolge der letzten abgeschlossenen Session desselben Tags uebernommen (server-seitig in create_session, in session.fields.exerciseOrder) - Tests: 170 Checks gruen (40+18+52+32+28)
269 lines
13 KiB
JavaScript
269 lines
13 KiB
JavaScript
/* 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().querySelector(".accent-row"));
|
|
check("Setup: Fortschritt aktualisiert",
|
|
gate().querySelectorAll(".wp-step.is-done").length === 1
|
|
&& gate().querySelectorAll(".wp-step.is-current").length === 1);
|
|
Array.from(gate().querySelectorAll(".wizard-nav .btn"))
|
|
.find(b => b.textContent === "Weiter").click();
|
|
await waitFor(dom, d => gate().querySelectorAll(".setup-option").length === 2);
|
|
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 === "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 (Griff) sortieren ------------------------
|
|
gymCard().querySelectorAll(".ex-block")[0].querySelector(".ex-grip")
|
|
.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); }); |