Barrierefreies Webdesign: Ausgewählte Kriterien der EN 301 549 schnell per JavaScript testen

Eine barrierefreie Webseite ist heutzutage mehr als nur eine nette Geste – sie ist eine Notwendigkeit und oft auch eine gesetzliche Anforderung. Ziel ist es, Webinhalte für alle Menschen zugänglich zu machen, unabhängig von ihren körperlichen oder technischen Einschränkungen. Doch wie kann man schnell und unkompliziert prüfen, ob die eigene Seite die grundlegenden Anforderungen erfüllt?

In diesem Artikel stelle ich Ihnen ein praktisches Werkzeug vor, mit dem Sie direkt im Browser eine erste, automatisierte Teil-Prüfung nach der wichtigen europäischen Norm EN 301 549 durchführen können. Das Skript deckt dabei häufige und wichtige Prüfschritte, die auf den WCAG basieren, ab und hilft, offensichtliche Fehler schnell zu identifizieren. Es ersetzt keine vollständige manuelle Prüfung, ist aber ein wertvoller erster Schritt.

Was ist barrierefreies Webdesign?

Barrierefreies Webdesign bedeutet, Webseiten so zu gestalten und zu entwickeln, dass Menschen mit Behinderungen sie ohne fremde Hilfe nutzen können. Das schließt ein breites Spektrum von Einschränkungen ein, wie zum Beispiel:

  • Sehbehinderungen: Von Farbenblindheit bis zur vollständigen Blindheit (hier kommen Screenreader zum Einsatz, die den Inhalt vorlesen).
  • Hörbehinderungen: Untertitel für Videos sind hier ein klassisches Beispiel.
  • Motorische Einschränkungen: Die Seite muss vollständig ohne Maus, nur mit der Tastatur, bedienbar sein.
  • Kognitive Einschränkungen: Eine klare Struktur, verständliche Sprache und eine vorhersehbare Navigation sind hier entscheidend.

Die Norm EN 301 549 legt die Anforderungen für die Barrierefreiheit von IT-Produkten und -Dienstleistungen in Europa fest und ist damit der maßgebliche Standard für öffentliche und zunehmend auch private Webseiten.

Was ist JavaScript und wie hilft es uns dabei?

JavaScript ist eine Programmiersprache, die in fast jeder modernen Webseite steckt. Sie ist dafür verantwortlich, Webseiten interaktiv und dynamisch zu machen – von einfachen Animationen bis hin zu komplexen Webanwendungen.

Für unseren Zweck nutzen wir JavaScript jedoch nicht, um etwas auf der Seite zu verändern, sondern um sie zu analysieren. Mit dem unten stehenden Code können wir die Struktur (HTML) und den Zustand einer Webseite direkt im Browser auslesen und auf häufige Barrierefreiheits-Probleme überprüfen.

Ein Monitor in dem das Javascriptsymbol angezeigt wird. Eine Tastatur, ein Rollstuhlsymbol und ein Symbol für hohen Kontrast

Für Menschen die lieber ein Video anschauen möchten als einen Blogartikel lesen, gibt es jetzt ein Video:

Welche Prüfkriterien der EN 301 549 deckt das Skript ab?

Das JavaScript-Werkzeug wurde entwickelt, um eine breite Palette an häufigen und kritischen Barrierefreiheitsanforderungen, die in der EN 301 549 verankert sind (und auf den WCAG basieren), automatisiert zu überprüfen. Es dient als effektiver erster Filter, um offensichtliche Probleme zu finden.

Konkret werden die folgenden Prüfschritte von dem Skript analysiert:

  • 9.1.1.1a & 9.1.1.1b: Alternativtexte: Prüfung, ob Bilder, Grafiken und Bedienelemente über ein alt-Attribut zur Beschreibung verfügen.

  • 9.1.3.1a: Überschriften: Analyse der korrekten Struktur und Hierarchie von H1-H6-Überschriften.

  • 9.1.3.1b: Listen: Erkennt Textabschnitte, die wie Listen aussehen, aber nicht mit den korrekten HTML-Tags (<ul><ol>) ausgezeichnet sind.

  • 9.1.3.1d: Zitate und Gliederung: Identifiziert falsch ausgezeichnete Zitate und strukturelle Probleme wie die Verwendung von Zeilenumbrüchen statt Absätzen.

  • 9.1.3.1e & 9.1.3.1f: Datentabellen: Überprüft, ob Tabellen korrekt mit Kopfzeilen (<th>) und Zuordnungen (scopeheaders) strukturiert sind.

  • 9.1.4.3: Kontrastverhältnis: Misst den Farbkontrast zwischen Text und Hintergrund, um die Lesbarkeit sicherzustellen.

  • 9.1.4.4: Textvergrößerung: Sucht nach technischen Blockaden (z.B. im viewport-Tag), die das Zoomen durch den Benutzer verhindern könnten.

  • 9.2.1.1 & 9.2.4.3: Tastaturbedienung: Analysiert, ob der Fokusindikator für Tastaturbenutzer sichtbar ist und startet eine manuelle Prüfung der Tab-Reihenfolge.

  • 9.2.4.1: Bereiche überspringbar: Prüft auf das Vorhandensein von Sprunglinks oder main-Elementen und kontrolliert, ob iframes einen Titel haben.

  • 9.2.4.2: Seitentitel: Verifiziert, dass jede Seite einen aussagekräftigen title-Tag besitzt.

  • 9.2.4.4: Linkzweck: Findet mehrdeutige Links wie „hier klicken“ oder „weiterlesen“, die manuell im Kontext geprüft werden müssen.

  • 9.3.1.1: Sprache der Seite: Kontrolliert, ob die Hauptsprache der Webseite im <html>-Tag korrekt deklariert ist.

  • 9.3.3.2 & 9.4.1.2: Beschriftungen und Namen: Stellt sicher, dass Formularfelder eine korrekte Beschriftung (<label>) haben und ARIA-Attribute auf gültige Elemente verweisen.

Anleitung: So führen Sie den Test im Browser durch

Sie können den Test auf jeder beliebigen Webseite durchführen. Kopieren Sie einfach den gesamten Code aus dem klappbaren Block weiter unten und fügen Sie ihn in die Entwicklerkonsole Ihres Browsers ein.

Anleitung für Google Chrome:

  1. Öffnen Sie die Webseite, die Sie testen möchten.
  2. Drücken Sie die Taste F12, um die Entwicklertools zu öffnen (oder klicken Sie mit der rechten Maustaste auf die Seite und wählen Sie „Untersuchen“).
  3. Wechseln Sie in den Reiter „Konsole“ (engl. „Console“).
  4. Fügen Sie den kopierten JavaScript-Code in die Eingabezeile der Konsole ein.
  5. Drücken Sie die Enter-Taste. Die Analyse startet sofort.

Anleitung für Mozilla Firefox:

  1. Öffnen Sie die Webseite, die Sie testen möchten.
  2. Drücken Sie die Taste F12, um die Entwicklerwerkzeuge zu öffnen (oder Rechtsklick -> „Element untersuchen“).
  3. Wählen Sie den Tab „Konsole“ aus.
  4. Fügen Sie den gesamten kopierten Code ein. Es kann eine Sicherheitswarnung erscheinen, die Sie bestätigen müssen (oftmals müssen Sie „einfügen erlauben“ eintippen).
  5. Drücken Sie Enter, um das Skript auszuführen.

Wichtiger Hinweis: Dieses Skript ist ein mächtiges Werkzeug für einen ersten Überblick. Da jede Webseite anders aufgebaut ist, kann es in Einzelfällen vorkommen, dass das Skript für eine spezifische Seite oder ein besonderes Element noch angepasst werden muss. Es deckt viele, aber nicht alle denkbaren Szenarien ab und dient als Ausgangspunkt für eine tiefere Analyse. Das Script wurde von mir bei mehreren Kunden-Webseiten erfolgreich eingesetzt.

Das All-in-One-Prüfskript für die Browser-Konsole

Klicken Sie hier, um das JavaScript-Prüfskript anzuzeigen und zu kopieren
// ===================================================================================
// ==                                                                             ==
// ==  Automatisierte Accessibility-Tests für EN 301 549 (basierend auf WCAG)     ==
// ==                                                                             ==
// ==  Anleitung: Gesamten Code kopieren und in die Browser-Entwicklerkonsole     ==
// ==  (F12) einfügen, dann Enter drücken.                                        ==
// ==                                                                             ==
// ===================================================================================

(function () {
  console.clear();
  console.log(
    "%c? Accessibility-Prüfskript v3.4 gestartet...",
    "color: #3b82f6; font-size: 1.4em; font-weight: bold;"
  );
  console.log(
    "Die Ergebnisse für die einzelnen Prüfschritte werden unten angezeigt. Eine Gesamtübersicht folgt am Ende."
  );

  const summary = {
    errors: 0,
    warnings: 0,
    manual: 0,
    passed: 0,
    results: [],
  };

  /**
   * Führt einen einzelnen Test aus, fasst die Ergebnisse zusammen und behandelt Fehler.
   */
  function runTest(title, testFunction) {
    let issues = { errors: 0, warnings: 0, manual: 0 };
    console.group(title);
    try {
      issues = testFunction() || { errors: 0, warnings: 0, manual: 0 };
    } catch (e) {
      console.error("Ein unerwarteter Fehler ist im Test aufgetreten:", e);
      issues.errors = (issues.errors || 0) + 1;
    }

    summary.errors += issues.errors || 0;
    summary.warnings += issues.warnings || 0;
    summary.manual += issues.manual || 0;

    let resultText, resultColor;
    if (issues.errors > 0) {
      resultText = `Fehler gefunden (${issues.errors})`;
      resultColor = "red";
    } else if (issues.warnings > 0 || issues.manual > 0) {
      const manualText =
        issues.manual > 0 ? `${issues.manual} manuelle Prüfung(en)` : "";
      const warningText =
        issues.warnings > 0 ? `${issues.warnings} Warnung(en)` : "";
      resultText = `Warnungen / Manuelle Prüfung (${[manualText, warningText]
        .filter(Boolean)
        .join(", ")})`;
      resultColor = "orange";
    } else {
      resultText = "Bestanden";
      resultColor = "green";
      summary.passed++;
    }

    summary.results.push({ title, resultText, resultColor });
    console.log(
      `%cErgebnis für "${title}": ${resultText}`,
      `font-weight: bold; color: ${resultColor}`
    );
    console.groupEnd();
  }

  // ===================================================================================
  // HIER BEGINNEN DIE EIGENTLICHEN PRÜFUNGEN (sortiert nach EN-Norm)
  // ===================================================================================

  runTest("9.1.1.1a: Alternativtexte für Bedienelemente", () => {
    let errors = 0;
    const problematicElements = [];
    document.querySelectorAll("a img, button img").forEach((img) => {
      if (!img.hasAttribute("alt") || img.alt.trim() === "") {
        errors++;
        const filename = img.src.split("/").pop() || img.src;
        problematicElements.push({
          "Bild-Dateiname": filename,
          Problem: "Fehlender oder leerer Alternativtext.",
          Container: img.closest("a, button"),
          "Bild-Element": img,
        });
      }
    });

    if (problematicElements.length > 0) {
      console.error(
        `FEHLER: ${problematicElements.length} aktive(s) Bild(er) ohne beschreibenden Alternativtext gefunden. Ein Bedienelement MUSS einen zugänglichen Namen haben.`
      );
      console.table(problematicElements);
    }

    return { errors };
  });

  runTest("9.1.1.1b: Alternativtexte für Grafiken und Objekte", () => {
    let errors = 0,
      manual = 0,
      warnings = 0;
    const imagesWithoutAltText = [];

    document.querySelectorAll("img:not(a img, button img)").forEach((img) => {
      const filename = img.src.split("/").pop() || img.src;
      if (!img.hasAttribute("alt")) {
        errors++;
        imagesWithoutAltText.push({
          Dateiname: filename,
          Grund: "FEHLER: 'alt'-Attribut fehlt komplett.",
          Element: img,
        });
      } else if (img.alt.trim() === "") {
        manual++;
        imagesWithoutAltText.push({
          Dateiname: filename,
          Grund: "MANUELL PRÜFEN: Als dekorativ (alt='') markiert.",
          Element: img,
        });
      } else if (/\.(jpg|jpeg|png|gif|svg)$/i.test(img.alt)) {
        warnings++;
        console.warn(
          `WARNUNG: Der Alternativtext "${img.alt}" sieht wie ein Dateiname aus.`,
          img
        );
      }
    });

    if (imagesWithoutAltText.length > 0) {
      console.warn(
        "Zusammenfassung der Bilder ohne (oder mit leerem) Alternativtext:"
      );
      console.table(imagesWithoutAltText);
    }

    return { errors, manual, warnings };
  });

runTest("9.1.3.1a HTML-Strukturelemente für Überschriften", () => {
    let errors = 0,
      warnings = 0;
    const headings = document.querySelectorAll("h1, h2, h3, h4, h5, h6");
    if (headings.length === 0) {
      errors++;
      console.error("FEHLER: Keine HTML-Überschriften (H1-H6) gefunden.");
      return { errors };
    }

    let lastLevel = 0,
      h1Count = 0;
    const usedLevels = new Set();

    headings.forEach((h) => {
      const level = parseInt(h.tagName.substring(1), 10);
      usedLevels.add(level);

      if (level === 1) h1Count++;
      if (level > lastLevel + 1 && lastLevel !== 0) {
        warnings++;
        console.warn(
          `WARNUNG: Falsche Überschriften-Reihenfolge. Auf eine H${lastLevel} folgt direkt eine H${level}. Das Überspringen von Ebenen unterbricht die logische Struktur.`,
          h
        );
      }
      lastLevel = level;
    });

    if (h1Count === 0) {
      warnings++;
      console.warn("WARNUNG: Keine H1-Überschrift gefunden.");
    } else if (h1Count > 1) {
      warnings++;
      console.warn(
        `WARNUNG: Es wurden ${h1Count} H1-Überschriften gefunden. Es sollte nur eine pro Seite geben.`
      );
    }
    
    if (usedLevels.size > 1) {
        const maxLevel = Math.max(...usedLevels);
        for (let i = 2; i < maxLevel; i++) { if (!usedLevels.has(i)) { warnings++; console.warn(`WARNUNG: Die Überschriften-Ebene H${i} fehlt, obwohl tiefere Ebenen (z.B. H${i+1} oder höher) verwendet werden. Dies könnte auf eine unlogische Gliederung hindeuten.`); } } } return { errors, warnings }; }); runTest("9.1.3.1b: HTML-Strukturelemente für Listen", () => {
    let warnings = 0;
    console.info(
      "HINWEIS: Dieser Test sucht nach Absätzen, die mit typischen Listen-Zeichen (*, -, 1.) beginnen, aber nicht als HTML-Liste ausgezeichnet sind."
    );
    document.querySelectorAll("p, div").forEach((el) => {
      if (el.closest("ul, ol, dl, li")) return;
      const text = el.textContent.trim();
      const isFakeListItem = /^\s*(\*|•|-|–|(\d+\.?)|([a-zA-Z][\.\)]))\s+/.test(
        text
      );
      if (isFakeListItem) {
        warnings++;
        console.warn(
          "WARNUNG: Dieses Element sieht wie ein Listenpunkt aus, verwendet aber keine korrekten HTML-Listen-Tags (ul, ol, li).",
          el
        );
      }
    });
    return { warnings };
  });

runTest("9.1.3.1e & 9.1.3.1f: Datentabellen (Struktur & Zuordnung)", () => {
    let warnings = 0;
    const tables = document.querySelectorAll("table");

    if (tables.length === 0) {
        return {};
    }

    tables.forEach((table, index) => {
        if (table.getAttribute("role") === "presentation" || table.getAttribute("role") === "none") {
            console.info(`INFO (Tabelle ${index + 1}): Layout-Tabelle wird ignoriert.`, table);
            return;
        }

        console.group(`Prüfung für Tabelle ${index + 1}`);

        const headers = table.querySelectorAll("th, [role='columnheader'], [role='rowheader']");
        if (headers.length === 0 && table.querySelectorAll("tr").length > 1) {
            warnings++;
            console.warn("FEHLENDE STRUKTUR (9.1.3.1e): Diese Datentabelle hat keine ausgezeichneten Kopfzeilen (weder 'th' noch role='columnheader'/'rowheader').", table);
            console.groupEnd();
            return;
        }

        let usesHeadersAttr = false;
        table.querySelectorAll("td[headers]").forEach(cell => {
            usesHeadersAttr = true;
            const headerIds = cell.getAttribute("headers").split(' ');
            headerIds.forEach(id => {
                if (id.trim() !== '' && !document.getElementById(id)) {
                    warnings++;
                    console.error(`FEHLERHAFTE ZUORDNUNG (9.1.3.1f): Die Datenzelle verweist via 'headers' auf eine nicht-existente ID "${id}".`, cell);
                }
            });
        });

        headers.forEach(th => {
            if (th.tagName.toLowerCase() === 'th' && !th.hasAttribute('scope')) {
                if (usesHeadersAttr) {
                    console.info(`INFO (9.1.3.1f): In dieser komplexen Tabelle wird die 'headers'-Methode verwendet. Das 'scope'-Attribut an diesem TH-Tag wäre dennoch eine gute Ergänzung.`, th);
                } else {
                    warnings++;
                    console.warn("FEHLENDE ZUORDNUNG (9.1.3.1f): In dieser einfachen Tabelle fehlt der TH-Kopfzeile das 'scope'-Attribut ('col' oder 'row').", th);
                }
            }
            if (usesHeadersAttr && !th.id) {
                 warnings++;
                 console.warn(`FEHLENDE ID FÜR ZUORDNUNG (9.1.3.1f): Dieser Kopfzeile fehlt eine 'id', obwohl die Tabelle die 'headers'-Methode zur Zuordnung verwendet.`, th);
            }
        });
        
        console.groupEnd();
    });

    return { warnings };
});

  runTest("9.1.3.1d: HTML-Strukturelemente für Zitate", () => {
    let warnings = 0;
    console.info(
      "HINWEIS: Dieser Test sucht nach Textabschnitten mit Anführungszeichen oder starkem Einzug, die nicht als Blockquote ausgezeichnet sind."
    );
    document.querySelectorAll("p, div").forEach((el) => {
      if (
        el.closest("blockquote") ||
        el.offsetParent === null ||
        !el.textContent.trim()
      )
        return;
      const text = el.textContent.trim();
      const style = window.getComputedStyle(el);
      const hasQuotes =
        (text.startsWith('"') && text.endsWith('"')) ||
        (text.startsWith("“") && text.endsWith("”"));
      const indent =
        parseFloat(style.marginLeft) + parseFloat(style.paddingLeft);
      const hasSignificantIndent = indent > 30;
      if (hasQuotes || hasSignificantIndent) {
        warnings++;
        const reason = hasQuotes
          ? "beginnt/endet mit Anführungszeichen"
          : "hat deutlichen Einzug";
        console.warn(
          `WARNUNG: Dieses Element sieht wie ein Zitat aus (${reason}), verwendet aber kein blockquote-Tag.`,
          el
        );
      }
    });
    return { warnings };
  });

  runTest("9.1.3.1d: Inhalt gegliedert und hervorgehoben", () => {
    let warnings = 0;
    
    console.info("HINWEIS: Prüfe auf doppelte br-Tags als Absatz-Ersatz.");
    document.querySelectorAll("br + br").forEach((br) => {
      warnings++;
      console.warn(
        "WARNUNG: Zwei (oder mehr) br-Tags werden nacheinander verwendet. Dies sollte durch korrekte Absätze (p-Tags) oder CSS-Margins ersetzt werden.",
        br
      );
    });

    console.info(
      "HINWEIS: Prüfe auf Texte, die nur visuell (per CSS, b, i) statt semantisch (strong, em) hervorgehoben sind."
    );
    
    document.querySelectorAll("body *").forEach((el) => {
      if (
        el.offsetParent === null ||
        el.children.length > 0 ||
        !el.textContent.trim()
      ) {
        return;
      }
      
      const style = window.getComputedStyle(el);
      const isBold = parseInt(style.fontWeight) >= 700;
      const isItalic = style.fontStyle === "italic" || style.fontStyle === "oblique";

      if (!isBold && !isItalic) {
        return;
      }
      
      if (el.closest("strong, em, h1, h2, h3, h4, h5, h6, a, button")) {
        return;
      }

      warnings++;
      const reason = isBold ? "fett (font-weight oder b-Tag)" : "kursiv (font-style oder i-Tag)";
      console.warn(
        `WARNUNG: Dieser Text ist nur visuell ${reason} formatiert. Wenn es sich um eine wichtige Hervorhebung handelt, sollte 'strong' oder 'em' verwendet werden.`,
        el
      );
    });

    return { warnings };
});

  runTest("9.1.4.3: Kontrastverhältnis (Minimum)", () => {
    let warnings = 0;
    function getRgb(c) {
      if (!c) return null;
      let m = c.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/);
      return m ? { r: +m[1], g: +m[2], b: +m[3] } : null;
    }
    function getLuminance(r, g, b) {
      const a = [r, g, b].map((v) =>
        (v /= 255) <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4) ); return a[0] * 0.2126 + a[1] * 0.7152 + a[2] * 0.0722; } function getContrast(rgb1, rgb2) { const l1 = getLuminance(rgb1.r, rgb1.g, rgb1.b), l2 = getLuminance(rgb2.r, rgb2.g, rgb2.b); return (Math.max(l1, l2) + 0.05) / (Math.min(l1, l2) + 0.05); } function findSolidBackgroundColor(el) { let c = el; while (c && c !== document.body) { const s = window.getComputedStyle(c), b = s.backgroundColor, m = b.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)(?:,\s*([\d.]+))?/); if (m && (m[4] === undefined || +m[4] > 0.95)) return b;
        c = c.parentElement;
      }
      return window.getComputedStyle(document.body).backgroundColor;
    }
    console.warn(
      "HINWEIS: Dieser Test ist eine Annäherung und kann keine Kontraste über Hintergrundbildern oder Farbverläufen prüfen."
    );
    document.querySelectorAll("body *").forEach((el) => {
      if (el.offsetParent === null || !el.textContent.trim()) return;
      const s = window.getComputedStyle(el),
        cRgb = getRgb(s.color),
        bgRgb = getRgb(findSolidBackgroundColor(el));
      if (!cRgb || !bgRgb) return;
      const contrast = getContrast(cRgb, bgRgb),
        fS = parseFloat(s.fontSize),
        isBold = parseInt(s.fontWeight) >= 700,
        reqContrast = fS >= 24 || (fS >= 18.66 && isBold) ? 3 : 4.5;
      if (contrast < reqContrast) { warnings++; console.warn( `WARNUNG: Geringer Kontrast (${contrast.toFixed( 2 )}:1). Benötigt: ${reqContrast}:1.`, { Element: el, Textfarbe: s.color, Hintergrund: findSolidBackgroundColor(el), } ); } }); return { warnings }; }); runTest("9.1.4.4: Textvergrößerung (Analyse)", () => {
    let errors = 0,
      warnings = 0;
    const viewport = document.querySelector('meta[name="viewport"]');
    if (
      viewport?.content.includes("user-scalable=no") ||
      viewport?.content.includes("maximum-scale=1")
    ) {
      errors++;
      console.error(
        "KRITISCHER FEHLER: User-Zoom ist im Viewport-Meta-Tag deaktiviert.",
        viewport
      );
    }
    document.querySelectorAll("body *").forEach((el) => {
      if (!el.textContent.trim() || el.offsetParent === null) return;
      const style = window.getComputedStyle(el);
      if (
        (style.height.endsWith("px") && style.height !== "0px") ||
        style.overflow === "hidden" ||
        style.overflowY === "hidden"
      ) {
        warnings++;
        console.warn(
          `WARNUNG: Potenzielles Problem bei Textvergrößerung (feste Höhe / overflow:hidden).`,
          el
        );
      }
    });
    return { errors, warnings };
  });

  runTest("9.2.1.1 & 9.2.4.3: Tastaturbedienung", () => {
    let warnings = 0,
      manual = 1;
    console.log("--- Teil 1: Prüfung auf unsichtbaren Fokus ---");
    document
      .querySelectorAll(
        'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
      )
      .forEach((el) => {
        const focusStyle = window.getComputedStyle(el, "::focus");
        if (
          focusStyle.outlineStyle === "none" ||
          parseInt(focusStyle.outlineWidth) === 0
        ) {
          if (
            focusStyle.boxShadow === "none" ||
            focusStyle.boxShadow === window.getComputedStyle(el).boxShadow
          ) {
            warnings++;
            console.warn(
              `WARNUNG: Element könnte einen unterdrückten Fokus-Indikator haben.`,
              el
            );
          }
        }
      });
    console.log(
      "\n--- Teil 2: Interaktiver Test der Fokus-Reihenfolge (MANUELL) ---"
    );
    console.info(
      "MANUELL PRÜFEN: Tab-Reihenfolge, Sichtbarkeit des Fokus und Tastaturfallen."
    );
    console.log(
      "%c? LIVE-PROTOKOLL GESTARTET: Drücke jetzt die Tab-Taste.",
      "background-color: #f87171; color: white; font-weight: bold; padding: 4px; border-radius: 4px;"
    );
    let focusCounter = 1;
    const focusHandler = (event) => {
      console.log(`${focusCounter++}. Fokus auf:`, event.target);
    };
    document.addEventListener("focusin", focusHandler, { once: false });
    window.stopFocusLogging = () => {
      document.removeEventListener("focusin", focusHandler);
      console.log(
        "%c? LIVE-PROTOKOLL GESTOPPT.",
        "background-color: #4ade80; color: white; font-weight: bold; padding: 4px; border-radius: 4px;"
      );
      delete window.stopFocusLogging;
    };
    console.info(
      "Tipp: Um das Protokoll zu beenden, gib 'stopFocusLogging()' in die Konsole ein."
    );
    return { warnings, manual };
  });

  runTest("9.2.4.1: Bereiche überspringbar", () => {
    let errors = 0,
      warnings = 0;
    const hasMain = !!document.querySelector('main, [role="main"]');
    let hasSkipLink = false;
    document.querySelectorAll('a[href^="#"]').forEach((link) => {
      try {
        const targetId = link.getAttribute("href").substring(1);
        if (targetId && document.getElementById(targetId)) hasSkipLink = true;
      } catch (e) {}
    });
    if (!hasMain && !hasSkipLink) {
      warnings++;
      console.warn(
        "WARNUNG: Kein Mechanismus zum Überspringen von Blöcken gefunden (main-Element oder Sprunglink).",
        document.body
      );
    }
    document.querySelectorAll("iframe").forEach((i) => {
      if (!i.title?.trim()) {
        errors++;
        console.error("FEHLER: Iframe ohne `title`-Attribut gefunden.", i);
      }
    });
    return { errors, warnings };
  });

  runTest("9.2.4.2: Seite hat einen Titel", () => {
    let errors = 0;
    const title = document.title;
    if (
      !title ||
      title.trim().length === 0 ||
      title.toLowerCase().includes("untitled")
    ) {
      errors++;
      console.error(
        "FEHLER: Seitentitel fehlt, ist leer oder ein Platzhalter.",
        { "Aktueller Titel": title }
      );
    }
    return { errors };
  });

  runTest("9.2.4.4: Linkzweck (im Kontext)", () => {
    let manual = 0;
    const genericTexts = [
      "hier klicken",
      "mehr",
      "weiterlesen",
      "click here",
      "more",
      "read more",
      "info",
    ];
    document.querySelectorAll("a").forEach((link) => {
      const linkText = link.textContent.trim().toLowerCase();
      const hasAriaLabel =
        link.hasAttribute("aria-label") &&
        link.getAttribute("aria-label").trim() !== "";
      if (genericTexts.includes(linkText) && !hasAriaLabel) {
        manual++;
        console.warn(
          `MANUELL PRÜFEN: Generischer Linktext "${link.textContent.trim()}". Prüfen, ob der Kontext den Zweck verdeutlicht.`,
          link
        );
      }
    });
    return { manual };
  });

  runTest("9.3.1.1: Sprache der Seite", () => {
    let errors = 0;
    const lang = document.documentElement.lang;
    if (!lang || lang.trim().length === 0) {
      errors++;
      console.error(
        "FEHLER: Dem html-Element fehlt ein gültiges 'lang'-Attribut.",
        document.documentElement
      );
    }
    return { errors };
  });

  runTest("9.3.3.2 & 9.4.1.2: Beschriftungen für Formularfelder", () => {
    let errors = 0;
    const controls = document.querySelectorAll(
      'input:not([type="hidden"]):not([type="submit"]):not([type="button"]):not([type="reset"]), select, textarea'
    );
    controls.forEach((control) => {
      const id = control.id;
      const hasAriaLabel = control.getAttribute("aria-label")?.trim();
      const hasAriaLabelledby = control.getAttribute("aria-labelledby")?.trim();
      const parentLabel = control.closest("label");
      let hasLabelFor = false;
      if (id) {
        hasLabelFor = !!document.querySelector(`label[for="${id}"]`);
      }
      if (!hasAriaLabel && !hasAriaLabelledby && !parentLabel && !hasLabelFor) {
        errors++;
        console.error(
          "FEHLER: Formularfeld ohne zugängliche Beschriftung.",
          control
        );
      }
    });
    return { errors };
  });

  runTest("9.4.1.2: Name, Rolle, Wert (ARIA-Validierung)", () => {
    let errors = 0;
    document
      .querySelectorAll(
        "[role], [aria-labelledby], [aria-describedby], [aria-controls]"
      )
      .forEach((el) => {
        ["aria-labelledby", "aria-describedby", "aria-controls"].forEach(
          (attrName) => {
            if (el.hasAttribute(attrName)) {
              const ids = el.getAttribute(attrName).split(" ");
              ids.forEach((id) => {
                if (id.trim() !== "" && !document.getElementById(id)) {
                  errors++;
                  console.error(
                    `FEHLER: Das Attribut ${attrName} verweist auf eine nicht existierende ID "${id}".`,
                    el
                  );
                }
              });
            }
          }
        );
      });
    return { errors };
  });

  // ===================================================================================
  // Finale Zusammenfassung
  // ===================================================================================
  console.group("? Finale Zusammenfassung aller Tests");
  const totalTests = summary.results.length;
  console.log(
    `%cGesamtergebnis (${totalTests} Tests): ${summary.errors} Fehler, ${summary.warnings} Warnungen, ${summary.manual} manuelle Prüfungen, ${summary.passed} Tests bestanden`,
    `font-size: 1.2em; font-weight: bold; color: ${
      summary.errors > 0
        ? "red"
        : summary.warnings > 0 || summary.manual > 0
        ? "orange"
        : "green"
    }`
  );

  summary.results
    .sort((a, b) => a.title.localeCompare(b.title))
    .forEach((res) => {
      console.log(
        `%c? ${res.title}: ${res.resultText}`,
        `color: ${res.resultColor};`
      );
    });

  console.log(
    "\nBitte prüfen Sie die Details zu den einzelnen Tests in den Gruppen oben."
  );
  console.groupEnd();
})();

Was bedeuten die Ergebnisse?

Nachdem das Skript durchgelaufen ist, sehen Sie eine detaillierte Aufschlüsselung der Ergebnisse direkt in der Konsole. Diese sind farblich markiert:

  • Fehler (rot): Dies sind klare Verstöße gegen die Richtlinien, die sehr wahrscheinlich zu Problemen für Nutzer führen und dringend behoben werden sollten.
  • Warnungen (orange): Diese weisen auf potenzielle Probleme hin, die nicht immer eindeutig maschinell als Fehler identifiziert werden können. Hier ist oft eine manuelle Prüfung nötig.
  • Manuelle Prüfung (orange): Das Skript hat etwas gefunden, das es nicht bewerten kann, wie z.B. einen Link mit dem Text „mehr“. Ob dieser Link im Kontext verständlich ist, muss ein Mensch beurteilen.
  • Bestanden (grün): In diesem spezifischen Testpunkt wurden keine automatisiert erkennbaren Probleme gefunden.

Schlussbemerkung

Automatisierte Tests wie dieser sind ein fantastischer erster Schritt, um die „niedrig hängenden Früchte“ zu ernten und offensichtliche Mängel aufzudecken. Sie können jedoch niemals eine vollständige, manuelle Prüfung durch Experten oder Tests mit echten Nutzern ersetzen.

Nutzen Sie dieses Skript als Ihren ersten Helfer auf dem Weg zu einer inklusiveren und besseren Webseite für alle Ihre Besucher.

Wenn Sie Fragen zu den obigen Themen haben, schreiben Sie mir eine Mail an info@marlem-software.de oder rufen Sie mich an unter 07072/1278463 .

Weiterlesen

Praxistest ChatGPT: Wie barrierefrei ist die künstliche Intelligenz ChatGPT im Betriebssystem Windows 11?
Praxistest Google AI: Googles KI: Google AI Studio und Barrierefreiheit
Barrierefreiheit und Künstliche Intelligenz: Der komplette Leitfaden (nach EN 301 549 + WCAG 2.2)

Autor: Markus Lemcke

Ich bin Markus Lemcke, Softwareentwickler, Webentwickler, Appentwickler, Berater und Dozent für barrierefreies Webdesign, barrierefreie Softwareentwicklung mit Java, C# und Python, Barrierefreiheit bei den Betriebssystemen Windows, Android, IOS, Ubuntu und MacOS.

Schreibe einen Kommentar