Mundarija (34)
- Bu darsda
- 1. Nega bu kerak?
- 2. render va screen
- 2.1 Birinchi komponent testi
- 2.2 Har testdan keyin tozalash
- 3. So'rovlar oilasi: getBy, queryBy, findBy
- 3.1 Uch oila, uch savol
- 3.2 Xato xabarini o'qish
- 4. Qaysi so'rov: ustuvorlik tartibi
- 4.1 Tartib
- 4.2 getByRole sozlamalari
- 4.3 Nomsiz tugma
- 5. within: qidiruvni bir bo'lakka toraytirish
- 6. jest-dom matcherlari
- 6.1 Nega oddiy expect yetmaydi
- 6.2 Eng kerakli matcherlar
- 7. Ichiga qarash: screen.debug va logRoles
- 8. act: yangilanish tugaguncha kutish
- 9. Portal: screen uni topadi, container — yo'q
- 10. jsdom — brauzer emas
- 11. Vazifalar qadami: jest-dom va rol so'rovlari (holat 04)
- 11.1 O'rnatish va tur muammosi
- 11.2 tekshiruv/jest-dom.ts
- 11.3 sorovlar.test.tsx (5 test)
- 11.4 Natija va commit
- 12. Ko'p uchraydigan xatolar
- 13. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): taom qatorlari
- 3-mashq (qiyin): sekin profil
- 4-mashq: Vazifalar qadami
- 14. Real ishda
- Xulosa
- Manbalar
React Testing Library: render, screen, getByRole va jest-dom matcherlari
Qisqacha: React Testing Library (RTL) komponentni test ichida chizadi (
render) va elementlarni foydalanuvchi ko'rganidek topadi — birinchi navbatda roli va nomi bilan:screen.getByRole("button", { name: "Savatga qo'shish" }). So'rovlar uch oila:getBy— topilmasa xato,queryBy—null(yo'qligini tekshirish uchun),findBy— paydo bo'lishini kutadi (async). Tartib: rol → label → placeholder → matn → … →data-testid(oxirgi chora). jest-dom matcherlari (toBeVisible,toBeDisabled,toHaveAccessibleName) tekshiruvni o'qiladigan va xato xabarini aniq qiladi. Nomsiz tugmani rol bo'yicha test ham, ekran o'quvchi ham topa olmaydi.
Bu darsda
rendervascreenbilan komponentni chizib, elementni topasiz.getBy,queryBy,findByvaAllvariantlarini to'g'ri tanlaysiz.- So'rovlar ustuvorligini qo'llaysiz:
getByRoleva uningname,level,selected,hiddensozlamalari. - jest-dom matcherlari,
within,screen.debugvalogRolesbilan ishlaysiz. act, portal vacleanupqachon muhimligini tushuntirasiz.
Oldin bilishingiz kerak: Vitest II, React'da accessibility asoslari, ARIA patternlar, Portallar.
1. Nega bu kerak?
Oldingi darsda sof funksiyalarni soxta bog'liqliklar bilan sinadik. Komponent testi boshqacha savol beradi: foydalanuvchi ekranda nimani ko'radi va nima qila oladi?
17-qismdagi eski testlarda shunday qatorlar bor: container.querySelector(".narx")?.textContent. Dizayner .narx sinfini .price ga o'zgartirsa — test yiqiladi, ilova esa ishlayveradi. Bu — implementatsiyani tekshirish. Nega test yozamiz darsida ko'rganimizdek, bunday test foydasizdan ham yomon: u yolg'on signal beradi.
Malika ekran o'quvchida sahifani qanday "ko'radi"? Sinf nomlarini emas — rollar va nomlarni: "tugma, Savatga qo'shish", "sarlavha, 2-daraja, Buyurtma #12". RTL ham aynan shunday qidiradi. Testning sharti — "foydalanuvchi buni topa oladimi?" Shuning uchun yaxshi test bir vaqtda a11y'ni ham tekshiradi: rolsiz yoki nomsiz elementni test topa olmasa, Malika ham topa olmaydi.
flowchart LR
R["render(<DishCard />)"] --> D["DOM (jsdom)"]
D --> A["a11y daraxti: rol + nom"]
A --> Q["getByRole('button', name)"]
D --> T["getByText, getByLabelText"]
Q --> E["expect(…).toBeEnabled()"]
T --> ERasmda: render DOM yasaydi, brauzer (yoki jsdom) undan a11y daraxtini quradi, getByRole shu daraxtdan qidiradi. Natija — oddiy DOM element, unga jest-dom matcheri qo'llanadi.
2. render va screen
2.1 Birinchi komponent testi
render(<Komponent />) — komponentni yangi <div> ga chizadi va bu div ni document.body ga qo'shadi. screen — butun document.body bo'yicha so'rovlar to'plami:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function DishCard({ name, price }) {
return (
<article>
<h3>{name}</h3>
<p>{price} so'm</p>
<button>Savatga qo'shish</button>
</article>
);
}
test("taom kartasi", () => {
const { container } = render(<DishCard name="Osh" price={35000} />);
console.log(container.innerHTML);
const title = screen.getByRole("heading", { name: "Osh" });
expect(title).toBeInTheDocument();
expect(screen.getByText("35000 so'm")).toBeVisible();
expect(
screen.getByRole("button", { name: "Savatga qo'shish" }),
).toBeEnabled();
});Konsolda:
<article><h3>Osh</h3><p>35000 so'm</p><button>Savatga qo'shish</button></article>
✓ taom kartasi
Testlar: 1 o'tdi (jami 1)render qaytaradigan obyektda: container (o'sha div), rerender(<Yangi />) (yangi props bilan qayta chizish), unmount() (o'chirish — effekt tozalanishini sinash uchun). container.querySelector ham bor, lekin u — favqulodda chiqish yo'li. Asosiy yo'l — screen.
<h3> ning roli — heading, <button> niki — button: bular yashirin rollar (implicit role), HTML tegining o'zidan keladi (React'da accessibility). Nomi — ichidagi matn.
2.2 Har testdan keyin tozalash
Har render body ga yangi div qo'shadi. Tozalanmasa, ikkinchi testda birinchi testning komponenti ham turadi. RTL tozalashni (cleanup) o'zi ulaydi — lekin faqat global afterEach bo'lsa, ya'ni Vitest'da globals: true sozlamasi bilan. Kanonda importlar aniq yoziladi (globals yo'q), shuning uchun sozlash faylida:
// vitest-sozlash.ts — har test faylidan oldin. Testing Library har
// testdan keyin chizilganni o'zi tozalaydi, lekin faqat global
// afterEach bo'lsa (globals: true). Bizda importlar aniq —
// tozalashni o'zimiz ulaymiz
import { afterEach, vi } from "vitest";
import { cleanup } from "@testing-library/react";afterEach(cleanup);Bu qator bo'lmasa nima bo'ladi — biz haqiqiy Vitest'da sinab ko'rdik. Pastdagi «act» bo'limidagi ikki test ketma-ket ishladi, birinchisining komponenti body da qoldi:
TestingLibraryElementError: Found multiple elements with the text: /Savatda/Ikki komponent bor edi, ikkalasi ham hodisani eshitgan — birinchisi "Savatda: 2" ko'rsatdi. Natija oynasidagi o'quv modeli esa har testdan keyin cleanup() ni o'zi chaqiradi — u yerda bu xatoni ko'rmaysiz.
Tekshirib ko'ring:
screen.getByText("Osh")vacontainer.querySelector("h3")— qaysi biriDishCarddagi sarlavhani<h3>dan<h2>ga almashtirganda ham ishlaydi?
Javob
getByText("Osh") — u teg nomiga qaramaydi. querySelector("h3") esa null qaytaradi. Yanada yaxshisi — getByRole("heading", { name: "Osh" }): daraja o'zgarsa ham topadi, { level: 2 } bilan esa darajani ataylab tekshirish mumkin.
3. So'rovlar oilasi: getBy, queryBy, findBy
3.1 Uch oila, uch savol
Har so'rov turi (ByRole, ByText …) uch oldqo'shimcha bilan keladi:
| Oila | 0 ta | 1 ta | ko'p |
|---|---|---|---|
getBy… |
xato | element | xato |
queryBy… |
null |
element | xato |
findBy… (Promise) |
xato (1 s kutib) | element | xato |
All variantlari (getAllBy…, queryAllBy…, findAllBy…) massiv qaytaradi: queryAllBy — bo'sh massiv, getAllBy — 0 tada xato.
Jadval jadvalligicha qolmasin — sekin yuklanadigan menyu bilan uchalasini ko'ramiz. Server o'rnida 200 ms dan keyin javob beradigan Promise:
import { render, screen, within } from "@testing-library/react";
import { useEffect, useState } from "react";
import { expect, test } from "vitest";
function Menu({ loadDishes }) {
const [dishes, setDishes] = useState(null);
useEffect(() => {
loadDishes().then(setDishes);
}, [loadDishes]);
if (dishes === null) return <p>Yuklanmoqda…</p>;
return (
<ul aria-label="Taomlar">
{dishes.map((d) => (
<li key={d}>{d}</li>
))}
</ul>
);
}
const loadDishes = () =>
new Promise((resolve) => {
setTimeout(() => resolve(["Osh", "Lag'mon", "Manti"]), 200);
});
test("getBy — topilmasa xato tashlaydi", () => {
render(<Menu loadDishes={loadDishes} />);
expect(() => screen.getByRole("list")).toThrow(
'Unable to find an accessible element with the role "list"',
);
});
test("queryBy — topilmasa null", () => {
render(<Menu loadDishes={loadDishes} />);
expect(screen.queryByRole("list")).toBeNull();
expect(screen.getByText("Yuklanmoqda…")).toBeInTheDocument();
});
test("findBy — paydo bo'lishini kutadi", async () => {
render(<Menu loadDishes={loadDishes} />);
const list = await screen.findByRole("list", { name: "Taomlar" });
const items = within(list).getAllByRole("listitem");
console.log(items.map((li) => li.textContent));
expect(screen.queryByText("Yuklanmoqda…")).toBeNull();
});
test("getBy — bir nechta topilsa ham xato", async () => {
render(<Menu loadDishes={loadDishes} />);
await screen.findByRole("list");
expect(() => screen.getByRole("listitem")).toThrow(
'Found multiple elements with the role "listitem"',
);
});Konsolda:
✓ getBy — topilmasa xato tashlaydi
✓ queryBy — topilmasa null
(3) ['Osh', 'Lag'mon', 'Manti']
✓ findBy — paydo bo'lishini kutadi
✓ getBy — bir nechta topilsa ham xato
Testlar: 4 o'tdi (jami 4)Qaysi biri qachon:
getBy— element hozir bo'lishi kerak. Yo'q bo'lsa, xatoning o'zi tekshiruv: alohidaexpectshart emas.queryBy— faqat yo'qligini tekshirish uchun:expect(screen.queryByText("Yuklanmoqda…")).toBeNull().getBybu yerda xato tashlab, testni to'xtatardi.findBy— element keyin paydo bo'ladi (server javobi, effekt). UwaitForustiga qurilgan: har 50 ms qayta qidiradi, sukut bo'yicha 1 s kutadi.awaitni unutmang.
Kutishning chuqur tomonlari (waitFor, foydalanuvchi harakatidan keyingi o'zgarish) — RTL II da.
3.2 Xato xabarini o'qish
Haqiqiy Vitest'da getBy topa olmasa, xabar juda foydali — u sahifadagi hamma rollarni sanab beradi (qisqartirilgan):
TestingLibraryElementError: Unable to find an accessible element with the role "button" and name "Saqlash"
Here are the accessible roles:
heading:
Name "Menyu":
<h2
id="t"
/>
--------------------------------------------------
button:
Name "Qo'shish":
<button />
…Tarjima: "button roli va «Saqlash» nomli kirish mumkin bo'lgan element topilmadi. Mana sahifadagi rollar:". Ro'yxatdan darhol ko'rinadi: tugma bor, lekin nomi boshqa («Qo'shish»). Xabar oxirida butun DOM ham chiqadi (Ignored nodes: comments, script, style dan keyin).
Tekshirib ko'ring: «Saqlandi» xabari ekrandan 3 soniyadan keyin yo'qolishi kerak. Yo'qolganini qaysi so'rov bilan tekshirasiz?
Javob
queryByText("Saqlandi") va toBeNull() (soxta soat bilan 3 s o'tkazib — Vitest II). getByText yo'q elementda xato tashlaydi, findByText esa paydo bo'lishini kutadi — ikkalasi ham bu savolga mos emas.
4. Qaysi so'rov: ustuvorlik tartibi
4.1 Tartib
Testing Library hujjati so'rovlarni shu tartibda tavsiya qiladi — yuqoridagisi foydalanuvchiga yaqinroq:
| # | So'rov | Qachon |
|---|---|---|
| 1 | getByRole |
deyarli hamma narsa: tugma, havola, maydon, sarlavha |
| 2 | getByLabelText |
forma maydoni (label bilan) |
| 3 | getByPlaceholderText |
label yo'q bo'lsa (bu ham a11y kamchiligi) |
| 4 | getByText |
rolsiz matn: <p>, <span> |
| 5 | getByDisplayValue |
maydondagi joriy qiymat |
| 6 | getByAltText |
rasm (alt) |
| 7 | getByTitle |
title atributi |
| 8 | getByTestId |
boshqa yo'l qolmaganda (data-testid) |
getByTestId — oxirgi chora: foydalanuvchi data-testid ni ko'rmaydi. Uni faqat foydalanuvchiga ko'rinadigan biror belgisi yo'q element uchun ishlating (masalan, grafikning o'rami).
Bitta bron formasida hammasi:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function BookingForm() {
return (
<form aria-label="Stol bron qilish">
<label htmlFor="name">Ism</label>
<input id="name" placeholder="Masalan, Dilshod" required />
<label>
Mehmonlar soni
<input type="number" defaultValue={2} />
</label>
<label>
<input type="checkbox" defaultChecked /> Deraza yonida
</label>
<img src="data:," alt="Zal sxemasi" />
<button type="submit" disabled>
Bron qilish
</button>
<span data-testid="eslatma" title="Bron 10:00–22:00">
?
</span>
</form>
);
}
test("bitta maydon — uch xil so'rov", () => {
render(<BookingForm />);
const byRole = screen.getByRole("textbox", { name: "Ism" });
const byLabel = screen.getByLabelText("Ism");
const byHint = screen.getByPlaceholderText("Masalan, Dilshod");
console.log(byRole === byLabel, byLabel === byHint);
expect(byRole).toBeRequired();
});
test("qolgan so'rovlar", () => {
render(<BookingForm />);
const guests = screen.getByRole("spinbutton");
expect(guests).toHaveValue(2);
expect(screen.getByDisplayValue("2")).toBe(guests);
const seat = screen.getByRole("checkbox", {
name: "Deraza yonida",
});
expect(seat).toBeChecked();
const plan = screen.getByAltText("Zal sxemasi");
expect(plan).toHaveAttribute("src", "data:,");
const hint = screen.getByTitle("Bron 10:00–22:00");
expect(hint).toBe(screen.getByTestId("eslatma"));
const submit = screen.getByRole("button", { name: "Bron qilish" });
expect(submit).toBeDisabled();
});Konsolda:
true true
✓ bitta maydon — uch xil so'rov
✓ qolgan so'rovlar
Testlar: 2 o'tdi (jami 2)true true — uchala so'rov bitta elementni topdi. Lekin ma'nosi har xil. Rol so'rovi ikki narsani aytadi: maydon bor, va unga «Ism» label'i ulangan. Tajriba qiling. htmlFor ni o'chirsangiz, birinchi ikki so'rov yiqiladi, getByPlaceholderText esa o'tib ketaveradi — va label yo'qligini hech kim bilmaydi. type="number" ning roli spinbutton — buni yod olish shart emas, logRoles aytadi (pastda).
4.2 getByRole sozlamalari
| Sozlama | Misol | Nima tekshiradi |
|---|---|---|
name |
{ name: "Ism" } yoki /ism/i |
kirish nomi (label, matn, aria-label) |
level |
{ level: 2 } |
sarlavha darajasi |
checked, pressed |
{ pressed: true } |
katak / tugma holati |
selected, expanded |
{ selected: true } |
tab, menyu |
hidden |
{ hidden: true } |
yashirinlarni ham qidirish |
ARIA patternlar darsida va'da qilgan edik: tablarni rol bo'yicha topamiz. Mana:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function Tabs() {
return (
<div>
<div role="tablist" aria-label="Menyu bo'limlari">
<button role="tab" aria-selected="true" id="t1">
Issiq taomlar
</button>
<button role="tab" aria-selected="false" id="t2">
Salatlar
</button>
</div>
<div role="tabpanel" aria-labelledby="t1">
Osh, Manti
</div>
<div role="tabpanel" aria-labelledby="t2" hidden>
Achchiq-chuchuk
</div>
</div>
);
}
test("tablar — tanlangani va ko'rinadigan panel", () => {
render(<Tabs />);
const selected = screen.getByRole("tab", { selected: true });
console.log(selected.textContent);
const panel = screen.getByRole("tabpanel");
expect(panel).toHaveTextContent("Osh, Manti");
const all = screen.getAllByRole("tabpanel", { hidden: true });
expect(all).toHaveLength(2);
});Konsolda:
Issiq taomlar
✓ tablar — tanlangani va ko'rinadigan panel
Testlar: 1 o'tdi (jami 1)getByRole("tabpanel") bitta topdi — ikkinchisi hidden, u a11y daraxtida yo'q. Rol so'rovi sukut bo'yicha ko'rinmaydiganlarni o'tkazib yuboradi — Malika uchun ham ular yo'q. { hidden: true } faqat testda "yashirin bo'lsa ham top" degani.
4.3 Nomsiz tugma
React'da accessibility darsida aytgan edik: nomsiz tugmani test ham topa olmaydi. Faqat ikonkali tugma — eng ko'p uchraydigan holat:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function DeleteButtons() {
return (
<div>
<button>
<svg aria-hidden="true" width="16" height="16" />
</button>
<button aria-label="Oshni o'chirish">
<svg aria-hidden="true" width="16" height="16" />
</button>
</div>
);
}
test("nomsiz tugmani rol bo'yicha topib bo'lmaydi", () => {
render(<DeleteButtons />);
const all = screen.getAllByRole("button");
const labels = all.map((b) => b.getAttribute("aria-label"));
console.log(all.length, labels);
const named = screen.getByRole("button", {
name: "Oshni o'chirish",
});
expect(named).toHaveAccessibleName("Oshni o'chirish");
const plain = screen.queryByRole("button", { name: "O'chirish" });
expect(plain).toBeNull();
});Konsolda:
2 (2) [null, 'Oshni o'chirish']
✓ nomsiz tugmani rol bo'yicha topib bo'lmaydi
Testlar: 1 o'tdi (jami 1)Ikkala tugma ham bor, lekin birinchisining nomi yo'q — testda uni "qaysi tugma" deb atashning iloji yo'q. Ekran o'quvchi ham faqat "tugma" deydi. Test sizni aria-label qo'yishga majburlaydi — bu a11y'ga ham foyda.
Tekshirib ko'ring: Ro'yxatda har taom yonida bir xil «Qo'shish» tugmasi bor.
getByRole("button", { name: "Qo'shish" })nima qiladi va qanday tuzatasiz?
Javob
Found multiple elements… xatosi — tugmalar bir nechta. Ikki yo'l: (1) avval taomning qatorini topib, uning ichidan qidirish (within, keyingi bo'lim); (2) tugmaga aniq nom berish (aria-label="Oshni qo'shish") — ekran o'quvchi foydalanuvchisi uchun ham yaxshiroq.
5. within: qidiruvni bir bo'lakka toraytirish
within(element) — o'sha element ichida ishlaydigan so'rovlar to'plami (screen ning kichik nusxasi). Yuqoridagi menyu testida ham ishlatdik: within(list).getAllByRole("listitem"). Kanonning filtr testida:
const guruh = screen.getByRole("group", { name: "Filtr" });
const tugmalar = within(guruh).getAllByRole("button");
expect(tugmalar.map((t) => t.textContent)).toEqual([
"Hammasi",
"Faol",
"Bajarilgan",
]);Avval nomli guruh (role="group" + aria-label="Filtr"), keyin uning ichidagi tugmalar. Sahifada boshqa tugmalar ham bor — within ularni chalg'itmaydi. <section aria-labelledby>, <form aria-label>, <article aria-label> kabi nomli o'ramlar within uchun eng yaxshi "langar".
6. jest-dom matcherlari
6.1 Nega oddiy expect yetmaydi
Ikkalasi bir narsani tekshiradi — xabarlarni solishtiring:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
test("oddiy expect", () => {
render(<button disabled={false}>To'lash</button>);
const pay = screen.getByRole("button", { name: "To'lash" });
expect(pay.disabled).toBe(true);
});
test("jest-dom", () => {
render(<button disabled={false}>To'lash</button>);
const pay = screen.getByRole("button", { name: "To'lash" });
expect(pay).toBeDisabled();
});Konsolda:
× oddiy expect
→ expected false to be true // Object.is equality
× jest-dom
→ expected <button> to be disabled
Testlar: 2 yiqildi, 0 o'tdi (jami 2)Birinchisi "false true emas" deydi — nima false ekanini topish sizga qoladi. Ikkinchisi "tugma o'chirilgan bo'lishi kerak edi". Haqiqiy jest-dom'ning xabari yanada aniq (Vitest 5.0.4 terminali):
Error: expect(element).toBeDisabled()
Received element is not disabled:
<button />toBeDisabled yana <fieldset disabled> ichidagi tugmani ham hisobga oladi — pay.disabled buni bilmaydi.
6.2 Eng kerakli matcherlar
| Matcher | Tekshiradi |
|---|---|
toBeInTheDocument() |
element DOM'da |
toBeVisible() |
ko'rinadi (display, visibility, opacity, hidden, ota-onasi ham) |
toBeDisabled() / toBeEnabled() |
o'chirilgan / yoqilgan |
toBeChecked() |
katak, radio, aria-checked |
toHaveTextContent(t) |
matn (qismi yoki RegExp) |
toHaveValue(v) / toHaveDisplayValue(t) |
maydon qiymati |
toHaveAttribute(n, v) |
atribut |
toHaveAccessibleName(n) |
kirish nomi |
toHaveFocus() |
fokus shu elementda |
toBeRequired() / toBeInvalid() |
forma holati |
Kanon yana ikkitasini ishlatadi: toBePressed() (aria-pressed="true" li tugma) va toHaveAccessibleDescription(t) (aria-describedby dan olingan tavsif). Ikkalasi natija oynasida ham ishlaydi.
toBeInTheDocument va queryBy juftligi: expect(screen.queryByText("Xato")).not.toBeInTheDocument() — toBeNull() bilan bir xil, lekin o'qilishi tabiiyroq. getBy + toBeInTheDocument() esa ortiqcha: getBy topmasa, xatoni o'zi tashlaydi.
7. Ichiga qarash: screen.debug va logRoles
Test yiqildi, lekin nega — tushunarsiz. DOM'ga qarash kerak:
import { render, screen } from "@testing-library/react";
import { logRoles } from "@testing-library/dom";
import { test } from "vitest";
function Order() {
return (
<section aria-labelledby="order-title">
<h2 id="order-title">Buyurtma #12</h2>
<p>Osh × 2</p>
<button>To'lash</button>
</section>
);
}
test("debug va logRoles", () => {
const { container } = render(<Order />);
screen.debug(screen.getByRole("heading"));
logRoles(container);
});Konsolda:
<h2
id="order-title"
>
Buyurtma #12
</h2>
region:
Name "Buyurtma #12":
<section
aria-labelledby="order-title"
/>
--------------------------------------------------
heading:
Name "Buyurtma #12":
<h2
id="order-title"
/>
--------------------------------------------------
paragraph:
Name "":
<p />
--------------------------------------------------
button:
Name "To'lash":
<button />
--------------------------------------------------
✓ debug va logRoles
Testlar: 1 o'tdi (jami 1)screen.debug()— butunbodyni,screen.debug(el)— bitta elementni chiroyli HTML qilib chiqaradi (prettyDOM).logRoles(container)— har elementning roli va nomi.<section>nomi bo'lgani uchunregionbo'ldi (nomsiz<section>— rolsiz). Qaysi so'rov yozishni shu ro'yxatdan ko'chiring.
Haqiqiy Vitest terminalida logRoles har yozuv orasida bo'sh qator qoldiradi; natija oynasi bo'sh qatorlarni ko'rsatmaydi — mazmuni bir xil. Yana bir yordamchi — Testing Playground (testing-playground.com): HTML qo'ysangiz, element uchun eng yaxshi so'rovni taklif qiladi.
8. act: yangilanish tugaguncha kutish
Zustand I darsida act(() => …) ni ko'rgan edik va "RTL darsida to'liq" degan edik. React holat yangilanishini darhol ekranga chiqarmaydi — navbatga qo'yadi (State II dagi batching). act — "ichidagi hamma yangilanish va effekt tugasin, keyin keyingi qatorga o't".
RTL render, fireEvent va findBy/waitFor ni act ichida o'zi bajaradi — shuning uchun odatda uni yozmaysiz. Yozish kerak bo'lgan joy: holatni RTL'dan tashqarida o'zgartirganda (store, hodisa manbai, taymer):
import { act, render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { expect, test } from "vitest";
const cart = new EventTarget();
function CartBadge() {
const [count, setCount] = useState(0);
useEffect(() => {
const add = () => setCount((c) => c + 1);
cart.addEventListener("add", add);
return () => cart.removeEventListener("add", add);
}, []);
return <p>Savatda: {count}</p>;
}
test("act'siz — ekran hali eski", () => {
render(<CartBadge />);
cart.dispatchEvent(new Event("add"));
expect(screen.getByText(/Savatda/)).toHaveTextContent("Savatda: 1");
});
test("act bilan — yangilanish tugagach tekshiramiz", () => {
render(<CartBadge />);
act(() => {
cart.dispatchEvent(new Event("add"));
});
expect(screen.getByText(/Savatda/)).toHaveTextContent("Savatda: 1");
});Konsolda:
× act'siz — ekran hali eski
→ expected <p> to have text content 'Savatda: 1'
Received: 'Savatda: 0'
✓ act bilan — yangilanish tugagach tekshiramiz
Testlar: 1 yiqildi, 1 o'tdi (jami 2)Birinchi testda hodisa ishladi, setCount chaqirildi — lekin React ekranni hali yangilamagan, tekshiruv eski "0" ni ko'rdi. act ichida esa React navbatni bo'shatdi. Bu misol «Har testdan keyin tozalash» dagi tajribaning ham manbai: cleanup siz ikkinchi testda birinchi komponent ham hodisani eshitgan edi.
Foydalanuvchi harakatlari (bosish, yozish) va act ogohlantirishi (… was not wrapped in act(...)) — RTL II da.
9. Portal: screen uni topadi, container — yo'q
Portallar darsidagi tuzoq: portal kontenti render ning div ida emas, document.body da. screen butun body ni qidiradi — topadi:
import { render, screen } from "@testing-library/react";
import { createPortal } from "react-dom";
import { expect, test } from "vitest";
function Toast({ text }) {
return createPortal(<div role="status">{text}</div>, document.body);
}
function Checkout() {
return (
<main>
<h1>Savat</h1>
<Toast text="Osh savatga qo'shildi" />
</main>
);
}
test("portal — container'da yo'q, screen'da bor", () => {
const { container } = render(<Checkout />);
console.log(container.querySelector("[role=status]"));
expect(screen.getByRole("status")).toHaveTextContent(
"Osh savatga qo'shildi",
);
});Konsolda:
null
✓ portal — container'da yo'q, screen'da bor
Testlar: 1 o'tdi (jami 1)Radix Dialog, tooltip, sonner toast'lari (20-qism) ham portal — ularni doim screen bilan qidiring.
10. jsdom — brauzer emas
Kanon vazifalar da shunday topilma bo'ldi: jsdom 30.1.2 da HTMLDialogElement.showModal yo'q. O'chirish dialogi testda ochilmaydi. Natija oynasi esa haqiqiy Chrome — u yerda ishlaydi:
import { render, screen } from "@testing-library/react";
import { expect, test } from "vitest";
function DeleteDialog() {
return (
<dialog aria-labelledby="del-title">
<h2 id="del-title">Vazifa o'chirilsinmi?</h2>
<form method="dialog">
<button value="bekor">Bekor qilish</button>
<button value="ochir">O'chirish</button>
</form>
</dialog>
);
}
test("showModal — haqiqiy brauzerda", () => {
render(<DeleteDialog />);
const dialog = screen.getByRole("dialog", { hidden: true });
console.log(typeof dialog.showModal);
dialog.showModal();
expect(
screen.getByRole("dialog", { name: "Vazifa o'chirilsinmi?" }),
).toBeVisible();
expect(
screen.getByRole("button", { name: "Bekor qilish" }),
).toHaveFocus();
});Chrome 155 da (natija oynasi) konsol: function, ✓ showModal — haqiqiy brauzerda, Testlar: 1 o'tdi (jami 1). Xuddi shu fayl Vitest + jsdom'da:
stdout | sinov/dialog.test.jsx > showModal — haqiqiy brauzerda
undefined
FAIL sinov/dialog.test.jsx > showModal — haqiqiy brauzerda
TypeError: dialog.showModal is not a functionShuning uchun kanon jsdom testida dialogni open atributi bilan ochadi. Izohda halol yozilgan: haqiqiy modal (fokus, Esc, ::backdrop) — Browser Mode va Playwright da. jsdom'da yana yo'q: layout (o'lchamlar 0), CSS fayllari, matchMedia (kanon soxtasini sozlash fayliga qo'ygan).
11. Vazifalar qadami: jest-dom va rol so'rovlari (holat 04)
11.1 O'rnatish va tur muammosi
git switch -c test/jest-dom
npm i -D -E @testing-library/jest-dom@7.0.1Narx: +~1 MB node_modules; ilova bundle'i o'zgarmaydi (JS 206.7 KB gzip). Hujjatdagi rasmiy yo'l — sozlash faylida bitta import: import "@testing-library/jest-dom/vitest". Kanon uni sinadi — npm run tip (tsc -b) yiqildi:
node_modules/@testing-library/jest-dom/types/vitest.d.ts(5,13): error TS2428: All declarations of 'Assertion' must have identical type parameters.
node_modules/vitest/dist/chunks/config.d.QBNYCgFU.d.ts(1710,11): error TS2428: All declarations of 'Assertion' must have identical type parameters.
node_modules/vitest/dist/chunks/task-utils.d.PFh2BV6Q.d.ts(40,12): error TS2428: All declarations of 'Assertion' must have identical type parameters.Tarjima: "Assertion ning hamma e'lonlari bir xil tur parametrlariga ega bo'lishi kerak." Sabab: jest-dom 7.0.1 ning tur fayli Vitest 4 dagi Assertion<T> shaklini kengaytiradi, Vitest 5 da esa u Assertion<R, T>. Matcherlarning o'zi ishlaydi — faqat turlar mos emas. skipLibCheck bilan yashirish mumkin edi, kanon buni qilmadi: kutubxona turlaridagi boshqa xatolar ham ko'rinmay qolardi.
11.2 tekshiruv/jest-dom.ts
Kanon ulashni va turni o'zi yozdi — Vitest 5 hujjatidagi Matchers interfeysi orqali:
import { expect } from "vitest";
import * as matcherlar from "@testing-library/jest-dom/matchers";
import type { TestingLibraryMatchers } from "@testing-library/jest-dom/matchers";
expect.extend(matcherlar);
declare module "vitest" {
// R — matcher natijasi (void yoki Promise<void>),
// T — tekshirilayotgan qiymat
// eslint-disable-next-line @typescript-eslint/no-empty-object-type, @typescript-eslint/no-unused-vars
interface Matchers<R, T> extends TestingLibraryMatchers<unknown, R> {}
}expect.extend — matcherlarni qo'shadi (Vitest I). declare module "vitest" — TypeScript'ga "endi expect(…) da toBeVisible ham bor" deydi (module augmentation). vitest-sozlash.ts ga bitta qator: import "./jest-dom.ts";. Endi npm run tip toza.
11.3 sorovlar.test.tsx (5 test)
Fayl boshidagi izoh darsning xulosasi: "Tartib: getByRole → getByLabelText → getByText → … → getByTestId (oxirgi chora). get* — yo'q bo'lsa xato, query* — null, find* — kutadi". Testlar: Filtrlar (guruh nomi, within, toBePressed, { pressed: false } so'rov filtri bilan — 2 ta tugma), OchirishDialog (yopiq — queryByRole null, { hidden: true } bilan topiladi va not.toBeVisible; ochiq — nom, tavsif, tugma qiymati) va Statistika (rolsiz <p> — getByText, aria-live). Ochiq dialog testi:
// jsdom 30 da showModal() YO'Q (TypeError, o'lchandi) — open
// atributi bilan ochamiz. Haqiqiy modal (fokus, Esc,
// ::backdrop) — brauzerda (#08 Browser Mode, #09 Playwright)
expect(ref.current?.showModal).toBeUndefined();
ref.current?.setAttribute("open", "");
const dialog = screen.getByRole("dialog");
expect(dialog).toBeVisible();
expect(dialog).toHaveAccessibleName("Vazifa o'chirilsinmi?");expect(…showModal).toBeUndefined() — jsdom cheklovini qotiradi: jsdom bir kun showModal ni qo'shsa, test yiqiladi va bu vaqtinchalik yechimni olib tashlash vaqti kelganini aytadi.
11.4 Natija va commit
Testlar 332/332 (327 + 5), npm test 12.94 s; test:qamrov — chegaradan o'tdi; lint, format:check, tip, knip — toza. JS 206.7 KB. 17-qismdan qolgan container.querySelector li testlar bu holatda qayta yozilmadi — ular ishlaydi; yangi testlar rol so'rovlari bilan.
git add -A
git commitCommit (kanon): test: jest-dom matcherlari va rol so'rovlari (Filtrlar, dialog, statistika).
12. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
Unable to find an accessible element… |
nom boshqa yoki nomsiz | xabardagi rollar ro'yxatiga qarang, aria-label |
Found multiple elements… |
bir xil nomli elementlar | within, aniq nom yoki getAllBy |
Found multiple… — har testda bittadan ko'p |
cleanup ulanmagan |
afterEach(cleanup) yoki globals: true |
findBy — [object Promise] |
await yo'q |
await screen.findBy… |
yo'qlikni getBy bilan tekshirish |
getBy xato tashlaydi |
queryBy + toBeNull() |
| portal topilmaydi | container dan qidirilgan |
screen |
toBeVisible is not a function |
jest-dom ulanmagan | sozlash faylida expect.extend |
TS2428 … 'Assertion' |
jest-dom 7 turlari va Vitest 5 | jest-dom.ts (kanon) |
showModal is not a function |
jsdom'da yo'q | open atributi yoki Browser Mode |
13. Mashqlar
Mashqlar kurs/mashqlar/22/04-rtl-sorovlar/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.
1-mashq (oson): bo'sh joylarni to'ldiring
- Yo'qlikni tekshirish uchun oila:
Role - Async paydo bo'ladigan element:
await screen.Text("Tayyor") <h2>ning roli:getByRole("", { level: 2 })- Faqat bir element ichida qidirish:
(qator).getByRole("button") - Hamma rollarni chiqarish:
(container)
Yechim
- queryBy — topilmasa
null;getByxato tashlardi. - findBy — Promise, sukut bo'yicha 1 s kutadi.
- heading —
h1–h6;leveldarajani tekshiradi. - within —
screenning shu element bilan cheklangan nusxasi. - logRoles —
@testing-library/domdan; har rol va nom.
2-mashq (o'rta): taom qatorlari
DishList uchta taomni chizadi: har biri <article aria-label="Osh"> ichida nom, narx va «Qo'shish» tugmasi; Manti tugay qolgan — tugmasi disabled. Test yozing: (1) maqolalar 3 ta; (2) Manti qatoridagi tugma o'chirilgan, Osh qatoridagi — yoqilgan; (3) «Lag'mon» qatorida narx matni 28 000 so'm. getByRole("button") ni screen da emas, within bilan ishlating.
Ishora: article roli nomni aria-label dan oladi — getByRole("article", { name: "Manti" }).
Yechim
import { render, screen, within } from "@testing-library/react";
import { expect, test } from "vitest";
const dishes = [
{ name: "Osh", price: "35 000", left: 12 },
{ name: "Lag'mon", price: "28 000", left: 5 },
{ name: "Manti", price: "30 000", left: 0 },
];
function DishList() {
return (
<div>
{dishes.map((d) => (
<article key={d.name} aria-label={d.name}>
<h3>{d.name}</h3>
<p>{d.price} so'm</p>
<button disabled={d.left === 0}>Qo'shish</button>
</article>
))}
</div>
);
}
test("har taom — o'z qatori", () => {
render(<DishList />);
console.log(screen.getAllByRole("article").length);
const manti = screen.getByRole("article", { name: "Manti" });
const mantiBtn = within(manti).getByRole("button");
expect(mantiBtn).toBeDisabled();
const osh = screen.getByRole("article", { name: "Osh" });
expect(within(osh).getByRole("button")).toBeEnabled();
const lagmon = screen.getByRole("article", { name: "Lag'mon" });
expect(lagmon).toHaveTextContent("28 000 so'm");
});Konsolda:
3
✓ har taom — o'z qatori
Testlar: 1 o'tdi (jami 1)screen.getByRole("button", { name: "Qo'shish" }) uchta tugma topib, xato berardi. within "qaysi taomning tugmasi" degan savolni aniq qiladi. Narx satri qo'lda yozilgan ("28 000") — toLocaleString natijasi Chrome va Node'da farq qilishi mumkin.
3-mashq (qiyin): sekin profil
Profile komponenti loadUser() dan foydalanuvchini oladi: yuklanayotganda <p role="status">Yuklanmoqda…</p>, keyin <h2> da ism va «Chiqish» tugmasi. Xato bo'lsa — <p role="alert"> da xabar. Ikki test: (1) muvaffaqiyat — avval status bor, keyin sarlavha Sardor, status yo'qolgan; (2) xato — findByRole("alert") matni «Server javob bermadi», sarlavha yo'q. loadUser ni vi.fn() bilan bering (Vitest II).
Ishora: mockResolvedValue / mockRejectedValue; yo'qlik — queryByRole.
Yechim
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { expect, test, vi } from "vitest";
function Profile({ loadUser }) {
const [user, setUser] = useState(null);
const [error, setError] = useState(null);
useEffect(() => {
loadUser().then(setUser, (e) => setError(e.message));
}, [loadUser]);
if (error) return <p role="alert">{error}</p>;
if (!user) return <p role="status">Yuklanmoqda…</p>;
return (
<section aria-label="Profil">
<h2>{user.name}</h2>
<button>Chiqish</button>
</section>
);
}
test("muvaffaqiyat", async () => {
const loadUser = vi.fn().mockResolvedValue({ name: "Sardor" });
render(<Profile loadUser={loadUser} />);
const status = screen.getByRole("status");
expect(status).toHaveTextContent("Yuklanmoqda…");
const title = await screen.findByRole("heading", { level: 2 });
console.log(title.textContent);
expect(screen.queryByRole("status")).toBeNull();
const logout = screen.getByRole("button", { name: "Chiqish" });
expect(logout).toBeEnabled();
});
test("xato", async () => {
const loadUser = vi
.fn()
.mockRejectedValue(new Error("Server javob bermadi"));
render(<Profile loadUser={loadUser} />);
const alert = await screen.findByRole("alert");
expect(alert).toHaveTextContent("Server javob bermadi");
expect(screen.queryByRole("heading")).toBeNull();
expect(loadUser).toHaveBeenCalledTimes(1);
});Konsolda:
Sardor
✓ muvaffaqiyat
✓ xato
Testlar: 2 o'tdi (jami 2)Birinchi getByRole("status") — await siz, chunki yuklanish holati darhol bor. role="status" va role="alert" — ekran o'quvchiga e'lon qilinadigan hududlar (accessibility): ularni rol bilan topish ham testni, ham a11y'ni tekshiradi.
4-mashq: Vazifalar qadami
test/jest-dom branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: @testing-library/jest-dom@7.0.1, tekshiruv/jest-dom.ts, vitest-sozlash.ts dagi import, sorovlar.test.tsx. Avval rasmiy import "@testing-library/jest-dom/vitest" ni sinab, npm run tip dagi TS2428 ni o'zingiz ko'ring.
Yechim
Tekshiruv ro'yxati:
- Rasmiy import bilan
npm run tip— uchta TS2428;jest-dom.tsbilan — toza. npm test— 332 test;sorovlar.test.tsx— 5.Filtrlartestida"Faol"ni"Bajarilgan"ga almashtiring —toBePressedyiqiladi va qaysi tugma ekanini aytadi. Qaytaring.OchirishDialogdagiaria-labelledbyni vaqtincha olib tashlang —toHaveAccessibleNameyiqiladi: test a11y'ni ham ushlaydi.- Qo'shimcha: 17-qismdan qolgan bitta
container.querySelector(…).textContenttekshiruvinigetByRole/getByTextga almashtirib ko'ring (kanonda bu qilinmagan — o'z mashqingiz).
14. Real ishda
- React jamoalarida RTL — komponent testining standarti; Vue, Svelte, Angular uchun ham o'sha
@testing-library/*g'oyasi va o'sha so'rovlar. - Kod ko'rib chiqishda:
getByTestIdvacontainer.querySelectorko'p bo'lsa — "foydalanuvchi buni qanday topadi?" deb so'raladi. ESLint'ningeslint-plugin-testing-libraryqoidalari buni avtomat tekshiradi. - A11y bonus: rol so'rovlari nomsiz tugma, label'siz maydonni darhol ko'rsatadi — to'liq a11y testlari Accessibility testlari da.
- Intervyu savollari: "
getBy,queryBy,findByfarqi?", "negagetByRolebirinchi?", "actnima va qachon kerak?", "jsdom'da nima yo'q?".
Xulosa
renderkomponentnibodydagidivga chizadi,screenbutunbodydan qidiradi (portal ham); har testdan keyincleanup.getBy— hozir bor,queryBy— yo'qligi uchun,findBy— keyin paydo bo'ladi (await);All— massiv.- Ustuvorlik: rol → label → placeholder → matn → … →
data-testid;getByRoleyashirin va nomsiz elementni topmaydi — Malika ham. within— qidiruvni nomli o'ramga toraytiradi;screen.debugvalogRoles— ichiga qarash.- jest-dom matcherlari aniq xabar beradi; Vitest 5 + jest-dom 7 da turlarni
jest-dom.tsbilan ulaymiz. act— RTL'dan tashqaridagi yangilanish uchun; jsdom brauzer emas (showModalyo'q).vazifalar: 332 test.
Keyingi dars: RTL II: foydalanuvchi harakatlari va async — user-event bilan haqiqiy bosish va yozish, findBy va waitFor bilan kutish, forma testlari va act ogohlantirishi.
Manbalar
- Testing Library: Queries — "About Queries", "Priority" (testing-library.com/docs/queries/about)
- Testing Library:
ByRole(name,hidden,selected,pressed,level),within,screen.debug,logRoles - React Testing Library API:
render,cleanup,act(testing-library.com/docs/react-testing-library/api) - jest-dom 7 — github.com/testing-library/jest-dom (matcherlar ro'yxati)
- Kent C. Dodds, "Common mistakes with React Testing Library" (kentcdodds.com)
- Haqiqiy chiqishlar: Vitest 5.0.4, RTL 16.3.3, jsdom 30.1.2, Node 24.21.0; Chrome 155 (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!