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]>
126 lines
5.9 KiB
JavaScript
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;
|