Files
dogfather-universe/pruef-barrierefrei.mjs
DogFatherGitandClaude Opus 5 8a2943df35 Fußzeilen-Spaltentitel h4 -> h2 (Überschriftenordnung, WCAG 1.3.1)
Die drei Fußzeilen-Spaltentitel (Filipe, Dogi&Hasi & Manager, Mehr)
waren <h4>, obwohl der Hauptinhalt bei h2/h3 endet -- ein Sprung in der
Überschriftenordnung (h2 -> h4) auf jeder Seite. Für Bildschirmleser ist
die Überschriftenliste das Inhaltsverzeichnis; eine übersprungene Ebene
stört die Orientierung.

Jetzt <h2> (eigenständige Abschnitte unter der Seiten-h1). Die Optik
bleibt exakt: Der CSS-Selektor .footer-grid h4 wurde zu .footer-grid h2
umgezogen, die kleine, gedämpfte Darstellung (.85rem, uppercase,
Akzentfarbe) überschreibt weiterhin die große globale h2-Größe.

Teil des Barrierefreiheits-Durchgangs (pruef-barrierefrei.mjs). Behebt
die reinen Fußzeilen-Fälle; die Hauptinhalt-Überschriften folgen separat.

Co-Authored-By: Claude Opus 5 <[email protected]>
2026-08-27 10:57:55 +02:00

126 lines
5.9 KiB
JavaScript

/* =====================================================================
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) — <html lang> 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;