/* ===================================================================== Prueft, ob die Verwaltung als EIGENE App installierbar ist. Der Fehler, den man hier am ehesten macht, faellt erst beim zweiten Installieren auf: Haben zwei Manifeste dieselbe "id", halten Browser sie fuer dieselbe App. Die zweite Installation ueberschreibt dann die erste, statt danebenzustehen -- und man sucht den Fehler ueberall, nur nicht in einer Zeile, die man fuer Beiwerk gehalten hat. Ebenso still: ein Symbol, das im Manifest steht, aber nicht existiert oder andere Masse hat als angegeben. Der Browser meldet das nicht -- er zeigt einfach ein leeres oder verzerrtes Kaestchen. ===================================================================== */ import { chromium } from "playwright"; import http from "http"; import fs from "fs"; import path from "path"; const TYP = { ".html": "text/html", ".css": "text/css", ".js": "text/javascript", ".png": "image/png", ".svg": "image/svg+xml", ".webp": "image/webp", ".json": "application/json", ".ico": "image/x-icon", ".jpg": "image/jpeg", /* Ohne diesen Eintrag liefert der Testserver den Typ "text/plain", und der Browser ignoriert das Manifest stillschweigend. Genau so wuerde es auch auf einem falsch eingerichteten Server aussehen. */ ".webmanifest": "application/manifest+json", }; const srv = http.createServer((q, r) => { const p = path.join(process.cwd(), decodeURIComponent(q.url.split("?")[0])); if (fs.existsSync(p) && !p.endsWith("/")) { r.writeHead(200, { "content-type": TYP[path.extname(p)] || "text/plain" }); return r.end(fs.readFileSync(p)); } r.writeHead(404); r.end(""); }); let ok = 0, fehl = 0; const pruefe = (name, gut, info) => { if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); } }; /* Masse aus dem Dateikopf lesen, ohne Fremdpaket. */ function masse(datei) { const b = fs.readFileSync(datei); if (b[0] === 0x89 && b[1] === 0x50) { return { breite: b.readUInt32BE(16), hoehe: b.readUInt32BE(20) }; } let i = 2; while (i < b.length) { if (b[i] !== 0xFF) { i++; continue; } const m = b[i + 1]; if (m >= 0xC0 && m <= 0xCF && m !== 0xC4 && m !== 0xC8 && m !== 0xCC) { return { hoehe: b.readUInt16BE(i + 5), breite: b.readUInt16BE(i + 7) }; } i += 2 + b.readUInt16BE(i + 2); } return null; } srv.listen(4410, async () => { const browser = await chromium.launch(); const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } }); console.log("1. DIE VERWALTUNG HAT EIN EIGENES MANIFEST"); await seite.goto("http://127.0.0.1:4410/webdesign/verwaltung.html"); await seite.waitForTimeout(900); const verweis = await seite.evaluate(() => document.querySelector('link[rel="manifest"]')?.getAttribute("href") || null); pruefe("verwaltung.html verweist auf ihr eigenes Manifest", verweis === "/webdesign/verwaltung.webmanifest", verweis); const a = await fetch("http://127.0.0.1:4410/webdesign/verwaltung.webmanifest"); pruefe("Manifest ist abrufbar", a.ok, "HTTP " + a.status); pruefe("und wird als Manifest ausgeliefert", (a.headers.get("content-type") || "").includes("manifest"), a.headers.get("content-type")); const m = JSON.parse(await a.text()); console.log("\n2. IST ES WIRKLICH EINE ZWEITE APP?"); const alt = JSON.parse(fs.readFileSync("webdesign/app.webmanifest", "utf8")); /* Der eine Wert, an dem alles haengt. */ pruefe("die Kennung unterscheidet sich vom Webdesign-Bereich", m.id !== alt.id, `"${m.id}" gegen "${alt.id}"`); pruefe("auch der Startpunkt ist ein anderer", m.start_url !== alt.start_url, m.start_url); pruefe("und der Name", m.name !== alt.name, `"${m.name}"`); pruefe("Kurzname passt auf einen Startbildschirm (max. 12 Zeichen)", m.short_name && m.short_name.length <= 12, `"${m.short_name}" (${m.short_name.length})`); console.log("\n3. WAS BROWSER ZUM INSTALLIEREN VERLANGEN"); pruefe("Anzeigeart ist eigenstaendig", m.display === "standalone", m.display); pruefe("Startadresse liegt im Geltungsbereich", m.start_url.startsWith(m.scope), `${m.start_url} in ${m.scope}`); pruefe("Hintergrund- und Themenfarbe gesetzt", !!m.background_color && !!m.theme_color, m.theme_color); const gross = m.icons.filter((i) => i.sizes === "512x512"); pruefe("es gibt ein Symbol mit 512 Punkten", gross.length > 0, gross.length + " Stueck"); pruefe("und eines fuers Beschneiden (maskable)", m.icons.some((i) => (i.purpose || "").includes("maskable"))); console.log("\n4. GIBT ES DIE SYMBOLE AUCH WIRKLICH?"); for (const i of m.icons) { const datei = path.join(process.cwd(), i.src); const da = fs.existsSync(datei); if (!da) { pruefe(` ${i.src}`, false, "fehlt"); continue; } const g = masse(datei); const [b, h] = i.sizes.split("x").map(Number); /* Ein Symbol, dessen Masse nicht zur Angabe passen, wird verzerrt dargestellt -- ohne jede Fehlermeldung. */ pruefe(` ${i.src}`, g && g.breite === b && g.hoehe === h, g ? `${g.breite}x${g.hoehe}, ${Math.round(fs.statSync(datei).size / 1024)} KB` : "unlesbar"); } console.log("\n5. DIE VERKNUEPFUNGEN ZEIGEN AUF ECHTE ZIELE"); for (const v of (m.shortcuts || [])) { const r = await fetch("http://127.0.0.1:4410" + v.url.split("?")[0]); pruefe(` ${v.short_name} -> ${v.url}`, r.ok, "HTTP " + r.status); } console.log("\n5b. TUN DIE VERKNUEPFUNGEN AUCH ETWAS?"); /* Dass die Adresse antwortet, heisst noch nicht, dass sie den versprochenen Bereich oeffnet. Genau das war beim Bauen der Fall: "?bereich=anfragen" stand in den Verknuepfungen UND in den Push-Meldungen -- ausgewertet hat den Parameter niemand. Man landete immer auf der Uebersicht, und nichts sah kaputt aus. */ for (const wunsch of ["anfragen", "projekte", "postfach"]) { const p = await browser.newPage({ viewport: { width: 1280, height: 900 } }); await p.goto(`http://127.0.0.1:4410/webdesign/verwaltung.html?bereich=${wunsch}`); await p.waitForTimeout(600); /* Ohne Anmeldung sind die Reiter verborgen. Fuer diese Pruefung wird der Bereich sichtbar geschaltet und die Auswertung angestossen -- geprueft wird die Auswertung, nicht die Anmeldung. */ const offen = await p.evaluate((w) => { document.getElementById("vw-anmeldung").hidden = true; document.getElementById("vw-bereich").hidden = false; /* dieselbe Funktion, die die Seite nach der Anmeldung aufruft */ if (typeof window.vwBereichAusAdresse === "function") window.vwBereichAusAdresse(); const aktiv = document.querySelector("[data-tab].vw-an"); return aktiv ? aktiv.dataset.tab : null; }, wunsch); pruefe(` ?bereich=${wunsch} oeffnet den Reiter`, offen === wunsch, offen === wunsch ? "geoeffnet" : "geoeffnet wurde: " + offen); await p.close(); } /* Gegenprobe: Unsinn in der Adresse darf nichts kaputtmachen. */ { const p = await browser.newPage(); const fehler = []; p.on("pageerror", (e) => fehler.push(String(e).slice(0, 70))); await p.goto("http://127.0.0.1:4410/webdesign/verwaltung.html?bereich="); await p.waitForTimeout(500); await p.evaluate(() => { document.getElementById("vw-bereich").hidden = false; if (typeof window.vwBereichAusAdresse === "function") window.vwBereichAusAdresse(); }); pruefe(" unsinniger Wert richtet keinen Schaden an", fehler.length === 0, fehler[0] || "kein Fehler"); await p.close(); } console.log("\n6. DIE ANDERE APP BLEIBT UNBERUEHRT"); const b2 = await seite.goto("http://127.0.0.1:4410/webdesign/index.html"); await seite.waitForTimeout(500); const verweis2 = await seite.evaluate(() => document.querySelector('link[rel="manifest"]')?.getAttribute("href") || null); pruefe("der oeffentliche Bereich nutzt weiterhin sein eigenes", verweis2 === "/webdesign/app.webmanifest", verweis2); await browser.close(); srv.close(); console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); process.exit(fehl ? 1 : 0); });