Ładny przycisk, puste drzewo
Jest 15 sierpnia. W listopadzie 2022 porównałem bramki wyglądu: toHaveScreenshot, Percy, Eyes, Backstop. W maju dodałem opis PNG modelem, ale bramką nadal został piksel. Dziś otwieram ten sam suite Playwright i widzę inną dziurę. Przycisk-ikona na syntetycznym logowaniu jest równo w siatce. Snapshot jest zielony. W drzewie dostępności nie ma nazwy. Czytnik usłyszy “button”.
Moja teza na sierpień 2024 jest wąska. Automat a11y w Playwright to dwa tanie sygnały na tym samym page load: katalog reguł axe-core i twarda asercja nazwy albo roli jednej kontrolki. To nie zastępuje czytnika ani audytu WCAG. To nie jest visual z tamtego listopada. Piksele i drzewo łapią inne regresje.
Stan warsztatu. Runner to Playwright 1.46.0 z 5 sierpnia. Paczka @axe-core/playwright stoi na 4.9.1 z 20 maja. Asercje toHaveAccessibleName, toHaveAccessibleDescription i toHaveRole mam od 1.44.0 z 6 maja; w kwietniu Chat proponował toHaveAccessibleName na pinie 1.43.1, którego tam nie było. WCAG 2.2 jest Recommendation od 5 października 2023 i zostaje mapą, nie certyfikatem, który wystawiam po zielonym jobie.
Czego tu nie będzie. Nie będzie kursu audytora i listy osiemdziesięciu kryteriów sukcesu. Nie będzie toMatchAriaSnapshot, bo tej asercji w 1.46 nie ma. Nie będzie pinu 1.46.1 ani @axe-core/playwright 4.10.0. Nie będzie zdania, że LLM naprawia a11y. Będzie syntetyczny formularz logowania: ikona bez nazwy, input bez <label>, AxeBuilder i dwie asercje.
Dwa sygnały, nie jeden skaner
Pierwszy sygnał to katalog. Na page load odpalam new AxeBuilder({ page }).withTags(['wcag2a', 'wcag2aa', 'wcag21aa']).analyze(). Dostaję listę znanych wzorców: brak etykiety, kontrast, który axe umie policzyć, duplikat id. Fail ustawiam na violation, nie na 400 reguł bez tagów.
Drugi sygnał to pin jednej kontrolki. expect(page.getByRole('button')).toHaveAccessibleName('Zaloguj') nie pyta katalogu, czy “wygląda OK”. Pyta, czy ta konkretna ikona ma nazwę, której oczekuję. toHaveRole robi to samo dla pola hasła, które ktoś owinął w div z role.
Kontrolka może przejść axe i stracić etykietę przy refaktorze copy. Axe patrzy na wzorzec. Asercja patrzy na kontrakt. Dlatego obie warstwy siedzą w jednym specu dymu, nie w dwóch religiach.
Demo, od którego zacząłem ten tydzień, jest syntetyczne i celowo zepsute. Formularz logowania ma przycisk SVG bez aria-label i input hasła bez <label>. AxeBuilder zwraca button-name i label. toHaveAccessibleName('Zaloguj') pada, bo nazwa jest pusta. Dopiero po aria-label na ikonie i <label for> na haśle oba sygnały milkną. Nie wrzucam tu zrzutu produkcyjnego formularza i nie cytuję wyniku audytu klienta.
Gdzie w CI
Dym a11y siada tam, gdzie dym UI: na PR albo na main, na ścieżce logowania, nie na całej aplikacji. Pełniejszy skan z szerszym zestawem tagów idzie na nightly. Tę granicę wpisałem we wrześniu 2022 i nie zamierzam jej rozmywać, bo axe ładnie maluje raport.
Fail na HIGH i na wybranych tagach wcag2a / wcag2aa / wcag21aa. Nie na każdym best-practice przy każdym pushu. Job, który czerwieni się na regule, której i tak nie naprawimy w tym sprincie, uczy zespół ignorować czerwony.
To nie jest nowy etat. To jest ten sam runner, w którym od maja 2022 mam E2E. Plugin screenshotów z Cypressa tu nie mieszam: inna oś, inny silnik, ten sam błąd kategorii, jeśli zlewę visual z a11y.
Czego automat nie widzi
Kolejność fokusa w głowie użytkownika. Sens etykiety, która istnieje i kłamie. Kontrast w motywie, którego axe nie dostał jako CSS. Czytnik, który czyta inną kolejność niż DOM. To zostaje przy człowieku i przy jednym przejściu z klawiatury.
Trzy fałszywe pewności z tego tygodnia, wszystkie do skreślenia.
Skreślenie pierwsze: “WCAG AA zaliczone”. Zielone axe na trzech tagach to nie Recommendation 2.2 i nie orzeczenie prawne. To jest katalog reguł na jednym URL. Proces nie kończy się skanerem.
Skreślenie drugie: visual złapie brak nazwy. toHaveScreenshot z listopada 2022 widzi ikonę w siatce. Nie widzi, że w drzewie nie ma Zaloguj. W maju model opisze PNG. Nadal nie podpisze aria-label.
Skreślenie trzecie: “dopiszę snapshot ARIA i będzie jak pixel gate”. Na 1.46.0 nie mam toMatchAriaSnapshot. Gdybym wkleił tę asercję z dokumentacji przyszłości, dostałbym TypeScript albo runtime, zanim dostałbym werdykt o nazwie. Zostaję przy AxeBuilder i przy toHaveAccessibleName.
GPT-4o mini mam od 18 lipca. Nie jest tematem tego wpisu. Nie wklejam mu HTML-a z prośbą “napraw a11y” i nie traktuję odpowiedzi jako merdża. Cursor w tym warsztacie nie jest daily driverem.
Kontrast z visual
Visual łapie layout. A11y łapie nazwę, rolę, etykietę. Nie zlewam ich w jeden job “UI quality”. Rok 2022 dał mi Playwright i visual. Sierpień 2024 dokładam drzewo, nie kasuję piksela.
Gdy ikona ucieknie o kolumnę, padnie toHaveScreenshot. Gdy zostanie w miejscu i straci aria-label, padnie a11y. Oba czerwone są poprawne. Jeden job, który ma “naprawić UI”, ukryje, które z nich jest prawdziwe.
Podsumowanie
W sierpniu 2024 tani automat a11y to AxeBuilder 4.9 na Playwright 1.46 plus asercja nazwy. Zielone axe nie zamyka ticketu dostępności.
Trzy zdania na rok. Dym na logowaniu pada, gdy ikona nie ma nazwy, nawet gdy snapshot jest zielony. toHaveAccessibleName pinuje kontrakt jednej kontrolki, którego katalog może nie zauważyć przy zmianie copy. Czytnik i człowiek zostają, bo skaner nie zna sensu etykiety.
Czego z tego nie wynika. Nie wynika, że mam certyfikat WCAG 2.2. Nie wynika, że kasuję visual. Nie wynika, że 1.46.1 już u mnie siedzi, bo go nie ma.
Warunek dojrzałości: dwa przebiegi pod rząd na tym samym formularzu zgadzają się z przejściem z klawiatury i nie wołają “AA zaliczone” tylko dlatego, że violations jest puste.

