Bekannte Einschränkungen

Was die Engine nicht bewertet, was davon abhängt, wo ein Scan läuft (jsdom, ein Browser oder die eigenen Skripte der Seite), und was eine Person prüfen muss. Manche Punkte sind Designentscheidungen, jede mit ihrer Begründung; jeder Punkt sagt, was ein Scan nicht feststellen kann, und, wo es etwas gibt, was dagegen zu tun ist. Lesen Sie das, bevor Sie sich auf ein Ergebnis ohne Befunde verlassen.

Was dagegen zu tun ist

Wenn …Dann …
Inhalt erscheint erst nach einem Klick, einem Hover oder dem Laden von DatenBringen Sie die Seite selbst in diesen Zustand und scannen Sie sie dann. Scannen Sie jeden Zustand, der zählt.
Eine Prüfung braucht Layout: Zielgröße, Textabstand, Kontrast gegen das, was unter dem Text gezeichnet wirdScannen Sie in einem echten Browser. jsdom hat kein Layout (im Ergebnis ist engine.environment.layout false), daher messen diese Regeln dort weniger oder melden notApplicable, wie die Abschnitte unten beschreiben.
Eine Anforderung betrifft die Bedienung per Tastatur, etwa eine TastaturfalleTesten Sie sie mit einer Tastatur. Kein Scan deckt sie ab.
Ein Ergebnis ist cantTellFolgen Sie den Schritten in howToCheck des Vorkommens und entscheiden Sie von Hand. uncertainty sagt, was fehlte, und lean, wohin die Hinweise der Seite deuten (siehe Ausgabeschema).
Die Seite ist responsivScannen Sie bei jeder Breite, die Ihnen wichtig ist.
Zwei Läufe derselben Seite weichen voneinander abVergleichen Sie engine.version und engine.environment (Viewport, noch ladende Schriften und Bilder), und laden Sie die Seite zwischen Scans neu.
Ein Scan meldet keine VerstößeDas ist keine Konformitätsaussage. Siehe WCAG-Konformitätszuordnung.

Die Abschnitte unten nennen die Gründe.

Außerhalb dessen, was ein Scan sehen kann

Ein Scan liest die Seite zu einem Zeitpunkt, in einem Zustand, bei einem Viewport: das DOM, seine berechneten Styles und, in einem echten Browser, sein Layout. Er benutzt die Seite nicht so, wie eine Person es tut. Er bewegt sich nicht per Tastatur durch sie, öffnet keine Menüs, wartet nicht auf ihre Daten und ändert nicht ihre Größe. (Im Browser fokussieren einige Regeln ein Element oder fügen für die Dauer ihrer Prüfung ein Stylesheet hinzu und setzen es danach zurück; siehe den nächsten Abschnitt.) Die Punkte hier brauchen diese Art von Interaktion über die Zeit, daher kann keine weitere Regel sie schließen. Sie brauchen einen Interaktionstest oder eine Person:

  • Erkennung von Tastaturfallen (WCAG 2.1.2): Dafür muss man den Fokus über die Seite bewegen und beobachten, wo er landet, was kein einzelnes Lesen des DOM leisten kann. Eine Technik, die funktionieren würde, ist konzipiert (siehe den Abschnitt zu Tastaturfallen in ACT_RULE_MAPPING.md), aber sie ist nicht gebaut: Ein Scan erkundet keine Benutzerinteraktionen und nicht die Zustände, zu denen sie führen, und keine der Prüfungen, die die Seite kurz verändern, bewegt den Fokus so durch sie, wie es die Erkennung von Tastaturfallen täte. Bis dahin ersetzt nichts im statischen Markup sie.
  • Reflow / Abschneiden beim Zoomen (WCAG 1.4.10): Erfordert echte Layout-Messung (clientWidth/scrollWidth) bei einem simulierten Viewport, der 320px entspricht. Anders als bei einigen Heuristiken dieser Engine, die auf CSS-Deklarationen beruhen, gibt es für „wird Inhalt bei 400 % Zoom abgeschnitten“ überhaupt keinen Ersatz im statischen Markup.
  • Dynamischer Zustand / Zustand nach Interaktion: Alles, was erst nach einem Klick, Hover oder asynchronen Datenladen existiert (der Inhalt eines Modals, die Optionen eines Dropdowns, Fehlermeldungen der Formularvalidierung), ist für einen Scan des aktuellen DOM der Seite unsichtbar. Rendert Ihr Framework es vorab (auch außerhalb des sichtbaren Bereichs oder mit hidden), lässt es sich scannen; existiert es erst nach einer Interaktion, nicht, es sei denn, Sie lösen diese Interaktion vor dem Scan selbst aus (z. B. Button klicken, dann scannen). Ein Scan des geöffneten Zustands prüft, was darin ist, nicht, ob Personen mit Tastatur oder Screenreader ihn öffnen, bedienen und schließen können.

Umgebungsabhängig: hängt davon ab, wie Sie sie ausführen

  • jsdom (Node, kein echter Browser) hat keine CSS-Layout-Engine. Regeln, die echte Geometrie brauchen, allen voran target-size-minimum (WCAG 2.5.8, braucht ein echtes getBoundingClientRect()), melden unter reinem jsdom notApplicable, statt zu raten. Führen Sie sie in einem echten Browser aus (Puppeteer/Playwright, siehe INTEGRATION.md Pattern 2), um von diesen Regeln echte Befunde zu bekommen.
  • Ob Text umbricht, erfordert Layout, deshalb werden Befunde zum Textabstand bei nicht umbrechendem Text zur Prüfung gemeldet. WCAG 1.4.12 und die zugehörigen ACT-Regeln gelten nur für Text mit einem weichen Zeilenumbruch, was ein statischer Scan nicht feststellen kann. avoid-inline-spacing behandelt Text standardmäßig als umbrechend, ein gewöhnlicher erzwungener Wert unter dem Grenzwert ergibt also weiterhin fail. Wo aus einem Grund, der ohne Layout erkennbar ist, kein Umbruch möglich ist (Text, der nicht umbrechen darf, oder ein Element mit fester Breite in einem horizontal scrollenden Vorfahren), meldet sie stattdessen cantTell. Text, der aus einem anderen Grund nie umbricht, wird weiterhin als Verstoß gemeldet.
  • Die Engine läuft auf den eigenen JavaScript-Built-ins der Seite. Eine Seite, deren Skripte eines beschädigen, auf das sie sich verlässt (Array.prototype.filter, Map, JSON.stringify, String.prototype.toLowerCase und weitere), beendet den Scan mit einem Fehler mit dem Code PAGE_BUILTINS_BROKEN, der jedes davon nennt, statt ein Ergebnis zu liefern, das sich gut liest und falsch ist; ist eines schon beschädigt, bevor das Bundle eingefügt wird (Object.keys), lädt das Bundle nicht. Eine Seite, die Object.prototype um then ergänzt, wird wie jede andere gescannt. DOM-Methoden, die eine Seite ersetzt (getComputedStyle, querySelectorAll, getBoundingClientRect), werden nicht geprüft: Regeln lesen, was sie zurückgeben.
  • Ein Name wird nicht aus Inhalt ermittelt, der mehr als 256 Ebenen tief verschachtelt ist. Der Durchlauf endet dort (Chrome ergibt aus Inhalt 100 Ebenen tiefer keinen Namen), daher fragen button-name-present, link-name-present, die Namensregeln für Menüeinträge, Optionen, summary, Tabs, Tooltips und Baumeinträge sowie empty-heading bei einem Element, dessen einziger Name tiefer liegt, nach (name_contentTooDeep); andere Regeln, die Namen aus Inhalt bilden, lesen solchen Inhalt als leer.
  • Inhalt in einem geschlossenen Shadow Root lässt sich nicht lesen. Kein Skript gelangt in attachShadow({ mode: 'closed' }) oder ein deklaratives shadowrootmode="closed", daher kann die Engine nicht sehen, was ein Browser von dort als Namen ermittelt. button-name-present und link-name-present sowie die Namensregeln für Menüeinträge, Optionen, Tabs, Baumeinträge, summary und Tooltips fragen bei einem Element nach, dessen Name von einem Custom Element stammen könnte, das gerendert wird, aber keinen lesbaren Inhalt zeigt, statt es scheitern zu lassen. Ein geschlossener Root an einem eingebauten Element (einem <span>) sieht aus wie ein leerer, etwa ein per CSS gezeichnetes Icon, daher scheitert ein Button darüber weiterhin. Prüfen Sie von Hand, was eine solche Komponente bereitstellt, etwa mit einem Screenreader.
  • Ein Name oder Zustand, den ein Custom Element über ElementInternals setzt, lässt sich nicht lesen. this.attachInternals().ariaLabel = 'Save' benennt das Element in jedem Browser, aber kein Skript kann das zurücklesen. Wo eine Komponente ihre Internals in einer Eigenschaft hält (_internals, internals, internals_, __internals), liest die Engine sie darüber und lässt das Element nach dem bestehen oder scheitern, was sie sagen. Bei jedem anderen definierten Custom Element ohne Namen (oder ohne erforderlichen Zustand) fragen die Namensregeln und aria-required-attr nach (name_internals, state_internals), statt es scheitern zu lassen. Prüfen Sie solche Elemente im Accessibility Tree des Browsers, oder installieren Sie den optionalen Internals-Hook, bevor die Skripte der Seite laufen: installInternalsHook() in jedem Binding (siehe Erste Schritte) und in test-matchers, oder internalsHookScript() aus core für eine eigene Seite (siehe INTEGRATION.md, wo gezeigt wird, wo er zu installieren ist). Der Hook zeichnet für den Scan die Internals jedes Elements auf und erkennt so auch ein Element, das nie welche angehängt hat. Eine Rolle, die nur über Internals gesetzt ist (internals.role), wird dort gelesen, wo die Internals lesbar sind, über eine Eigenschaft oder den Hook, als Standardrolle des Elements; liegt sie ohne Hook in einem privaten Feld, wird sie nicht gesehen, und zum Element wird nicht nachgefragt, da fast jede Komponente eine haben könnte. Eingebaute Elemente und nicht definierte Custom Elements werden wie bisher beurteilt.
  • Per Skript hinzugefügte Event-Listener lassen sich nicht auflisten. Ein Listener, der mit addEventListener hinzugefügt oder als Eigenschaft gesetzt wird (el.onclick = fn), hinterlässt nichts im Markup, und kein Skript kann ihn zurücklesen. Die Tastaturregeln (click-handler-keyboard-operable, mouse-only-event-handlers) beurteilen daher nur Inline-Handler-Attribute: Ein fokussierbares role="button", das seinen Klick-Listener per Skript bekommt, wird nicht gemeldet (nach einem, das keinen Fokus annehmen kann, fragt trotzdem widget-keyboard-focusable), ein per Skript hinzugefügter Tasten-Listener kann ein Inline-onclick nicht entschuldigen, und password-paste-enabled kann nicht erkennen, ob ein per Skript hinzugefügter Einfüge-Listener den Text zurückgibt, den ein Inline-onpaste verwirft. Installieren Sie den optionalen Listener-Hook (listenerHookScript()), bevor die Skripte der Seite laufen: Dann beurteilen die Regeln auch die Listener, die er aufzeichnet, und lassen dort, wo er gar keinen Tasten-Listener gesehen hat, ein Bedienelement fehlschlagen, das sich mit Enter und Leertaste nicht bedienen lässt. Ein Klick-Listener, den ein Framework an sein Wurzelelement hängt, bedient Elemente, die nichts benennt; auch mit dem Hook werden diese nach ihrem Markup beurteilt. Wo er installiert wird: INTEGRATION.md auf GitHub.
  • Wie eine Live-Region gefüllt wurde, ist im Nachhinein nicht zu sehen. Eine Statusmeldung wird angesagt, wenn sich eine Live-Region ändert, während assistive Technologien sie erreichen können (4.1.3), ein Scan sieht die Seite aber zu einem Zeitpunkt. Ohne Hilfe beurteilt live-region-announcements nur, was da ist: eine Live-Region, die durch aria-hidden oder inert vor assistiven Technologien verborgen ist, hinter einem offenen modalen <dialog> liegt oder auf aria-busy stehen geblieben ist. Mit dem optionalen Live-Region-Hook (liveRegionHookScript()) fragt sie auch nach einer Region, die die Seite schon mit ihrem Text eingefügt oder in einem Zug angezeigt und gefüllt hat, und getAnnouncements() listet, was die Seite zur Ansage bereitgestellt hat. Keines von beiden sagt, was ein Screenreader gesprochen hat: Das hängt vom Screenreader ab. Der Hook sieht nicht in Shadow Roots hinein. Details: INTEGRATION.md auf GitHub.
  • Ein Scan bei geöffnetem modalem Dialog sieht den Dialog, nicht die Seite. Ein mit showModal() geöffneter <dialog> macht den Rest des Dokuments inert, und Browser geben assistiven Technologien nur den Dialog. Ein Dialog, der mit dem Attribut open oder mit show() geöffnet wurde, ist nicht modal, auch mit aria-modal="true": Die Seite dahinter bleibt erreichbar, und die Engine beurteilt sie. Unter jsdom, das keinen Top Layer hat, gilt ein offener <dialog aria-modal="true"> stattdessen als modal, so nah, wie die Engine es dort erkennen kann. Die Engine folgt dem: Inhalt hinter dem Dialog bleibt bei jeder Regel außen vor, wie anderer verborgener Inhalt, und die Regeln zur Struktur der Seite selbst (page-has-heading-one, landmark-one-main, bypass-blocks-present, region) melden notApplicable, da der Scan diese Struktur nicht sehen kann. Scannen Sie die Seite mit geschlossenem Dialog erneut, um zu beurteilen, was dahinter liegt. Ein eigenes Modal, ein angezeigtes Element mit role="dialog" oder role="alertdialog" und aria-modal="true", macht diese Strukturregeln ebenfalls notApplicable; aria-modal macht nichts inert, daher beurteilen alle anderen Regeln die Seite dahinter weiterhin. Ein modaler Dialog in einem offenen Shadow Root zählt wie einer im Dokument; einer in einem geschlossenen Shadow Root lässt sich nicht finden. Ein eigenes Modal ohne aria-modal="true" wird nur über das inert oder aria-hidden behandelt, das die Seite auf den Rest setzt; das verbirgt die Seite, teilt den Strukturregeln aber nicht mit, dass ein Dialog offen ist.
  • <dialog> und andere Elemente, die das Standard-Stylesheet des Browsers ausblendet (kein open-Attribut, laut Spezifikation display: none), sowie jeder andere Teilbaum, der über display:none, visibility:hidden, [hidden] oder ein geschlossenes <details> verborgen ist, und ein [inert]-Teilbaum (den Regeln zum Aussehen von Inhalt, etwa Kontrast und Textabstand, weiterhin beurteilen, da er gezeichnet wird), werden standardmäßig von der Regelauswertung ausgeschlossen. Das ist ein bewusster Standard (engineOptions.includeHiddenElements: false), kein Versehen: Verborgener Inhalt ist für assistive Technologien und die Tastatur erst erreichbar, wenn er angezeigt wird, daher wäre es oft nur Rauschen, einen Markup-Fehler darin standardmäßig zu melden. Setzen Sie engineOptions.includeHiddenElements: true, um verborgene oder eingeklappte Teilbäume trotzdem auszuwerten, etwa um einen Markup-Fehler (wie eine kaputte ARIA-ID-Referenz) zu finden, bevor ein Dialog je geöffnet wird. Die Option und genau welche Mechanismen zum Verbergen sie abdeckt, beschreibt ENGINE_OPTIONS.md.
  • Ein aria-details, das verborgenen Inhalt nennt, wird nicht gemeldet. aria-valid-attr-value prüft, dass die ID ein Element nennt, nicht, dass das Element angezeigt wird. Chromium lässt die Beziehung aus seinem Accessibility Tree heraus, solange das Element verborgen ist, und nimmt sie wieder auf, sobald es angezeigt wird; Details, die sich auf Anforderung öffnen, werden also angesagt, wenn sie offen sind, und mehr verlangt WCAG nicht. Um die Beziehung so zu prüfen, wie assistive Technologien sie bekommen, scannen Sie die Seite mit angezeigten Details.
  • jsdom hat keine Popover. Es bringt die HTML-Regel mit, die ein geschlossenes [popover] verbirgt, aber kein showPopover(), und liest :popover-open uneinheitlich. Wo das DOM kein showPopover hat, behandelt die Engine jedes [popover] außer einem geöffneten <dialog> als geschlossen und verborgen, sein Inhalt wird also nicht geprüft; auch ein Popover, das das CSS der Seite sichtbar hält, wird unter jsdom nicht geprüft. Im Browser liest die Engine, was angezeigt wird. Um den Inhalt eines Popovers zu prüfen, scannen Sie die Seite in einem Browser mit geöffnetem Popover.
  • Unter jsdom benennt Text aus CSS-content nichts. jsdom berechnet keinen Style für ::before und ::after, daher hat ein Button oder Link, dessen einziger Text generiert ist (content: "Save", content: attr(data-label)), im Browser einen Namen und unter jsdom keinen; dort lassen button-name-present, link-name-present und die anderen Namensregeln ihn fehlschlagen. Scannen Sie solche Seiten im Browser.
  • jsdom behält eine Schriftgröße so, wie sie geschrieben ist. Ein Browser meldet eine berechnete Schriftgröße in px; jsdom behält ein Schlüsselwort oder ein calc() so, wie es geschrieben ist. Die Engine liest die Schlüsselwörter mit den Größen eines Browsers bei einem Standard von 16px (xx-large ist 32px) und calc() aus Längen und Zahlen, sodass großer Text wie im Browser erkannt wird. larger und smaller, die von der Größe des Elternelements abhängen, werden unter jsdom nicht aufgelöst: Solcher Text wird am Verhältnis für normalen Text gemessen, und seine Meldung gibt seine Größe mit 0px an. Ein Browser löst beide auf.
  • jsdom ignoriert die eigenen Stylesheets eines Shadow Root. Bestätigt in jsdom 29.1.1: Weder ein <style> in einem Shadow Root noch seine adoptedStyleSheets erreichen den berechneten Style der Elemente darin, daher sieht jsdom dort gesetzte Farben, Größen und display: none oder visibility: hidden nicht. Die Regeln beurteilen den Inhalt einer Komponente dann wie ungestylt: Kontrast wird an Standardfarben gemessen, und Inhalt, den ihr CSS verbirgt, wird als angezeigt beurteilt. Die einzige Ausnahme ist die Farbe eines Links, die die Kontrast-Helfer selbst aus den Stylesheets des Shadow Root ermitteln. Scannen Sie Seiten aus gestylten Web Components im Browser.
  • Das berechnete text-shadow von jsdom ist beim zweiten Lesen desselben Elements unzuverlässig. Bestätigt in jsdom 29.1.1: Liest man einen berechneten text-shadow-Wert ein zweites Mal am selben Element, egal über welchen Accessor, aus einer frisch angeforderten CSSStyleDeclaration für dieses Element und unabhängig von Caching, kommt stillschweigend ein anderer, falscher Wert „kein Schatten“ zurück statt des tatsächlich deklarierten. Das erste Lesen ist immer korrekt. Die Engine umgeht das intern, indem sie text-shadow jedes Elements genau einmal pro Lauf liest und das Ergebnis zwischenspeichert (siehe __textShadowInfoEl in src/core/contrast-helpers.js), ein einzelner Scan ist also nicht betroffen. Das Problem tritt nur auf, wenn Sie getComputedStyle(el).textShadow selbst mehr als einmal am selben von jsdom geparsten Element lesen. Ein echter Browser hat diesen Fehler nicht.
  • Ein zweiter Scan desselben jsdom-DOM kann einen Style verlieren, der eine Kurzschreibweise überschreibt. jsdom behält den berechneten Style eines Elements nach dem ersten Lesen, bis sich das DOM das nächste Mal ändert, und der Kopie, die es aus diesem Cache zurückgibt, fehlt eine Einzeleigenschaft, die eine Kurzschreibweise aus einer weniger spezifischen Regel überschreibt: Mit *{background:#888} und style="background-image:url(hero.png)" liefert das erste Lesen das Bild und jedes weitere none. Ein Scan behält den zuerst gelesenen Style und ist daher nicht betroffen, aber ein zweiter Scan desselben unveränderten DOM kann das Bild übersehen und Text bestehen lassen, zu dem der erste nachgefragt hat (contrast-computable). Scannen Sie ein frisch geparstes DOM, oder ändern Sie das DOM zwischen den Scans, was den Cache von jsdom leert; @surea11y/core/testing tut das für seinen eigenen zweiten Scan. Ein echter Browser hat diesen Fehler nicht.
  • Unter jsdom wächst die Scan-Dauer mit dem Quadrat der DOM-Tiefe, nicht mit der Anzahl der Elemente. jsdom löst geerbtes CSS auf, indem es bei jedem Aufruf von getComputedStyle die Kette der Vorfahren eines Elements durchläuft; ein Aufruf pro Element kostet also O(Elemente x Tiefe). Gemessen mit jsdom 29.1.1 ohne jeglichen Engine-Code: 4000 Elemente in einer Kette brauchen allein für getComputedStyle 8,3 s, gegenüber 0,25 s für dieselben 4000 als Geschwister. Die eigenen Durchläufe der Engine über Vorfahren sind begrenzt und bleiben linear; das sind also die Kosten von jsdom, nicht die der Regeln. Betroffen sind runDomRulesInPage und alles, was darauf aufbaut, einschließlich @surea11y/test-matchers; ein echter Browser berechnet geerbte Styles nativ und zeigt dieses Verhalten nicht. Komponenten-Frameworks verschachteln üblicherweise 100 bis 300 Ebenen tief, was problemlos schnell ist; spürbar wird es ab etwa 1000.
  • Unter jsdom wächst die Scan-Dauer auch mit der Größe des Stylesheets. jsdom berechnet einen Style, indem es jede CSS-Regel gegen das Element abgleicht. Die Engine ändert das DOM während eines Scans nicht, daher wird jedes Element einmal berechnet, aber dieses eine Mal kostet O(Regeln): Auf einer gespeicherten Nachrichtenseite (7.916 Elemente, 2.683 CSS-Regeln, jsdom 29.1.1) braucht die erste Berechnung von 5.735 Elementen etwa 224 s eines Scans von 243 s. Das sind die Kosten von jsdom; ein echter Browser berechnet dieselbe Seite in einem Bruchteil einer Sekunde. jsdom setzt außerdem Custom Properties nicht ein, daher ist eine mit var() gesetzte Farbe dort nicht lesbar: Zum Text oder Hintergrund wird nachgefragt (contrast-computable), statt ihn zu messen. Verwenden Sie für große Produktionsseiten einen echten Browser.
  • Unter jsdom wird die Farbe eines Links aus den Regeln der Seite ermittelt. jsdom ordnet sein eigenes :link { color: #0000EE } mit den Regeln der Seite nur nach Spezifität ein, daher unterliegt ihm das a { color: #fff } einer Seite, wo ein Browser die Regel der Seite anwendet. Unter jsdom ermittelt die Engine die Farbe eines Links und des Texts darin aus den Regeln der Seite, wie es die Kaskade eines Browsers tut; ein Link, den keine Regel der Seite färbt, behält die Linkfarbe des Browsers, und ein Link gilt als nicht besucht. Wo das nicht geht (ein Stylesheet, das sich nicht lesen lässt, ein var(), eine Regel unter @supports, @layer, @container oder in einer anderen verschachtelt), wird zum Text nachgefragt (contrast-computable, FOREGROUND_UNPARSABLE), statt ihn im Blau von jsdom zu messen. Im Browser wird die berechnete Farbe gelesen, wie sie ist.
  • Unter jsdom wird Text nach seinem berechneten font-size beurteilt. Im Browser beurteilen die Kontrastregeln und p-as-heading Text in der Größe, in der er gezeichnet wird, einschließlich CSS-zoom, einer SVG-Transformation und font-size-adjust, Letzteres aus den Metriken der Schrift selbst (font-size: 24px; font-size-adjust: 0.3 zeichnet Arial bei etwa 14px, was kein großer Text ist). jsdom berechnet kein Layout und liest keine Schrift, daher wird font-size dort genommen, wie es ist. Scannen Sie eine Seite, die ihren Text so skaliert, im Browser.
  • In einem echten Browser verändern einige Regeln die Seite für die Dauer ihrer Prüfung, und die Skripte der Seite können darauf reagieren. Um zu messen, was der Browser zeichnet, fokussiert css-hidden-focus Elemente wie die Tastatur und schaltet Übergänge an dem Element ab, das es misst; aria-hidden-focus fokussiert ein Element in einem aria-hidden-Teilbaum, um zu sehen, ob die Seite den Fokus sofort wegbewegt (ein Wächterelement einer Fokusfalle); text-spacing-content-loss fügt ein Stylesheet mit den Abständen aus WCAG 1.4.12 hinzu; die Kontrastregeln fügen einen leeren Frame außerhalb des sichtbaren Bereichs hinzu, um die eigenen Farben des Browsers für einen Platzhalter oder ein Aufklapp-<select> zu lesen. Der Scan setzt außerdem jede laufende Animation für die Dauer der Regeln auf einen festen Punkt (eine endliche an ihr Ende, eine endlose an ihren Anfang), sodass ein Einblenden im Endzustand gemessen wird. All das wird zurückgesetzt, bevor die Regel oder der Scan endet: Fokus, Scrollposition, style-Attribute, Animationen, das hinzugefügte Stylesheet und der Frame. Nicht zurücksetzen lässt sich, was die Skripte der Seite daraufhin tun, denn das Fokussieren eines Elements löst seine focus- und focusin-Handler aus: Ein Karussell springt vielleicht zur fokussierten Folie und verschiebt dabei aria-hidden, oder es aktiviert einen Button. Die Befunde dieses Scans sind die der Seite, wie sie vorgefunden wurde: aria-hidden-focus liest, welche Teilbäume verborgen sind, und das Markup, das es meldet, bevor es irgendetwas fokussiert. Die Seite bleibt aber so, wie ihre Skripte sie hinterlassen haben, daher kann ein zweiter Scan derselben Seite etwas anderes finden: Wenn Sie eine Seite scannen, die Sie weiter benutzen, oder eine Seite zweimal scannen, laden Sie sie dazwischen neu.
  • Ein Stylesheet von einem anderen Ursprung lässt sich von der Seite aus nicht lesen. Das Skript einer Seite kann die Regeln eines Stylesheets, das ein anderer Ursprung ausliefert (ein CDN, ein Schriftendienst), nicht auflisten, daher fragt css-orientation-lock dazu nach (STYLESHEETS_NOT_READABLE), und css-hidden-focus und css-focus-indicator-suppressed sehen keine Fokusregel darin. Übergeben Sie seinen Text in engineOptions.styleSheetTexts: preloadStyleSheets() lädt die, die der Server freigibt (CORS), und ein Werkzeug, das den Browser steuert, kann die übrigen von außerhalb der Seite laden (siehe INTEGRATION.md auf GitHub).
  • Statisches Markup vs. Live-DOM bzw. Zustand nach der Hydration. Die Regellogik selbst ist unabhängig von der DOM-Quelle: Sie wertet das DOM aus, das sie bekommt, ob von jsdom geparstes statisches HTML (Pattern 1) oder ein bereits geladener, bereits hydrierter Tab in einem echten Browser (Pattern 2, siehe INTEGRATION.md). Die CLI (npx @surea11y/cli scan <url>) holt dagegen ausschließlich statisches HTML, ohne JS-Ausführung; siehe die CLI-Dokumentation. Bei einem von einem JS-Framework hydrierten Widget, dessen serverseitig gerendertes Markup absichtlich einen Zustand ausliefert, bevor Client-JS ihn synchronisiert (z. B. <input type="checkbox" aria-checked="true">, ausgeliefert, bevor Client-JS bei der Hydration die native Eigenschaft checked passend setzt, ein sehr verbreitetes, völlig legitimes Muster), sieht ein CLI-Scan nur das Markup vor der Hydration. Ein Scan in einem tatsächlich geladenen Browser-Tab sieht dagegen den Zustand nach der Hydration; die beiden können sich also genau bei dieser Art von Element widersprechen, aus Gründen, die nichts mit der Korrektheit der Regeln zu tun haben. Deshalb ist aria-checked-state-mismatch auf manual/cantTell begrenzt statt auf ein hartes fail. Wenn Sie für hydrationsempfindliche Prüfungen die Genauigkeit des Live-DOM brauchen, führen Sie die Bibliothek direkt über Pattern 2 gegen eine bereits geladene Seite aus, nicht über die CLI mit statischem Abruf.
  • Ein Layout wird bei einem einzigen Viewport gemessen, mit den Schriften, die die Seite in diesem Moment hat. Regeln, die messen, was der Browser zeichnet, text-spacing-content-loss, target-size-minimum und skip-link, gelten für den Viewport, bei dem die Seite gescannt wurde: Eine responsive Seite kann bei einer Breite Text abschneiden oder ein Ziel verkleinern und bei einer anderen nicht, und Text, der gemessen wird, bevor seine Webfont geladen ist, hat eine andere Breite. Das Ergebnis hält beides in engine.environment fest, und die Befunde dieser Regeln wiederholen ihren Viewport und nennen, was gemessen wurde (wie weit Text über den Rand hinausging, wie groß ein Ziel ist und wie nah sein Nachbar liegt), sodass sich ein Befund, der zwischen Läufen abweicht, auf die Bedingungen zurückführen lässt. Scannen Sie bei jeder Breite, die Ihnen wichtig ist, und warten Sie vor dem Scan, bis die Seite fertig geladen ist: waitForPageReady() wartet auf das load-Ereignis, die Webfonts und die Bilder (siehe INTEGRATION.md, wie es vor einem Scan aufgerufen wird), und das Ergebnis hält in engine.environment fest, ob noch Schriften oder Bilder geladen wurden.
  • Im Browser wird Kontrast gegen das gemessen, was unter dem Text gezeichnet wird, so wie die Seite in diesem Moment ist. Zu Text über einem Bild, einem Verlauf oder einer Fläche, die ihn nur teilweise abdeckt, wird nachgefragt (contrast-computable, reasonCode BACKGROUND_OVERLAP), statt ihn zu messen. Text, den eine Box mit overflow: hidden vollständig außer Sicht hält, etwa eine Karussellfolie außerhalb ihrer Spur, bleibt außen vor, bis er sichtbar ist; scannen Sie die Seite in diesem Zustand, um ihn zu prüfen. Feste und sticky Boxen über Text werden ignoriert. Unter jsdom wird Text nur gegen seine Vorfahren gemessen. Bei einem Scan unter Forced Colors (Windows-Kontrastdesign oder ein emuliertes forced-colors: active) behält die Seite beim Messen ihre eigenen Farben, die Ergebnisse sind also die ohne Forced Colors, außer das CSS der Seite reagiert selbst auf Forced Colors (eine Media Query forced-colors oder eine Systemfarbe wie GrayText), was der Scan in engine.environment.forcedColorsCss meldet; wo das nicht geht (eine Content Security Policy, die das <style> der Engine in einem Browser blockiert, der kein Stylesheet übernehmen kann), stammen die gemessenen Farben teils vom Browser, und jedes Kontrast-Vorkommen sagt das (details.forcedColors). Wie die Messung Schritt für Schritt funktioniert: LAYOUT_MEASUREMENT.md, auf GitHub.
  • Text auf einer Seite ohne Hintergrund wird auf der Leinwand des Farbschemas der Seite gemessen, wo diese Leinwand bekannt ist. Der Browser malt seine Leinwand hinter eine Seite, die keinen deckenden Hintergrund malt, in der Farbe Canvas des Farbschemas, in dem die Seite gezeichnet wird; das entscheidet das color-scheme der Seite: weiß für eine Seite, die keines angibt, gleich, was die Person bevorzugt, und die dunkle Leinwand für eine, die dunkel gezeichnet wird. Die Kontrastregeln messen Text dort in beiden Modi. Nicht gemessen, und von contrast-computable nachgefragt (BACKGROUND_NOT_OPAQUE_AT_ROOT): Text in einem Frame, dessen Leinwand die umgebende Seite durchscheinen lassen kann, und Text unter Forced Colors, wo die Leinwand die Palette der Person ist.
  • Unter jsdom wird Text in den Farben des Schemas gemessen, in dem die Seite gezeichnet wird, wo sie sich ermitteln lassen, und sonst nachgefragt. jsdom berechnet die Farben des hellen Schemas und wendet keine prefers-color-scheme-Regel an. Auf einer dunkel gezeichneten Seite (color-scheme: dark an der Wurzel oder in <meta name="color-scheme">) nimmt Text, den das Schema färbt, die Farben des dunklen Schemas an (die von Chromium), und eine als light-dark(), CanvasText, LinkText oder Canvas angegebene Textfarbe wird für dunkel ermittelt. Zu Text, dessen Farben in diesem Schema sich nicht ermitteln lassen, wird nachgefragt (contrast-computable, COLOR_SCHEME_NOT_RENDERED; link-in-text-block, COLOR_NOT_COMPUTABLE), statt ihn in den hellen Farben zu messen. Im Browser sind die Farben die, die er zeichnet.
  • Auf einer Seite, die Hell und Dunkel unterstützt, wird das Schema, das der Scan nicht sieht, nur dort gemessen, wo sich die Farben des Texts damit nicht ändern können. Ein Scan sieht das Schema, das der Browser bevorzugt (jsdom: hell). Text auf der Leinwand wird auch im anderen Schema gemessen, außer wo eine prefers-color-scheme-Regel, ein light-dark()-Wert oder eine Systemfarbe, die die Seite setzt, die Textfarbe oder einen Hintergrund dahinter erreicht, oder wo sich ein Stylesheet nicht lesen lässt; ebenso wenig Text, der durch eine Kontur, einen Schatten, ::first-line oder ein Pseudoelement gezeichnet wird, oder Text, der über anderer Farbe gemessen wird. data.details.colorSchemesChecked listet die gemessenen Schemata; scannen Sie mit dem Browser im anderen Schema erneut, um es zu prüfen.
  • Generierter Text wird nur im Browser gemessen, und nur dort, wo er sich lesen lässt. Der Text, den ein content in ::before oder ::after schreibt, und der ::marker eines Listeneintrags werden in Farbe und Schrift des Pseudo-Elements gemessen; zu Inhalt, der sich nicht lesen lässt (ein Zähler in einem eigenen @counter-style der Seite), und zu einem ::before oder ::after, das ohne eigenen Hintergrund aus dem Fluss positioniert ist, wird stattdessen nachgefragt (contrast-computable). Marker werden nur an <li> gesucht. Unter jsdom, das keine Styles für Pseudo-Elemente berechnet, bleibt generierter Text außen vor.
  • Text und Platzhalter eines Formularfelds werden so gemessen, wie das Feld sie zeichnet, mit Lücken. Der Text, den ein Browser in ein leeres Datums- oder Zeitfeld zeichnet, sowie Button und Dateiname eines Dateifelds werden nicht gemessen. Ein Dropdown-<select> wird so gemessen, wie das geschlossene Bedienelement es zeichnet, seine angezeigte Option in den eigenen Farben des select; eine andere seiner Optionen mit eigenen Farben erscheint nur in der Liste, die die Plattform beim Öffnen zeichnet, und ist cantTell (SELECT_OPTION_COLOR). Ein Platzhalter unter seinem Verhältnis ergibt fail (PLACEHOLDER_BELOW_THRESHOLD), wenn die Seite seine Farben gewählt hat, erfüllt das Verhältnis, wenn alle Farben die des Browsers sind (Technik G148), und ist cantTell, wenn sich das nicht erkennen lässt. Unter jsdom werden Platzhalter gar nicht gemessen. Details: LAYOUT_MEASUREMENT.md.
  • target-size-minimum ermittelt die Trefferfläche aus dem Layout statt durch Hit-Testing. Ziele unterhalb des sichtbaren Bereichs oder in einer scrollenden Box werden ganz gemessen, als wären sie in den sichtbaren Bereich gescrollt; feste und sticky Boxen über einem Ziel werden ignoriert; 3D-Transformationen, die meisten clip-path-Formen und die abgerundete Beschneidung eines Vorfahren werden als umgebende Box gemessen; und Links, die nur durch Trennzeichen getrennt sind („Edit | Delete“) und einander zu nahe kommen, werden nachgefragt statt ausgenommen. Unter jsdom ist die Regel notApplicable. Details: LAYOUT_MEASUREMENT.md.
  • Kontrast wird in sRGB berechnet. Farben in oklch(), oklab(), lab(), lch() oder color() (auch die berechnete Form von color-mix() und relativen Farben) werden nach sRGB umgerechnet, wie ein Standardbildschirm sie zeigt; eine Farbe außerhalb von sRGB, etwa ein gesättigtes display-p3-Rot, wird auf dessen Rand beschnitten, so wie Chromium sie auf einem sRGB-Bildschirm zeichnet. Ein Wide-Gamut-Bildschirm kann diese Farbe gesättigter zeigen, als das Verhältnis annimmt. Eine Hintergrundfarbe, die die Engine nicht umrechnen kann, ein color() in einem ihr unbekannten Farbraum, wird nicht als transparent behandelt: contrast-computable fragt zu diesem Text mit dem reasonCode BACKGROUND_UNPARSABLE nach, und die anderen Kontrastregeln lassen ihn aus.

Nicht versucht: Ermessensfragen, die sich nicht sicher automatisieren lassen

Hierfür gibt es keine vergleichbar sichere Heuristik auf dem Niveau dieser Engine (fail bleibt deterministischen Verstößen vorbehalten, Punkt). Sie trotzdem zu bauen, würde entweder fast nichts finden (zu eng, um nützlich zu sein) oder echte Falsch-Positive riskieren (zu breit, um ihnen zu trauen):

  • „Ist dieser Überschriften- oder Beschriftungstext aussagekräftig?“ Echte Überschriften und Beschriftungen sind enorm vielfältig und oft zu Recht kurz („FAQ“, „Name“, „Übersicht“ sind alle in Ordnung), daher lässt sich am Markup nicht entscheiden, ob eine Überschrift den Abschnitt darunter beschreibt oder eine Beschriftung das Feld daneben. Entscheidbar ist, dass manche Strings gar nichts beschreiben können: heading-quality und form-control-label-quality markieren übrig gebliebene Platzhalter, nummerierte Vorlagen-Slots, Dateinamen und URLs anhand kuratierter Listen exakter Treffer, derselbe Kompromiss aus Präzision vor Vollständigkeit, den auch link-name-quality eingeht. Beide sind manual-Regeln, begrenzt auf cantTell: Sie melden einen Kandidaten zur Prüfung, behaupten aber nie, dass der Text falsch ist.
  • „Beschreibt diese Fehlermeldung das Problem?“ Was einen Validierungsfehler auslöst und was er enthält, wird fast immer von JS oder einer Validierungsbibliothek gesteuert und ist für einen statischen Scan von vornherein unsichtbar; das ist nicht nur eine Frage des Heuristik-Designs.
  • Feinere Teilprüfungen für zeitbasierte Medien (WCAG 1.2.x hat 13 ACT-Regeln ohne Regel hier; siehe den Abschnitt zu den Lücken in ACT_RULE_MAPPING.md): Audio- und Videoinhalte selbst lassen sich aus statischem Markup grundsätzlich nicht überprüfen. Die zwei breitesten, sichersten Fälle sind abgedeckt (media-alternative-transcript-evidence, video-caption), die engeren bewusst nicht.
  • Inhaltsanalyse von Schriftgrafiken (WCAG 1.4.5/1.4.9): Ob ein Bild Text enthält, steckt in seinen Pixeln, was ein Bildverständnis auf OCR-Niveau erfordern würde. image-of-text fragt nur dort, wo das Markup ein Anzeichen zeigt: bei einem <input type="image"> und einem <img>, das eine SVG-Data-URL zeigt, deren Markup <text> enthält. Das Foto eines Satzes, ein Banner mit eingebranntem Text oder ein CSS-Hintergrundbild mit Text gibt kein Anzeichen, und dazu wird nicht gefragt. 1.4.9, das keine Ausnahmen zulässt, hat keine Regel.
  • Betätigung durch Bewegung (WCAG 2.5.4): Nischig, in der Praxis selten; nicht priorisiert, aber nicht strukturell unmöglich.
  • Die zwei ACT-Randfälle von img-alt-decorative: Ein <img>, dessen aktueller Netzwerkstatus nicht „vollständig verfügbar“ ist (lädt noch oder ist defekt), und ein vollständig transparentes <canvas> (auf dem tatsächlich nichts gezeichnet ist) sind beide von der Anwendbarkeit nach ACT ausgenommen. Keins von beiden lässt sich aus einem statischen DOM-Scan entscheiden (kein Dekodieren von Bildern, kein Auslesen von Canvas-Pixeln), daher bleiben beide im Umfang, statt ausgenommen zu werden: ein seltenes Falsch-Positiv, das eine prüfende Person auf einen Blick verwirft, ist sicherer, als ein echtes ausgeschlossenes oder nicht dekoratives Element stillschweigend zu übersehen.

Was das in der Praxis bedeutet

Das meiste davon ist nicht spezifisch für surea11y. Jedes Tool, das einen Zustand des DOM beurteilt, teilt die Grenzen aus Außerhalb dessen, was ein Scan sehen kann, und die meisten automatischen Tools überlassen die Ermessensfragen ebenfalls einer Person, auch wenn Tools sich darin unterscheiden, wie sie laufen, was sie prüfen und wie sie melden, was sie nicht entscheiden können. Der Grund, es hier ausdrücklich zu sagen: Ein pass dieser Engine (oder jedes anderen automatischen Tools) ersetzt nie die manuelle Prüfung, die WCAG selbst für die oben genannten Kriterien verlangt. Was das pass einer Regel genau aussagt und was nicht, und warum ein WCAG-Kriterium, dessen Regeln alle pass ergeben, trotzdem cantTell ist, steht in WCAG_CONFORMANCE.md.