Mundarija (31)
- Bu darsda
- 1. Nega bu kerak?
- 2. Birinchi qatlam: komponent testi allaqachon a11y testi
- 2.1 Nom, tavsif va holat
- 2.2 Label olib tashlansa
- 3. Ikkinchi qatlam: axe-core
- 3.1 axe qanday ishlaydi
- 3.2 Komponent darajasida: jest-axe va vitest-axe
- 3.3 Sahifa darajasida: @axe-core/playwright
- 3.4 Story darajasida
- 4. Avtomatika chegarasi
- 4.1 axe nimani ko'rmaydi
- 4.2 Klaviatura bilan buyurtma
- 4.3 Fokus qaytishi
- 4.4 Tab tartibi
- 5. Qo'lda nima qoladi
- 6. Vazifalar qadami: axe va klaviatura oqimi (holat 14)
- 6.1 O'rnatish va narx
- 6.2 e2e/a11y.spec.ts: axe qismi
- 6.3 Klaviatura oqimi
- 6.4 Birinchi o'tishdagi ikki xato
- 6.5 Natija va commit
- 7. Ko'p uchraydigan xatolar
- 8. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): bron formasining a11y testi
- 3-mashq (qiyin): tablar klaviatura bilan
- 4-mashq: Vazifalar qadami
- 9. Real ishda
- Xulosa
- Manbalar
Accessibility testlari: axe, klaviatura oqimi va avtomatika chegarasi
Qisqacha: A11y testi uch qatlamdan iborat. (1) Rol so'rovlari va jest-dom matcherlari (
toHaveAccessibleName,toBeInvalid) — har komponent testida bepul. (2) axe-core — tayyor qoidalar dvigateli: nomsiz tugma, label'siz maydon, kontrast; Playwright'da@axe-core/playwrightbilanexpect(buzilishlar).toEqual([]). (3) Klaviatura oqimi testi — Tab, Enter, Esc va fokus qayerga borishi. axe xatolarning faqat bir qismini topadi:<div onClick>tugmani ham, fokus yo'qolishini ham ko'rmaydi. Qolganini klaviatura testi va odam topadi.
Bu darsda
- Komponent testida a11y'ni rol, nom, tavsif va holat bilan tekshirasiz.
- axe-core natijasini (
violations,incomplete,passes) o'qiysiz va qoida teglarini tanlaysiz. @axe-core/playwrightbilan sahifa testini yozasiz;jest-axevavitest-axeholatini bilasiz.user-eventva Playwright bilan klaviatura oqimini sinaysiz: Tab tartibi, Esc, fokus qaytishi.- Avtomatika nimani ko'rmasligini misollarda ko'rasiz va qo'lda tekshiruvga nima qolishini aytasiz.
Oldin bilishingiz kerak: Accessibility'ni tekshirish, Fokus boshqaruvi va klaviatura navigatsiyasi, RTL I, RTL II, Playwright II, Storybook II.
1. Nega bu kerak?
Accessibility'ni tekshirish darsida axe DevTools'ni qo'lda bosgan edik va va'da qilgan edik: "axe-core'ni avtomatik testlarga ulaymiz". Qo'lda tekshiruvning muammosi bitta — uni har safar kimdir eslashi kerak. Sardor yangi tugma qo'shdi, ikonkasini chiroyli qildi, aria-label ni unutdi. Malika ekran o'quvchida faqat "tugma" eshitadi. Buni kim, qachon sezadi?
vazifalar da axe 13-qismdan beri ishlaydi — lekin kurs tomonidagi tekshiruvda. Nega test yozamiz darsidagi testlar xaritasida a11y qatori 0 edi: o'quvchi reposida bitta ham a11y testi yo'q. Bu darsda shu qatorni to'ldiramiz.
A11y testi bitta vosita emas. Uch qatlam bor va ular bir-birini to'ldiradi:
flowchart TB
A["1. Rol so'rovlari<br/>getByRole, toHaveAccessibleName"] --> D["Har komponent testida"]
B["2. axe-core qoidalari<br/>nom, label, kontrast, ARIA"] --> E["Sahifa yoki story"]
C["3. Klaviatura oqimi<br/>Tab, Enter, Esc, fokus"] --> F["Komponent va e2e"]
G["4. Odam<br/>ekran o'quvchi, 200% zoom"] --> H["Relizdan oldin"]Rasmda: yuqoridagi uchtasi avtomatik, to'rtinchisi — qo'lda. Pastga tushgan sari tekshiruv qimmatroq, lekin ko'proq narsani ko'radi.
2. Birinchi qatlam: komponent testi allaqachon a11y testi
2.1 Nom, tavsif va holat
RTL I da ko'rdik: nomsiz tugmani getByRole topa olmaydi. Endi buni ataylab ishlatamiz. Maydonning to'rt a11y xususiyatini tekshiramiz: nomi (label), tavsifi (aria-describedby dagi yordam matni), holati (aria-invalid) va fokusi.
«Bahor» SMS-xabarnomaga yozilish formasi:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { expect, test } from "vitest";
function SmsForm() {
const [error, setError] = useState("");
const input = useRef(null);
function submit(e) {
e.preventDefault();
if (!/^\+998\d{9}$/.test(input.current.value)) {
setError("Raqam +998 bilan boshlanib, 12 raqam bo'lsin");
input.current.focus();
}
}
return (
<form onSubmit={submit} noValidate>
<label htmlFor="tel">Telefon</label>
<input
id="tel"
ref={input}
aria-invalid={error ? "true" : undefined}
aria-describedby={
error ? "tel-yordam tel-xato" : "tel-yordam"
}
/>
<p id="tel-yordam">Aksiyalar haqida SMS keladi</p>
<p id="tel-xato" role="alert">
{error}
</p>
<button>Yozilish</button>
</form>
);
}
test("bo'sh forma — xato e'lon, fokus joyida", async () => {
const user = userEvent.setup();
render(<SmsForm />);
const tel = screen.getByRole("textbox", { name: "Telefon" });
expect(tel).toHaveAccessibleDescription(
"Aksiyalar haqida SMS keladi",
);
expect(tel).not.toBeInvalid();
await user.click(screen.getByRole("button", { name: "Yozilish" }));
expect(tel).toBeInvalid();
expect(tel).toHaveFocus();
const alert = screen.getByRole("alert");
console.log(alert.textContent);
// ikkala matn birga, bo'sh joy bilan ulanadi
expect(tel).toHaveAccessibleDescription(
/^Aksiyalar haqida SMS keladi Raqam .*12 raqam/,
);
});Konsolda:
Raqam +998 bilan boshlanib, 12 raqam bo'lsin
✓ bo'sh forma — xato e'lon, fokus joyida
Testlar: 1 o'tdi (jami 1)Test Malika eshitadigan hamma narsani tekshirdi:
getByRole("textbox", { name: "Telefon" })— maydonning nomi bor (label ulangan).toHaveAccessibleDescription— ekran o'quvchi nomdan keyin o'qiydigan matn. Xato chiqqach,aria-describedbyikkiidni ko'rsatadi:"tel-yordam tel-xato". Brauzer ikkala matnni ro'yxat tartibida, bo'sh joy bilan ulaydi — Malika avval yordam matnini, keyin xatoni eshitadi. Yordam matni yo'qolmaydi: xatoni tuzatayotganda ham u kerak.toBeInvalid—aria-invalid="true": "maydon xato" deb e'lon qilinadi.toHaveFocus— Bobur Tab bosib xatoni qidirmaydi, fokus o'zi maydonga qaytdi.
Bu oddiy komponent testi. Lekin uning har qatori a11y talabidan kelib chiqqan. Bu — birinchi va eng arzon qatlam.
2.2 Label olib tashlansa
Endi htmlFor ni unutgan versiya. Rol so'rovi buni darhol ko'radi:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function BrokenForm() {
return (
<form>
<label>Telefon</label>
<input id="tel" placeholder="+998 90 123 45 67" />
</form>
);
}
test("label ulanmagan — nom yo'q", () => {
render(<BrokenForm />);
const field = screen.getByPlaceholderText("+998 90 123 45 67");
console.log(field.labels.length);
expect(() =>
screen.getByRole("textbox", { name: "Telefon" }),
).toThrow("Unable to find an accessible element");
});Konsolda:
0
✓ label ulanmagan — nom yo'q
Testlar: 1 o'tdi (jami 1)labels.length — 0: <label> matni ko'rinadi, lekin maydonga ulanmagan. Ko'zi ko'radigan odam farqni sezmaydi. Ekran o'quvchi esa maydonga kelganda faqat "tahrirlash maydoni" deydi. Testni getByPlaceholderText bilan yozsak — o'tib ketardi va hech kim bilmasdi. RTL I dagi ustuvorlik tartibining ma'nosi shu.
Tekshirib ko'ring: Tugmada faqat ikonka bor va
title="O'chirish".getByRole("button", { name: "O'chirish" })uni topadimi? Bu yaxshi yechimmi?
Javob
Topadi: title ham kirish nomi manbai (eng oxirgisi). Lekin yaxshi emas: title faqat sichqoncha ustida turganda ko'rinadi, telefonda va klaviaturada ko'rinmaydi. aria-label="O'chirish" yoki ko'rinmas matn (sr-only) aniqroq.
3. Ikkinchi qatlam: axe-core
3.1 axe qanday ishlaydi
axe-core — Deque kompaniyasining ochiq kodli a11y dvigateli. axe DevTools kengaytmasi, Lighthouse'ning a11y bo'limi va Storybook a11y addon'i — hammasi ichida shu kutubxona. U DOM'ni olib, qoidalar (rules) bo'yicha tekshiradi — bizning sinovimizda 89 ta qoida ishladi. Natija to'rt guruh:
| Guruh | Ma'nosi |
|---|---|
violations |
aniq buzilish — test yiqilishi kerak |
incomplete |
axe ishonchi komil emas — "odam ko'rsin" |
passes |
tekshirildi, toza |
inapplicable |
qoidaga mos element yo'q (masalan, sahifada rasm yo'q) |
Har buzilishda: id (qoida nomi, masalan button-name), impact (critical, serious, moderate, minor), nodes (qaysi elementlar, CSS selektor bilan) va helpUrl (Deque hujjati). Qoidalar teglar bilan guruhlangan: wcag2a, wcag2aa, wcag21aa, wcag22aa (WCAG darajalari), best-practice (standart talab qilmaydi, lekin tavsiya).
H-79 dagi tamoyilni eslang: axe noto'g'ri signal bermaslikka harakat qiladi. Ishonchi komil bo'lmasa — violations ga emas, incomplete ga qo'yadi. Bu muhim: incomplete bo'sh bo'lmasa, test o'tadi, lekin kimdir qarashi kerak.
3.2 Komponent darajasida: jest-axe va vitest-axe
Komponent testida axe'ni ulaydigan ikki mashhur paket bor. Ularning holati (2026-10-20, npm):
| Paket | Oxirgi versiya | Holat |
|---|---|---|
jest-axe |
11.0.0 (2026-07-26) | faol; toHaveNoViolations matcheri; axe-core 4.12.1 ga qotirilgan |
vitest-axe |
0.1.0 (2022-10-21) | barqaror versiya eski; 1.0.0-pre.5 (2025-01) — sinov versiyasi |
Kanon vitest-axe ni olmadi: barqaror versiyasi 2022 yilniki. Lekin bu axe'ni komponent testida ishlatib bo'lmaydi degani emas — axe-core ning o'zi yetadi. Biz Vitest 5.0.4 + jsdom 30.1.2 + axe-core 4.13.0 bilan alohida papkada sinab ko'rdik (kanon emas):
import { render } from "@testing-library/react";
import axe from "axe-core";
import { test } from "vitest";
test("axe nimani ko'radi", async () => {
const { container } = render(
<form>
<input id="a" placeholder="Taom qidirish" />
<input id="b" />
<div onClick={() => {}}>Buyurtma berish</div>
<span tabIndex={3}>Savat</span>
</form>,
);
const n = await axe.run(container);
console.log(n.violations.map((v) =>
`${v.id}: ${v.nodes.map((x) => x.target.join(" ")).join(", ")}`));
const label = n.passes.find((v) => v.id === "label");
console.log("label passes:", label?.nodes.map((x) => x.target[0]));
});Terminalda:
Not implemented: HTMLCanvasElement's getContext() method: without installing the canvas npm package
stdout | t/axe2.test.jsx > placeholder va label
[ 'label: #b', 'tabindex: span' ]
label passes: [ '#a' ]To'rt elementdan axe ikkitasini topdi:
label: #b— nomsiz maydon. To'g'ri.tabindex: span— musbattabIndexTab tartibini buzadi (Fokus boshqaruvi). To'g'ri.#ao'tdi. Faqatplaceholderli maydonni axe nomli deb hisobladi: placeholder ham kirish nomi manbai. Bizning ustuvorlik tartibimizda esa bu zaif joy — yozish boshlanganda placeholder yo'qoladi.<div onClick>— jim. Sichqonchada ishlaydigan, klaviaturada ishlamaydigan "tugma". axe buni ko'rmadi:divda hech qanday ARIA xato yo'q, u shunchaki matnli blok.
Birinchi qator ham saboq: jsdom'da <canvas> yo'q. axe kontrastni hisoblash uchun canvas'dan foydalanadi, shuning uchun color-contrast qoidasi jsdom'da doim incomplete ga tushadi. Kontrast — faqat haqiqiy brauzerda (Playwright, Storybook, Browser Mode).
Maslahat: Komponent darajasidagi axe arzon, lekin jsdom chegaralangan. Kontrast, ko'rinish va fokus halqasi kabi narsalar uchun axe'ni haqiqiy brauzerda ishlating.
Natija oynasida axe-core yuklanmaydi — yuqoridagi blok faqat terminal uchun. Saytda shu g'oyani rol so'rovlari bilan ko'rdik — «Birinchi qatlam» bo'limida.
3.3 Sahifa darajasida: @axe-core/playwright
Haqiqiy brauzerda butun sahifani tekshirish — Deque'ning rasmiy paketi @axe-core/playwright. API — quruvchi (builder):
import AxeBuilder from "@axe-core/playwright";
const natija = await new AxeBuilder({ page })
.withTags(["wcag2a", "wcag2aa", "wcag21aa", "wcag22aa"])
.include("main") // faqat shu qism (ixtiyoriy)
.exclude(".reklama") // tashqi vidjetni chiqarish
.disableRules(["color-contrast"]) // faqat sabab bilan!
.analyze();analyze() — o'sha to'rt guruhli natija. Uni testda qanday tekshirish kerak? Eng sodda yo'l expect(natija.violations).toEqual([]) — lekin yiqilganda u har buzilishni 50 qatorlik obyekt sifatida chiqaradi (yuqoridagi button-name xatosida biz shunday uzun farqni ko'rdik). Kanon yaxshiroq yo'l tanladi: buzilishni bitta satrga aylantiradi — qoida va element. Bu «Vazifalar qadami» da.
Tekshirib ko'ring:
disableRules(["color-contrast"])qachon to'g'ri, qachon xato?
Javob
Xato — "test qizil, tez yashil bo'lsin" deb o'chirsangiz: muammo qoladi, test esa yolg'on gapiradi. To'g'ri — aniq sabab bo'lsa va izohda yozilsa. Masalan, tashqi vidjet sizning nazoratingizda emas (unda exclude aniqroq) yoki axe SVG grafikda fonni aniqlay olmaydi (incomplete). Har o'chirish — izoh va TEXNIK-QARZ qatori bilan.
3.4 Story darajasida
Storybook II da addon-a11y ni ulagan edik: har story render bo'lgach axe ishlaydi, a11y: { test: "error" } bilan buzilish testni yiqitadi. Kanon u yerda nomsiz tugma story'sini ataylab yozib ko'rgan — button-name bilan yiqildi. Story darajasi bitta komponentning hamma holatini (bo'sh, xato, yuklanmoqda) tekshiradi; sahifa darajasi — ular birga qanday ishlashini.
| Qatlam | Vosita | Kontrast | Fokus va Tab |
|---|---|---|---|
| Komponent (jsdom) | axe-core / jest-axe |
yo'q (incomplete) |
user-event bilan |
| Story (Chrome) | addon-a11y |
bor | play bilan |
| Sahifa (Chrome) | @axe-core/playwright |
bor | Playwright keyboard |
4. Avtomatika chegarasi
4.1 axe nimani ko'rmaydi
H-79 da aytgan edik: avtomat vositalar muammolarning taxminan uchdan biridan yarmigacha qismini topadi. Qolgani xulq va ma'no:
| axe topadi | axe topa olmaydi |
|---|---|
| nomsiz tugma, label'siz maydon | <div onClick> — klaviaturada ishlamaydigan "tugma" |
| noto'g'ri ARIA atribut va rol | dialog yopilganda fokus yo'qolishi |
| kontrast (brauzerda) | Tab tartibi mantiqsiz (lekin texnik to'g'ri) |
alt yo'q rasm |
alt="rasm" — bor, lekin ma'nosiz |
musbat tabindex |
xato e'lon qilinmaydi yoki kech e'lon qilinadi |
O'ng ustundagilar uchun xulq testi kerak: "Bobur faqat klaviatura bilan buyurtma bera oladimi?".
4.2 Klaviatura bilan buyurtma
Bir xil ko'rinadigan ikki "tugma": biri <div>, biri <button>. axe ikkalasida jim. Klaviatura testi esa farqni ko'radi:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test, vi } from "vitest";
function DivButton({ onOrder }) {
return <div onClick={onOrder}>Buyurtma berish</div>;
}
function RealButton({ onOrder }) {
return <button onClick={onOrder}>Buyurtma berish</button>;
}
for (const [name, Order] of [
["div", DivButton],
["button", RealButton],
]) {
test(`${name}: Tab + Enter bilan buyurtma`, async () => {
const user = userEvent.setup();
const onOrder = vi.fn();
render(<Order onOrder={onOrder} />);
await user.tab();
console.log(name, "fokusda:", document.activeElement.tagName);
await user.keyboard("{Enter}");
expect(onOrder).toHaveBeenCalledTimes(1);
});
}Konsolda:
div fokusda: BODY
× div: Tab + Enter bilan buyurtma
→ expected "vi.fn()" to be called 1 times, but got 0 times
button fokusda: BUTTON
✓ button: Tab + Enter bilan buyurtma
Testlar: 1 yiqildi, 1 o'tdi (jami 2)div ga Tab bilan yetib bo'lmaydi — fokus BODY da qoldi. user.tab() brauzerdagi Tab tugmasini taqlid qiladi: keyingi fokuslanadigan elementga o'tadi. user.keyboard("{Enter}") — fokusdagi elementga Enter. <button> da Enter va Space'ni brauzer o'zi click ga aylantiradi, div da — yo'q.
div ga tabIndex={0} va role="button" qo'shsangiz, Tab yetib boradi, lekin Enter baribir ishlamaydi — onKeyDown ni ham o'zingiz yozishingiz kerak. To'g'ri yechim bitta: <button>.
4.3 Fokus qaytishi
Fokus boshqaruvi darsidagi qoida: menyu yoki dialog yopilganda fokus uni ochgan tugmaga qaytadi. Aks holda fokus body ga tushadi va Bobur sahifa boshidan yana Tab bosadi. Menyu Esc bilan yopiladi — bir versiyada fokus qaytadi, ikkinchisida yo'q:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { describe, expect, test } from "vitest";
function SortMenu({ returnFocus }) {
const [open, setOpen] = useState(false);
const trigger = useRef(null);
function close() {
setOpen(false);
if (returnFocus) trigger.current.focus();
}
return (
<div>
<button
ref={trigger}
aria-expanded={open}
onClick={() => setOpen(!open)}
>
Saralash
</button>
{open && (
<ul onKeyDown={(e) => e.key === "Escape" && close()}>
<li>
<button autoFocus>Narx bo'yicha</button>
</li>
</ul>
)}
</div>
);
}
describe.each([[false], [true]])("returnFocus=%s", (returnFocus) => {
test("Esc — menyu yopiladi, fokus tugmada", async () => {
const user = userEvent.setup();
render(<SortMenu returnFocus={returnFocus} />);
const trigger = screen.getByRole("button", { name: "Saralash" });
await user.click(trigger);
expect(trigger).toHaveAttribute("aria-expanded", "true");
const first = screen.getByRole("button", { name: /Narx/ });
expect(first).toHaveFocus();
await user.keyboard("{Escape}");
expect(trigger).toHaveAttribute("aria-expanded", "false");
expect(trigger).toHaveFocus();
});
});Konsolda:
× returnFocus=false > Esc — menyu yopiladi, fokus tugmada
→ expected <button> to have focus, but focus is on <body>
✓ returnFocus=true > Esc — menyu yopiladi, fokus tugmada
Testlar: 1 yiqildi, 1 o'tdi (jami 2)Birinchi versiyada menyu yopildi, aria-expanded to'g'ri — axe uchun hammasi joyida. Lekin fokus yo'q bo'lib ketdi: fokusdagi tugma DOM'dan o'chdi. Xabar aniq aytadi: fokus <body> da. Buni faqat xulq testi ko'radi.
Tekshirib ko'ring: Nega testda
user.clickdan keyintoHaveFocus()ni menyu bandi uchun tekshirdik? Bu talabmi?
Javob
Ha — APG menyu naqshi (ARIA patternlar): menyu ochilganda fokus birinchi bandga o'tadi, shunda strelka va Enter darhol ishlaydi. Test talabni yozib qo'ydi: kimdir autoFocus ni olib tashlasa, test aytadi.
4.4 Tab tartibi
Musbat tabIndex ni axe topdi. Lekin uning zarari qanday ko'rinadi? Tab'ni ketma-ket bosib, fokus yo'lini yozamiz:
import { render } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { test } from "vitest";
function Header() {
return (
<header>
<a href="#menyu">Menyu</a>
<a href="#bron">Bron</a>
<button tabIndex={1}>Savat (2)</button>
<input aria-label="Qidirish" />
</header>
);
}
test("Tab yo'li", async () => {
const user = userEvent.setup();
render(<Header />);
const path = [];
for (let i = 0; i < 4; i++) {
await user.tab();
const el = document.activeElement;
path.push(el.textContent || el.getAttribute("aria-label"));
}
console.log(path.join(" → "));
});Konsolda:
Savat (2) → Menyu → Bron → Qidirish
✓ Tab yo'li
Testlar: 1 o'tdi (jami 1)Ko'zga «Savat» uchinchi turibdi, Tab esa birinchi unga boradi. Malika va Bobur uchun sahifa "sakraydi". Testda yo'lni massiv bilan tekshirsangiz (expect(path).toEqual(["Menyu", "Bron", "Savat (2)", "Qidirish"])) — tartib talabga aylanadi.
5. Qo'lda nima qoladi
Avtomatik qatlamlar qanchalik ko'p bo'lmasin, ba'zi savollarga faqat odam javob beradi:
- Ekran o'quvchi bilan haqiqiy vazifa (NVDA, VoiceOver, TalkBack): "Malika buyurtma bera oladimi?". Testlar alohida xususiyatlarni tekshiradi, odam esa butun yo'lni.
- Ma'no:
altrasmni to'g'ri tasvirlaydimi, xato matni tushunarlimi, tugma nomi harakatni aytadimi ("Bosing" emas, "Buyurtma berish"). - 200% kattalashtirish va 320 px en — matn kesilmaydimi (H-79).
incompletero'yxati — axe "qarang" degan joylar.- Harakat va vaqt: animatsiya
prefers-reduced-motionda to'xtaydimi, xabar o'qib bo'linguncha yo'qolmaydimi.
Amaliy tartib: har PR'da avtomatik qatlamlar (CI), har yangi vidjet yoki sahifada klaviatura va ekran o'quvchi bilan 5 daqiqa, relizdan oldin to'liq tekshiruv ro'yxati.
6. Vazifalar qadami: axe va klaviatura oqimi (holat 14)
6.1 O'rnatish va narx
git switch -c test/a11y
npm i -D -E @axe-core/playwright@4.13.0Versiya tanlovi halol: @axe-core/playwright 4.13.0 — 2026-08-11 reliz. 4.14.0 ham bor, lekin u 2026-10-15 da chiqqan — loyiha qoidasi bo'yicha 7 kundan yangi reliz olinmaydi.
Narx: +2 paket, +3.1 MB node_modules. Bitta nozik joy: @axe-core/playwright 4.13.0 axe-core ~4.13.0 ni talab qiladi, Storybook II dagi addon-a11y esa axe-core 4.14.0 ni olib kelgan. npm ularni birlashtira olmaydi — axe-core ikki nusxada turadi. Bu TEXNIK-QARZ 72-qatoriga yozildi: 4.14.0 ga ko'tarilganda bitta nusxa qoladi. Ilova bundle'i o'zgarmaydi: JS 206.7 KB gzip. Qamrov ham o'zgarmadi — e2e testlar coverage'ga kirmaydi (TEXNIK-QARZ 74).
6.2 e2e/a11y.spec.ts: axe qismi
Fayl boshidagi izoh — darsning qisqa mazmuni (qatorlari telefon uchun bo'lingan):
// a11y.spec.ts — 22/#14: accessibility testlari. Ikki qism:
// 1) axe-core (@axe-core/playwright) — avtomatik qoidalar
// (WCAG 2.2 AA + best-practice): kontrast, nomlar, rollar,
// ro'yxat tuzilishi … Avtomatika xatolarning taxminan uchdan
// birini topadi — qolgani:
// 2) klaviatura oqimi — sichqonchasiz foydalanuvchi yo'li,
// fokus qayerdaTeglar va yordamchi funksiya:
// Zavod tekshiruvi (olchov/a11y.mjs) bilan bir xil teglar
const TEGLAR = [
"wcag2a",
"wcag2aa",
"wcag21a",
"wcag21aa",
"wcag22aa",
"best-practice",
];
async function axeBuzilishlari(page: Page) {
const natija = await new AxeBuilder({ page })
.withTags(TEGLAR)
.analyze();
// Xato bo'lsa — qaysi qoida va qaysi element (hisobotda o'qiladi)
return natija.violations.map(
(b) =>
`${b.id}: ${b.nodes.map((n) => n.target.join(" ")).join(", ")}`,
);
}Kurs tomonidagi tekshiruv qaysi teglar bilan ishlagan bo'lsa, o'quvchi testi ham aynan shular bilan. Endi kurs tekshiruvi "0 buzilish" desa, sizning testingiz ham shuni aytadi — ular bitta o'lchov.
Har test bir holatni ochadi va oxirida bitta qator: expect(await axeBuzilishlari(page)).toEqual([]). Holatlar — 8 ta: ro'yxat yorug' va qorong'i temada (page.emulateMedia({ colorScheme })), buyruqlar oynasi ochiq, o'chirish dialogi ochiq, /sozlamalar, /vazifa/1, /ru/sozlamalar va kirmagan foydalanuvchi uchun /kirish. Hammasida — 0. Ochiq oynalar alohida sinalgani muhim: axe faqat hozir DOM'da bor narsani ko'radi, yopiq dialog uning uchun yo'q.
Testning o'zi ishlashini kanon tekshirdi: «Yangi vazifa» maydonining <label> i test ichida vaqtincha o'chirildi.
- Expected - 1
+ Received + 3
+ "label: #yangi-matn",Mana nega buzilish satrga aylantirildi: farq bir qatorda — qoida (label) va element (#yangi-matn). Obyektni to'g'ridan solishtirganda shu ma'lumot 50 qator ichida yo'qolardi.
6.3 Klaviatura oqimi
To'rt test — Bobur yo'li:
- Birinchi Tab — «Ro'yxatga o'tish» havolasi (Fokus boshqaruvi dagi skip link); Enter — fokus havoladan ketadi, keyingi Tab ko'rinadigan elementga tushadi.
- Ctrl+K → «Statistika» → Enter —
/statistikasahifasi, fokush2sarlavhada. - O'chirish dialogi — tugmada Enter ochadi, fokus «Bekor qilish» da; Esc — dialog yopiladi, fokus tugmaga qaytadi.
- Bo'sh kirish formasi — Enter: maydonda
aria-invalid="true", fokus maydonda, xato matni bo'sh emas.
Dialog testi — «Fokus qaytishi» bo'limidagi menyuning haqiqiy brauzerdagi varianti:
const tugma = page.getByRole("button", {
name: "O'chirish: Non olish #bozor",
});
await tugma.focus();
await page.keyboard.press("Enter");
await expect(
page.getByRole("button", { name: "Bekor qilish" }),
).toBeFocused();
await page.keyboard.press("Escape");
await expect(page.getByRole("dialog")).toBeHidden();
await expect(tugma).toBeFocused();Bu yerda native <dialog> va showModal() ishlaydi — jsdom'da yo'q narsa (RTL I). toBeFocused() — Playwright'ning web-first tekshiruvi: fokus kelguncha kutadi.
6.4 Birinchi o'tishdagi ikki xato
Testlar birinchi yozilishda ikki joyda yiqildi. Ikkalasi ham foydali saboq:
getByRole("combobox")— 2 ta element. Sahifada buyruqlar oynasining qidiruv maydonidan tashqari tartib<select>i ham bor (selectning roli hamcombobox). Yechim — qidiruvni toraytirish:page.getByRole("dialog", { name: "Buyruqlar" }).getByRole("combobox").- Ctrl+K dan keyin darhol yozish — harflar yo'qoldi. Buyruqlar oynasi kech yuklanadi (lazy va code splitting, 20-qismdagi cmdk qarori). Playwright harflarni yozdi, lekin maydon hali yo'q edi. Yechim — avval
toBeFocused()ni kutish, keyin yozish. Izohi kodda qoldi.
Ikkinchisi faqat test muammosi emas. Tez yozadigan haqiqiy foydalanuvchi ham sekin internetda birinchi harflarni yo'qotadi. Test buni ko'rsatdi; ilova o'zgarmadi, cheklov izohda.
6.5 Natija va commit
Testlar: Vitest 348, brauzer 6, Storybook 12, e2e 21 (10 + a11y 11), vizual 6. npm test issiq 9.1 s. Yangi paketdan keyingi birinchi ishga tushirish sovuq — 44.1 s (Vite keshi qayta yig'iladi). Kurs tomonidagi axe tekshiruvi ham 0 buzilish. JS 206.7 KB — o'zgarmadi.
git add -A
git commitCommit (kanon): test: axe (@axe-core/playwright) — 8 holat 0 buzilish; klaviatura oqimi testlari.
7. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| axe 0, lekin klaviaturada ishlamaydi | axe xulqni ko'rmaydi | Tab/Enter/Esc testi |
| dialog ichini axe tekshirmadi | dialog yopiq edi | avval oching, keyin analyze() |
jsdom'da color-contrast doim incomplete |
canvas yo'q | kontrast — brauzerda |
| yiqilganda 50 qatorlik obyekt | violations to'g'ridan solishtirilgan |
id: selektor satrlari |
| qoida o'chirildi, sababi noma'lum | "tez yashil bo'lsin" | exclude yoki izoh + TEXNIK-QARZ |
getByRole("combobox") — 2 ta |
<select> ham combobox |
ota element bilan toraytirish |
| yozilgan harflar yo'qoldi | maydon hali yuklanmagan | avval toBeFocused() |
| placeholder'li maydon o'tdi | axe placeholder'ni nom deb sanaydi | getByRole(…, { name }) + label |
8. Mashqlar
Mashqlar kurs/mashqlar/22/14-a11y-testlar/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
aria-describedbydagi matnni tekshiradigan matcher:toHaveAccessiblearia-invalid="true"ni tekshiradigan matcher:toBe()- axe'ning "ishonchim komil emas" guruhi:
natija. - Keyingi fokuslanadigan elementga o'tish:
await user.() - Playwright'da axe:
new ({ page }).analyze()
Yechim
- Description — ekran o'quvchi nomdan keyin o'qiydigan tavsif.
- Invalid —
aria-invalidyoki brauzer validatsiyasi xatosi. - incomplete — odam qarashi kerak; test yiqilmaydi.
- tab —
user-eventdagi Tab tugmasi;{ shift: true }— orqaga. - AxeBuilder —
@axe-core/playwrightdan;withTags,include,exclude.
2-mashq (o'rta): bron formasining a11y testi
BookingForm: «Ism» maydoni (label bilan, required), «Mehmonlar soni» (type="number", tavsifi: «1 dan 12 gacha»), «Bron qilish» tugmasi. Ism bo'sh yuborilsa: ism maydoni aria-invalid, xato matni role="alert" da («Ismni yozing»), fokus ism maydonida. Test yozing: (1) mehmonlar maydonining tavsifi; (2) bo'sh yuborishdan keyingi uch holat; (3) ism yozilib yuborilsa — alert bo'sh va maydon aria-invalid emas.
Ishora: noValidate — brauzerning o'z xabarini o'chiradi; toBeInvalid aria-invalid ga qaraydi.
Yechim
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { expect, test } from "vitest";
function BookingForm() {
const [error, setError] = useState("");
const name = useRef(null);
function submit(e) {
e.preventDefault();
if (!name.current.value.trim()) {
setError("Ismni yozing");
name.current.focus();
} else setError("");
}
return (
<form onSubmit={submit} noValidate>
<label htmlFor="ism">Ism</label>
<input
id="ism"
ref={name}
required
aria-invalid={error ? "true" : undefined}
aria-describedby="ism-xato"
/>
<p id="ism-xato" role="alert">
{error}
</p>
<label htmlFor="soni">Mehmonlar soni</label>
<input
id="soni"
type="number"
defaultValue={2}
aria-describedby="mehmon-yordam"
/>
<p id="mehmon-yordam">1 dan 12 gacha</p>
<button>Bron qilish</button>
</form>
);
}
test("tavsif", () => {
render(<BookingForm />);
const guests = screen.getByRole("spinbutton", {
name: "Mehmonlar soni",
});
expect(guests).toHaveAccessibleDescription("1 dan 12 gacha");
});
test("bo'sh ism — xato, fokus, e'lon", async () => {
const user = userEvent.setup();
render(<BookingForm />);
const submit = screen.getByRole("button", { name: "Bron qilish" });
await user.click(submit);
const name = screen.getByRole("textbox", { name: "Ism" });
expect(name).toBeInvalid();
expect(name).toHaveFocus();
console.log(screen.getByRole("alert").textContent);
});
test("ism bor — xato yo'q", async () => {
const user = userEvent.setup();
render(<BookingForm />);
const name = screen.getByRole("textbox", { name: "Ism" });
await user.type(name, "Dilshod");
const submit = screen.getByRole("button", { name: "Bron qilish" });
await user.click(submit);
expect(screen.getByRole("alert")).toBeEmptyDOMElement();
expect(name).not.toBeInvalid();
});Konsolda:
✓ tavsif
Ismni yozing
✓ bo'sh ism — xato, fokus, e'lon
✓ ism bor — xato yo'q
Testlar: 3 o'tdi (jami 3)role="alert" doim DOM'da turadi, faqat matni o'zgaradi — ekran o'quvchilar bo'sh hududga keyin qo'shilgan matnni ishonchliroq e'lon qiladi. required bo'lsa ham toBeInvalid yuborishdan oldin ishlatilmadi: noValidate formada brauzer validatsiyasi faqat checkValidity() da ishlaydi.
3-mashq (qiyin): tablar klaviatura bilan
MenuTabs — uchta tab («Issiq taomlar», «Salatlar», «Ichimliklar») va panel. APG tab naqshi (ARIA patternlar): faqat tanlangan tab Tab tartibida (tabIndex 0, qolganlari −1, roving tabindex); ArrowRight / ArrowLeft — keyingi/oldingi tab (oxiridan boshiga aylanadi), tanlanadi va fokus oladi. Test: (1) birinchi Tab — «Issiq taomlar» ga; (2) ArrowRight ×2 — «Ichimliklar» tanlangan va fokusda, panel matni mos; (3) yana ArrowRight — «Issiq taomlar» ga qaytadi; (4) Tab — tablardan chiqib, panelga o'tadi.
Ishora: user.keyboard("{ArrowRight}"); tanlanganini getByRole("tab", { selected: true }) bilan toping.
Yechim
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { expect, test } from "vitest";
const tabs = [
["Issiq taomlar", "Osh, Manti"],
["Salatlar", "Achchiq-chuchuk"],
["Ichimliklar", "Ko'k choy"],
];
function MenuTabs() {
const [active, setActive] = useState(0);
const refs = useRef([]);
function onKeyDown(e) {
const step = { ArrowRight: 1, ArrowLeft: -1 }[e.key];
if (!step) return;
const next = (active + step + tabs.length) % tabs.length;
setActive(next);
refs.current[next].focus();
}
return (
<div>
<div role="tablist" aria-label="Menyu" onKeyDown={onKeyDown}>
{tabs.map(([name], i) => (
<button
key={name}
role="tab"
id={`tab-${i}`}
ref={(el) => (refs.current[i] = el)}
aria-selected={i === active}
tabIndex={i === active ? 0 : -1}
onClick={() => setActive(i)}
>
{name}
</button>
))}
</div>
<div
role="tabpanel"
tabIndex={0}
aria-labelledby={`tab-${active}`}
>
{tabs[active][1]}
</div>
</div>
);
}
test("tablar klaviatura bilan", async () => {
const user = userEvent.setup();
render(<MenuTabs />);
await user.tab();
const hot = screen.getByRole("tab", { name: "Issiq taomlar" });
expect(hot).toHaveFocus();
await user.keyboard("{ArrowRight}{ArrowRight}");
const selected = screen.getByRole("tab", { selected: true });
console.log(selected.textContent);
expect(selected).toHaveFocus();
expect(screen.getByRole("tabpanel")).toHaveTextContent("Ko'k choy");
await user.keyboard("{ArrowRight}");
expect(screen.getByRole("tab", { selected: true })).toBe(hot);
await user.tab();
expect(screen.getByRole("tabpanel")).toHaveFocus();
});Konsolda:
Ichimliklar
✓ tablar klaviatura bilan
Testlar: 1 o'tdi (jami 1)Oxirgi qadam eng muhimi: Tab tablar orasida yurmaydi, guruhdan chiqadi — roving tabindex shuning uchun. Hamma tabda tabIndex={0} bo'lsa, (4) yiqiladi: fokus «Salatlar» ga tushadi. axe bu farqni ko'rmaydi — ikkala variant ham ARIA bo'yicha to'g'ri.
4-mashq: Vazifalar qadami
test/a11y branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: @axe-core/playwright@4.13.0, e2e/a11y.spec.ts (8 ta axe holati, klaviatura oqimi, kirish formasi). Keyin testning o'zini sinang.
Yechim
Tekshiruv ro'yxati:
npm run test:e2e— 21 test o'tadi (a11y — 11).npm ls axe-core— ikki versiya (4.13.0 va 4.14.0); TEXNIK-QARZ'ga qator.- Sinov: test ichida
page.locator('label[for="yangi-matn"]').evaluate((e) => e.remove())— axe testilabel: #yangi-matnbilan yiqiladi. Olib tashlang. - Sinov: o'chirish dialogidagi fokus qaytishini vaqtincha buzing (yopilganda tugmaga
focus()chaqirilmasin) — klaviatura testi yiqiladi, axe testi esa o'tadi. Shu farq — darsning asosiy fikri. Qaytaring. npm run build— JS 206.7 KB o'zgarmagan.
9. Real ishda
- Qonun va shartnomalar: Yevropa Ittifoqining European Accessibility Act talablari 2025-06-28 dan amalda; ko'p xalqaro buyurtmachilar WCAG 2.1/2.2 AA ni shartnomaga yozadi. Avtomatik a11y testlari — "biz tekshiramiz" degan da'voning isboti.
- CI'da: axe testlari e2e bilan birga ishlaydi (Playwright II); yangi buzilish PR'ni to'xtatadi. Katta eski loyihada avval hozirgi buzilishlarni ro'yxatga olib, faqat yangilarini taqiqlash mumkin — keyin ro'yxatni kamaytirish.
- Dizayn tizimlarida — Storybook a11y addon har komponent holatida (Storybook II); Radix, React Aria kabi kutubxonalar o'z klaviatura testlari bilan keladi.
- Intervyu savollari: "axe nimani topa olmaydi?", "
incompletenima?", "fokus qaytishini qanday test qilasiz?", "roving tabindex nima?".
Xulosa
- A11y testi — uch avtomatik qatlam (rol so'rovlari, axe, klaviatura oqimi) va odam.
- Rol so'rovlari va
toHaveAccessibleName/Description,toBeInvalid,toHaveFocus— har komponent testida bepul a11y tekshiruvi. - axe-core:
violations— yiqitadi,incomplete— odam qarasin; jsdom'da kontrast yo'q, u brauzer ishi. jest-axefaol,vitest-axebarqaror versiyasi 2022 yilniki; sahifa uchun@axe-core/playwright.- axe
<div onClick>, fokus yo'qolishi va mantiqsiz Tab tartibini ko'rmaydi —user.tab(),user.keyboard()va Playwrightkeyboardko'radi. vazifalar: 8 holatda axe 0, klaviatura oqimi 4 test, e2e 21;axe-coreikki nusxa — halol TEXNIK-QARZ.
Keyingi dars: Test strategiyasi, test ma'lumotlari va flaky testlar — nimani soxtalash kerak, test ma'lumotlarini fabrika bilan yaratish va vazifalar dagi haqiqiy beqaror testlarni topib tuzatish.
Manbalar
- axe-core: Rule descriptions, API (
axe.run, natija guruhlari) — github.com/dequelabs/axe-core @axe-core/playwright—AxeBuilder(withTags,include,exclude,disableRules) — github.com/dequelabs/axe-core-npm- Playwright: Accessibility testing (playwright.dev/docs/accessibility-testing)
- Testing Library:
ByRole, jest-domtoHaveAccessibleDescription,toBeInvalid; user-eventtab,keyboard - W3C WAI-ARIA Authoring Practices: Menu Button, Tabs (w3.org/WAI/ARIA/apg)
- npm reestri (2026-10-20):
jest-axe11.0.0,vitest-axe0.1.0 / 1.0.0-pre.5,axe-core4.14.0 - Haqiqiy chiqishlar: Vitest 5.0.4, jsdom 30.1.2, axe-core 4.13.0, Playwright 1.64.0, Chrome 155, Node 24.21.0 (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!