// @ts-check const { test, expect } = require('@playwright/test'); /** * Stufe 1 · Link-Crawler * * Pro zentraler Seite: alle internen -Links extrahieren und prüfen, * dass das Ziel ≤ 399 antwortet. Externe Links (http(s):// fremder Host) und * mailto:/tel:/javascript:/#-Links werden ignoriert. * * Das ist eine breitere „kein Link tot"-Garantie als der reine Slug-Check * — fängt Links auf z. B. Modul-Info-Seiten, im Handbuch oder im Lehrplan, * die wir in known-paths nicht explizit gelistet haben. */ const startSeiten = [ '/', // Landing '/handbuch.html', // Handbuch (12 Sim-Beschreibungen + Lehrplanbezug) '/lehrplan', // Lehrplan-Page '/glossar', // Glossar-Index '/mindmap', // Mindmap '/forschung/', // Forschung '/modul-klima', // Modul-Info-Seiten (eine genügt, sie nutzen das gleiche Partial) '/modul-weltkueche', '/teacher', // Lehrer-Cockpit (Login-Form, aber Links zum Drilldown da) '/schueler', // Schüler-Dashboard ]; // Konfiguriert sequenziell — sonst hämmern wir uns selbst mit hunderten // parallelen Requests an die gleiche Prod-Domain. test.describe.configure({ mode: 'serial' }); for (const seite of startSeiten) { test(`Crawl · ${seite}`, async ({ page, request }, testInfo) => { const res = await page.goto(seite); expect(res, `Startseite ${seite} lädt nicht`).not.toBeNull(); expect(res.status(), `Startseite ${seite} antwortet ${res.status()}`).toBeLessThan(400); // Alle hrefs sammeln — auch von Buttons mit data-href oder onclick, aber wir // beschränken uns auf , das ist das einfachste und 90 % der Links. const hrefs = await page.$$eval('a[href]', as => as.map(a => a.getAttribute('href')).filter(Boolean) ); // Filtern: nur interne, sinnvolle Pfade. // WICHTIG: relative URLs (z. B. `geografie-didaktik.html` auf /forschung/) // müssen GEGEN page.url() aufgelöst werden — NICHT gegen origin, sonst // landen wir fälschlich im Root. const pageUrl = page.url(); const origin = new URL(pageUrl).origin; const checkedSet = new Set(); const toCheck = []; for (const h of hrefs) { if (!h) continue; if (h.startsWith('#')) continue; // Anker if (h.startsWith('mailto:')) continue; if (h.startsWith('tel:')) continue; if (h.startsWith('javascript:')) continue; if (h.startsWith('data:')) continue; // Auflösen relativ zur aktuellen Seite (nicht zur origin!) let absolute; try { absolute = new URL(h, pageUrl).toString(); } catch { continue; } if (!absolute.startsWith(origin)) continue; // externer Host raus if (checkedSet.has(absolute)) continue; checkedSet.add(absolute); toCheck.push(absolute); } // Jeden Link einzeln prüfen — sequentiell, höchstens 30 pro Seite, damit // wir bei sehr großen Index-Seiten nicht ewig laufen const sample = toCheck.slice(0, 50); const broken = []; for (const url of sample) { try { const r = await request.get(url, { timeout: 15000 }); if (r.status() >= 400) broken.push({ url, status: r.status() }); } catch (e) { broken.push({ url, status: 'TIMEOUT/ERROR: ' + (e.message || e).toString().split('\n')[0].slice(0, 80) }); } } // Bericht als Attachment, plus Soft-Assertion: wir melden jeden gebrochenen // Link, akzeptieren aber bekannte Sonderfälle (404 für „geplant" laut // bestehender known-paths.json wäre OK — hier ignorieren wir den expliziten // Filter und stellen nur sicher dass keine 5xx kommen) const fiveXX = broken.filter(b => typeof b.status === 'number' && b.status >= 500); const fourXX = broken.filter(b => typeof b.status === 'number' && b.status >= 400 && b.status < 500); console.log(`[${seite}] geprüft: ${sample.length} Links, 4xx: ${fourXX.length}, 5xx: ${fiveXX.length}`); if (broken.length > 0) { console.log(' Gebrochene Links:'); broken.forEach(b => console.log(` ${b.status} ${b.url}`)); } // 5xx ist immer ein Bug. 4xx auch — aber wir nehmen nur die echten ans Bein expect(broken, `Gebrochene Links auf ${seite}: ${JSON.stringify(broken, null, 2)}`).toHaveLength(0); }); }