/* ===================================================================== Barrierefreiheit jenseits der Tastatur (WCAG 2.1 AA) Die Tastaturbedienung (Sprung zum Inhalt, Fokus, keine Falle) ist in pruef-tastatur.mjs abgedeckt und live grün. Dieser Test prüft die übrigen Punkte, die man beim bloßen Ansehen nicht bemerkt, für einen Bildschirmleser oder bei starkem Zoom aber entscheidend sind: 1. BILDBESCHREIBUNGEN (WCAG 1.1.1) — Jedes inhaltstragende Bild braucht ein alt. Ein DEKO-Bild braucht umgekehrt ein LEERES alt="", sonst liest der Bildschirmleser den Dateinamen vor. Was hier zählt, ist: Gibt es überhaupt ein alt-Attribut? Ein fehlendes ist der Fehler. 2. ÜBERSCHRIFTENORDNUNG (WCAG 1.3.1) — Genau eine h1, und keine übersprungene Ebene (h2 -> h4). Für Bildschirmleser ist die Überschriftenliste das Inhaltsverzeichnis der Seite. 3. FORMULARBESCHRIFTUNG (WCAG 1.3.1, 4.1.2) — Jedes Eingabefeld braucht eine programmatisch verknüpfte Beschriftung (label[for], umschließendes label, aria-label oder aria-labelledby). Ein bloßer Platzhalter zählt NICHT: er verschwindet beim Tippen und wird nicht überall vorgelesen. 4. SPRACHE (WCAG 3.1.1) — muss gesetzt sein und zur angezeigten Sprache passen, sonst spricht ein Bildschirmleser den Text in der falschen Aussprache. 5. ZOOM (WCAG 1.4.4) — Der Viewport darf Zoom nicht verbieten (user-scalable=no oder maximum-scale=1 sperrt Sehbehinderte aus). Rein statische Prüfung des ausgelieferten HTML -- schnell, und deckt alle Seiten ab. ===================================================================== */ import { chromium } from "playwright"; const BASIS = "https://dogfather-universe.com"; const SEITEN = [ "/", "/kontakt.html", "/supporter.html", "/abonnieren.html", "/stimmen.html", "/bewerben.html", "/dogfather.html", "/hasidog.html", "/links.html", "/werte.html", ]; let ok = 0, fehl = 0; const funde = []; const pruefe = (seite, name, gut, info) => { if (gut) { ok++; console.log(" ok " + name + (info ? " -> " + info : "")); } else { fehl++; console.log(" FEHL " + name + (info ? " -> " + info : "")); funde.push(`${seite}: ${name} — ${info}`); } }; const browser = await chromium.launch(); for (const pfad of SEITEN) { console.log(`\n===== ${pfad} =====`); const seite = await browser.newPage({ viewport: { width: 1280, height: 900 } }); await seite.goto(BASIS + pfad, { waitUntil: "networkidle", timeout: 45000 }); await seite.waitForTimeout(600); const befund = await seite.evaluate(() => { /* 1. Bilder ohne alt-Attribut (leeres alt ist erlaubt). */ const bilderOhneAlt = [...document.querySelectorAll("img")] .filter((im) => { const r = im.getBoundingClientRect(); return r.width > 4 && r.height > 4 && !im.hasAttribute("alt"); }) .map((im) => (im.currentSrc || im.src || "").split("/").pop().slice(0, 40)) .slice(0, 6); /* 2. Überschriften einsammeln. */ const ueber = [...document.querySelectorAll("h1,h2,h3,h4,h5,h6")] .filter((h) => { const r = h.getBoundingClientRect(); const st = getComputedStyle(h); return r.height > 0 && st.visibility !== "hidden" && st.display !== "none"; }) .map((h) => Number(h.tagName[1])); const h1Zahl = ueber.filter((n) => n === 1).length; let sprung = null; for (let i = 1; i < ueber.length; i++) { if (ueber[i] - ueber[i - 1] > 1) { sprung = `h${ueber[i - 1]} -> h${ueber[i]}`; break; } } /* 3. Eingabefelder ohne Beschriftung. Verstecktes und Knöpfe raus. */ const felder = [...document.querySelectorAll("input, select, textarea")] .filter((f) => { const typ = (f.getAttribute("type") || "text").toLowerCase(); if (["hidden", "submit", "button", "reset", "image"].includes(typ)) return false; const r = f.getBoundingClientRect(); return r.width > 0 && r.height > 0; }); const ohneLabel = felder.filter((f) => { if (f.getAttribute("aria-label")?.trim()) return false; if (f.getAttribute("aria-labelledby")?.trim()) return false; if (f.id && document.querySelector(`label[for="${CSS.escape(f.id)}"]`)) return false; if (f.closest("label")) return false; if (f.getAttribute("title")?.trim()) return false; return true; }).map((f) => (f.getAttribute("name") || f.getAttribute("type") || f.tagName).slice(0, 30)).slice(0, 6); /* 4. Sprache. */ const lang = document.documentElement.getAttribute("lang"); /* 5. Zoom-Sperre im Viewport. */ const vp = document.querySelector('meta[name="viewport"]')?.getAttribute("content") || ""; const zoomGesperrt = /user-scalable\s*=\s*(no|0)/i.test(vp) || /maximum-scale\s*=\s*1(\.0)?\b/.test(vp); return { bilderOhneAlt, h1Zahl, sprung, ohneLabel, lang, zoomGesperrt, felderGesamt: felder.length }; }); pruefe(pfad, " alle Bilder haben ein alt-Attribut", befund.bilderOhneAlt.length === 0, befund.bilderOhneAlt.length ? befund.bilderOhneAlt.join(", ") : "ok"); pruefe(pfad, " genau eine h1", befund.h1Zahl === 1, `${befund.h1Zahl} Stück`); pruefe(pfad, " keine übersprungene Überschriftenebene", !befund.sprung, befund.sprung || "lückenlos"); pruefe(pfad, " jedes Eingabefeld ist beschriftet", befund.ohneLabel.length === 0, befund.ohneLabel.length ? befund.ohneLabel.join(", ") : `${befund.felderGesamt} Felder ok`); pruefe(pfad, " Sprache ausgezeichnet", !!befund.lang, befund.lang || "kein lang-Attribut"); pruefe(pfad, " Zoom nicht gesperrt", !befund.zoomGesperrt, befund.zoomGesperrt ? "user-scalable/maximum-scale sperrt" : "frei"); await seite.close(); } await browser.close(); console.log(`\n===== ${ok} bestanden, ${fehl} fehlgeschlagen =====`); if (funde.length) { console.log("\nZu beheben:"); for (const f of funde) console.log(" - " + f); } process.exitCode = fehl ? 1 : 0;