/* ===================================================================== pruef-rollstelle.mjs — Beim Zurückgehen dort bleiben, wo man war Filipe, zum dritten Mal und in Großbuchstaben (20.09.2026): „WENN ICH IN EINE KATEGORIE REIN GEHE UND DAN ZURÜCK, GEHT DIE HAUPTSEITE IMMER WIEDER GANZ HOCH; DAS NERVT ... SO DASS WENN MAN ZURÜCK GEHT MAN DAN GENAU DA IST UND BLEIBT WO MAN VORHER WAR." --------------------------------------------------------------------- WAS GEMESSEN WURDE, BEVOR ETWAS GEBAUT WURDE Scrollen auf 900, eine Kachel anklicken, zurück: gemerkt: 900 gelandet: 1054 <- 154 px daneben, zuverlässig Die Wiederherstellung LIEF also — sie traf nur nicht. Die Erklärung ist Chromes Scroll-Verankerung: Wächst Inhalt OBERHALB der Stelle, verschiebt der Browser den Bildlauf mit, damit das Sichtbare stehen bleibt. Im Alltag richtig, beim Wiederherstellen das Gegenteil: Wir setzen 900, danach lädt der Inhalt darüber nach, und aus 900 wird 1054. --------------------------------------------------------------------- ZWEI FALLEN BEIM MESSEN, IN BEIDE BIN ICH GELAUFEN 1. `scrollIntoViewIfNeeded()` vor dem Klick scrollt SELBST. Gemerkt wurde dann die neue Stelle, und die Prüfung meldete einen Fehler, den es nicht gab. Deshalb wird hier ein Link genommen, der schon im Bild steht. 2. Playwright schaltet den Vor-/Zurück-Speicher des Browsers ab. Diese Prüfung misst deshalb ausdrücklich den Weg OHNE ihn — also den schlechtesten Fall. Ist er im echten Browser aktiv, ist es ohnehin sofort und exakt. AUFRUF: node server/pruef-rollstelle.mjs ===================================================================== */ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { portMussFreiSein } from "./helfer-port.mjs"; import { notbremse } from "./helfer-notbremse.mjs"; const PORT = await portMussFreiSein(4432, "pruef-rollstelle"); const ordner = mkdtempSync(join(tmpdir(), "ws-rollstelle-")); process.env.WORKSPACE_DB = join(ordner, "workspace.db"); process.env.PORT = String(PORT); process.env.SITE_ACCESS_SECRET = "lokaler-test"; process.env.SITE_PUBLIC_LAUNCH_AT = "2020-01-01T00:00:00+01:00"; const express = (await import("express")).default; const ec = express.response.cookie; express.response.cookie = function (n, w, o) { return ec.call(this, n, w, { ...(o || {}), secure: false }); }; await import("./index.js"); notbremse(240_000, "pruef-rollstelle"); await new Promise((r) => setTimeout(r, 900)); const BASIS = `http://127.0.0.1:${PORT}`; process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); }); process.on("unhandledRejection", (f) => { console.error("ABSTURZ (Versprechen):", f); process.exit(7); }); let fehler = 0, geprueft = 0; const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; }; console.log("\n=== Beim Zurueckgehen dort bleiben, wo man war ===\n"); const { DatabaseSync } = await import("node:sqlite"); const { scryptSync, randomBytes } = await import("node:crypto"); const d = new DatabaseSync(process.env.WORKSPACE_DB); const salt = randomBytes(16).toString("hex"); const hash = scryptSync("CODE-DOGI-0001", 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 (?,?,?,?,?,1,?)").run("Filipe", "admin", hash, salt, 32768, new Date().toISOString()); d.close(); const pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs"); const browser = await pw.chromium.launch(); try { const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } }); const s = await ctx.newPage(); await s.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); await s.click('.rolle[data-rolle="admin"]'); await s.fill("#code", "CODE-DOGI-0001"); await s.click("button[type=submit]"); await s.waitForURL("**/start.html", { timeout: 25000 }); await s.waitForTimeout(1800); const hoch = await s.evaluate(() => document.documentElement.scrollHeight); ok(hoch > 1600, `die Startseite ist hoch genug zum Messen (${hoch} px)`); const ZIEL = 900; await s.evaluate((y) => window.scrollTo(0, y), ZIEL); await s.waitForTimeout(400); const vorKlick = await s.evaluate(() => Math.round(window.scrollY)); ok(vorKlick === ZIEL, `gescrollt auf ${vorKlick}`); /* EINEN LINK NEHMEN, DER SCHON IM BILD STEHT -- siehe Falle 1 oben. */ const ziel = await s.evaluate(() => { const mitte = window.scrollY + window.innerHeight / 2; let best = null, dist = 1e9; for (const a of document.querySelectorAll("a.kachel__link")) { const r = a.getBoundingClientRect(); if (r.top < 0 || r.bottom > window.innerHeight) continue; const y = window.scrollY + r.top + r.height / 2; if (Math.abs(y - mitte) < dist) { dist = Math.abs(y - mitte); best = a; } } if (!best) return null; best.id = "pruef-ziel"; return best.getAttribute("href"); }); ok(!!ziel, `eine Kachel im Bild gefunden (${ziel})`); await s.click("#pruef-ziel"); await s.waitForLoadState("networkidle"); await s.waitForTimeout(1200); ok((await s.url()).includes(String(ziel).split("?")[0]), "die Kachel hat wirklich geöffnet"); /* MITSCHREIBEN, WAS NACH DEM ZURUECKGEHEN PASSIERT. Ein Endwert sagt nicht, ob die Seite erst richtig stand und dann verschoben wurde. */ await s.addInitScript(() => { window.__spur = []; setInterval(() => window.__spur.push(Math.round(window.scrollY)), 120); }); await s.click("#zurueck"); await s.waitForLoadState("networkidle"); await s.waitForTimeout(2600); const nachher = await s.evaluate(() => Math.round(window.scrollY)); const spur = await s.evaluate(() => (window.__spur || []).slice(0, 25)); ok(Math.abs(nachher - ZIEL) <= 8, `nach dem Zurückgehen wieder bei ${nachher} (war ${ZIEL})`); /* UND SIE BLEIBT DORT. Der erste Anlauf traf 900 und wurde danach von der Scroll-Verankerung auf 1054 geschoben -- ein Endwert allein hätte das nicht gezeigt. */ const spaet = spur.slice(-8); ok(spaet.length >= 4 && spaet.every((y) => Math.abs(y - ZIEL) <= 8), `und bleibt dort (letzte Messwerte: ${spaet.join(", ")})`); /* ---- GEGENPROBE 1: ohne gemerkte Stelle darf nichts springen ---- OHNE KLICK. Zweimal bin ich in dieselbe Falle gelaufen: Playwright scrollt von selbst zu einem Element ausserhalb des Bildes, die Seite merkt sich diese Stelle, und die Gegenprobe meldet einen Fehler, den es nicht gibt. Hier wird deshalb direkt navigiert. ganz oben (unter 80 px) LOESCHT die gemerkte Stelle sogar -- so steht es in kopf.js: Wer zehn Pixel gescrollt hat, merkt vom Wiederherstellen nichts. Genau das wird hier ausgenutzt. */ await s.evaluate(() => window.scrollTo(0, 0)); await s.waitForTimeout(300); await s.goto(`${BASIS}/workspace/uebersicht.html`, { waitUntil: "networkidle" }); await s.waitForTimeout(700); await s.goBack({ waitUntil: "networkidle" }); await s.waitForTimeout(2000); const ohne = await s.evaluate(() => Math.round(window.scrollY)); ok(ohne < 80, `Gegenprobe: ohne gemerkte Stelle beginnt die Seite oben (${ohne})`); /* ---- GEGENPROBE 2: die Verankerung ist danach wieder an -------- Sie wird nur waehrend des Wiederherstellens abgeschaltet. Bliebe sie aus, spraenge einem im Chat der Text unter dem Finger weg -- eine Reparatur, die woanders etwas kaputt macht. */ const anker = await s.evaluate(() => getComputedStyle(document.documentElement).overflowAnchor); ok(anker !== "none", `die Scroll-Verankerung ist danach wieder an (${anker})`); await ctx.close(); } finally { await browser.close(); } console.log(`\n${fehler ? fehler + " FEHLER" : "Alles in Ordnung"} — ${geprueft} Prüfungen\n`); try { rmSync(ordner, { recursive: true, force: true }); } catch { /* egal */ } process.exit(fehler ? 1 : 0);