Files
geograsim/.dontDeploy/playwright/tests/stufe1-smoke/link-crawler.spec.js
T
Adminator 1e51ef7def Nachtrag: alle bisher untracked Ordner + hängende Änderungen mit-committen
- Konzept/, didaktik_geografie/, didaktik_simulation/, v2-modules/, v2-platform/
- 12 code-workspace-Files
- STATUS-*.md
- viele M/D/R-Änderungen an bereits getrackten Files
- .gitignore verstärkt: **/.humaninput/, **/secret_keys.txt

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-08 02:27:02 +02:00

98 lines
4.3 KiB
JavaScript

// @ts-check
const { test, expect } = require('@playwright/test');
/**
* Stufe 1 · Link-Crawler
*
* Pro zentraler Seite: alle internen <a href>-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 <a href>, 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);
});
}