Nagtapos ang post 11 sa isang pangako: accessibility sa seam, isang shared testing package na sumusuri ng touch target, contrast at focus order sa mga remote na nakikilala lang ng host sa runtime. Kasya sa isang Jest suite ang dalawa sa tatlong iyon. Hindi kasya ang focus order, at sinasabi ng Ang hindi nakikita ng mga check na ito kung kanino ito nauukol.
Ang problema ay ang problema ng post 11 na isang antas pataas. Tatlong team ang naglalabas ng tatlong bundle sa sarili nilang iskedyul, may sariling ideya ang bawat isa kung ano ang matatawag na accessible, at naghihiwalay ang mga ideyang iyon gaya mismo ng paghihiwalay ng mga grey bago pa may design system. Hindi napipigilan ng nakasulat na bar sa isang wiki page ang paghihiwalay na iyon, dahil walang bersyon at walang install step ang isang convention. May dalawa ang isang package.
Kaya nagiging @pokedex/a11y-testing ang bar: isang Jest preset, isang render na nakakaintindi ng NativeWind, isang maliit na set ng mga WCAG (Web Content Accessibility Guidelines) assertion helper, ang catalogue ng A at AA criteria, at isang reporter. Ini-publish ito sa parehong lokal na Verdaccio registry gaya ng lahat ng package sa seam, at ini-install ito ng dalawang source package at ng dalawang remote. Ito rin ang unang @pokedex package na hindi kailanman nakakarating sa isang bundle. Kinukuha ito ng bawat consumer bilang devDependency, kaya walang shared map na nagbabago at walang gumagalaw sa runtime.
Hindi imbento ng proyektong ito ang mga criteria na sinusuri. Ipinatutupad na ang European Accessibility Act (Directive (EU) 2019/882) mula 28 Hunyo 2025 sa mga produkto at serbisyong nakalista rito. Naglalatag ng functional requirements ang Act mismo at wala itong pinapangalanang pamantayan; ang EN 301 549, na naglalapat ng A at AA criteria ng WCAG 2.1 sa mga mobile app, ang pamantayang sinusukatan ng praktika sa Europa, at inaasahan ang rebisyon nito papuntang WCAG 2.2.
Bakit hindi jest-axe
Sa web, solved na ito at may kasamang package. Binabalot ng jest-axe ang axe-core, sakop ng expect(await axe(container)).toHaveNoViolations() ang malaking bahagi ng mga rule sa isang linya, at para sa isang React web app iyon ang tamang unang hakbang.
HTML ang sinusuri ng axe-core. Kailangan ng jest-axe ang isang jsdom environment, at sinasabi mismo ng README nito na hindi gumagana ang colour-contrast check sa jsdom, kaya naka-off ang mga iyon kahit doon. Hindi package ang @axe-core/react-native: 404 ang sagot ng npm registry. May binebenta ngang React Native tooling ang Deque, na siyang nagmementena ng axe-core, kasama ang mga device SDK na matatawag ng isang native test run. Iyon ang device-audit layer na pinapangalanan ng Ang hindi nakikita ng mga check na ito, hindi ang Jest layer na binubuo ng post na ito.
Nagbibigay ang React Native Testing Library (RNTL) ng mga query at matcher, at doon na ito huminto bago pa ang isang audit. Wala sa API nito ang nagbabalik ng listahan ng violation. Nang maghanap ng naunang gawa tungkol sa accessibility testing sa mga Module Federation remote, wala akong nakita, maging sa React Native o sa web. Dalawang community package ang lumalabas kapag naghanap ka ng React Native accessibility assertion, at wala sa dalawa ang may hugis na kailangan dito. Huling nag-publish ang react-native-accessibility-engine noong 2022. Buhay pa ang react-native-ama, pero kailangan mong malaman na lumipat ito: huminto sa 0.7.5 ang unscoped package noong 2023 at lumipat sa isang scope ang trabaho. Pito sa walong @react-native-ama/* package ang naglabas ng 1.2.1 noong Agosto 2025, at lima sa walo ang naglabas ng 2.0 beta noong Hulyo 2026. Isa itong component library na may dev-time runtime check, hindi isang Jest assertion layer, kaya ibang tanong ang sinasagot nito kaysa sa tanong ng post na ito.
Binubuo na lang ang bar: isang render na nagre-resolve ng class, ilang assertion, at isang report.
Apat na arrow, dalawang trabaho. Dalawa ang papunta sa mga package na nagmamay-ari ng shared na pixel, kung saan isang beses lang tumatakbo ang check para sa lahat. Dalawa ang papunta sa mga team, kung saan sinasakop ng bawat suite ang mga screen lang na binubuo ng team na iyon.
Ang package
Magsimula sa tag ng post 11:
git clone https://github.com/warrendeleon/react-native-module-federation
cd react-native-module-federation
git checkout post-11-design-system
May tatlong bagay na kailangang totoo bago gumana ang kahit ano rito, at madaling ipagpalagay ang
tatlo. Kailangang nakabukas ang lokal na Verdaccio registry na may @pokedex/contracts,
@pokedex/ui at @pokedex/detail na naka-publish dito, at trabaho iyon ng post 11. Kailangang alam
ng npm na nandoon ang @pokedex scope. At kailangan kang naka-login sa registry na iyon, at hakbang
iyon ng post 5 at hindi ng post 11, dahil
authenticated ang mga publish sa ibaba kahit hindi ang mga pagbabasa.
Patakbuhin mo muna ang registry, dahil kailangan ng npm adduser sa ibaba ng malo-login-an:
npx verdaccio@6.2.0 # :4873, leave it running
I-set ang dalawa pang iyon nang isang beses, para sa user mo, dahil binabasa ng npm ang project
config nito mula sa direktoryong may package.json, hindi mula sa root ng repository, kaya hindi
nakikita ang .npmrc ng repo ng isang utos na pinatakbo sa loob ng packages/ui:
npm config set @pokedex:registry http://localhost:4873/
npm adduser --registry http://localhost:4873/ # only if you have not already
Kung hindi mo binuo ang post 11, ini-publish ng mga seksyon nitong Isang package para sa mga pixel at
Tapos na ang bihis ng detail ang tatlong package na ini-install nito. Kailangan ng apps/host ng mga dependency nito; makukuha ng dalawang remote ang sa kanila kasama ng mga package nila sa ibaba:
( cd apps/host && npm install )
Mas marami ang source kaysa sa kayang muling i-type nang may pakinabang ng isang post. Kunin mo ang reference copy nang isang beses at i-publish ito gaya ng ginawa ng post 5 at ng post 6. Inilalabas ang tag sa mismong araw na lumabas ang post na ito, kaya maaabot ito ng degit:
npx degit@3.8.0 --force warrendeleon/react-native-module-federation#post-12-a11y-testing /tmp/pokedex-a11y-ref
cp -R /tmp/pokedex-a11y-ref/packages/a11y-testing packages/
( cd packages/a11y-testing && npm install && npm run build && npm publish )
Sumusunod ang anatomy sa mga package bago nito: nakaturo ang publishConfig sa Verdaccio, gaya na mula pa post 5, at bumubuo ang react-native-builder-bob ng ES-module na lib/, ang ayos na ipinakilala ng @pokedex/ui sa post 11. May isang sinadyang naiwan. Walang exports map ang package. Ginagamit ng mga consumer ang root, ang /jest-preset sa bawat Jest config, at ang /reporter.js sa bawat test:a11y script, at kailangang pangalanan ng isang map ang tatlo. Kapag may naiwan kang isa, ERR_PACKAGE_PATH_NOT_EXPORTED ang isasagot ng Node, at para sa preset ang ibig sabihin niyan ay walang test na tatakbo.
Ini-extend ng jest-preset.js ang @react-native/jest-preset sa halip na palitan ito, at iyon ang dahilan kung bakit patuloy na pumapasa ang lahat ng suite na naroon na nang hindi ginagalaw. Ang dalawang dagdag na mahalaga rito ay parehong tungkol sa NativeWind. Sumasama ang nativewind/babel sa mga transform preset, kaya nakakarating ang className sa styling runtime bilang style at hindi nananatiling isang patay na prop na nagdudulot na undefined ang mabasa ng bawat colour assertion. At sumasama ang react-native-css-interop/dist/test/setupAfterEnv.js sa setupFilesAfterEnv, at doon naroon ang toHaveStyle matcher. Hindi ang dalawang linyang iyon ang nagpapagana sa mga check ng post na ito: direktang binabasa ng contrast matrix ang colour object ng preset, at mga class string ang binabasa ng mga component check. Pero kailangan ding paglingkuran ng isang shared preset ang suite na talagang nag-a-assert sa isang na-resolve na style, at parehong sinusuri kung naroon sila para hindi sila tahimik na matanggal ng isang susunod na edit.
Dalawa sa tatlong entry point na ini-import ng package na ito ay walang dokumentasyon. Ayos lang ang
nativewind/babel: hakbang 3 ito ng sariling installation guide ng NativeWind. Ang dalawa pa ay wala kahit saan nakasulat. Angreact-native-css-interop/dist/test/setupAfterEnv.js, na idinadagdag ng preset sasetupFilesAfterEnv, at angnativewind/test, na ini-import ng render, ay totoo, naka-ship, at sila ang bumubuhay sa sariling suite ng NativeWind, at wala man lang testing section ang nativewind.dev. Kaya nakatala ang mga bersyong pinagpatunayan nito sa halip na ipagpalagay: nativewind 4.2.6 at react-native-css-interop 0.2.6, nasa caret range at sinusuri muli tuwing gumagalaw ang NativeWind.
Pinananatili sa ibaba ng 14 ang RNTL sa kaugnay na dahilan: isinulat muli ng bersyon 14 ang render, fireEvent at act bilang async, at hindi pa nasusubok ang render path na ito laban doon. Huwag ding gamitin ang @testing-library/jest-native; deprecated na ito, at may sariling matcher na ang RNTL mula pa 12.4.
Isang beses lang nag-bind ang createThemedRender ng isang Tailwind preset at ibinabalik nito ang render na ginagamit ng bawat suite sa package na iyon:
const renderWithTheme = createThemedRender(require('@pokedex/ui/tailwind.preset.js'));
const { getByRole } = await renderWithTheme(<SomeButton disabled />);
Sa pagtanggap sa preset bilang argument, nananatiling walang dependency sa design system ang accessibility package. Kasangkapan ito, hindi miyembro ng federation. Ang pag-bind nito sa sariling preset ng @pokedex/ui ang nagpapahintulot sa isang suite na i-resolve ang isang class gaya ng gagawin ng app, at iyon ang binabasa ng mga touch-target check at pinagbabatayan ng mga contrast helper.
Ordinaryong function ang mga helper na tumatawag sa global na expect, hindi mga expect.extend matcher. Hindi nangangailangan ng setup file ang isang helper na ini-import sa pangalan nito, pareho ang kilos nito sa bawat package, at lumilitaw ito sa stack trace bilang sarili nito. Walang isa man sa kanila ang nag-i-import ng React Native: kahit ano na may props ay isang test element. Nakatali ang bawat isa sa isang success criterion (SC) at sa bar na itinatakda ng criterion na iyon.
| Ano ang sinusuri | Criterion | Level | Ang bar |
|---|---|---|---|
| Teksto sa isang surface | SC 1.4.3 Contrast (Minimum) | AA | 4.5:1 normal na teksto, 3:1 malaki |
| Ang sariling hangganan ng isang control | SC 1.4.11 Non-text Contrast | AA | 3:1 |
| Ang binabasa ng isang screen reader | SC 4.1.2 Name, Role, Value | A | pangalan, role at state |
| Kahulugang hindi nakasalalay sa kulay | SC 1.4.1 Use of Color | A | nasa salita rin ang impormasyon |
| Pagbabagong inanunsyo nang hindi ginagalaw ang focus | SC 4.1.3 Status Messages | AA | may role o live region na nagdadala nito |
| Touch target | SC 2.5.5 Target Size (Enhanced) | AAA | 44 by 44 CSS pixel |
Ang row ng touch target ang dapat basahin nang dalawang beses. Ang 44pt ang bar ng proyektong ito, at numero ito ng Apple at hindi ng WCAG. Sa kasalukuyang Human Interface Guidelines, 44 by 44 points ang default na laki ng control sa iOS at iPadOS at 28 by 28 ang minimum, kaya 44 ang laki na hinihiling ng Apple na pagbatayan mo sa pagdidisenyo, hindi ang pinakamaliit na tinatanggap nito. May mas lumang page ang Apple, madalas pa ring sinisipi, na “at least 44 points x 44 points” lang ang sinasabi, at doon nanggaling ang daglat na “ang minimum ng Apple”. Sa panig ng WCAG, nasa Level AAA ang SC 2.5.5, at ang AA criterion ng WCAG 2.2, ang SC 2.5.8 Target Size (Minimum), ay humihingi ng 24 by 24 CSS pixel na may limang exception. Malaki ang lamang ng apatnapu’t apat doon. Hindi nito nalalampasan ang lahat: humihingi ang gabay ng Android ng hindi bababa sa 48dp, kaya ang proyektong naglalabas sa dalawang platform at huminto sa 44 ay kinuha ang inirerekomendang laki ng Apple at tinanggap na mas gusto pa ng Android. Desisyon iyon na dapat gawin nang sinasadya. Ang dapat iwasan ay ang tawaging “hinihingi ng WCAG AA” ang alinman doon.
Gumagamit ng 0.04045 ang contrast helper sa channel linearisation. Nasa 0.03928 ang depinisyon ng relative luminance bago ang Mayo 2021 at kinokopya pa rin nang malawakan ang lumang constant. Sinasabi mismo ng note ng spec na walang praktikal na epekto sa resulta ang pagbabago, at wala namang gastos ang paggamit ng kasalukuyang constant.
May isang kilos ang touch-target helper na nagpapasya kung gaano kahalaga ang buong layer: nagta-throw ito kapag hindi nito nakikita, sa halip na pumasa. At bawat axis iyon. Ang control na nagdedeklara ng taas pero walang lapad ay hindi nasukat sa lapad nito, at ang paghalili ng bar sa axis na walang nagdeklara ay magsasabing accessible ito sa mismong sandaling hindi ito nakikita ng test. Sinusukat din ang hitSlop, hindi lang tinitingnan kung naroon, dahil walang pinapalawak ang hitSlop: 0.
Ang panuntunang iyon ang pagkakaiba ng isang suite at isang palamuti, at iyon mismo ang unang namali sa package na ito: humiram ng bar ang isang maagang bersyon para sa isang axis na walang deklarasyon, kaya pumasa sa isang lapad na walang nakasukat ang isang button na taas lang ang sinabi. May sarili na itong regression test ngayon, sa suite mismo ng package.
Suriin ang source
Ini-install ng dalawang source package ang bar at itinuturo nila roon ang Jest. Hindi opsyonal dito ang @tailwindcss/container-queries: kapag wala ito, pumapalya ang test render nang may Cannot find module.
( cd packages/ui && npm install -D @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.0 )
( cd packages/detail && npm install -D @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 @types/jest@29.5.14 @types/react-test-renderer@19.1.0 )
Galing sa reference copy ang mga suite, ang dalawang Jest config at ang per-suite na deklarasyon ng hindi naaangkop, at nakakakuha ang bawat package ng test:a11y script:
for pkg in ui detail; do
cp /tmp/pokedex-a11y-ref/packages/$pkg/jest.config.js \
/tmp/pokedex-a11y-ref/packages/$pkg/a11y-report.config.js \
/tmp/pokedex-a11y-ref/packages/$pkg/.gitignore packages/$pkg/
done
mkdir -p packages/ui/src/tokens/__tests__ packages/ui/src/components/__tests__
cp /tmp/pokedex-a11y-ref/packages/ui/src/tokens/__tests__/contrast.accessibility.ts packages/ui/src/tokens/__tests__/
cp /tmp/pokedex-a11y-ref/packages/ui/src/components/__tests__/components.accessibility.tsx packages/ui/src/components/__tests__/
cp /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.accessibility.tsx packages/detail/__tests__/
cp /tmp/pokedex-a11y-ref/packages/detail/__tests__/detail-view.test.tsx packages/detail/__tests__/
cp /tmp/pokedex-a11y-ref/packages/ui/tsconfig.json packages/ui/
cp /tmp/pokedex-a11y-ref/packages/detail/tsconfig.json \
/tmp/pokedex-a11y-ref/packages/detail/tsconfig.build.json packages/detail/
Sa parehong paraan dumarating ang mga pagbabago sa design system. Ito ang mga token repair na pinag-uusapan ng natitirang bahagi ng seksyong ito, at kailangan nilang nasa tree na bago makapasa ang matrix:
cp /tmp/pokedex-a11y-ref/packages/ui/tailwind.preset.js packages/ui/
cp /tmp/pokedex-a11y-ref/packages/ui/src/tokens/colours.ts \
/tmp/pokedex-a11y-ref/packages/ui/src/tokens/typeColours.ts packages/ui/src/tokens/
cp /tmp/pokedex-a11y-ref/packages/ui/src/components/type-badge.tsx \
/tmp/pokedex-a11y-ref/packages/ui/src/components/back-pill.tsx \
/tmp/pokedex-a11y-ref/packages/ui/src/components/pokemon-card.tsx \
/tmp/pokedex-a11y-ref/packages/ui/src/components/empty-slot.tsx packages/ui/src/components/
Dala ng type-badge.tsx ang desisyon sa dalawang surface na malapit nang patunayan ng matrix. Ang tatlo
pa ay parehong depekto sa mas maliliit na control: ang madilim na scrim ng lumulutang na back pill, na
sinusukat ng susunod na seksyon kasabay ng sa badge; ang remove badge ng card, na binabalikan ng Parehong
bar para sa bawat remote ang touch target; at ang dalawang linya ng secondary text na ipinipinta ng card
at ng walang lamang slot, na inaayos ng seksyon pagkatapos noon.
for pkg in packages/ui packages/detail; do
( cd $pkg && npm pkg set 'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js' )
done
Kailangan ng packages/detail ng isa pang pagbabago sa script, at kapag nilaktawan mo ito, isang package na hindi mai-import ang mai-publish. Nasa __tests__ ang suite nito, kaya kasama na ngayon ng tsconfig.json nito ang direktoryong iyon, at nagbibigay iyon ng dalawang root sa tsc at ibinababa ang output sa dist/src/. dist/index.js pa rin ang idineklara ng package.json. Kaya i-set mo nang mano-mano ang build script ng packages/detail, sa package.json nito, sa config na nagpapanatili sa src bilang tanging root at naglilinis muna sa dist para walang matira mula sa lumang ayos:
"build": "node -e \"require('fs').rmSync('dist',{recursive:true,force:true})\" && tsc -p tsconfig.build.json"
Ang tsc na lumalabas nang 0 at nagsusulat ng mga file nang isang direktoryo ang lalim ay parehong depekto ng check na berde pero mali ang sinusukat: walang magrereklamo hanggang sa huminto sa Cannot find module ang unang app na nag-install ng package.
May unit test na ang packages/ui para sa mga component nito. Nakakakuha ito ng accessibility layer sa tabi ng mga iyon, at ang token matrix ang kawili-wiling kalahati. Isang beses lang sinusuri ang bawat pares ng foreground at background na ipinapangako ng design system, sa package na nagmamay-ari ng mga token:
describe('WCAG 1.4.3 Contrast (Minimum) — type colours on the solid fill', () => {
test.each(TYPE_NAMES)('%s badge text clears AA on its type fill', (type: string) => {
expectColorContrast(
hexForClass(textOnTypeClass(type)),
hexForClass(bgClassForType(type)),
'normalText',
);
});
});
Ang hexForClass ang kawili-wiling bahagi, at naroon ito dahil wala nito ang unang bersyon ng file na ito. Nagma-map ang bersyong iyon ng class papunta sa isang literal: 'text-black': '#000000'. Pumasa ang labingwalong type, at totoo ang numerong inilimbag nito para sa isang kulay na hindi kailanman ipininta ng app.
May sariling
blackang design system. Itinatakda ngtailwind.preset.jsangblack: '#2E3138'para sa mga halos itim na surface, at sinasapawan niyan ang default ng Tailwind. Kaya#2E3138ang ipininta ngtext-black, at sa water fill 3.74:1 iyon samantalang 4.5:1 ang hinihingi ng test mismo. Dalawa sa labingwalo ang pumapalya sa screen habang iniuulat ng matrix na berde ang labingwalo, dahil isang constant ang sinusukat ng matrix at hindi ang token.
May dalawang bahagi ang repair, at ang pangalawa lang ang matibay. Lumipat sa sariling token ang foreground ng badge, ang typeInk, at iyon ang tunay na itim na pinagbatayan ng contrast map mula pa sa simula. At tumigil na ang matrix sa pagsusulat ng hex: nire-resolve nito ang dalawang panig ng bawat pares mula sa preset, kaya ang token na pinalitan ng pangalan o nasapawan ay nagpapapalya sa suite sa halip na makalusot. Mahalaga ang dalawang panig. May pansamantalang bersyon na nire-resolve lang ang foreground at patuloy na binabasa ang background mula sa token module, at iniwan niyon ang parehong butas sa kabilang kalahati: kahit padilimin mo ang isang type fill hanggang 1.64:1, berde pa rin ang labingwalong pares. Ituro mo ngayon ang typeInk pabalik sa #2E3138 sa preset at anim na check ang agad na pupula: ang water at psychic badge sa sarili nilang fill, ang rock, ghost at dragon badge sa hero surface, at ang check na nag-uugnay sa preset at sa mga token module.
Pumapasa na ngayon ang labingwalong fill, at may kahulugan na ang pangungusap na wala noon. Ang pares na pumasa rito ay pumapasa sa bawat remote na bumubuo nito, at wala nang muling sumusuri ng type badge sa Party app.
Malaki ang bigat ng “bawat remote na bumubuo nito” sa pangungusap na iyon, at dapat linawin kung ano ang ibig sabihin ng pagbuo ng isang pares. Nakapatong sa kulay ng type ang badge sa isang card. Hindi gayon ang badge sa detail hero: ang hero mismo ang kulay ng type, kaya malulusaw dito ang isang solidong pill, at sa halip ay naglalatag ang hero variant ng 30% na puting scrim. Translucent ang scrim na iyon pero hindi transparent, at mali ang foreground na pinili para sa solidong fill sa apat sa anim na type na sapat ang dilim para tanggapin ang puting teksto. Nasa 3.17:1, 3.14:1, 3.39:1 at 2.71:1 ang rock, ghost, dragon at steel sa surface na talagang pinagguhitan sa kanila, habang ibang surface ang sinusuri ng matrix.
Kaya dalawa ang desisyon, hindi isa, at kinakalkula na ngayon ng design system ang dalawa. Binubuo ng matrix ang scrim nang eksakto sa paraang binubuo ito ng runtime at sinusuri nito ang badge laban doon:
const scrim = composite(hexForClass('bg-white'), HERO_SCRIM_ALPHA, hexForClass(bgClassForType(type)));
expectColorContrast(hexForClass(textOnHeroScrimClass(type)), scrim, 'normalText');
Pareho ang hugis ng dalawang depekto: isang check na sumusukat ng bagay na katabi lang ng talagang inilalabas. Walang berdeng suite ang naghahayag nito.
Ang lumulutang na back pill ang pangatlo sa hugis na iyon, isang control pa palabas. Naglalatag ito ng
sarili nitong translucent na scrim sa aling hero man ito dumapo, at bg-black/35 ang ipininta nito, ang
halos itim na neutral ulit, sa 35%. Sa tatlong pinakamaputlang fill, 2.81:1 ang lumabas na puting chevron
sa flying, 2.83 sa ice at 2.84 sa electric, mas mababa sa 3:1 na hinihingi ng SC 1.4.11 para sa sariling
hangganan ng isang control. Nalalampasan ng tunay na itim sa parehong alpha ang bawat fill, 3.48 sa
pinakamasamang kaso.
Hindi nito hinihiram ang typeInk para roon. Foreground ang typeInk: text- class ang bawat paggamit
nito, at binabalik ito ng eksperimento sa itaas para patunayang gumagalaw ang teksto ng badge. Ang scrim
na ipininta mula sa parehong token ay makakaladkad ng eksperimentong iyon, at sa gayon ang desisyon
tungkol sa tinta ng badge ang mangangasiwa sa isang control na walang kinalaman dito. May sariling
pangalan ang kalahating background ng parehong bagay:
export const BACK_PILL_SCRIM_ALPHA = 0.35;
export const BACK_PILL_SCRIM_CLASS = 'bg-scrim/35';
Binabasa ng matrix ang token at ang alpha mula sa isang class na iyon, kaya hindi maaaring maghiwalay ang halagang ipininta ng pill at ang halagang binubuo ng map.
May isang token na hindi pumasa. Nagpinta ang midGrey (#9A9AB0) ng secondary text sa apat na lugar sa tatlong maliwanag na surface: dalawang section heading sa detail sheet (2.60:1), ang linya ng numero sa loob ng kulay-abong pill ng card (2.46:1), at ang label ng naka-disable na Add button sa fill nito (2.02:1). Hiwalay na itinatala ng matrix ang bawat surface, sa pamamagitan ng isang helper na ini-export ng package:
knownFinding('disabled button label on its fill', '2.02:1, exempt under 1.4.3 Incidental', () => {
expectColorContrast(colours.midGrey, colours.lightGrey);
});
Binabalot ng knownFinding ang it.failing, na iniuulat ng Jest bilang pumasa, kaya nananatiling berde ang suite habang nananatiling nakikita ang finding at inililista ito ng reporter nang hiwalay sa mga violation. Naroon ang helper dahil sinulat nang mano-mano ng unang bersyon ang tawag na iyon, na may titulong nagsisimula sa (known at doon nagma-match ang reporter. Ang markang string lang ay isang typo na lang ang layo sa katahimikan: mali ang baybay mo at hihinto ang reporter sa pagkakita ng nakatalang finding, patuloy na mag-uulat nang berde ang it.failing, at mababasa ang isang tunay na violation bilang ordinaryong pumasa. Ang pagsulat ng titulo sa isang lugar lang ang dahilan kung bakit imposible nang mali-mali ang marka.
May dalawa pang surface na sumali sa listahan noong nagsimulang i-resolve ng matrix ang sinusukat nito. text-midGrey/70 ang caption ng walang lamang slot, hindi ang token: kapag binuo sa ibabaw ng background ng app, 1.88:1 ito. At light theme ang lahat ng nauna. Nag-mo-mount ang dalawang remote ng theme toggle sa header nila, kaya may madilim na katapat ang bawat isa sa mga surface na iyon na binubuo rin ng design system, at walang dark: override ang text-midGrey sa alinman sa dalawang lugar. Nakapatong sa bg-white/10 sa ibabaw ng halos itim ang linya ng numero ng card sa 3.44:1, at nasa navy ang caption ng walang lamang slot sa 3.82:1. xs na teksto ang dalawa, kaya 4.5:1 ang kailangan ng dalawa.
Limang pares, kung gayon, pawang nasa ibaba ng bar at nasa screen na inilalabas ng dalawang app. Nanatili silang naka-park sa loob ng limang round sa likod ng dahilang maganda basahin at hindi nakakaligtas sa aritmetika: walang isang mas madilim na halaga ang makakaayos sa lahat. Mula sa mga #5F5F6D, nalalampasan ng token ang AA sa tatlong maliwanag na surface, at dark:bg-navy ang detail sheet, kaya ang pares na nasa komportableng 6.48:1 ngayon ay babagsak sa 2.84:1. Totoo lahat, at wala ni isa roon ang mapagpasya, dahil walang kailangang isang halaga rito. Nakakaintindi ng theme ang design system. Naglalabas na ito ng dark:text-lightGrey sa walo pang lugar, at ang secondary text lang talaga ang hindi kailanman nabigyan ng parehong pagtrato:
<Text size="xs" className="text-darkGrey dark:text-lightGrey">
Dalawa sa apat na lugar na iyon ang nasa packages/ui, at dumarating sila kasama ng mga component na kinopya kanina: ang linya ng numero ng card at ang caption ng walang lamang slot, na ipinipinta na ngayon ang token nang buong lakas at hindi sa 70%. Ang dalawa pa ay ang mga section heading ng detail sheet, kaya mano-manong edit sila sa packages/detail/src/PokemonDetailView.tsx, sa dalawa:
// Ang dalawang section heading. Tinetema rito ang secondary text gaya ng bawat ibang
// secondary na linya sa design system; walang dark override ang naka-park na bersyon.
<Text size="xs" bold className="uppercase tracking-widest text-darkGrey dark:text-lightGrey">
6.94:1 na ngayon ang pinakamasama sa anim na pares. Tunay na opsyon ang pag-park ng finding at may isa pang itinatago ang post na ito, pero ang “desisyon ito sa palette” ay naging pantakip lang pala sa isang pagbabagong isang class lang ang kinailangan.
At saka naging berde ang matrix, at hindi sapat ang berde. Nang ibalik ang text-midGrey sa card pagkatapos, pumasa pa rin ang lahat ng isang daan at apatnapu’t siyam na check, dahil mga pares ang sinusukat ng matrix at hindi nito nakikita kailanman kung anong class ang ipininta ng isang component. Iyon din ang butas na dinaraanan ng mga tab tint ng host, at pareho ang sagot: isang check na bumabasa ng sariling source ng component.
expect(secondaryClass('pokemon-card.tsx')).toEqual({ light: 'darkGrey', dark: 'lightGrey' });
Hindi ito maisasara ng isang assertion sa render. Ang tree lang na ibinibigay mo sa render ang kino-compile ng nativewind/test, kaya hindi kailanman naiko-compile ang class na pinipili ng isang component sa loob ng sarili nitong render, at pumapasa ang assertion nang walang sinusuri.
May isang finding na nananatiling nakatala, at hindi ito pagkabigo sa isang criterion. Ipinagbubukod ng Incidental exception ng SC 1.4.3 ang “text or images of text that are part of an inactive user interface component”, kaya walang contrast requirement na maaaring palyahan ang label ng naka-disable na Add button. Nakatala ito dahil mas gusto ng proyektong ito na manatiling nababasa ang isang naka-disable na control, at nasa ilalim ito ng heading na Project bars na nagsasabi niyan, sa halip na nasa ilalim ng 1.4.3, kung saan ito magiging kaparehong maling pagsipi na maingat na iniiwasan ng pagtingin sa touch target.
Susunod na nakakakuha ng sariling suite ang packages/detail, at mas mahalaga roon ang hugis kaysa sa mga assertion: isang component library na naglalabas ng sarili nitong accessibility test. Direktang ibinibigay ng mga test ang props sa PokemonDetailView, at iyon ulit ang props seam mula sa post 8. Walang store, walang query client, walang navigator. Minamana ng dalawang consumer ang anumang mapatunayan ng file na ito, at kapag may nahanap ito, isang patch release ang nag-aayos sa dalawa. Isang paalala sa pagkakasunod kung maaga mong patakbuhin ang suite na ito: binabasa nito ang desisyon sa hero scrim mula sa @pokedex/ui, at nakapako sa lockfile ng post 11 ang bersyong nauna sa desisyong iyon. Tumatakbo ang suite, at dalawampu’t apat sa tatlumpung check nito ang pumapalya, mula sa apat na sanhi. Apat ang nagta-throw ng textOnHeroScrimClass is not a function, at inaayos iyon ng design-system release sa ibaba. Isa ang touch target ng Add button, at isa ang mga section heading ng sheet na nasa naka-park pa ring token. Ang labingwalo pa ay isang check kada type, at pare-pareho ang sinasabi nila tungkol sa dex number ng hero, na inaayos ng susunod na seksyon sa isang linya. Tatlo sa apat na sanhing iyon ay mga finding na pinag-uusapan ng post na ito, kaya ang inaasahang hugis ang pulang run dito at hindi isang sirang clone.
Isang pumalyang test, at ang ayos
Hindi nangyari ang pagpalyang pinagbuuan ng suite na ito. Ang inaasahan ay hindi mag-uulat ng anumang accessibilityState ang naka-disable na Add button, kaya makakatagpo ang isang screen reader ng button na kumukupas sa screen at walang sinasabi tungkol sa hindi ito magamit. Tama ang iniuulat nitong state. Kinokopya mismo ng Pressable ng React Native ang disabled prop papasok sa accessibilityState:
_accessibilityState =
disabled != null ? {..._accessibilityState, disabled} : _accessibilityState;
Nananatili pa rin ang test, bilang regression net at hindi bilang ayos. Sa araw na i-disable ng isang tao ang button na iyon sa pamamagitan ng pagpalit ng handler ng walang lamang function at mano-manong pagpapakupas ng fill, mananahimik ang state at sasabihin iyon ng test na ito.
Ang pumalya ay ang check sa tabi nito:
● WCAG 2.5.5 Target Size — the Add action › the Add button declares at least 44pt on both axes
Element has no measurable size and no hitSlop; cannot verify the 44pt touch target.
Give the control an explicit width/height or a hitSlop, or assert on a parent that
has one.
May dalawang bagay sa likod ng linyang iyon, at lumalampas pareho sa button na ito.
Walang idineklarang laki ang button na kayang basahin ng suite. Galing ang taas nito sa size="lg" variant ng gluestack, na bumubuo ng px-6 h-11 habang nagre-render ang component. Sa app, ayos lang ang class na iyon: sini-scan ng Tailwind ang source ng design system, nahahanap nito ang literal na h-11, at dala ng binuong bundle ang rule. Sa test tree, hindi, at mas malawak ang dahilan kaysa sa button na ito. Ang mga class string na nakikita nito sa element tree na ibinibigay mo sa render ang kino-compile ng nativewind/test, at ibinibigay nito ang mga iyon sa Tailwind bilang content nito. Wala sa tree na iyon ang class na lumilitaw sa loob ng isang child component, kaya hindi ito naiko-compile kailanman: hindi ang binubuo ng isang variant, at hindi rin ang literal. Dumating ang Add button na may {"alignSelf": "stretch"} at wala nang iba, at nagta-throw ang helper kapag wala, at iyon lang ang dahilan kung bakit ito lumitaw.
At hindi naman talaga 44 ang
h-11. Gumagawa ang Tailwind ng.h-11 { height: 2.75rem }, at 14 ang rem ng NativeWind sa React Native, hindi ang 16 ng browser. Ang literal na<View className="h-11" />ay nagre-render ngheight: 38.5, sa test tree at sa app nang magkapareho. Kaya nasa ilalim ng 44 ng Apple mula pa noon ang laki na hinihingi ng variant na iyon, nakita man ito ng kahit anong suite o hindi. Dalawang magkahiwalay na depekto sa isang control: isang laki na hindi kayang basahin ng test, at isang laki na hindi rin sana nakalampas sa bar kung nabasa man. Dahil dito, binabasa ng mga touch-target check ang idineklarang style at props, sa halip na kumuha ng numero mula sa isang class.
Inilalagay ng ayos ang bar sa lugar na hindi tahimik na maigagalaw ng isang variant. Sa packages/detail/src/PokemonDetailView.tsx, sa Add button:
// Idineklara rito ang 44pt na bar sa halip na manahin ito mula sa size variant.
// Desisyong biswal ang isang variant at puwedeng magbago; commitment ang pinakamaliit
// na napipindot na laki ng pangunahing aksyon ng screen, at ang idineklara lang ng
// control ang kayang beripikahin ng accessibility suite. Ginagawang mas malapad sa 44
// ng `alignSelf: 'stretch'` ang button, pero layout instruction ang stretch at hindi
// sukat, kaya nakasaad ang minimum sa dalawang axis.
style={{ alignSelf: 'stretch', minWidth: 44, minHeight: 44 }}
Dala ng parehong file ang kabaligtarang depekto, isang daang linya pataas, sa hero na kinapapatungan ng button. Ipininta ng hero ang kulay ng type nang buong lakas, at ipinapaliwanag ng komento sa itaas nito na hindi puwedeng nakapirming kulay ang teksto nito, dahil nagdesisyon na ang design system kada type at dapat magtanong ang hero sa token sa halip na magpalagay. Nagpalagay ang dex number sa ilalim ng pangalan. Pinakupas nito ang sarili:
// Dati: pangalawang sagot, inimbento isang linya sa ilalim ng komentong nagsasabing huwag.
const heroMuted = heroInk ? 'text-white/70' : 'text-black/60';
Nire-resolve dito ang text-black sa #2E3138, ang neutral na sinasabi ng preset na hindi foreground,
at sa 60% sa ibabaw ng fill 2.21:1 ito sa water. Pumapalya ang labing-anim sa labingwalong type. Walang
alpha ang makakasagip nito: i-sweep mo ang halaga mula 1% hanggang 100% at buong lakas ang pinakamagandang
kaso, at 3.74:1 pa rin ang water doon, dahil maling tinta ang tinta. Kaya nagtatanong ang linya ng
tinatanong ng pangalan, sa packages/detail/src/PokemonDetailView.tsx:
// Burahin ang heroMuted. Parehong desisyon ang kinukuha ng dex number at ng pangalan.
<Text size="sm" className={`font-head ${onHero}`}>
{dexNumber}
</Text>
Nagiging steel sa 4.71:1 ang pinakamasamang kaso, at sinusukat na ng token matrix ang pares na iyon, kaya walang bagong kailangang idagdag doon. Kinailangan sana ng sariling row ng isang muted na variant.
Umaabot pa isang antas pababa ang depekto ng Add button. Binubuo ng ErrorState ng design system ang retry nito mula sa size="md", na h-10, o 35 points, at ang retry na iyon ang daan pabalik mula sa pumalyang load sa tatlong app. Napupunta sa design system ang deklarasyon nito at hindi sa app na unang nakapansin, sa packages/ui/src/components/error-state.tsx:
// Ang retry ang daan pabalik mula sa pumalyang load, kaya idineklara rito ang napipindot
// na laki nito sa halip na iwan sa size variant. Desisyong biswal ang isang variant;
// commitment ang pinakamaliit na target, at ang idineklara lang ang kayang beripikahin ng
// isang suite. Idineklara ang dalawang axis: mas malapad sa 44 ang button sa bawat layout
// kung saan ito lumilitaw, pero ang lapad na walang nagsasabi ay lapad na walang nakasukat.
<Button
action="primary"
size="md"
onPress={onRetry}
style={{ minWidth: 44, minHeight: 44 }}>
May isa pang finding sa tabi ng mga iyon, nasa dalawang component nang sabay, at iyon ang hindi kailanman nahuhuli ng isang biswal na review. Wala ni isang status semantics ang ErrorState at ang LoadingState: walang role, walang live region, wala talaga. Mukha silang tama, at para sa isang screen reader, tahimik lang na nangyari ang pumalyang load.
Hindi pareho ang repair na natatanggap nila, dahil hindi sila magkaparehong uri ng mensahe. Dapat manggambala ang pumalyang load; hindi dapat ang spinner. Kaya nagiging alert ang error-state.tsx, na nag-aanunsyo sa dalawang platform nang walang live region:
<Center className="flex-1 px-6" accessible accessibilityRole="alert">
at nakakakuha ang loading-state.tsx ng magalang na variant, na naghihintay munang matapos ang sinasabi na ng screen reader:
<Center className="flex-1" accessible accessibilityLiveRegion="polite">
Lima sa mga repair na ito ang dala ng design system at tatlo ang sa packages/detail, kaya isang release
ang bawat isa at hindi marami:
( cd packages/ui && npm version 1.0.13 --no-git-tag-version && npm run build && npm publish )
( cd packages/detail && npm install @pokedex/ui@1.0.13 && npm version 4.0.12 --no-git-tag-version && npm run build && npm publish )
Kinukuha rin ng host ang release, at may sarili itong repair: ang tanging depekto sa post na ito na nasa shell at hindi sa isang package o remote.
cp /tmp/pokedex-a11y-ref/apps/host/App.tsx apps/host/
cp /tmp/pokedex-a11y-ref/apps/host/__tests__/TabBar.test.tsx apps/host/__tests__/
cp /tmp/pokedex-a11y-ref/apps/host/tsconfig.json apps/host/
10pt ang dalawang tab label, at pumapalya pareho. Kinuha ng naka-focus ang colours.blue, ang brand fill:
3.48:1 sa maliwanag na bar, 3.74:1 sa madilim. Hindi kailanman naitakda ang hindi naka-focus, kaya
kinuha ito ng React Navigation sa paghahalo ng teksto ng theme sa gitna ng sariling kulay ng bar, at 3.27:1
iyon sa puti. Laging may isang tab na hindi naka-focus, kaya nasa screen ang pares na iyon habang nasa
screen ang app. May dalang dalawang nababasang asul ang colours.ts; ang mga grey ay ang ginagamit na ng
bawat ibang secondary na linya:
tabBarActiveTintColor: mode === 'dark' ? colours.blueTextDark : colours.blueText,
tabBarInactiveTintColor: mode === 'dark' ? colours.lightGrey : colours.darkGrey,
Ipinapaliwanag ng TabBar.test.tsx kung bakit karapat-dapat sa sariling file ang check na ito.
Pinapatunayan ng matrix sa @pokedex/ui na nalalampasan ng apat na pares na iyon ang AA, at hindi nito
kayang patunayan na ginagamit sila ng host: kapag ibinalik mo ang active tint sa colours.blue, berde pa
rin ang bawat check ng design system. Mga pares ang sinusukat ng matrix; ang app lang ang matatanong kung
binubuo niya ang mga ito. Source ang binabasa nito at hindi isang render, dahil nire-resolve sa loob ng
React Navigation ang isang navigator option at wala itong naaabot na element na kayang tanungin ng suite.
( cd apps/host && npm install @pokedex/ui@1.0.13 )
Dalawang patch release, tatlong app, at walang koordinasyon sa mga team na naglalabas nito. Ang caret range na dala na ng bawat consumer ang dahilan kung bakit patch ito at hindi negosasyon. Itinatala ng suite ng Pokédex kung ano ang ibig sabihin niyan sa lugar na pinakamadaling mabasa nang mali:
// Pumapasa ito dahil idineklara ng design system ang laki sa button ng ErrorState, hindi
// dahil may ginawa ang app na ito. Isang package release, sakop ang retry ng bawat remote.
expectMinTouchTarget(buttonContaining(getByText('Try again')));
Nire-render nito ang parehong ErrorState sa remote-load boundary nito, kaya lumilipat ito sa parehong bersyon ng dalawang remote sa halip na maiwan, at para sa isang package na ibinibigay ng host bilang eager singleton, hindi iyon opsyonal.
Nakadepende sa kung ano ang pumalya kung hanggang saan ka dadalhin ng retry na iyon. Malinis na nagre-retry ang pumalyang data request. Mas mahirap na kaso ang remote na hindi kailanman sumagot: itinatapon ng boundary ang naka-cache na rejection ng React at nagsisimula ito ng bagong import, kaya may nakikitang ginagawa ang button, pero bumabalik ang parehong pagpalya sa tuwina. Kumikilos ang runtime na parang hawak pa rin nito sa ilalim ang pumalyang manifest fetch. Obserbasyon iyon at hindi dokumentadong garantiya, kaya ituring mong finding ang kilos at hindi ang sanhi. Kailangan ng cache-busting sa runtime para malinis ito, at may saysay lang ang cache-busting kapag may mababalikan na, kaya sabay dumarating ang dalawa sa resilience post sa hulihan ng seryeng ito. Mas makitid ang kayang sabihin ng mga check sa post na ito at sulit pa ring sabihin: anuman ang gawin ng retry, naaabot ito, may pangalan ito, at sapat ang laki nito para mapindot.
Parehong bar para sa bawat remote
Ini-install ng dalawang remote ang parehong tatlong devDependency, at kinukuha nila ang dalawang naayos na package sa parehong utos:
( cd apps/list && npm install @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 && npm install -D @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 )
( cd apps/party && npm install @pokedex/detail@4.0.12 @pokedex/ui@1.0.13 && npm install -D @pokedex/a11y-testing@1.0.18 @testing-library/react-native@13.3.3 @tailwindcss/container-queries@0.1.1 )
Kinukuha ng bawat app ang sarili nitong suite, ang Jest config nito at ang mga deklarasyon nito ng hindi naaangkop, at humahabol ang README sa tag:
for app in list party; do
cp /tmp/pokedex-a11y-ref/apps/$app/jest.config.js \
/tmp/pokedex-a11y-ref/apps/$app/a11y-report.config.js \
/tmp/pokedex-a11y-ref/apps/$app/tsconfig.json apps/$app/
cat /tmp/pokedex-a11y-ref/apps/$app/.gitignore > apps/$app/.gitignore
done
cp /tmp/pokedex-a11y-ref/apps/list/__tests__/ListStack.accessibility.tsx apps/list/__tests__/
cp /tmp/pokedex-a11y-ref/apps/party/__tests__/PartyStack.accessibility.tsx apps/party/__tests__/
cp /tmp/pokedex-a11y-ref/apps/list/src/PokedexScreen.tsx apps/list/src/
cp /tmp/pokedex-a11y-ref/apps/party/src/PartyScreen.tsx apps/party/src/
mkdir -p apps/party/__mocks__
cp /tmp/pokedex-a11y-ref/apps/party/__mocks__/styleMock.js apps/party/__mocks__/
cp /tmp/pokedex-a11y-ref/README.md .
Dala ng dalawang screen ang mga repair sa counter. Nasa PokedexScreen.tsx ang live-region na ayos sa
ibaba, at ipinipinta ng dalawang screen ang parehong count pill, na ang numeral ay text-darkGreen sa
bg-lightGreen: 1.53:1, 10.5pt bold, sa header ng bawat remote. #A6D3A0 ang darkGreen, parehong
halaga ng grass fill, kaya ang pangalan lang ang madilim doon. text-darkGrey na ang ginagamit ng dalawa,
ang kulay na ginagamit na ng label sa tabi nila, sa 7.18:1, isang pares na hawak na ng token matrix mula
pa noon. Iba ang binubuo ng counter na walang nakasukat.
Mas walang kaabog-abog ang styleMock.js at kasinghalaga rin, at naroon ito dahil hinihila ng PartyStack ang global.css, isang hakbang ang layo sa pamamagitan ng ./styles, para makarating sa shared styling runtime ang mga class ng remote kapag ini-load ng host ang exposed module. Sakop na ng sariling entry ng app ang standalone build; kapag wala ang import sa PartyStack, gumagana ang mga style nang standalone at tahimik na walang ginagawa kapag federated. Walang CSS loader ang Jest, kaya ini-map ng kinopyang config ang import na iyon sa stub. Kapag wala ito, hindi pumapalya ang suite ng Party: tumatangging tumakbo.
Pagkatapos, ang script na nagpapadaan sa mga accessibility file sa reporter:
for app in apps/list apps/party; do
( cd $app && npm pkg set 'scripts.test:a11y=jest --testPathPattern accessibility --reporters=default --reporters=@pokedex/a11y-testing/reporter.js' )
done
Nakaturo sa shared preset ang dalawang Jest config. Dahil ini-extend nito ang React Native preset, patuloy na tumatakbo ang mga suite na mayroon na ang bawat app, at ang panuntunan ay palawakin ang allowlist nito sa halip na palitan. Sa balangkas, dahil mas marami ang entry ng mga kinopyang file kaysa rito:
const preset = require('@pokedex/a11y-testing/jest-preset');
module.exports = {
preset: '@pokedex/a11y-testing',
transformIgnorePatterns: [
`node_modules/(?!(${[...preset.uncompiledPackages, '@react-navigation'].join('|')})/)`,
],
};
Ang pagpapalit sa array na iyon ang tahimik na paraan para tumigil sa pagiging shared ang isang shared preset. Hindi naman ito tahimik na pumapalya: naglalabas ng ES module ang styling stack, kaya natatamaan ang Jest ng SyntaxError: Cannot use import statement outside a module at wala itong pinapatakbo. Iyon ang magandang kaso. Ang bitag: tinutukoy ng error ang isang file na malalim sa loob ng node_modules, at nababasa ito bilang sirang dependency sa halip na bilang linya ng config na pag-aari ng app. Ginagawang madali ang pagpapalawak ng pag-export sa listahan.
Sinasakop lang ng bawat suite ang binubuo ng team nito, at ang paraan para manatiling tapat iyon ay i-render ang sariling screen ng team sa halip na ang mga component ng design system. Ini-mount ng dalawang file ang totoong stack sa totoong store at totoong navigator, ang parehong harness na ginagamit ng mga test na mayroon na ang mga app:
const store = configureStore({ reducer: rootReducer });
for (const member of party) {
store.dispatch(addToParty(member));
}
await renderWithTheme(
<Provider store={store}>
<SafeAreaProvider initialMetrics={metrics}>
<NavigationContainer>
<PartyStack />
</NavigationContainer>
</SafeAreaProvider>
</Provider>,
);
Mas maraming setup iyon kaysa sa direktang pag-mount ng isang PokemonCard, at iyon ang pagkakaiba ng pagsusuri sa app na ito at pagsusuri sa package ng iba. Ang suite na nagre-render ng mga component ng design system ay muling sumusuri ng inayos na ng source, at iyon mismo ang duplication na inaalis ng source contract.
Kaya sinusuri ng Pokédex ang mga row na gawa ng sarili nitong query data, ang mga state na kayang abutin ng screen nito, at ang party counter nito. Sinusuri ng Party ang grid nito: ang punong slot ay isang button na nagngangalan sa Pokémon nito, ang mga walang laman ay mga may pangalang puwang at hindi apat na magkakaparehong katahimikan, at ang pag-alis ay isang accessibility action sa card at hindi pangalawang focus stop sa tabi nito.
expect(getByLabelText(/^Pikachu, number 025/).props.accessibilityActions).toEqual([
{ name: 'remove', label: 'Remove from party' },
]);
Maliit ang ✕ badge, 21 points, dahil 1.5rem ang h-6 at 14 ang rem ng NativeWind, kaya dinadala ito ng hitSlop nito sa 45 imbes na sa 41 na dati nito, at kung gagawin itong sariling focus stop, madodoble ang bilang ng dadaanan mo sa pag-swipe sa punong party. Itinatago ito ng design system at inilalabas ang pag-alis bilang action, kaya naaabot ito ng gumagamit ng screen reader sa rotor ng VoiceOver o sa actions menu ng TalkBack. Ang check ay kung umiiral man lang ang action, dahil ang nakatagong control na walang laman sa likod ay talagang hindi maaabot.
Ang pagre-render ng totoong screen din ang nagpalit sa pangalawang depekto ng counter mula sa pagiging tala tungo sa pagiging ayos. Ipinapakita ng header ng Pokédex ang “My Party” sa tabi ng bilang kung ilan sa anim, at nagbabago ang bilang na iyon kapag may naidagdag mula sa isa pang screen, nang hindi gumagalaw ang focus. Nakikita ng nakakakita ang pag-usad ng numero; walang sinasabi sa gumagamit ng screen reader, dahil static text ang header. Iyon ang buong SC 4.1.3, at live region na ito ngayon na may label na nagsusulat ng ratio sa mga salita, dahil ang “3/6” lang ay naiiwan sa kung paano ito babasahin ng bawat screen reader:
<Box
accessible
accessibilityLiveRegion="polite"
accessibilityLabel={`My Party, ${partyCount} of ${MAX_PARTY}`}>
Kailangan ng header ng Party mismo ang parehong tatlong props, at iyon ang halaga ng depektong nasa app code nang dalawang beses: dalawang screen, dalawang suite, at walang nagpipilit tumingin sa pangalawa.
Walang mapapatunayan tungkol sa app ang test na sumusuri niyan laban sa isang object na sinulat nang mano-mano. Laban sa na-render na screen, pumapalya ito kapag tinanggal ang props, at iyon lang ang bersyong sulit itago.
Ang report
May lumalabas na isang accessibility-report.md mula sa bawat suite, isinusulat ng reporter pagkatapos ng run:
( cd packages/ui && npm run test:a11y )
Binabasa ng reporter ang criterion mula sa titulo ng bawat describe, kaya WCAG 1.4.3 … na lang ang buong setup na kailangan. Ganito nagbubukas ang run ng design system:
# Accessibility report — ui
Automated coverage: **5 of 13** WCAG 2.1 A + AA criteria that a Jest suite can decide,
after 2 declared not applicable here.
## Violations (0)
None.
## Known findings (0)
None.
Wala roon ang pares na exempt. May sarili itong heading, dahil ang threshold na pinipili ng isang proyekto ay hindi resulta laban sa isang criterion, at ang pag-file sa isa bilang isa pa ang dahilan kung bakit tumitigil sa pagkakaroon ng kahulugan ang isang coverage number:
## Project bars
- **not held** — pairs held above what the criteria require · disabled button label
on its fill (known: 2.02:1, exempt under 1.4.3 Incidental)
Ang denominator ang pinakamadaling palakihin sa isang coverage number, kaya dalawang hakbang ang pagbuo nito. Dala ng catalogue ang lahat ng 50 A at AA criteria ng WCAG 2.1 at tina-tag nito ang bawat isa ng layer na makakapagpasya nito: 15 ang kayang pagpasyahan ng isang Jest process, at ang iba ay nauukol sa device audit, sa manual na pagsusuri, o sa wala rito, gaya ng audio at video criteria sa isang app na walang alinman sa dalawa. Pagkatapos, inaalis ng bawat suite ang mga idineklara nitong hindi naaangkop, at kaya sa 13 sinusukat ang design system. Nasa isang a11y-report.config.js sa root ng suite ang mga deklarasyong iyon, at may dahilan ang bawat isa, nasa binilang na set man ang criterion o hindi. Walang nagpipilit na maganda ang dahilan; naroon ito para may mapagtutulan ang isang tagasuri.
Lumilitaw ang gawa sa touch target sa mga report ng mga suite na gumagawa nito, at hindi kailanman sa fraction na iyon. Sinasabi iyon ng run ng design system sa sarili nitong seksyon:
## Checked beyond the counted scope
These ran and are reported, but they sit outside the WCAG 2.1 A + AA set the coverage
number is measured against, so they do not raise it.
- **WCAG 2.5.5** not in the A + AA catalogue — 4 checks
Level AAA ang SC 2.5.5, kaya papalakihin ng pagbilang dito ang isang bilang na A at AA. Ang tahimik na pagtatapon dito ay magtatago ng check na tumakbo naman. Wala sa dalawa ang ginagawa ng report.
Iyon na halos ang artefact na ibinebenta ng bayad na accessibility scanner, gawa mula sa mga test na pag-aari na ng team.
Ang hindi nakikita ng mga check na ito
Mas kaunti ang napapatunayan ng berdeng suite dito kaysa sa hitsura nito.
| Layer | Ano ang napapatunayan | Ano ang hindi kaya |
|---|---|---|
| Jest suite | mga pares ng token, idineklarang laki, ang pangalan, role at state na inilalabas ng isang control | kung ano ang ipininta ng device, ang totoong traversal order, ang hit region pagkatapos ng clipping |
| Device audit | ang contrast gaya ng naiguhit, ang mga naputol na target, ang laki sa totoong screen | kung maganda bang basahin nang malakas ang isang label |
| Manual na pagsusuri | ang totoong karanasan, kasama ang traversal order | ang mahuli ang bawat regression, sa bawat commit |
Ang device audit ay performAccessibilityAudit sa iOS at ang Accessibility Test Framework sa Android; ang manual na pagsusuri ay isang taong may VoiceOver o TalkBack.
Nauukol sa manual na pagsusuri ang focus order, at iyon ang tapat na sagot sa pangatlong bagay na ipinangako ng post 11. Kayang sabihin ng isang Jest suite na naroon ang mga focusable na element. Hindi nito kayang sabihin kung anong pagkakasunod ang dinaraanan ng isang screen reader, dahil galing sa layout sa totoong screen ang pagkakasunod na iyon. At hindi rin malinis na lugar para rito ang device layer: may traversal-order check ang Accessibility Test Framework ng Android at walang katumbas nito ang performAccessibilityAudit ng iOS, at ang criterion na isang platform lang ang makakapag-automate ay hindi criterion na dapat angkinin ng isang report. Kaya nananatili ito sa taong humahawak ng screen reader, at doon ito tina-tag ng catalogue.
Hindi hagdan ang mga layer kung saan sakop ng nasa itaas ang nasa ibaba. Sinusuri ng token matrix ang mga pares na binubuo ng design system, sa mismong mga surface na kinapapatungan ng mga ito, may screen mang nagpapakita ngayon ng isang pares o wala; sinusuri ng device audit ang nasa screen na itinuro rito, at ibang set iyon at hindi kailanman ang buong palette. Kailangan ang malinis na automated run at hindi ito sapat, at ganoon din sa malinis na device pass.
Patakbuhin mo
Lahat ng suite, kasama ang mga nauna sa post na ito:
( cd packages/a11y-testing && npm test )
( cd packages/ui && npm test )
( cd packages/detail && npm test )
( cd apps/list && npm test )
( cd apps/party && npm test )
( cd apps/host && npm test )
Ang unang linya ang bar na sinusuri ang sarili nito. Ang package na nagsasabi sa apat na workspace kung ano ang accessible ay dapat kayang patunayan ang sarili nitong helper, at ang mga kaso roon ay ang mga namali sa mga naunang bersyon.
Pagkatapos, ang accessibility layer nang mag-isa, kasama ang report:
( cd packages/ui && npm run test:a11y )
( cd packages/detail && npm run test:a11y )
( cd apps/list && npm run test:a11y )
( cd apps/party && npm run test:a11y )
Walang simulator at walang dev server. Ini-publish ang bawat package habang binubuo ito, kaya walang muling ini-publish dito.
Ang binuo mo, at ang susunod
Isang package na ang humahawak ng accessibility bar ng buong federation, at limang suite ang tumatakbo laban dito: ang mga helper ng bar mismo, ang mga token at component sa @pokedex/ui, ang shared screen sa @pokedex/detail, at ang sariling screen ng bawat remote.
Nahahati ang lumabas doon ayon sa kung saan naroon ang depekto, at iyon mismo ang punto. Labing-isa ang nasa @pokedex/ui: dalawang badge foreground sa ibaba ng AA sa sarili nilang fill, apat pa sa ibaba nito sa hero scrim, ang glyph ng back pill sa ibaba ng non-text bar sa tatlo sa labingwalong hero, dalawang linya ng secondary text na pumapalya sa dalawang theme, at dalawang component na tahimik na nag-aanunsyo ng pumalyang load. Idagdag ang touch target ng retry button at ng badge ng card, at labintatlo na. Kinuha ng tatlong app ang bawat isa sa kanila sa isang version bump, at iyon ang buong argumento para sa isang design system na nakasaad bilang numero.
Tatlo ang nasa @pokedex/detail, at hindi ito ini-install ng host, kaya sa dalawang remote lang tumigil ang tatlo: ang target ng Add button, ang dex number ng hero, at ang dalawang section heading ng sheet.
Apat ang hindi talaga nakabiyahe, at sila ang kawili-wili. Dalawa ang sariling tab label ng host, sa shell na walang pag-aaring screen. Ang dalawa pa ay ang party counter: app code, parehong header na sinulat nang dalawang beses, isa sa bawat remote, walang package na humahawak nito, isang depekto sa bawat uri, isang bilang na walang inaanunsyo at isang numeral sa 1.53:1 sa sarili nitong pill. Kinailangang ayusin ang bawat isa sa apat na iyon kung nasaan sila, at dalawa sa kanila ang kinailangang ayusin nang dalawang beses.
Nasa mga check ang mga depektong mas mahalaga. Apat ang ipinakita ng post na ito: isang helper na humiram ng bar para sa isang axis na walang nagdeklara, isang matrix na bumabasa ng constant sa halip na ng token, ang parehong matrix na bumabasa ng maling surface, at isang markang string lang. May lumitaw pang iba pagkatapos, sa mga suite ng design system at sa mga suite ng bar mismo: isang scrim check sa design system na naghahambing ng dalawang constant na ini-export ng parehong module, isang clamp test na ang halaga ay hindi kailanman umabot sa hangganang linilimitahan nito, isang use-of-colour helper sa bar mismo na bumabasa ng screen-reader metadata sa halip na ng teksto sa screen, at isang target check na bumabalik sa numerong nakasulat sa test kapag tumigil sa pagma-match ang sarili nitong pattern. May kaso na ngayon ang bawat isa na pumapalya kapag inalis muli ang repair, at iyon lang ang patunay na totoo ang isang check. Ang check na nakatutok nang bahagyang katabi ng sinasabi nitong sinusukat ay nag-uulat nang berde, at patuloy na nag-uulat nang berde. Wala sa output ang nagbubukod dito sa check na gumagana.
Walang natitirang bukas laban sa isang criterion. Limang pares ang naganoon sa loob ng limang round, pawang parehong secondary-text token, naka-park sa dahilang walang mas madilim na halaga ang makakalampas sa lahat ng surface. Totoo iyon at maling tanong ang sinasagot nito: nakakaintindi ng theme ang design system, at kumuha ng themed na class ang mga pares sa halip na bagong token. May isang finding na nananatiling nakatala at hindi ito resulta laban sa isang criterion, dahil ipinagbubukod ng SC 1.4.3 ang label ng isang hindi aktibong control. Inililimbag ito ng report sa ilalim ng Project bars, at hindi ito binibilang kahit saan.
Ang layer ang tapat na hangganan. Binubuo ng post na ito ang Jest layer at wala sa dalawa pa. Totoong trabaho ang device audit harness na hindi ginawa rito, at hindi maaautomate sa disenyo pa lang ang manual na pagsusuri sa VoiceOver at TalkBack. Umiiral ang layer na ito para sa regression: manatiling ayos ang naayos mo na, sa bawat commit, sa bawat remote, laban sa iisang depinisyon.
Susunod: ipinapahiram ng host sa mga remote ang native side nito. May navigation bridge na naglulunsad ng isang native screen mula sa Party tab at naghihintay ng resulta nito.
Mga Sanggunian
- WCAG 2.2, SC 2.5.5 Target Size (Enhanced) — Level AAA, 44 by 44 CSS pixel
- WCAG 2.2, SC 2.5.8 Target Size (Minimum) — ang AA criterion, 24 by 24 na may limang exception
- WCAG 2.2, contrast (minimum) — 4.5:1 para sa normal na teksto, 3:1 para sa malaki
- WCAG 2.2, relative luminance — ang pormula, at ang note na nagtatala ng pagbabago noong Mayo 2021 mula 0.03928 papuntang 0.04045
- Apple Human Interface Guidelines: Accessibility — ang kasalukuyang control-size table: 44 by 44 points bilang default sa iOS at iPadOS, 28 by 28 bilang minimum
- Apple: UI Design Dos and Don’ts — ang lumang page, “at least 44 points x 44 points”
- Android accessibility help: touch target size — ang gabay sa 48dp
- jest-axe — ang kailangang jsdom at ang mga naka-off na contrast check
- Directive (EU) 2019/882 — ang European Accessibility Act, ipinatutupad mula 28 Hunyo 2025
- EN 301 549 — ang pamantayang Europeo na naglalapat ng WCAG 2.1 sa mga mobile app
- React Native Testing Library — ang mga query at matcher na pinagbubuuan nito
- nativewind —
nativewind/test, naka-ship at walang dokumentasyon - react-native-css-interop — ang
toHaveStylematcher, at ang rem na pinagre-resolve ng stack na ito - react-native-module-federation — ang companion repo, ang build sa tag na
post-12-a11y-testing