Přeskočit na obsah

Tato stránka byla automaticky přeložena z angličtiny, jediného jazyka, ve kterém se tento web píše. Číst originál.

Shoda s UI

shoda UI: sada stránek a sada šířek objevené, ne vypsané, responzivní, sémantické, komponentové a WCAG invarianty, které prohlížeč kontroluje na každé stránce, co build normalizuje v markupu, který nenapsal, a kde je report

Vykresleno z docs/UI.md — stejný Markdown, který ukazuje GitHub.

Jak se každá stránka, kterou tento repozitář servíruje, drží jednoho standardu a proč je tímto standardem audit, ne kontrolní seznam. Chování tak, jak je implementováno a testováno; kde se implementace a tento dokument rozcházejí, je špatně dokument a mění se ve stejném commitu jako oprava. Pravidlo je project.ui-conformance@1; rozhodnutí a co odmítlo je ADR 0022.

Problém, který to řeší

Standard UI, který žije v komentářích review, je vynucován na stránkách, na které se někdo podíval. Tento web je generovaný: stovky stránek z několika desítek šablon a jednoho tématu, takže rozhodnutí učiněné jednou se objeví všude a nikde se neměří. První strojový audit každé stránky při každé šířce, kterou téma deklaruje, zjistil, že drtivou většinu selhání způsobila čtyři rozhodnutí — jedna zvýrazňovací paleta, jedno typografické pravidlo, jeden idiom odkazů, jedna trasa připojená nad sekci webu. Žádné z nich není chybou stránky.

Standard je tedy spustitelný, běží nad každou stránkou, ne jen nad vzorkem, a nález se považuje za sdílený zdroj, dokud se neprokáže opak.

Šířky, při nichž se každá stránka auditují, jsou media queries, které motiv zkompiloval, a šířky, které deklaruje share/design/tokens.yaml (viewports), sjednocené; dvě browserové sondy (scripts/site-probe, scripts/cockpit-probe) měří jen při deklarovaných šířkách a také ověřují kontrakt designu — že stylesheet, který stránka načetla, samotná stránka a exekutor nesou jeden otisk. Jak je samotný design deklarován a promítán, je DESIGN_SYSTEM.md.

Příkazy

PříkazNa co odpovídá
scripts/ui pages [--json]každá stránka, která se bude auditovat, její úroveň a kde byla nalezena
scripts/ui viewports [--json]každá šířka, při které se bude auditovat, a media queries, z nichž pocházejí
scripts/ui checkstatické invarianty nad značkováním, bez prohlížeče a bez sestaveného webu
scripts/ui audit [--report]každá stránka ve skutečném prohlížeči; zapíše dokument s výsledky, volitelně vykreslí /tests/ui
scripts/ui testvlastní testy zjišťování

majordomus web report ui --from target/web/run-ui.json vykreslí výsledky jako oddíl testovacího povrchu. Exit kódy sledují konvenci repozitáře: 0 čisté, 10 nálezy, 12 chybějící předpoklad, 13 chybějící závislost.

Co se objevuje a co to přináší

Stránky sestaveného povrchu jsou sjednocením dvou zdrojů: každého index.html pod sestaveným webem a každého <loc> jeho sitemapy. Ani jeden sám nestačí — stránka, kterou sitemap vynechá, je sirotek a stále musí fungovat, a stránka, kterou sitemap inzeruje, musí existovat. Položky sitemapy nesou publikovanou základní URL, takže společný prefix se odvodí a odřízne místo konfigurace.

Stránky povrchu, který spustitelný soubor vykresluje — Cockpit, domovská stránka serveru, shell Swaggeru — nemohou pocházet z adresáře, protože žádný není. Pocházejí z povrchu samotného: jeho vlastních kotev <a href>, procházených od jeho mountu a zůstávajících uvnitř něj. Stránka, kterou čtenář dosáhne, je stránka, na kterou něco odkazuje, takže ten uzávěr je množina stránek a stránka, na kterou nic nevede, je mimo dosah procházení ze stejného důvodu, z jakého je mimo dosah čtenáře. Čtení kotev místo každého href je také důvod, proč nic nepotřebuje seznam přípon souborů, aby rozlišilo stránku od stylesheetu: markup ten rozdíl už udělal.

Zda povrch vůbec má stránky, je odpověď serveru, ne zde vedený seznam: mount, který na požadavek Accept: text/html vrací dokument, je stránkový povrch a /api/v1, /events a /mcp ne — žádný z nich nemusel být jmenován, aby byl vynechán.

Procházení je ohraničeno tím, co se naučí, ne hloubkou. Trasa se rozbaluje, dokud trasy jejího tvaru stále přinášejí trasy, které nikdo neviděl, takže stránkovaný výpis se sleduje až na poslední stránku a tisíc listových stránek, jejichž jediné odkazy vedou zpět do shellu, stojí dohromady dvě stažení. Změřeno: 2660 tras Cockpitu odvozeno ve 108 požadavcích, asi za čtrnáct sekund, aniž by kdekoli byla jmenována jediná trasa.

Toto odvození žije v scripts/lib/ui-routes.mjs a je jediné, které existuje: scripts/cockpit-probe importuje stejný modul, takže dva prohlížečové nástroje nad tímto repozitářem se nemohou rozcházet v tom, co je trasa Cockpitu. Liší se tím, co každý tvrdí — sonda vlastní shell, bezpečnostní hlavičky, designový otisk, interakce a tvrzení, že vrstva prohlížeče je volitelná; audit vlastní kontrast, přístupnostní engine, landmarky, komponenty a průchod šířkami.

Šířky jsou media queries min-width, které vydalo sestavení CSS, převedené z rem, kde je motiv používal, přičemž každá přispívá hranicí a pixelem pod ní, plus mez reflow 320 a šířku desktopu. Breakpoint přidaný do motivu se audituje v den, kdy se zkompiluje.

Úrovně se také odvozují. Každá stránka se navštíví na spodní hranici, při střední šířce a na desktopovém konci; jedna stránka na sekci dostane úplné prohnání přes všechny hranice. Na sestaveném povrchu je sekcí první segment cesty, protože tam se mění šablony; na procházeném povrchu je to rodina — tvar, který routa sdílí se svými sourozenci, klíče ?, když má query, a rodičovská cesta, když ji nemá. Routa se může lišit v obojím najednou — /cockpit/graphs/composed?nodes=3 je záznam v cestě a pohled v query — a rozhoduje o tom procházená množina: adresář, který procházení dosáhlo pod více než jedním jménem pod vstupem, drží záznamy a jeho potomci se sbalí na * jako kterýkoli jiný. Potomci samotného vstupu jsou oblasti povrchu a nikdy se nesbalují, takže /cockpit/objects?page a /cockpit/capabilities?page zůstávají dvěma renderery, zatímco osm stránek grafu, každá stránkující dvě tabulky, zůstává jednou rodinou místo čtyřiadvaceti. Obojí je tam, kde se renderer mění, takže prohnání kupuje strukturální pokrytí, aniž by někdo jmenoval stránku.

Vzorek se také odvozuje a mají ho jen procházené plochy. Navštíví se každá route, kterou plocha inzeruje, protože každá je samostatná stránka; z každé rodiny audit vezme čtyři členy rozprostřené po seřazené množině, protože rodina je jeden vykreslovač nad mnoha záznamy a proměnlivý je záznam. Čtyři je rozpočet vynaložený na členy, kteří existují, ne seznam — rodina s méně členy se navštíví celá, a proto se nikdy nevypustí žádná položka navigace. Cockpit s 2660 routes takto dává 108 stránek a zpráva vypíše obě čísla, aby se vzorek nikdy nedal číst jako celek.

Nic z toho není seznam. scripts/ui pages tiskne svůj vlastní původ, stejně jako generovaný report.

Co audit kontroluje

Nad rámec přístupnostního enginu (axe-core, sady pravidel WCAG 2.0 A/AA, 2.1 A/AA a 2.2 AA):

  • responsive.horizontal-overflow — dokument není širší než viewport a selhání jmenuje prvky, které sahají za okraj, přičemž přeskočí vše uvnitř vlastního posuvného boxu, který smí být širší.
  • semantics.main / semantics.h1 / semantics.heading-order — jeden hlavní landmark, jeden nadpis první úrovně, žádná přeskočená úroveň.
  • semantics.duplicate-id — duplicitní id rozbije každý odkaz na něj, včetně odkazu komponenty.
  • component.target-missing / component.trigger-unnamed — každý spouštěč Flowbite (data-collapse-toggle, data-dropdown-toggle, data-modal-target, data-drawer-target, data-accordion-target, data-tabs-toggle, data-tooltip-target, data-popover-target a přepínače vedle nich) pojmenovává existující cíl a má přístupný název.
  • runtime.console-error / runtime.asset-failed — stránka se načetla bez chyby a bez selhání požadavku na stejný origin. Výjimka, kterou nezachytí žádný skript, se počítá jako chyba (hlášená s prefixem uncaught:): prohlížeč ji vyvolá jako chybu stránky, ne jako zprávu konzole, a průchod, který poslouchal jen konzoli, přehlédl, že Mermaid padá na každé stránce s diagramem. Zrušený požadavek není selhaný: net::ERR_ABORTED je prohlížeč říkající, že odpověď už nepotřebuje, což vyvolá lazy načítaný asset za letu, když audit pokračuje dál, a o webu to neříká nic.
  • page.status / page.unreachable / page.audit-failed — stránka odpověděla a odpověděla včas. Stránka, která ne, je nález o této stránce, nikdy konec běhu.

Podstrom, který nám nepřísluší opravovat

Stránka může nést strom komponent, který tento repozitář nenapsal a rozhodl se jej nepřestylovat: připnutý na verzi, jeho značkování a stylopis přicházejí společně. Widget Swagger UI je zde jediný takový strom a ADR 0036 toto rozhodnutí už zaznamenalo.

Stránka deklaruje hranici — data-mj-foreign="swagger-ui-dist@5.17.14" na prvku, který ji drží — a engine dostane pokyn ji přeskočit. Nic v auditu neví, jak se widget jmenuje. To, co je kolem něj, se měří jako obvykle, a tak byl nalezen a opraven chybějící <main> a chybějící <h1> shellu Swaggeru v den, kdy se k němu audit poprvé dostal.

Každá deklarace se hlásí jednou, pod Neměřeno a proč, protože „neměřeno“ a „změřeno a čisté“ jsou různá tvrzení a zpráva, která by je zaměnila, by neměla cenu. Podstrom bez deklarace se měří jako cokoli jiného, takže to nelze použít k umlčení zjištění — jen k tomu, abychom na stránce řekli, čí zjištění to je.

Jak se engine dostane na stránku, která zakazuje skripty

Engine přístupnosti se vyhodnocuje přes ladicí protokol, ne jako připojený prvek <script>. Prvek <script> je v dokumentu, takže o tom, zda smí běžet, rozhoduje Content Security Policy samotného dokumentu — a politika Cockpitu je script-src 'self' s hashem na skript, přesně jak má být. Engine by byl odmítnut na každé stránce nejpřísnějšího povrchu zde, což je povrch, jehož přístupnost byla měřena nejméně; první běh nad Cockpitem vyrobil 507 zjištění page.audit-failed, která byla všechna jedním odmítnutím. Vyhodnocení z debuggeru je nástroj, který mluví, a ne stránka, takže běží, aniž by se politika uvolnila — na čemž záleží, protože stránka auditovaná s vypnutou politikou není tou stránkou.

Co to stojí

Plný audit je 996 stránek a 3483 návštěv a řídí fond záložek prohlížeče, ne jednu: návštěva je většinou čekání — na navigaci, na přístupnostní engine uvnitř stránky — takže sériový běh nechává stroj většinu času nečinný. MJ_UI_JOBS=N nastavuje fond; výchozí hodnota se řídí strojem, mezi dvěma a čtyřmi.

Dosažení servírovaných povrchů stálo 117 stránek a 516 návštěv — 91 s při šesti úlohách — navíc k 878, které přispívá sestavená dokumentace. To je cena vzorkování: 2645 tras Cockpitu navštívených celé při třech šířkách by bylo samo o sobě osm tisíc návštěv.

Změřeno na jednom notebooku, stejný strom, stejných 2430 návštěv: 873 s s jednou úlohou, 260 s se čtyřmi. Audit pokrývá o polovinu víc stránek než dřív, když četl jediný adresář, a skončí za méně než třetinu času.

Původ, který audit řídí, a kde jsou jeho stránky

Audit běží proti majordomus serve — vlastnímu serveru tohoto repozitáře, takže audit nemá druhý názor na to, jak se web servíruje, a trasa, kterou spustitelný soubor zodpovídá sám, je pro něj viditelná.

Sestavený adresář neví, odkud se servíruje. Stejná dokumentace se generuje jednou a spustitelný soubor ji připojuje na /docs, publikovaný web v kořeni. Audit proto nejmenuje adresář: čte majordomus web list, bere každý povrch, který spustitelný soubor servíruje, a navštíví stránky každého pod připojením, které mu dává topologie. Proto se auditují i /tests a /benchmarks — jsou to povrchy jako každý jiný a do žádného seznamu nebylo nutné nic přidávat.

Je to také důvod, proč scripts/ui pages nyní spouští server. Polovina sady stránek existuje, jen dokud běží exekutor, takže „které stránky se budou auditovat“ se stalo otázkou, na kterou umí odpovědět jen server; --origin URL míří na ten, který spustil někdo jiný, a --no-build přeskočí sestavení.

Toto sestavení jsou nyní dvě: dokumentace a vlastní assety Cockpitu. Polovina z nich se záměrně necommituje — dva megabajty knihoven pro kreslení, každá načítaná líně jednou stránkou — a checkout, který je nesestavil, servíruje stránky grafů, které žádají o knihovnu, jež tam není. Audit to četl jako 35 chyb konzole, které o Cockpitu nic neříkaly, proto sestavuje to, co měří. Stránka změřená bez svých assetů je stránka, kterou žádný čtenář nedostane.

Sestavuje se s scripts/site-build --serve, jehož základní URL je cesta, a ne origin, takže stránky řeší své assety, ať jsou servírovány kdekoli. --no-build to přeskočí, pro volajícího, který už sestavil; --origin URL auditu server, který spustil někdo jiný.

Publikovaný strom (site/public) je published-only a neobsluhuje se, takže jej audit nenavštěvuje. Oba stromy pocházejí z jednoho zdroje a jednoho generátoru; audit obsluhovaného pokrývá stejné šablony a stejná data.

Náprava: u zdroje, a jen u zdroje

Značkování, které tento repozitář píše, se drží u zdroje. scripts/ui check čte každý sledovaný .html a každou vykreslenou stránku a odmítne posuvný box, ke kterému se klávesnicí nelze dostat (WCAG 2.1.1). Stojí milisekundy, nepotřebuje prohlížeč ani sestavený web a běží v každém plánu CI.

Markup, který tento repozitář nepíše, se normalizuje v buildu, protože není zdroj, který by šel opravit:

  • <pre> zvýrazňovače syntaxe a <table> typografického pluginu se zařadí do pořadí tabulátoru — a které prvky se rolují, se čte ze stylopisu, který build právě zkompiloval, takže motiv, který přestane rolovat <pre>, přestane ovlivňovat průchod;
  • zaškrtávací políčko markdownového seznamu úkolů dostane přístupný název z textu vedle něj;
  • zarovnání sloupce tabulky Markdown (---:), které renderer zapisuje jako inline style, se místo toho kreslí třídou — politika obsahu odmítá každý inline styl a jeden zarovnaný doprava sloupec v jednom záznamu rozhodnutí zastavil tři publikace 2026-10-12; jakýkoli jiný inline styl se nechává scripts/site-check, aby jej odmítl jménem;
  • generované palety zvýraznění se zvyšují na kontrastní poměr 4,5:1 proti pozadí, které deklaruje tentýž stylesheet, se zachováním odstínu a sytosti a se ztrátou jen tolik světlosti, kolik práh vyžaduje.

Oba průchody jsou idempotentní: vyhovující vstup se přepíše sám na sebe a druhý průchod nic nezmění. Audit to dokazuje zvenčí, což je jediný důvod, proč mu věřit.

Co audit odmítá přisoudit stránce

Tři selhání vypadají jako nálezy a nejsou, a každé stálo jeden běh, než bylo klasifikováno:

  • zrušený požadavek (net::ERR_ABORTED) je prohlížeč, který říká, že odpověď už nepotřebuje — líně načítaný asset za letu, když audit pokračuje dál — ne selhaný;
  • odmítnuté spojení znamená, že server, který audit řídí, je pryč, takže běh se zastaví a řekne, kolik návštěv skutečně změřil, místo aby každou zbývající stránku zaznamenal jako nedosažitelnou;
  • návštěva, která překročí svůj termín, se hlásí jako zjištění té stránky a karta, kterou používala, se zahodí, protože termín přestane čekat, ne práci, kterou opustil.

Kde je report

/tests/ui, uvnitř testovacího povrchu — běh shody je běh testů a webová topologie odmítá povrch připojený uvnitř podstromu jiného, takže architektura už odpověděla, kam to patří. Stránka nese každé zjištění s routou, šířkou, pravidlem a prvkem, strojově čitelné results.json vedle a provenienci množiny cílů. Běh zúžený pro lokální iteraci to říká na své stránce, takže dílčí běh nelze nikdy číst jako čistý audit.

Smlouvou dokumentu výsledků je ui-audit/v1, čtená majordomus web report ui. Když audit dosáhl servírovaných povrchů, přibyla mu dvě pole, obě aditivní a obě na stránce, ne jen v souboru:

  • surfaces[] — id, mount, kind a u procházeného povrchu routes, které má, families, do nichž spadly, kolik jich bylo sampled a zda byl průchod truncated svým rozpočtem. Vykresleno jako Povrchy, které tento běh změřil, takže vzorek nelze nikdy číst jako celek.
  • foreign[] — každý podstrom, který stránka deklarovala data-mj-foreign, jednou na deklaraci, s prvkem a první trasou, na níž byl spatřen. Vykresleno jako Neměřeno, a proč.

Přidání stránky, šířky nebo invariantu

Stránka: přidejte ji. Auditu podléhá při vykreslení.

Šířka: změňte téma. Navštíví se při kompilaci CSS.

Invariant: přidejte jej do scripts/lib/ui-audit.mjs, pokud potřebuje prohlížeč, nebo do scripts/lib/ui-static.mjs, pokud jej lze rozhodnout ze značkování, a dejte mu název pravidla ve stejném tvaru area.thing jako ostatní. Nic jiného se nemění — ani report, ani kontrola, ani tabulky tohoto dokumentu, protože žádné z nich pravidla nevyjmenovávají.

Pravidlo je project.every-link-and-control-is-tested. Obě poloviny se objevují při buildu, takže stránka, odkaz nebo ovládací prvek přidaný zítra se zkontroluje zítra, bez seznamu k aktualizaci.

Odkazy. scripts/ci/link-check rozhoduje o každém href a src, které sestavený web vydává, i o každé položce sitemap, bez sítě:

  • URL se stejným původem se musí rozřešit na soubor pod site/public.
  • Fragment musí jmenovat id na stránce, na kterou míří.
  • Odkaz do tohoto repozitáře na GitHubu se rozlišuje proti gitu: blob/ sledovaný soubor, tree/ sledovaný adresář, commit/ commit v publikované historii a releases/tag/ a compare/ skutečné refy.
  • Hostitel, o kterém nelze offline rozhodnout, javascript:, prázdný href a holé # se odmítnou. Stejně tak verdikt z mělkého klonu, který nevidí historii.

scripts/site-check to spustí; --list vypíše každý odkaz a jeho třídu.

Relativní odkazy promítnutého dokumentu se řeší u jejich zdroje pomocí scripts/lib/project-links.awk, proti adresáři souboru, ze kterého pocházejí:

  • Dokument se stránkou se stává svou trasou.
  • Dokument tvrzení se stane stránkou své záruky.
  • Sledovaný soubor nebo adresář se na GitHubu stane svým blob nebo tree na commitu, ze kterého byl web sestaven. Co projekce zapisuje, je commitnuto, a commitnutý soubor nemůže jmenovat commit, ve kterém je, takže zapisuje zástupný ref, který scripts/site-build v sestavených stránkách připíná na commit, který zaznamenává data/build.json (master pro nečisté sestavení), ref, který site/templates/source-links.html dává odkazům vytvořeným šablonou. Zástupný ref ponechaný v sestavené stránce odmítne sestavení a scripts/ci/link-check odmítne odkaz, který jej nese. Připnutí běží jen na sestavených souborech, takže prosté zola serve ukáže zástupný ref; lokální náhled, scripts/site-serve, obsluhuje místo toho připnuté sestavení.
  • Cesta, kterou git nesleduje, například stav lokální pro checkout pod .ai/local, si ponechá text a ztratí odkaz.

Ovládací prvky. Ovládací prvek je cokoli, co čtenář může ovládat:

  • tlačítko, pole, souhrn
  • prvek, který rozpohybuje direktiva Alpine nebo vazba Flowbite
  • role widgetu ARIA
  • navázání grafu
  • rolovatelná oblast dosažitelná z klávesnice

Prosté odkazy patří kontrole odkazů. Zakázané prvky nejsou ovládací prvky a ovládací prvek uvnitř <template> se počítá.

  • scripts/ci/interaction-check vyžaduje, aby každý ovládací prvek na každé stránce nárokovala přesně jedna behaviorální spec pod scripts/lib/interaction-specs/ a aby každá spec něco nárokovala. --inventory uvádí každý podpis ovládacího prvku s jeho specem. scripts/site-check ji spouští.

    • Vyžaduje také, aby se zkompiloval každý výraz Alpine. Nikdy nevkládejte text do řetězce JavaScriptu v šabloně, jako v '{{ title }}'.includes(q): jediný apostrof v textu direktivu tiše zahodí.
    • Dejte text místo toho do atributu data, data-text="{{ title | lower }}", a čtěte $el.dataset.text.
  • scripts/interaction-probe otevře každou stránku s ovládacím prvkem, obsluhuje build na jeho publikovaném originu a každá spec ovládá každý prvek, který si nárokuje. Stránka selže, když:

    • chování neplatí
    • živý počet ovládacích prvků specifikace se liší od značkování
    • konzole hlásí chybu nebo se nezachytí výjimka
    • požadavek opouští origin webu
    • specifikace ovládá méně prvků, než tvrdí

    --only '<routes>' a --spec <id> zužují lokální smyčku. V CI běží jako kontrola interaction-probe v úloze interactions.

  • MJ_PROBE_LIVE=1 scripts/interaction-probe řídí samotný publikovaný web. Po deploymentu jej spusťte proti commitu, který hlásí scripts/pages verify.

Přidání ovládacího prvku. Sestavte stránku a spusťte scripts/ci/interaction-check --inventory. Ovládací prvek, který nenárokuje žádná specifikace, je uveden jako UNCLAIMED. Buď je instancí chování, které už nějaká specifikace tvrdí, a pak by jej měl její claims() zahrnout, nebo je nový. Nové chování potřebuje novou specifikaci: claims(el) je čistá funkce značky a atributů prvku a exercise(ctx) projde každý nárokovaný prvek a vrátí, kolik jich prošel.

Prohledává názvy a popisy všech stránek, které tento web publikuje.