Wizard-Texte, Admin-Onboarding ueberspringen, kompakte Set-Zeile mobil
Image bauen und in die Registry schieben / build (push) Successful in 6s
Image bauen und in die Registry schieben / build (push) Successful in 6s
- Wizard: Sprach-Hinweis gekuerzt, Repo-Link nur beim Erst-Setup (nicht im Onboarding), Hinweis 'erster Benutzer wird Administrator' - Erster Benutzer (Admin) ist automatisch onboarded und erbt die festgelegten Sprache/Design-Einstellungen (kein erneuter Wizard); Einstellungen werden nach Admin-Anlage sofort frisch gerendert - Set-Zeile mobil-tauglich: nowrap, width 100%, kg/reps-Inputs flexibel (flex 1, min-width 0), Einheit kompakt im Feld, 'S1' statt 'Set 1', Reset als SVG-Icon, kleinere Buttons - kein horizontales Scrollen - Tests: 169 Checks gruen (39+18+52+32+28)
This commit is contained in:
1 parent
135425c781
commit
6d57200032
5 files changed
+82
-47
No files matched your search
@@ -158,8 +158,9 @@ const LANG = {
|
|||||||
prev_week: "Vorige Woche", next_week: "Nächste Woche", week_of: "Woche vom",
|
prev_week: "Vorige Woche", next_week: "Nächste Woche", week_of: "Woche vom",
|
||||||
wizard_done: "Alles erledigt - die App ist bereit.",
|
wizard_done: "Alles erledigt - die App ist bereit.",
|
||||||
wizard_user_created: "Benutzer '{u}' angelegt.",
|
wizard_user_created: "Benutzer '{u}' angelegt.",
|
||||||
wizard_lang_hint: "Bereit loszulegen? Anleitung, Demo-Daten und der Übungsplan stehen im Repo:",
|
wizard_lang_hint: "Anleitung, Demo-Daten und der Übungsplan stehen im Repo.",
|
||||||
wizard_user_later: "Der Benutzer wird beim Abschließen angelegt - du kannst dich im Wizard bis dahin umentscheiden.",
|
wizard_user_later: "Der Benutzer wird beim Abschließen angelegt - bis dahin kannst du dich umentscheiden.",
|
||||||
|
wizard_admin_hint: "Der erste Benutzer wird Administrator (verwaltet Konten und Kennwörter).",
|
||||||
logged_in: "Angemeldet.", user_created_toast: "Benutzer angelegt.",
|
logged_in: "Angemeldet.", user_created_toast: "Benutzer angelegt.",
|
||||||
app_load_fail: "App konnte nicht geladen werden: {m}",
|
app_load_fail: "App konnte nicht geladen werden: {m}",
|
||||||
},
|
},
|
||||||
@@ -275,8 +276,9 @@ const LANG = {
|
|||||||
prev_week: "Previous week", next_week: "Next week", week_of: "Week of",
|
prev_week: "Previous week", next_week: "Next week", week_of: "Week of",
|
||||||
wizard_done: "All done - the app is ready.",
|
wizard_done: "All done - the app is ready.",
|
||||||
wizard_user_created: "User '{u}' created.",
|
wizard_user_created: "User '{u}' created.",
|
||||||
wizard_lang_hint: "Ready to get started? The guide, demo data and the exercise plan are in the repo:",
|
wizard_lang_hint: "Guide, demo data and the exercise plan live in the repo.",
|
||||||
wizard_user_later: "The user will be created when you finish - you can still change your mind until then.",
|
wizard_user_later: "The user will be created when you finish - you can still change your mind until then.",
|
||||||
|
wizard_admin_hint: "The first user becomes the administrator (manages accounts and passwords).",
|
||||||
logged_in: "Logged in.", user_created_toast: "User created.",
|
logged_in: "Logged in.", user_created_toast: "User created.",
|
||||||
app_load_fail: "Could not load the app: {m}",
|
app_load_fail: "Could not load the app: {m}",
|
||||||
},
|
},
|
||||||
@@ -908,7 +910,7 @@ function buildSetRow(a, model, ex, entry, i) {
|
|||||||
chk.type = "button";
|
chk.type = "button";
|
||||||
chk.setAttribute("aria-pressed", entry.done ? "true" : "false");
|
chk.setAttribute("aria-pressed", entry.done ? "true" : "false");
|
||||||
chk.setAttribute("aria-label", t("set_done_aria", { n: i + 1 }));
|
chk.setAttribute("aria-label", t("set_done_aria", { n: i + 1 }));
|
||||||
chk.appendChild(el("span", "set-check-label", "Set"));
|
chk.appendChild(el("span", "set-check-label", "S"));
|
||||||
chk.appendChild(el("span", "set-check-num", String(i + 1)));
|
chk.appendChild(el("span", "set-check-num", String(i + 1)));
|
||||||
chk.appendChild(el("span", "set-check-box", entry.done ? "✓" : ""));
|
chk.appendChild(el("span", "set-check-box", entry.done ? "✓" : ""));
|
||||||
chk.addEventListener("click", () => toggleSetDone(a, model, ex.key, i, chk));
|
chk.addEventListener("click", () => toggleSetDone(a, model, ex.key, i, chk));
|
||||||
@@ -930,7 +932,7 @@ function buildSetRow(a, model, ex, entry, i) {
|
|||||||
entry.wInput = wBox.querySelector("input");
|
entry.wInput = wBox.querySelector("input");
|
||||||
entry.rInput = rBox.querySelector("input");
|
entry.rInput = rBox.querySelector("input");
|
||||||
|
|
||||||
const reset = el("button", "btn btn-small", t("reset_set"));
|
const reset = el("button", "icon-btn", resetSvg());
|
||||||
reset.type = "button";
|
reset.type = "button";
|
||||||
reset.setAttribute("aria-label", t("reset_set_title"));
|
reset.setAttribute("aria-label", t("reset_set_title"));
|
||||||
reset.addEventListener("click", () => resetSet(a, model, ex.key, i, entry));
|
reset.addEventListener("click", () => resetSet(a, model, ex.key, i, entry));
|
||||||
@@ -945,6 +947,15 @@ function buildSetRow(a, model, ex, entry, i) {
|
|||||||
return row;
|
return row;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function resetSvg() {
|
||||||
|
const svg = el("span", "");
|
||||||
|
svg.innerHTML = '<svg viewBox="0 0 16 16" width="14" height="14" fill="none" stroke="currentColor" stroke-width="1.8" aria-hidden="true">'
|
||||||
|
+ '<circle cx="8" cy="8" r="5" stroke-dasharray="23.5 8" transform="rotate(-55 8 8)"/>'
|
||||||
|
+ '<path d="M10.6 5.3 l2.4 -1.7 l0.7 3.4 l-2.4 -1.7 Z" fill="currentColor"/>'
|
||||||
|
+ '</svg>';
|
||||||
|
return svg;
|
||||||
|
}
|
||||||
|
|
||||||
function makeFieldBox(label, unit, value, onChange, inputmode) {
|
function makeFieldBox(label, unit, value, onChange, inputmode) {
|
||||||
const box = el("label", "field-box");
|
const box = el("label", "field-box");
|
||||||
box.setAttribute("aria-label", label + " in " + unit);
|
box.setAttribute("aria-label", label + " in " + unit);
|
||||||
@@ -1754,12 +1765,14 @@ function renderWizardStep() {
|
|||||||
row.appendChild(b);
|
row.appendChild(b);
|
||||||
}
|
}
|
||||||
body.appendChild(row);
|
body.appendChild(row);
|
||||||
|
if (wizard.mode === "setup") {
|
||||||
body.appendChild(el("p", "auth-sub", t("wizard_lang_hint")));
|
body.appendChild(el("p", "auth-sub", t("wizard_lang_hint")));
|
||||||
const repo = el("a", "btn", "git.somantis.net/J4stMarkus/trainingsplan");
|
const repo = el("a", "btn", "git.somantis.net/J4stMarkus/trainingsplan");
|
||||||
repo.href = "https://git.somantis.net/J4stMarkus/trainingsplan";
|
repo.href = "https://git.somantis.net/J4stMarkus/trainingsplan";
|
||||||
repo.target = "_blank";
|
repo.target = "_blank";
|
||||||
repo.rel = "noopener";
|
repo.rel = "noopener";
|
||||||
body.appendChild(repo);
|
body.appendChild(repo);
|
||||||
|
}
|
||||||
} else if (wizard.step === 1) {
|
} else if (wizard.step === 1) {
|
||||||
// Schritt 1: Darstellung (Theme + OLED + Akzent)
|
// Schritt 1: Darstellung (Theme + OLED + Akzent)
|
||||||
body.appendChild(el("h3", "wizard-title", t("appearance")));
|
body.appendChild(el("h3", "wizard-title", t("appearance")));
|
||||||
@@ -1816,6 +1829,7 @@ function renderWizardStep() {
|
|||||||
form.appendChild(authField(t("username"), "setup-user"));
|
form.appendChild(authField(t("username"), "setup-user"));
|
||||||
form.appendChild(authField(t("password"), "setup-pass"));
|
form.appendChild(authField(t("password"), "setup-pass"));
|
||||||
form.appendChild(el("div", "auth-register", t("wizard_user_later")));
|
form.appendChild(el("div", "auth-register", t("wizard_user_later")));
|
||||||
|
form.appendChild(el("div", "auth-register", t("wizard_admin_hint")));
|
||||||
form.addEventListener("submit", ev => ev.preventDefault());
|
form.addEventListener("submit", ev => ev.preventDefault());
|
||||||
body.appendChild(form);
|
body.appendChild(form);
|
||||||
// Felder in wizard-Zustand schreiben; Finish live aktivieren
|
// Felder in wizard-Zustand schreiben; Finish live aktivieren
|
||||||
@@ -2200,6 +2214,10 @@ add.addEventListener("click", async () => {
|
|||||||
localStorage.setItem("tp_token", r.token);
|
localStorage.setItem("tp_token", r.token);
|
||||||
toast(t("user_created", { u: r.username }), "ok");
|
toast(t("user_created", { u: r.username }), "ok");
|
||||||
await afterAuth();
|
await afterAuth();
|
||||||
|
// Einstellungen frisch rendern, falls gerade geoeffnet
|
||||||
|
if ($("#view-settings").classList.contains("is-active")) {
|
||||||
|
renderSettingsView();
|
||||||
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
if (e.status === 403) {
|
if (e.status === 403) {
|
||||||
toast(eMsg(e), "error");
|
toast(eMsg(e), "error");
|
||||||
|
|||||||
@@ -651,6 +651,8 @@ def create_user(username, password):
|
|||||||
# - Sessions wandern zum Owner (Alt-Daten gehoeren ihm),
|
# - Sessions wandern zum Owner (Alt-Daten gehoeren ihm),
|
||||||
# - Settings (Sprache/Akzent aus dem Wizard) werden KOPIERT, damit der
|
# - Settings (Sprache/Akzent aus dem Wizard) werden KOPIERT, damit der
|
||||||
# Server-Standard (user_id=0) fuer weitere Benutzer erhalten bleibt.
|
# Server-Standard (user_id=0) fuer weitere Benutzer erhalten bleibt.
|
||||||
|
# - Der Admin ist automatisch "onboarded" (Sprache/Darstellung kommen aus
|
||||||
|
# dem Server-Standard, kein erneuter Wizard).
|
||||||
if uid == 1:
|
if uid == 1:
|
||||||
con.execute("UPDATE sessions SET user_id=? WHERE user_id=?",
|
con.execute("UPDATE sessions SET user_id=? WHERE user_id=?",
|
||||||
(uid, OPEN_USER))
|
(uid, OPEN_USER))
|
||||||
@@ -658,6 +660,9 @@ def create_user(username, password):
|
|||||||
"INSERT INTO settings(user_id,key,value) "
|
"INSERT INTO settings(user_id,key,value) "
|
||||||
"SELECT ?, key, value FROM settings WHERE user_id=?",
|
"SELECT ?, key, value FROM settings WHERE user_id=?",
|
||||||
(uid, OPEN_USER))
|
(uid, OPEN_USER))
|
||||||
|
con.execute(
|
||||||
|
"INSERT INTO settings(user_id,key,value) VALUES(?, 'onboarded', 'true')",
|
||||||
|
(uid,))
|
||||||
token = _issue_token(con, uid)
|
token = _issue_token(con, uid)
|
||||||
con.commit()
|
con.commit()
|
||||||
con.close()
|
con.close()
|
||||||
|
|||||||
+26
-20
@@ -251,15 +251,16 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 30px;
|
width: 28px;
|
||||||
height: 30px;
|
height: 28px;
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.icon-btn:hover { border-color: var(--border-hover); color: var(--text); background: var(--bg-hover); }
|
.icon-btn:hover { border-color: var(--border-hover); color: var(--text); background: var(--bg-hover); }
|
||||||
@@ -439,12 +440,13 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
.set-row {
|
.set-row {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: nowrap;
|
flex-wrap: nowrap;
|
||||||
gap: 8px;
|
width: 100%;
|
||||||
|
gap: 6px;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 6px 10px;
|
padding: 6px 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.set-rows { overflow-x: auto; }
|
.set-rows { overflow: hidden; }
|
||||||
|
|
||||||
.set-row.is-done { background: var(--bg-subtle); }
|
.set-row.is-done { background: var(--bg-subtle); }
|
||||||
|
|
||||||
@@ -454,16 +456,17 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
appearance: none;
|
appearance: none;
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
gap: 7px;
|
gap: 4px;
|
||||||
min-height: 34px;
|
min-height: 30px;
|
||||||
padding: 0 12px;
|
padding: 0 8px;
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font: inherit;
|
font: inherit;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
font-size: 13px;
|
font-size: 12px;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.set-check:hover { border-color: var(--border-hover); background: var(--bg-hover); }
|
.set-check:hover { border-color: var(--border-hover); background: var(--bg-hover); }
|
||||||
@@ -477,10 +480,10 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
width: 14px;
|
width: 12px;
|
||||||
height: 14px;
|
height: 12px;
|
||||||
border: 2px solid currentColor;
|
border: 2px solid currentColor;
|
||||||
font-size: 11px;
|
font-size: 10px;
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -502,6 +505,8 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
.field-box {
|
.field-box {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
|
flex: 1 1 0;
|
||||||
|
min-width: 0;
|
||||||
border: 2px solid var(--border);
|
border: 2px solid var(--border);
|
||||||
background: var(--bg-input);
|
background: var(--bg-input);
|
||||||
cursor: text;
|
cursor: text;
|
||||||
@@ -511,15 +516,15 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
.field-box:hover { border-color: var(--border-hover); }
|
.field-box:hover { border-color: var(--border-hover); }
|
||||||
|
|
||||||
.field-input {
|
.field-input {
|
||||||
width: 74px;
|
flex: 1 1 0;
|
||||||
min-width: 60px;
|
min-width: 0;
|
||||||
height: 32px;
|
height: 30px;
|
||||||
padding: 6px 4px 6px 8px;
|
padding: 0 6px;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: transparent;
|
background: transparent;
|
||||||
color: var(--text);
|
color: var(--text);
|
||||||
font-family: var(--font-data);
|
font-family: var(--font-data);
|
||||||
font-size: 15px;
|
font-size: 14px;
|
||||||
font-variant-numeric: tabular-nums;
|
font-variant-numeric: tabular-nums;
|
||||||
outline: 0;
|
outline: 0;
|
||||||
}
|
}
|
||||||
@@ -529,12 +534,13 @@ h3 { font-size: 15px; font-weight: 700; }
|
|||||||
.field-unit {
|
.field-unit {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
padding: 0 8px;
|
padding: 0 4px;
|
||||||
border-left: 1px solid var(--border);
|
border-left: 1px solid var(--border);
|
||||||
color: var(--muted);
|
color: var(--muted);
|
||||||
font-family: var(--font-data);
|
font-family: var(--font-data);
|
||||||
font-size: 12px;
|
font-size: 11px;
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Cardio-Felder */
|
/* Cardio-Felder */
|
||||||
|
|||||||
+1
-1
@@ -122,7 +122,7 @@ async function main() {
|
|||||||
const firstBlock = () => gymCard().querySelector(".ex-block");
|
const firstBlock = () => gymCard().querySelector(".ex-block");
|
||||||
const chk1 = () => firstBlock().querySelectorAll(".set-check")[0];
|
const chk1 = () => firstBlock().querySelectorAll(".set-check")[0];
|
||||||
check("Set-Button mit Label 'Set' + Box",
|
check("Set-Button mit Label 'Set' + Box",
|
||||||
chk1().querySelector(".set-check-label").textContent === "Set"
|
chk1().querySelector(".set-check-label").textContent === "S"
|
||||||
&& !!chk1().querySelector(".set-check-box"));
|
&& !!chk1().querySelector(".set-check-box"));
|
||||||
check("Set-Button: Box leer (nicht abgehakt)", chk1().querySelector(".set-check-box").textContent === "");
|
check("Set-Button: Box leer (nicht abgehakt)", chk1().querySelector(".set-check-box").textContent === "");
|
||||||
firstBlock().querySelectorAll(".set-check")[2].click();
|
firstBlock().querySelectorAll(".set-check")[2].click();
|
||||||
|
|||||||
+27
-21
@@ -126,6 +126,8 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
|
|
||||||
const reg = await api("POST", "/api/register", { username: "alex", password: "geheim" });
|
const reg = await api("POST", "/api/register", { username: "alex", password: "geheim" });
|
||||||
check("API: Register", reg.status === 200 && !!reg.body.token, reg.body);
|
check("API: Register", reg.status === 200 && !!reg.body.token, reg.body);
|
||||||
|
// Admin ist automatisch eingestellt (kein Onboarding), Sprache fuer Tests auf Deutsch
|
||||||
|
await api("PUT", "/api/settings", { language: "de" }, reg.body.token);
|
||||||
|
|
||||||
// --- Szenario 1: ohne Token -> Auth-Gate, Inline-Fehler, Login in-place ---
|
// --- Szenario 1: ohne Token -> Auth-Gate, Inline-Fehler, Login in-place ---
|
||||||
const winNoToken = await loadApp(html, appJs, null);
|
const winNoToken = await loadApp(html, appJs, null);
|
||||||
@@ -137,23 +139,23 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
!!gate().querySelector('input#auth-user')
|
!!gate().querySelector('input#auth-user')
|
||||||
&& !!gate().querySelector('input#auth-pass')
|
&& !!gate().querySelector('input#auth-pass')
|
||||||
&& !!gate().querySelector(".btn-primary"));
|
&& !!gate().querySelector(".btn-primary"));
|
||||||
check("ohne Token: Login-Screen in Server-Sprache (en)",
|
check("ohne Token: Login-Screen in Server-Sprache",
|
||||||
Array.from(gate().querySelectorAll(".btn"))
|
Array.from(gate().querySelectorAll(".btn"))
|
||||||
.some(b => b.textContent === "Log in")
|
.some(b => b.textContent === "Anmelden")
|
||||||
&& gate().querySelector(".auth-sub").textContent.includes("Log in to see"));
|
&& gate().querySelector(".auth-sub").textContent.includes("Anmelden, um deine Trainingsdaten zu sehen"));
|
||||||
// falsches Kennwort -> Inline-Fehler
|
// falsches Kennwort -> Inline-Fehler
|
||||||
gate().querySelector('input#auth-user').value = "alex";
|
gate().querySelector('input#auth-user').value = "alex";
|
||||||
gate().querySelector('input#auth-pass').value = "falsch";
|
gate().querySelector('input#auth-pass').value = "falsch";
|
||||||
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Log in").click();
|
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Anmelden").click();
|
||||||
await sleep(500);
|
await sleep(500);
|
||||||
const authErr = gate().querySelector(".auth-error");
|
const authErr = gate().querySelector(".auth-error");
|
||||||
check("ohne Token: Inline-Fehler bei falschem Kennwort",
|
check("ohne Token: Inline-Fehler bei falschem Kennwort",
|
||||||
authErr && !authErr.hidden
|
authErr && !authErr.hidden
|
||||||
&& authErr.textContent.includes("Incorrect username or password"),
|
&& authErr.textContent.includes("Kennwort inkorrekt"),
|
||||||
authErr && authErr.textContent);
|
authErr && authErr.textContent);
|
||||||
// korrektes Kennwort -> Login wechselt OHNE Reload in-place
|
// korrektes Kennwort -> Login wechselt OHNE Reload in-place
|
||||||
gate().querySelector('input#auth-pass').value = "geheim";
|
gate().querySelector('input#auth-pass').value = "geheim";
|
||||||
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Log in").click();
|
Array.from(gate().querySelectorAll(".btn")).find(b => b.textContent === "Anmelden").click();
|
||||||
let loggedIn = false;
|
let loggedIn = false;
|
||||||
for (let i = 0; i < 60; i++) {
|
for (let i = 0; i < 60; i++) {
|
||||||
const og = winNoToken.document.querySelector("#setup-gate");
|
const og = winNoToken.document.querySelector("#setup-gate");
|
||||||
@@ -169,21 +171,8 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
await sleep(100);
|
await sleep(100);
|
||||||
}
|
}
|
||||||
check("Login: wechselt ohne Reload in den eingeloggten Zustand", loggedIn);
|
check("Login: wechselt ohne Reload in den eingeloggten Zustand", loggedIn);
|
||||||
// Onboarding abschliessen (im selben Fenster)
|
check("Login: Admin ohne Onboarding direkt in Heute-Ansicht",
|
||||||
const ogate = () => winNoToken.document.querySelector("#setup-gate");
|
winNoToken.document.querySelector("#setup-gate").hidden
|
||||||
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.querySelectorAll(".act-card").length === 3);
|
||||||
check("Login: kein Auth-Gate mehr",
|
check("Login: kein Auth-Gate mehr",
|
||||||
winNoToken.document.querySelector("#auth-gate").hidden);
|
winNoToken.document.querySelector("#auth-gate").hidden);
|
||||||
@@ -242,6 +231,23 @@ check("Setup: Sprache kommt zuerst",
|
|||||||
Array.from(rows()[0].querySelectorAll(".btn"))
|
Array.from(rows()[0].querySelectorAll(".btn"))
|
||||||
.some(b => b.textContent === "Kennwort ändern"));
|
.some(b => b.textContent === "Kennwort ändern"));
|
||||||
|
|
||||||
|
// Berta (normaler User) durchlaeuft einmaliges Onboarding (Sprache zuerst)
|
||||||
|
const loginB = await api("POST", "/api/login",
|
||||||
|
{ username: "berta", password: "geheim" });
|
||||||
|
const winB = await loadApp(html, appJs, loginB.body.token);
|
||||||
|
await sleep(1200);
|
||||||
|
const bg = () => winB.document.querySelector("#setup-gate");
|
||||||
|
check("Berta: Onboarding erscheint (einmalig)",
|
||||||
|
bg() && !bg().hidden);
|
||||||
|
Array.from(bg().querySelectorAll(".wizard-nav .btn"))
|
||||||
|
.find(b => b.textContent === "Weiter").click();
|
||||||
|
await sleep(150);
|
||||||
|
Array.from(bg().querySelectorAll(".wizard-nav .btn"))
|
||||||
|
.find(b => b.textContent === "Einrichtung abschließen").click();
|
||||||
|
await sleep(400);
|
||||||
|
check("Berta: nach Onboarding Heute-Ansicht",
|
||||||
|
bg().hidden && winB.document.querySelectorAll(".act-card").length === 3);
|
||||||
|
|
||||||
// --- Szenario 4: API-Login-Faelle --------------------------------------
|
// --- Szenario 4: API-Login-Faelle --------------------------------------
|
||||||
check("API: Login mit falschem Passwort -> 401",
|
check("API: Login mit falschem Passwort -> 401",
|
||||||
(await api("POST", "/api/login", { username: "alex", password: "falsch" })).status === 401);
|
(await api("POST", "/api/login", { username: "alex", password: "falsch" })).status === 401);
|
||||||
|
|||||||
Reference in new issue
Block a user