IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash4/16-dars24 daqiqa
Mundarija (34)

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

  • render va screen bilan komponentni chizib, elementni topasiz.
  • getBy, queryBy, findBy va All variantlarini to'g'ri tanlaysiz.
  • So'rovlar ustuvorligini qo'llaysiz: getByRole va uning name, level, selected, hidden sozlamalari.
  • jest-dom matcherlari, within, screen.debug va logRoles bilan ishlaysiz.
  • act, portal va cleanup qachon 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 --> E

Rasmda: 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:

jsx
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:

text
<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:

ts
// 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";
ts
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:

text
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") va container.querySelector("h3") — qaysi biri DishCard dagi 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:

jsx
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:

text
  ✓ 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: alohida expect shart emas.
  • queryBy — faqat yo'qligini tekshirish uchun: expect(screen.queryByText("Yuklanmoqda…")).toBeNull(). getBy bu yerda xato tashlab, testni to'xtatardi.
  • findBy — element keyin paydo bo'ladi (server javobi, effekt). U waitFor ustiga qurilgan: har 50 ms qayta qidiradi, sukut bo'yicha 1 s kutadi. await ni 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):

text
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:

jsx
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:

text
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:

jsx
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:

text
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:

jsx
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:

text
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:

tsx
    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:

jsx
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:

text
  × 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):

text
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:

jsx
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:

text
<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() — butun body ni, screen.debug(el) — bitta elementni chiroyli HTML qilib chiqaradi (prettyDOM).
  • logRoles(container) — har elementning roli va nomi. <section> nomi bo'lgani uchun region bo'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):

jsx
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:

text
  × 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:

jsx
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:

text
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:

jsx
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:

text
stdout | sinov/dialog.test.jsx > showModal — haqiqiy brauzerda
undefined

 FAIL  sinov/dialog.test.jsx > showModal — haqiqiy brauzerda
TypeError: dialog.showModal is not a function

Shuning 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

bash
git switch -c test/jest-dom
npm i -D -E @testing-library/jest-dom@7.0.1

Narx: +~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:

text
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:

ts
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:

tsx
    // 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.

bash
git add -A
git commit

Commit (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; getBy xato tashlardi.
  • findBy — Promise, sukut bo'yicha 1 s kutadi.
  • heading — h1–h6; level darajani tekshiradi.
  • within — screen ning shu element bilan cheklangan nusxasi.
  • logRoles — @testing-library/dom dan; 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
jsx
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:

text
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
jsx
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:

text
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.ts bilan — toza.
  • npm test — 332 test; sorovlar.test.tsx — 5.
  • Filtrlar testida "Faol" ni "Bajarilgan" ga almashtiring — toBePressed yiqiladi va qaysi tugma ekanini aytadi. Qaytaring.
  • OchirishDialog dagi aria-labelledby ni vaqtincha olib tashlang — toHaveAccessibleName yiqiladi: test a11y'ni ham ushlaydi.
  • Qo'shimcha: 17-qismdan qolgan bitta container.querySelector(…).textContent tekshiruvini getByRole/getByText ga 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: getByTestId va container.querySelector ko'p bo'lsa — "foydalanuvchi buni qanday topadi?" deb so'raladi. ESLint'ning eslint-plugin-testing-library qoidalari 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, findBy farqi?", "nega getByRole birinchi?", "act nima va qachon kerak?", "jsdom'da nima yo'q?".

Xulosa

  • render komponentni body dagi div ga chizadi, screen butun body dan qidiradi (portal ham); har testdan keyin cleanup.
  • getBy — hozir bor, queryBy — yo'qligi uchun, findBy — keyin paydo bo'ladi (await); All — massiv.
  • Ustuvorlik: rol → label → placeholder → matn → … → data-testid; getByRole yashirin va nomsiz elementni topmaydi — Malika ham.
  • within — qidiruvni nomli o'ramga toraytiradi; screen.debug va logRoles — ichiga qarash.
  • jest-dom matcherlari aniq xabar beradi; Vitest 5 + jest-dom 7 da turlarni jest-dom.ts bilan ulaymiz.
  • act — RTL'dan tashqaridagi yangilanish uchun; jsdom brauzer emas (showModal yo'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)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Testing Library: render, screen, getByRole va jest-dom matcherlari — IlmHamroh