Filipe, zum vierten Mal: "muss ich jedes mal wenn ich von einer seite
zurück scrolle immer wieder runter scrollen um weiter zu schauen und
das nervt."
DIE WIEDERHERSTELLUNG GAB ES SEIT DEM 20.09. -- sorgfaeltig gebaut,
mit Warten auf die nachgeladene Hoehe, mit Aufgeben nach drei
Sekunden, mit "wer selbst scrollt, gewinnt".
SIE LIEF NUR FAST NIE. Denn sie lief ausschliesslich bei
`navigation.type === "back_forward"`, also beim Zurueck-Knopf des
Browsers. Im Haus geht man anders zurueck: ueber die Kopfzeile ("TEAM
DOGI › HIGHLIGHTS"), und dort steht ein ganz gewoehnliches
`<a href="start.html">`. Fuer den Browser ist das eine Fahrt
VORWAERTS. Die Bedingung war also falsch -- und der else-Zweig
loeschte die eben gemerkte Stelle noch dazu.
IM KOPF DERSELBEN DATEI STAND DIE LOESUNG ALS ABSICHT: "Ein eigener
Merker faengt den Fall ab, dass der Zurueck-Knopf der Kopfleiste
benutzt wurde." Gebaut wurde er nie.
UND DESHALB HAT ES IN JEDEM TEST FUNKTIONIERT: Wer mit `goBack()`
prueft, prueft den einen Weg, den niemand geht. Gemessen am 21.09.:
ueber goBack sauber wiederhergestellt, ueber den Link in der
Kopfzeile oben gelandet. Gefunden hat es Filipes Bildschirmvideo --
17 Sekunden, drei Bilder, und der Ablauf war eindeutig.
Der Merker gibt es jetzt wirklich: Beim Klick auf einen EIGENEN
Zurueck-Weg (`.zurueck`, `.zurueck-knopf`) wird das Ziel notiert; wer
dort binnen 30 Sekunden ankommt, bekommt seine Stelle zurueck. Er
gilt fuer genau eine Ankunft -- wer die Seite eine Minute spaeter
normal oeffnet, faengt wieder oben an. Und NUR fuer diese zwei
Klassen, nicht fuer jeden Link: Sonst landete man beim Oeffnen eines
Brettes mitten darin.
Dazu der Rueckwaerts-Speicher des Browsers: Holt er eine Seite aus dem
Speicher, laeuft keine Zeile mehr, und unser `manual` verbietet ihm
gleichzeitig, die Stelle selbst wiederherzustellen. Ein `pageshow`
faengt das ab. Dieser Fall ist NICHT gemessen -- in Playwright greift
der Speicher nicht -- sondern aus der Spezifikation begruendet. Das
steht auch so im Kommentar, damit niemand spaeter glaubt, er sei
nachgewiesen.
NEU: pruef-stelle.mjs, 9 Pruefungen. Sie geht beide Wege zurueck --
Kopfzeile UND Browser -- und haelt ausdruecklich fest, dass der erste
KEIN "back_forward" ist. Dazu die Gegenprobe, dass eine frisch
geoeffnete Seite oben anfaengt: Ohne sie waere alles auch dann gruen,
wenn die Seite IMMER zur alten Stelle springt, und das waere
schlimmer als das Problem.
Co-Authored-By: Claude Opus 5 <[email protected]>
248 lines
10 KiB
JavaScript
248 lines
10 KiB
JavaScript
/* =====================================================================
|
|
pruef-stelle.mjs — Bleibt man, wo man war? (21.09.2026)
|
|
|
|
Filipe, zum vierten Mal: "muss ich jedes mal wenn ich von einer seite
|
|
zurück scrolle immer wieder runter scrollen um weiter zu schauen und
|
|
das nervt, wieso bleibt es nicht sofort da wo ich dran war anstatt
|
|
dass ich immer wieder ganz runter scrollen muss."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM ES DREIMAL ALS BEHOBEN GALT UND NIE BEHOBEN WAR
|
|
|
|
Die Wiederherstellung in kopf.js gibt es seit dem 20.09. Sie ist
|
|
sorgfaeltig gebaut -- sie merkt sich die Stelle je Adresse, wartet,
|
|
bis die nachgeladene Seite hoch genug ist, und hoert auf, sobald der
|
|
Mensch selbst scrollt.
|
|
|
|
Sie lief nur fast nie. Denn sie lief NUR bei
|
|
`navigation.type === "back_forward"`, also beim Zurueck-Knopf des
|
|
Browsers. Im Haus geht man aber anders zurueck: ueber die Kopfzeile,
|
|
und dort steht ein ganz normales `<a href="start.html">`. Fuer den
|
|
Browser ist das eine Fahrt VORWAERTS. Die Bedingung war falsch, und
|
|
der `else`-Zweig loeschte die eben gemerkte Stelle sogar noch.
|
|
|
|
IM KOPF DERSELBEN DATEI STAND DIE LOESUNG ALS ABSICHT: "Ein eigener
|
|
Merker faengt den Fall ab, dass der Zurueck-Knopf der Kopfleiste
|
|
benutzt wurde." Gebaut wurde er nie. Ein Kommentar, der eine Absicht
|
|
beschreibt, erfuellt sie nicht.
|
|
|
|
UND DESHALB HAT ES IN JEDEM TEST FUNKTIONIERT: Wer mit
|
|
`page.goBack()` prueft, prueft den einen Weg, den niemand geht.
|
|
Gemessen am 21.09.: ueber `goBack()` sauber wiederhergestellt,
|
|
ueber den Link in der Kopfzeile oben gelandet. Man muss den Weg
|
|
gehen, den der Mensch geht.
|
|
|
|
---------------------------------------------------------------------
|
|
WAS HIER GEMESSEN WIRD
|
|
|
|
1. Ueber den Link in der Kopfzeile zurueck -> Stelle bleibt
|
|
2. Ueber den Zurueck-Knopf des Browsers -> Stelle bleibt
|
|
3. GEGENPROBE: Eine Seite frisch oeffnen -> faengt oben an
|
|
|
|
Die dritte ist nicht Zierde. Ohne sie waere die Pruefung auch dann
|
|
gruen, wenn die Seite IMMER an die alte Stelle springt -- und das
|
|
waere schlimmer als das Problem: Wer eine Kachel antippt, will oben
|
|
anfangen.
|
|
|
|
Aufrufen mit: node server/pruef-stelle.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { request as httpAnfrage } from "node:http";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-stelle");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-stelle-"));
|
|
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 });
|
|
};
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
await import("./index.js");
|
|
notbremse(180_000, "pruef-stelle");
|
|
await new Promise((r) => setTimeout(r, 900));
|
|
|
|
process.on("uncaughtException", (f) => { console.error("ABSTURZ:", f); process.exit(7); });
|
|
|
|
let fehler = 0, geprueft = 0;
|
|
const melde = (t) => console.log(t);
|
|
const ok = (b, t) => { geprueft++; console.log((b ? " ok " : " FEHL ") + t); if (!b) fehler++; };
|
|
|
|
const CREW = "crew.dogfather-universe.com";
|
|
|
|
function roh(pfad, koerper) {
|
|
return new Promise((fertig, schief) => {
|
|
const a = httpAnfrage({
|
|
host: "127.0.0.1", port: PORT, path: pfad, method: koerper ? "POST" : "GET",
|
|
headers: { Host: CREW, ...(koerper ? { "Content-Type": "application/json" } : {}) },
|
|
}, (antwort) => {
|
|
let text = "";
|
|
antwort.on("data", (s) => { text += s; });
|
|
antwort.on("end", () => fertig({ code: antwort.statusCode, text, kopf: antwort.headers }));
|
|
});
|
|
a.on("error", schief);
|
|
if (koerper) a.write(JSON.stringify(koerper));
|
|
a.end();
|
|
});
|
|
}
|
|
|
|
const { DatabaseSync } = await import("node:sqlite");
|
|
const { scryptSync, randomBytes } = await import("node:crypto");
|
|
{
|
|
const d = new DatabaseSync(process.env.WORKSPACE_DB);
|
|
const salz = randomBytes(16).toString("hex");
|
|
const hash = scryptSync("CODE-DOGI-0001", salz, 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, salz, 32768, new Date().toISOString());
|
|
d.close();
|
|
}
|
|
|
|
let an = await roh("/workspace/api/anmelden", { rolle: "admin", code: "CODE-DOGI-0001" });
|
|
if (an.code === 400 && an.text.includes("alter_offen")) {
|
|
an = await roh("/workspace/api/anmelden", { rolle: "admin", code: "CODE-DOGI-0001", alter_ok: true });
|
|
}
|
|
const keks = [].concat(an.kopf["set-cookie"] || []).map((z) => z.split(";")[0]).join("; ");
|
|
ok(!!keks, "angemeldet");
|
|
|
|
/* ------------------------------------------------------------------- */
|
|
let pw = null;
|
|
try {
|
|
pw = await import("file:///C:/Users/qciga/Documents/Obelix/Analyse/node_modules/playwright/index.mjs");
|
|
} catch { /* dritter Ausgang */ }
|
|
|
|
if (!pw) {
|
|
melde(" -- Playwright ist nicht da -- im Browser wurde NICHT nachgesehen.");
|
|
melde(" Das ist kein Befund, aber auch kein bestandener Test.");
|
|
melde(`\n${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
process.exit(fehler ? 1 : 0);
|
|
}
|
|
|
|
const browser = await pw.chromium.launch();
|
|
|
|
/** Scrollt auf der Startseite nach unten, oeffnet ein Brett und kommt
|
|
* auf dem uebergebenen Weg zurueck. Gibt zurueck, wo man landet. */
|
|
async function hinUndZurueck(wieZurueck) {
|
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
await ctx.addCookies(keks.split("; ").filter(Boolean).map((z) => {
|
|
const [name, ...rest] = z.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
try {
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1700);
|
|
const hoch = await seite.evaluate(() => document.documentElement.scrollHeight);
|
|
/* DIE HOEHE GEHOERT IN DIE BEDINGUNG. Auf einer Seite, die kuerzer
|
|
ist als das Fenster, kann man gar nicht scrollen -- dann misst
|
|
alles Folgende nichts. */
|
|
if (hoch < 1600) return { zuKurz: hoch };
|
|
await seite.evaluate(() => window.scrollTo(0, 1200));
|
|
await seite.waitForTimeout(400);
|
|
const vor = await seite.evaluate(() => Math.round(window.scrollY));
|
|
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/bereich.html?b=highlight`,
|
|
{ waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1400);
|
|
|
|
if (wieZurueck === "link") {
|
|
/* Ueber die Kopfzeile -- der Weg, den Filipe im Video geht.
|
|
`evaluate` statt `click`, weil der Link je nach Breite hinter
|
|
dem Knopf liegen kann; das Ereignis ist dasselbe. */
|
|
const gabEs = await seite.evaluate(() => {
|
|
const a = document.querySelector("a.zurueck");
|
|
if (!a) return false;
|
|
a.click();
|
|
return true;
|
|
});
|
|
if (!gabEs) return { keinLink: true };
|
|
await seite.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {});
|
|
} else {
|
|
await seite.goBack({ waitUntil: "load" });
|
|
}
|
|
await seite.waitForTimeout(3000);
|
|
const nach = await seite.evaluate(() => ({
|
|
y: Math.round(window.scrollY),
|
|
art: performance.getEntriesByType("navigation")[0]?.type,
|
|
wo: location.pathname,
|
|
}));
|
|
return { vor, ...nach };
|
|
} finally {
|
|
await ctx.close();
|
|
}
|
|
}
|
|
|
|
try {
|
|
melde("\n=== 1. Zurueck ueber die Kopfzeile ===");
|
|
{
|
|
const r = await hinUndZurueck("link");
|
|
ok(!r.zuKurz && !r.keinLink,
|
|
r.zuKurz ? `die Startseite ist nur ${r.zuKurz}px hoch -- nichts zu messen`
|
|
: r.keinLink ? "es gibt keinen Zurueck-Link in der Kopfzeile"
|
|
: "der Weg ueber die Kopfzeile ist da");
|
|
if (!r.zuKurz && !r.keinLink) {
|
|
ok(r.wo === "/workspace/start.html", `man landet auf der Startseite (${r.wo})`);
|
|
/* DASS ES EBEN KEIN "back_forward" IST, ist der Kern der Sache --
|
|
und wird deshalb ausdruecklich festgehalten. Waere es eines,
|
|
pruefte dieser Abschnitt denselben Weg wie der naechste. */
|
|
ok(r.art !== "back_forward",
|
|
`und zwar als gewoehnliche Fahrt vorwaerts (${r.art}) -- genau der Fall, `
|
|
+ "der bisher nicht wiederhergestellt wurde");
|
|
ok(r.y >= r.vor - 80,
|
|
`die Stelle bleibt: ${r.vor}px hin, ${r.y}px zurueck`);
|
|
}
|
|
}
|
|
|
|
melde("\n=== 2. Zurueck ueber den Browser ===");
|
|
{
|
|
const r = await hinUndZurueck("back");
|
|
ok(!r.zuKurz, r.zuKurz ? `zu kurz (${r.zuKurz}px)` : "die Seite ist hoch genug");
|
|
if (!r.zuKurz) {
|
|
ok(r.art === "back_forward", `der Browser meldet ein Zurueck (${r.art})`);
|
|
ok(r.y >= r.vor - 80, `die Stelle bleibt: ${r.vor}px hin, ${r.y}px zurueck`);
|
|
}
|
|
}
|
|
|
|
melde("\n=== 3. Gegenprobe: frisch geoeffnet faengt oben an ===");
|
|
{
|
|
/* Ohne diese Zeile waere alles oben auch dann gruen, wenn die
|
|
Seite IMMER an die alte Stelle springt. Das waere schlimmer als
|
|
das Problem: Wer eine Kachel antippt, will oben anfangen. */
|
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 800 } });
|
|
await ctx.addCookies(keks.split("; ").filter(Boolean).map((z) => {
|
|
const [name, ...rest] = z.split("=");
|
|
return { name, value: rest.join("="), domain: "127.0.0.1", path: "/" };
|
|
}));
|
|
const seite = await ctx.newPage();
|
|
try {
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(1500);
|
|
await seite.evaluate(() => window.scrollTo(0, 1200));
|
|
await seite.waitForTimeout(400);
|
|
/* Neu aufrufen, ohne ueber einen Zurueck-Weg zu kommen. */
|
|
await seite.goto(`http://127.0.0.1:${PORT}/workspace/start.html`, { waitUntil: "networkidle" });
|
|
await seite.waitForTimeout(2600);
|
|
const y = await seite.evaluate(() => Math.round(window.scrollY));
|
|
ok(y < 80, `frisch geoeffnet steht die Seite oben (${y}px)`);
|
|
} finally {
|
|
await ctx.close();
|
|
}
|
|
}
|
|
} finally {
|
|
await browser.close();
|
|
}
|
|
|
|
melde(`\n${geprueft} Pruefungen, ${fehler} Fehler`);
|
|
melde(fehler ? "NICHT IN ORDNUNG" : "ALLES IN ORDNUNG");
|
|
try { rmSync(ordner, { recursive: true, force: true }); } catch { /* Aufraeumrest */ }
|
|
process.exit(fehler ? 1 : 0);
|