diff --git a/server/pruef-installieren.mjs b/server/pruef-installieren.mjs index c4eda236..483ffa5e 100644 --- a/server/pruef-installieren.mjs +++ b/server/pruef-installieren.mjs @@ -46,7 +46,9 @@ 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(220_000, "pruef-installieren"); +/* 220 -> 360 s: Abschnitt 0 startet einen weiteren Browser + und wartet auf den Service Worker. */ +notbremse(360_000, "pruef-installieren"); await new Promise((r) => setTimeout(r, 900)); const BASIS = `http://127.0.0.1:${PORT}`; @@ -101,6 +103,155 @@ try { return { ctx, s }; } + /* ===================================================================== + 0. BIETET DER BROWSER ES UEBERHAUPT AN? (neu am 22.09.2026) + ===================================================================== + DAS HIER IST DIE FRAGE, DIE ZWEI TAGE LANG NIEMAND GESTELLT HAT. + + Die Pruefungen darunter stellen `beforeinstallprompt` SELBST zu und + messen, was die Seite damit macht. Das war richtig gedacht und + vollstaendig wertlos: Sie waren gruen, waehrend der Knopf auf + Filipes Rechner seit dem 20.09. unsichtbar blieb, weil das echte + Ereignis nie kam. Er hat dreimal danach gefragt. + + ZWEI URSACHEN, beide ausserhalb dessen, was gemessen wurde: + + 1. Der Service Worker wurde nur in `glocke.js` angemeldet, also + erst beim ERLAUBEN von Benachrichtigungen. Auf Filipes + Rechner stehen sie auf "Vom Browser blockiert". + 2. Der Service Worker hatte keinen `fetch`-Zuhoerer. Chrome + verlangt ihn fuer sein Installationsangebot. + + Ohne Service Worker kein Angebot, ohne Angebot kein Knopf. + + GEMESSEN WIRD JETZT AN DER QUELLE: `Page.getInstallabilityErrors` + ist Chromes eigenes Urteil ueber genau diese Frage. Eine leere + Liste heisst, dass der Browser es anbietet. Das kann keine + nachgestellte Veranstaltung vortaeuschen. + + `beforeinstallprompt` selbst wird hier bewusst NICHT verlangt: In + dieser Umgebung (kein Profil, kein Nutzerverhalten, kein Fenster) + loest Chromium es nicht aus. Das Urteil oben ist die Aussage, auf + die es ankommt. + ===================================================================== */ + { + const ctx0 = await browser.newContext({ viewport: { width: 1280, height: 860 } }); + const s0 = await ctx0.newPage(); + const cdp = await ctx0.newCDPSession(s0); + await cdp.send("Page.enable"); + /* OHNE ANMELDUNG UND OHNE BENACHRICHTIGUNGSRECHT -- genau die Lage, + in der es bisher nicht ging. Die Anmeldewand ist ausserdem die + Seite, auf der jeder zuerst landet. */ + await s0.goto(BASIS + "/workspace/", { waitUntil: "networkidle" }); + await s0.waitForTimeout(2200); + + const sw = await s0.evaluate(async () => { + if (!("serviceWorker" in navigator)) return { da: false }; + const r = await navigator.serviceWorker.getRegistration("/workspace/"); + return { da: !!r, aktiv: !!r?.active, stand: r?.active?.state || null, + recht: Notification.permission }; + }); + ok(sw.recht !== "granted", + `Benachrichtigungen sind NICHT erlaubt (${sw.recht}) -- genau Filipes Lage`); + ok(sw.da === true && sw.aktiv === true, + `und trotzdem laeuft ein Service Worker (${sw.stand}) ` + + `-- vorher haette hier "keiner" gestanden`); + + /* Der `fetch`-Zuhoerer: ohne ihn bietet Chrome nichts an. Gemessen + an der Datei, nicht am Gedaechtnis. */ + const { readFileSync: lies } = await import("node:fs"); + const { fileURLToPath: alsPfad } = await import("node:url"); + const { dirname: ordnerVon } = await import("node:path"); + const wurzel = join(ordnerVon(alsPfad(import.meta.url)), ".."); + const swText = lies(join(wurzel, "workspace", "sw.js"), "utf8"); + ok(/addEventListener\(\s*["']fetch["']/.test(swText), + "sw.js hat einen fetch-Zuhoerer -- Chromes Bedingung fuers Anbieten"); + /* UND ER LEGT NICHTS AB. Das Versprechen im Kopf der Datei ist + keine Absichtserklaerung, sondern wird hier nachgesehen: Wer + `caches.open` einbaut, hebt Antworten auf -- hinter einer + Anmeldung waeren das persoenliche Daten auf fremden Geraeten. */ + ok(!/caches\.(open|match)|cache\.put/.test(swText), + "und er legt nichts ab -- keine persoenlichen Daten auf dem Geraet"); + + let urteil = null; + try { + urteil = (await cdp.send("Page.getInstallabilityErrors")).installabilityErrors; + } catch (f) { urteil = "unlesbar: " + f.message; } + /* DRITTER AUSGANG: Kann die Frage gar nicht gestellt werden, ist + das kein Bestehen. Eine Pruefung, die ihre Voraussetzung nicht + hat, muss das sagen. */ + ok(Array.isArray(urteil), + Array.isArray(urteil) ? "Chrome laesst sich nach der Installierbarkeit fragen" + : `KONNTE NICHT NACHSEHEN (${urteil})`); + ok(Array.isArray(urteil) && urteil.length === 0, + Array.isArray(urteil) && urteil.length + ? `Chrome nennt Hindernisse: ${urteil.map((f) => f.errorId).join(", ")}` + : "und nennt kein einziges Hindernis -- die Seite ist installierbar"); + + let manifest = null; + try { manifest = await cdp.send("Page.getAppManifest"); } catch { /* s.o. */ } + const mFehler = (manifest?.errors || []).filter((f) => f.critical !== 0); + ok(!!manifest?.url, `Chrome findet das Manifest (${manifest?.url || "nicht"})`); + ok(mFehler.length === 0 && (manifest?.errors || []).length === 0, + (manifest?.errors || []).length + ? `es meldet: ${(manifest.errors || []).map((f) => f.message).join(" | ").slice(0, 120)}` + : "und beanstandet nichts daran"); + + /* DER KNOPF STEHT AUF DER ANMELDEWAND. Sie hat keine Kopfleiste und + hatte deshalb frueher gar kein Angebot -- dabei landet dort + jeder zuerst. */ + const aufWand = await s0.evaluate(() => { + const k = document.querySelector(".install-knopf"); + if (!k) return { da: false }; + const r = k.getBoundingClientRect(); + return { da: true, sichtbar: !k.hidden, breit: Math.round(r.width), + hoch: Math.round(r.height), imPlatz: !!k.closest("[data-install-platz]"), + wort: k.textContent.trim() }; + }); + ok(aufWand.da && aufWand.imPlatz, + `der Knopf steht auf der Anmeldewand, an seinem vorgesehenen Platz`); + ok(aufWand.sichtbar === true && aufWand.hoch >= 44, + `und ist zu sehen und zu treffen (${aufWand.breit}x${aufWand.hoch} px, "${aufWand.wort}")`); + ok(/installieren/i.test(aufWand.wort || ""), + `und sagt, was er tut ("${aufWand.wort}")`); + + /* GEGENPROBE: nur EIN Knopf. Seit der Aufruf nicht mehr an der + Reihenfolge der Skriptzeilen haengt, kann er aus zwei Richtungen + kommen. */ + const anzahl = await s0.locator(".install-knopf").count(); + ok(anzahl === 1, `und zwar genau einer (${anzahl})`); + + /* UND ER TUT ETWAS, WENN MAN IHN DRUECKT. + Das ist hier keine Formalie, sondern der Fall, an dem der erste + Entwurf gescheitert waere: Der Knopf rief `window.frageNach?.()` + auf. Diese Seite laedt `nachfrage.js` NICHT -- also war die + Funktion `undefined`, und `?.()` verschluckt das lautlos. Auf + dem iPhone steht der Knopf hier von Anfang an da. Er haette + stumm dagestanden, und wer ihn drueckt, sucht den Fehler bei + sich. + + Gegenprobe gleich dahinter: ein zweiter Druck macht die + Erklaerung wieder zu. Sonst sammeln sich Absaetze. */ + const ohneDialog = await s0.evaluate(() => typeof window.frageNach !== "function"); + ok(ohneDialog, "auf der Anmeldewand gibt es den Dialogbaustein nicht (nachfrage.js fehlt dort)"); + + await s0.click(".install-knopf"); + await s0.waitForTimeout(400); + const hinweis = await s0.evaluate(() => { + const h = document.querySelector(".install-hinweis"); + return h ? h.textContent.replace(/\s+/g, " ").trim() : null; + }); + ok(!!hinweis && /Menü|Teilen|Home-Bildschirm/i.test(hinweis), + `er erklaert trotzdem den Weg ("${(hinweis || "nichts").slice(0, 60)}…")`); + + await s0.click(".install-knopf"); + await s0.waitForTimeout(300); + const zu = await s0.locator(".install-hinweis").count(); + ok(zu === 0, `und ein zweiter Druck macht sie wieder zu (${zu})`); + + await ctx0.close(); + } + /* ---- Zustand 2: der Browser bietet es an ----------------------- */ const a = await seiteAuf(); /* Chromium loest `beforeinstallprompt` in dieser Umgebung nicht von @@ -111,8 +262,19 @@ try { const sichtbarVorher = await a.s.evaluate(() => !!document.querySelector(".install-knopf:not([hidden])")); ok(vorher === 1, `der Knopf steht im Dokument (${vorher})`); - ok(sichtbarVorher === false, - "bleibt aber versteckt, solange der Browser nichts anbietet — kein Knopf ohne Wirkung"); + /* HIER STAND: "bleibt versteckt, solange der Browser nichts + anbietet". Das war bis zum 22.09.2026 richtig -- und genau der + Grund, warum Filipe ihn nie gesehen hat. `beforeinstallprompt` + kommt in Chrome nicht verlaesslich sofort und gar nicht mehr, + wenn es kurz vorher weggeklickt wurde. + + Jetzt zeigt er sich, sobald der Browser das Installieren + ueberhaupt KANN (`'onbeforeinstallprompt' in window`, wahr bei + allen Chromium-Browsern) -- und faellt auf die Erklaerung + zurueck, falls das Angebot ausbleibt. Wo auch das nicht zutrifft + und kein Apple-Geraet vorliegt, bleibt er weg. */ + ok(sichtbarVorher === true, + "und ist in einem Browser, der installieren kann, von Anfang an zu sehen"); await a.s.evaluate(() => { const e = new Event("beforeinstallprompt"); @@ -201,6 +363,13 @@ try { await browser.close(); } +/* Die Zahl gehoert in die Bedingung. Faellt sie, ist still ein + Abschnitt ausgefallen -- und genau der neue Abschnitt 0 ist der, + auf den es ankommt. */ +if (geprueft < 29) { + console.log(` FEHL nur ${geprueft} Pruefungen gelaufen -- erwartet waren 29+`); + fehler++; +} 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); diff --git a/workspace/anruf-probe.html b/workspace/anruf-probe.html index f89ac0d1..3314d4df 100644 --- a/workspace/anruf-probe.html +++ b/workspace/anruf-probe.html @@ -16,15 +16,15 @@ verdeckt. Gefunden hat es pruef-struktur. Richtig sind `gate.css` (die Farbwerte des Hauses) und `haus.css`. --> - - + + - + +
Dein Gerät ist offline — deshalb konnte die Seite nicht geladen werden.
+Es liegt nicht an der App. Sobald du wieder Empfang oder WLAN hast, geht es weiter.
+ +