Filipe, 22.09.2026: "und nochmal, ich will einen installieren button.
damit die leute es viel einfacher haben die seite auf dem pc oder auf
dem handy zu installieren!!! bitte das hab ich auch schon paar mal
gefragt. mach das es ist seeeeeeeehr wichtig."
Er hatte recht, und der Knopf war seit dem 20.09. gebaut. Er konnte
nur nie erscheinen. ZWEI URSACHEN, beide ausserhalb dessen, was
geprueft wurde:
1. DER SERVICE WORKER LIEF NIE. Er wurde ausschliesslich in glocke.js
angemeldet -- also erst, wenn jemand Benachrichtigungen ERLAUBT.
Auf Filipes Rechner stehen sie auf "Vom Browser blockiert"; das
steht sogar in seiner Kopfleiste auf dem Bildschirmfoto von heute
frueh. Ohne Service Worker macht Chrome kein Installationsangebot.
2. UND AUCH MIT WAERE ES NICHT GEGANGEN: Chrome verlangt einen Service
Worker MIT `fetch`-Zuhoerer. Dieser hatte keinen.
Ohne Angebot kommt `beforeinstallprompt` nie, und der Knopf bleibt
`hidden`. Fuer immer.
WAS SICH GEAENDERT HAT
- Der Service Worker wird jetzt auf JEDER Seite angemeldet, unabhaengig
von Benachrichtigungen. Das gehoert zum Installieren, nicht zur
Glocke.
- sw.js bekommt einen `fetch`-Zuhoerer, der NICHTS ablegt. Das
Versprechen im Kopf der Datei ("bewusst ohne Zwischenspeicher, der
Workspace liegt hinter einer Anmeldung") bleibt damit wortwoertlich
gueltig: Er reicht Seitenaufrufe durch und baut nur dann selbst eine
Antwort, wenn das Netz weg ist. Keine Serverantwort wird aufgehoben.
- Der Knopf zeigt sich, sobald der Browser installieren KANN, statt
erst nach dem Angebot. Geprueft wird die Faehigkeit
(`'onbeforeinstallprompt' in window`), nicht der Name des Browsers.
- DIE ANMELDEWAND BEKOMMT IHN AUCH. Sie hat keine Kopfleiste und
deshalb bisher gar kein Angebot -- dabei ist sie die Seite, auf der
jeder zuerst landet. Genau die Leute, um die es Filipe geht.
- Dafuer steht der Knopf jetzt in einer eigenen Datei
(assets/js/installieren.js) statt in kopf.js, das die Wand nicht
laedt. Und sein Stil in gate.css statt in module.css -- vierter Fall
derselben Art nach .knopf-still, dem Schalter und .feld-hinweis.
- Ohne `nachfrage.js` (also auf der Wand) erklaert er den Weg als
Absatz unter sich. Der erste Entwurf rief `window.frageNach?.()`
auf: Auf dem iPhone steht der Knopf dort von Anfang an da, und er
haette beim Antippen stumm nichts getan.
- Der Aufruf haengt nicht mehr an der Reihenfolge der Skriptzeilen.
chat.html und leistung.html laden kopf.js OHNE `defer`, dort waere
installieren.js immer zu spaet gekommen -- zwei von 37 Seiten ohne
Knopf, ohne Meldung.
- Chromes Manifest-Warnung zum `share_target` behoben (enctype).
WARUM DIE PRUEFUNG DAS NICHT GEFUNDEN HAT -- und was jetzt anders ist
Sie war gruen, die ganze Zeit. Sie hat `beforeinstallprompt` SELBST
zugestellt und gemessen, ob der Knopf darauf reagiert. Die eine Frage,
auf die es ankam -- "bietet der Browser es ueberhaupt an?" -- hat sie
nie gestellt.
Jetzt fragt sie Chrome direkt (`Page.getInstallabilityErrors`, sein
eigenes Urteil) und misst die Voraussetzungen statt der Reaktion:
laeuft ein Service Worker, OBWOHL Benachrichtigungen auf "denied"
stehen; hat sw.js einen fetch-Zuhoerer; legt er wirklich nichts ab;
steht der Knopf auf der Wand und tut er dort auch etwas.
Gemessen, mit Benachrichtigungen auf "denied":
Service Worker: activated
Chromes Urteil: kein einziges Hindernis
Manifest: nichts beanstandet
Knopf: 124x45 px, sichtbar, genau einer
pruef-installieren 29/0 (von 14). Dazu gruen: pruef-css-klassen,
pruef-struktur, pruef-crew-adresse, pruef-start-ansicht.
Co-Authored-By: Claude Opus 5 <[email protected]>
376 lines
18 KiB
JavaScript
376 lines
18 KiB
JavaScript
/* =====================================================================
|
|
pruef-installieren.mjs — Die Seite als App auf dem Gerät
|
|
|
|
Filipe, 20.09.2026: „kannst du da dazwischen in der starseite oben,
|
|
ein button reinsetzen damit die leute die seite installieren koennen
|
|
auf dem handy oder pc. das soll auch verschwinden wenn die leute es
|
|
installiert haben."
|
|
|
|
---------------------------------------------------------------------
|
|
WARUM DAS MEHR IST ALS EIN KNOPF
|
|
|
|
Ohne installierte App gibt es auf dem Handy keine verlässlichen
|
|
Benachrichtigungen — daran hing im September das Telefonieren. Das
|
|
Manifest war die ganze Zeit vollständig, die App ließ sich
|
|
installieren. Nur bot das ausschließlich der Browser an, versteckt
|
|
in seinem Dreipunktemenü.
|
|
|
|
DREI ZUSTÄNDE, DREI ANTWORTEN — und jede wird hier geprüft:
|
|
|
|
1. schon installiert -> gar kein Knopf
|
|
2. Browser bietet an -> Knopf fragt ihn
|
|
3. Safari/iPhone -> Knopf erklärt den Weg (dort gibt es
|
|
`beforeinstallprompt` nicht und wird es
|
|
nicht geben)
|
|
|
|
Der gefährlichste Fall ist der dritte: Ein Knopf, der auf dem iPhone
|
|
nichts tut, ist schlimmer als keiner — man drückt ihn und sucht den
|
|
Fehler bei sich.
|
|
|
|
AUFRUF: node server/pruef-installieren.mjs
|
|
===================================================================== */
|
|
|
|
import { mkdtempSync, rmSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { eigenerPort } from "./helfer-port.mjs";
|
|
import { notbremse } from "./helfer-notbremse.mjs";
|
|
|
|
const PORT = await eigenerPort(import.meta, "pruef-installieren");
|
|
const ordner = mkdtempSync(join(tmpdir(), "ws-install-"));
|
|
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");
|
|
/* 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}`;
|
|
|
|
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=== Die Seite als App installieren ===\n");
|
|
|
|
/* ---- 1. Das Manifest, ohne Browser -------------------------------- */
|
|
{
|
|
const { readFileSync } = await import("node:fs");
|
|
const { fileURLToPath } = await import("node:url");
|
|
const { dirname } = await import("node:path");
|
|
const HIER = dirname(fileURLToPath(import.meta.url));
|
|
const m = JSON.parse(readFileSync(join(HIER, "..", "workspace", "crew.webmanifest"), "utf8"));
|
|
/* Ohne diese vier Angaben bietet kein Browser das Installieren an --
|
|
der schoenste Knopf haette dann nichts zu fragen. */
|
|
ok(!!m.name && !!m.short_name, `Name und Kurzname stehen (${m.short_name})`);
|
|
ok(m.display === "standalone", `display ist "${m.display}"`);
|
|
ok(!!m.start_url && m.start_url.startsWith("/workspace/"),
|
|
`Startadresse zeigt in den Workspace (${m.start_url})`);
|
|
const gross = (m.icons || []).filter((i) => /512|384/.test(i.sizes || ""));
|
|
ok(gross.length >= 1, `mindestens ein grosses Symbol (${gross.length})`);
|
|
}
|
|
|
|
/* ---- 2. Am echten Bildschirm -------------------------------------- */
|
|
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 {
|
|
async function seiteAuf(extra = {}) {
|
|
const ctx = await browser.newContext({ viewport: { width: 1280, height: 860 }, ...extra });
|
|
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(1300);
|
|
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
|
|
selbst aus (kein echtes Profil, kein Nutzerverhalten). Das
|
|
Ereignis wird deshalb nachgestellt -- geprueft wird, was DIE SEITE
|
|
damit macht, und genau das ist hier die Frage. */
|
|
const vorher = await a.s.locator(".install-knopf").count();
|
|
const sichtbarVorher = await a.s.evaluate(() =>
|
|
!!document.querySelector(".install-knopf:not([hidden])"));
|
|
ok(vorher === 1, `der Knopf steht im Dokument (${vorher})`);
|
|
/* 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");
|
|
e.prompt = () => {};
|
|
e.userChoice = Promise.resolve({ outcome: "accepted" });
|
|
window.dispatchEvent(e);
|
|
});
|
|
await a.s.waitForTimeout(300);
|
|
const nachher = await a.s.evaluate(() => {
|
|
const k = document.querySelector(".install-knopf");
|
|
if (!k) return null;
|
|
const r = k.getBoundingClientRect();
|
|
return { sichtbar: !k.hidden, text: k.textContent.trim(),
|
|
satz: k.getAttribute("aria-label"),
|
|
breit: Math.round(r.width), hoch: Math.round(r.height) };
|
|
});
|
|
ok(nachher?.sichtbar === true, `sobald der Browser anbietet, steht er da ("${nachher?.text}")`);
|
|
ok((nachher?.hoch || 0) >= 44, ` und ist am Finger treffbar (${nachher?.hoch} px)`);
|
|
ok(/installieren/i.test(nachher?.satz || ""),
|
|
` mit einem ganzen Satz fuer Vorleseprogramme ("${nachher?.satz}")`);
|
|
|
|
/* Und ein Druck macht ihn fertig. */
|
|
await a.s.click(".install-knopf");
|
|
await a.s.waitForTimeout(500);
|
|
const weg = await a.s.locator(".install-knopf").count();
|
|
ok(weg === 0, `nach dem Installieren ist er weg (${weg})`);
|
|
|
|
/* ---- Zustand 1: schon installiert ------------------------------ */
|
|
const b = await browser.newContext({ viewport: { width: 1280, height: 860 } });
|
|
const bs = await b.newPage();
|
|
/* `display-mode: standalone` laesst sich nicht direkt setzen --
|
|
`matchMedia` wird deshalb vor dem Laden ueberschrieben. Damit
|
|
antwortet die Seite genau so, wie sie es in einer installierten
|
|
App taete. */
|
|
await bs.addInitScript(() => {
|
|
const echt = window.matchMedia.bind(window);
|
|
window.matchMedia = (q) => (q.includes("display-mode: standalone")
|
|
? { matches: true, media: q, addEventListener() {}, removeEventListener() {}, addListener() {}, removeListener() {} }
|
|
: echt(q));
|
|
});
|
|
await bs.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await bs.click('.rolle[data-rolle="admin"]');
|
|
await bs.fill("#code", "CODE-DOGI-0001");
|
|
await bs.click("button[type=submit]");
|
|
await bs.waitForURL("**/start.html", { timeout: 25000 });
|
|
await bs.waitForTimeout(1300);
|
|
const inApp = await bs.locator(".install-knopf").count();
|
|
ok(inApp === 0,
|
|
`Gegenprobe: in der installierten App gibt es den Knopf gar nicht erst (${inApp})`);
|
|
|
|
/* ---- Zustand 3: Safari auf dem iPhone -------------------------- */
|
|
const c = await browser.newContext({
|
|
viewport: { width: 390, height: 844 }, isMobile: true, hasTouch: true,
|
|
userAgent: "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X) "
|
|
+ "AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Mobile/15E148 Safari/604.1",
|
|
});
|
|
const cs = await c.newPage();
|
|
await cs.goto(BASIS + "/workspace/", { waitUntil: "networkidle" });
|
|
await cs.click('.rolle[data-rolle="admin"]');
|
|
await cs.fill("#code", "CODE-DOGI-0001");
|
|
await cs.click("button[type=submit]");
|
|
await cs.waitForURL("**/start.html", { timeout: 25000 });
|
|
await cs.waitForTimeout(1300);
|
|
const beiApple = await cs.evaluate(() => {
|
|
const k = document.querySelector(".install-knopf");
|
|
return k ? { sichtbar: !k.hidden, breit: Math.round(k.getBoundingClientRect().width) } : null;
|
|
});
|
|
/* DER WICHTIGSTE FALL: Dort gibt es `beforeinstallprompt` nicht --
|
|
der Knopf muss trotzdem dastehen und erklaeren. */
|
|
ok(beiApple?.sichtbar === true,
|
|
"auf dem iPhone steht er von Anfang an da (dort fragt der Browser nie)");
|
|
ok((beiApple?.breit || 0) >= 44,
|
|
` und ist auch dort treffbar (${beiApple?.breit} px breit)`);
|
|
|
|
await cs.click(".install-knopf");
|
|
await cs.waitForTimeout(700);
|
|
const erklaerung = await cs.evaluate(() => {
|
|
const d2 = document.querySelector("dialog.nachfrage[open]");
|
|
return d2 ? d2.textContent.replace(/\s+/g, " ").trim() : null;
|
|
});
|
|
ok(/Teilen|Home-Bildschirm/i.test(erklaerung || ""),
|
|
` und erklaert den Weg statt es zu versuchen ("${(erklaerung || "").slice(0, 70)}…")`);
|
|
|
|
await a.ctx.close(); await b.close(); await c.close();
|
|
} finally {
|
|
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);
|