#!/usr/bin/env bash # End-to-End-Check im echten Browser: Zeiger, Subdomain, neueste Ausgabe # (alle Seiten), Chronik-Live-Abgleich, Briefkasten, Archiv-Live-Liste. # Nutzung: bin/e2e.sh [erwartete-Manifest-ID] # Ohne Argument wird gegen publish/last-upload.json geprueft, aber nur # gewarnt (Index-Lag ist normal), nicht gefailt. set -euo pipefail cd "$(dirname "$0")/.." EXPECT="${1:-}" NODE_PATH=$(npm root -g) EXPECT="$EXPECT" node - << 'EOF' const { chromium } = require('playwright'); const fs = require('fs'); (async () => { let fails = 0, warns = 0; const ok = (name, cond, warnOnly) => { if (cond) console.log('ok ' + name); else if (warnOnly) { console.log('WARN ' + name); warns++; } else { console.log('FAIL ' + name); fails++; } }; const expect = process.env.EXPECT || JSON.parse(fs.readFileSync('publish/last-upload.json', 'utf8')) .manifestResponse.id; const hardExpect = !!process.env.EXPECT; const b = await chromium.launch(); const p = await b.newPage(); // 1) Zeiger loest auf eine Ausgabe await p.goto('https://arweave.net/BUprgAw-XbRvvQPDcpL6Kds6-xPa2vmJfzmf7OLiLT4', { waitUntil: 'networkidle', timeout: 90000 }); await p.waitForTimeout(4000); const purl = p.url(); ok('Zeiger landet auf Site (Titel)', (await p.title()).includes('Kiel')); const m = new URL(purl).pathname.match(/\/([A-Za-z0-9_-]{43})(\/|$)/); const resolved = m ? m[1] : '?'; console.log(' Zeiger -> ' + resolved); ok('Zeiger loest auf erwartete Ausgabe ' + expect.slice(0, 8) + '…', purl.includes(expect), !hardExpect); // 2) Subdomain await p.goto('https://kiel.overlkd.com/', { waitUntil: 'networkidle', timeout: 90000 }); await p.waitForTimeout(3000); ok('Subdomain landet auf Site', (await p.title()).includes('Kiel')); // 3) Alle intern verlinkten Seiten der aufgeloesten Ausgabe (aus Nav+Main) const base = 'https://arweave.net/' + resolved + '/'; const collect = () => p.evaluate(() => { const out = []; document.querySelectorAll('a[href$=".html"]').forEach(a => { const h = a.getAttribute('href'); if (h && !/^https?:/.test(h) && !h.includes('/')) out.push(h); }); return out; }); await p.goto(base, { waitUntil: 'networkidle', timeout: 90000 }); const set = new Set(['index.html', ...await collect()]); // EN-Seiten haengen nur an english.html — eine Ebene tiefer sammeln. if (set.has('english.html')) { await p.goto(base + 'english.html', { timeout: 60000 }); (await collect()).forEach(h => set.add(h)); } const pages = [...set]; for (const f of pages) { try { const r = await p.goto(base + f, { timeout: 60000 }); ok('Seite ' + f, r.status() === 200); } catch (e) { ok('Seite ' + f + ' (' + e.message.slice(0, 40) + ')', false); } } // 4) Chronik-Live-Abgleich liefert Eintraege await p.goto(base + 'chronik.html', { waitUntil: 'networkidle', timeout: 90000 }); await p.waitForTimeout(6000); const liveCount = await p.evaluate(() => { const h = [...document.querySelectorAll('h2')] .find(x => x.textContent.includes('Live-Abgleich')); if (!h) return 0; const ul = h.parentNode.querySelector('ul'); return ul ? ul.children.length : 0; }); ok('Chronik-Live-Abgleich (' + liveCount + ' Ausgaben)', liveCount >= 5); // 5) Briefkasten rendert Nachrichten await p.goto(base + 'briefkasten.html', { waitUntil: 'networkidle', timeout: 90000 }); await p.waitForTimeout(6000); const msgs = await p.evaluate(() => document.body.innerText.includes('IMdGW3Cm') || /Nachricht/i.test(document.body.innerText)); ok('Briefkasten laedt', msgs); // 6) Archiv: Live-Liste (neuester Schnappschuss laut GraphQL) const q = 'query { transactions(owners: ["5-NRCJ8Jg1fqtcvKaFYLsHBQ37_gyqsR9cOsxuzM748"], ' + 'tags: [{name: "App-Name", values: ["kiel-gedaechtnis"]}, ' + '{name: "Content-Type", values: ["application/x.arweave-manifest+json"]}], ' + 'sort: HEIGHT_DESC, first: 1) { edges { node { id } } } }'; const rsp = await fetch('https://arweave.net/graphql', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ query: q }) }).then(r => r.json()).catch(() => null); const arch = rsp && rsp.data.transactions.edges[0] && rsp.data.transactions.edges[0].node.id; if (arch) { await p.goto('https://arweave.net/' + arch + '/', { waitUntil: 'networkidle', timeout: 90000 }); await p.waitForTimeout(5000); ok('Archiv ' + arch.slice(0, 8) + '… laedt', (await p.title()).includes('Gedächtnis')); } else ok('Archiv im GraphQL-Index', false, true); await b.close(); console.log(''); console.log(fails === 0 ? 'e2e ok' + (warns ? ' (' + warns + ' Warnungen)' : '') : 'e2e: ' + fails + ' FEHLER'); process.exit(fails === 0 ? 0 : 1); })().catch(e => { console.error('e2e-Abbruch: ' + e.message); process.exit(1); }); EOF