IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash5/16-dars30 daqiqa
Mundarija (38)

React Testing Library II: user-event, findBy, waitFor va forma testlari

Qisqacha: fireEvent bitta hodisa yuboradi, foydalanuvchi esa bitta bosishda oltita hodisa hosil qiladi (pointerdown → mousedown → fokus → pointerup → mouseup → click). user-event shuni takrorlaydi: const user = userEvent.setup(), keyin await user.click(…), await user.type(maydon, "Osh{Enter}"), await user.tab(). U foydalanuvchi qila olmaydigan narsani ham qilmaydi — o'chirilgan maydonga yozmaydi. Keyin paydo bo'ladigan natijani findBy… yoki waitFor kutadi. act ogohlantirishi — "yangilanish testdan tashqarida bo'ldi" degani; uni yashirmang, sababini toping.

Bu darsda

  • fireEvent va user-event farqini hodisalar ketma-ketligida ko'rasiz.
  • setup, click, type, clear, keyboard, tab, selectOptions bilan ishlaysiz.
  • Formani foydalanuvchi kabi to'ldirib, xato va yuborishni tekshirasiz.
  • findBy, waitFor va waitForElementToBeRemoved bilan async UI'ni kutasiz; ularning tuzoqlarini bilasiz.
  • act ogohlantirishi va soxta soat bilan user-event qachon "qotib qolishini" tushuntirasiz.

Oldin bilishingiz kerak: React Testing Library I, Vitest II, Hodisalarni boshqarish.

1. Nega bu kerak?

Hodisalarni boshqarish darsida aytgan edik: testlar hodisani fireEvent yoki user-event bilan chaqiradi, foydalanuvchiga yaqinrog'i — user-event. Endi nega ekanini ko'ramiz.

Bobur sichqonchani ishlatmaydi — faqat klaviatura. U «Saqlash» tugmasiga Tab bilan boradi va Enter bosadi. Sardorning testi esa fireEvent.click(tugma) qiladi. Tugma <div onClick> bo'lsa ham, tabIndex siz bo'lsa ham — test o'tadi. Bobur esa tugmaga yetib bora olmaydi.

Muammo shunda: fireEvent — hodisani yuborish, foydalanuvchi harakati emas. U "bosildi" deydi, lekin fokus o'zgarmaydi, mousedown bo'lmaydi, element o'chirilganmi yoki ustini boshqa narsa yopganmi — tekshirmaydi.

O'xshatish: oshxonaga buyurtma yozilgan qog'ozni to'g'ridan oshpazning qo'liga berish (fireEvent) va mijozning eshikdan kirib, navbatda turib, kassaga aytishi (user-event). Birinchisida eshik yopiq bo'lsa ham buyurtma "keldi".

2. fireEvent va user-event

2.1 Bitta bosish — nechta hodisa?

Tugmaga oltita tinglovchi qo'yamiz va ikkala usulda bosamiz:

jsx
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { test } from "vitest";

const events = [];

function AddButton() {
  const log = (e) => events.push(e.type);
  return (
    <button
      onPointerDown={log}
      onMouseDown={log}
      onFocus={log}
      onPointerUp={log}
      onMouseUp={log}
      onClick={log}
    >
      Qo'shish
    </button>
  );
}

test("fireEvent.click", () => {
  render(<AddButton />);
  events.length = 0;
  fireEvent.click(screen.getByRole("button", { name: "Qo'shish" }));
  console.log(events.join(" → "));
});

test("user.click", async () => {
  const user = userEvent.setup();
  render(<AddButton />);
  events.length = 0;
  await user.click(screen.getByRole("button", { name: "Qo'shish" }));
  console.log(events.join(" → "));
});

Konsolda:

text
click
  ✓ fireEvent.click
pointerdown → mousedown → focus → pointerup → mouseup → click
  ✓ user.click
Testlar: 2 o'tdi (jami 2)

fireEvent.click — faqat click. user.click — brauzerdagi to'liq tartib, o'rtada fokus ham bor. Komponent onMouseDown da menyuni ochsa yoki onFocus da maslahat ko'rsatsa — fireEvent.click bilan test buni hech qachon ko'rmaydi. Haqiqiy Vitest + jsdom'da ham xuddi shu ikki qator chiqadi (biz sinadik).

2.2 Foydalanuvchi qila olmaydigan narsa

Ikkinchi farq muhimroq. O'chirilgan (disabled) maydonga foydalanuvchi yoza olmaydi. Ustida pointer-events: none bo'lgan tugmani bosa olmaydi:

jsx
import { fireEvent, render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { expect, test } from "vitest";

function Closed() {
  return (
    <form>
      <label>
        Izoh
        <input
          disabled
          onChange={(e) => console.log("onChange:", e.target.value)}
        />
      </label>
    </form>
  );
}

test("disabled maydon — fireEvent baribir yozadi", () => {
  render(<Closed />);
  const note = screen.getByRole("textbox", { name: "Izoh" });
  fireEvent.change(note, { target: { value: "Achchiq" } });
  console.log("qiymat:", note.value);
});

test("disabled maydon — user.type yozmaydi", async () => {
  const user = userEvent.setup();
  render(<Closed />);
  const note = screen.getByRole("textbox", { name: "Izoh" });
  await user.type(note, "Achchiq");
  expect(note).toHaveValue("");
});

test("ustidan yopilgan tugma", async () => {
  const user = userEvent.setup();
  render(<button style={{ pointerEvents: "none" }}>Yopish</button>);
  await user.click(screen.getByRole("button", { name: "Yopish" }));
});

Konsolda:

text
onChange: Achchiq
qiymat: Achchiq
  ✓ disabled maydon — fireEvent baribir yozadi
  ✓ disabled maydon — user.type yozmaydi
  × ustidan yopilgan tugma
    → Unable to perform pointer interaction as the element has `pointer-events: none`:
BUTTON(label=Yopish)
Testlar: 1 yiqildi, 2 o'tdi (jami 3)

Birinchi test — yolg'on yashilning tayyor manbai (Nega test yozamiz): o'chirilgan maydon onChange ni chaqirdi, komponent "yozildi" deb o'yladi. Uchinchi testdagi xabar: "Element pointer-events: none bo'lgani uchun sichqoncha harakatini bajarib bo'lmaydi". Bu — yaxshi xato: ustiga ko'rinmas qatlam tushib qolgan tugmani foydalanuvchi ham bosa olmaydi.

Tekshirib ko'ring: <div onClick={save}>Saqlash</div> ni fireEvent.click va user.click bilan bosish mumkinmi? user.tab() bilan unga yetib borish-chi?

Javob

Ikkalasi ham bosadi — div ga sichqoncha bilan bosish mumkin. Lekin user.tab() unga yetib bormaydi: div fokus olmaydi (Klaviatura navigatsiyasi). Bobur ham yetib bormaydi. Avval getByRole("button", { name: "Saqlash" }) yiqiladi — div ning roli yo'q. To'g'risi — <button>.

3. userEvent.setup() va asosiy amallar

3.1 Sozlash

js
const user = userEvent.setup();     // har test boshida
await user.click(tugma);            // har amal — Promise
  • setup() testda, render dan oldin yoki keyin — har test o'z user i bilan. Ichida klaviatura holati (bosib turilgan Shift), sichqoncha holati bor; testlar orasida aralashmasin.
  • Har amal await bilan. user-event amallari asinxron: harflar orasida (sukut delay: 0) navbatni bo'shatadi. await siz keyingi qator yozish tugamasdan ishlaydi.
  • Eski kodda userEvent.click(tugma) (to'g'ridan, setup siz) ko'rasiz — 13-versiyadan qolgan uslub, ishlaydi, lekin har chaqiruv yangi holat bilan boshlanadi. Hujjat setup() ni tavsiya qiladi.

3.2 Amallar jadvali

Amal Nima qiladi
click(el), dblClick(el) sichqoncha bilan bosish (to'liq tartib)
hover(el), unhover(el) ustiga olib borish / olib ketish
type(el, "matn") avval bosadi, keyin har harf uchun klaviatura hodisalari
clear(el) maydonni tozalaydi (tanlab, o'chirib)
keyboard("{Enter}") fokusdagi elementga tugmalar
tab(), tab({ shift: true }) keyingi / oldingi fokuslanadigan element
selectOptions(select, "Manti") <select> da variant tanlash
upload(input, fayl) <input type="file"> ga fayl
paste("matn") buferdan qo'yish

3.3 type va keyboard: maxsus tugmalar

type va keyboard matnida jingalak qavs — tugma nomi:

Yozuv Ma'nosi
{Enter}, {Escape}, {Tab} bitta tugma
{Backspace}, {Delete} o'chirish
{ArrowDown}, {Home}, {End} strelka va surish
{Shift>}a{/Shift} Shift ni bosib turib a
{{ oddiy { belgisi

Qidiruv qutisida hammasi birga:

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test, vi } from "vitest";

function SearchBox({ onSearch }) {
  const [query, setQuery] = useState("");
  function submit(e) {
    e.preventDefault();
    onSearch(query.trim());
  }
  return (
    <form role="search" onSubmit={submit}>
      <label htmlFor="q">Taom qidirish</label>
      <input
        id="q"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
      />
      <button type="button" onClick={() => setQuery("")}>
        Tozalash
      </button>
      <button>Qidirish</button>
    </form>
  );
}

test("yozish, xatoni o'chirish va Enter", async () => {
  const user = userEvent.setup();
  const onSearch = vi.fn();
  render(<SearchBox onSearch={onSearch} />);
  const box = screen.getByRole("textbox", { name: "Taom qidirish" });
  await user.type(box, "lag'monn{Backspace}{Enter}");
  expect(onSearch).toHaveBeenCalledExactlyOnceWith("lag'mon");
  await user.clear(box);
  expect(box).toHaveValue("");
});

test("faqat klaviatura: Tab va Enter", async () => {
  const user = userEvent.setup();
  render(<SearchBox onSearch={vi.fn()} />);
  const box = screen.getByRole("textbox", { name: "Taom qidirish" });
  await user.tab();
  expect(box).toHaveFocus();
  await user.keyboard("Osh");
  await user.tab();
  const clear = screen.getByRole("button", { name: "Tozalash" });
  expect(clear).toHaveFocus();
  await user.keyboard("{Enter}");
  expect(box).toHaveValue("");
  console.log(document.activeElement.textContent);
});

Konsolda:

text
  ✓ yozish, xatoni o'chirish va Enter
Tozalash
  ✓ faqat klaviatura: Tab va Enter
Testlar: 2 o'tdi (jami 2)

Uch narsaga e'tibor bering:

  • {Enter} maydonda — forma yuborildi (onSubmit chaqirildi). Brauzerdagi "yashirin yuborish" (implicit submission) — user-event uni ham takrorlaydi.
  • user.keyboard("Osh") — type dan farqli, hech narsani bosmaydi: fokus qayerda bo'lsa, o'sha yerga yozadi. Bobur shunday ishlaydi.
  • Enter tugmada — click. Ikkinchi test Bobur uchun to'liq yo'lni tekshirdi: maydonga yetib borish, yozish, keyingi tugmaga o'tish, bosish.

Tekshirib ko'ring: «Tozalash» tugmasidan type="button" ni olib tashlasangiz, ikkinchi test nima qiladi?

Javob

Formadagi type siz tugma — submit. Enter uni bosadi va forma yuboriladi, lekin onClick ham ishlaydi — maydon baribir tozalanadi. Test o'tadi, onSearch("Osh") esa jim chaqiriladi. Buni ushlash uchun onSearch ga expect(onSearch).not.toHaveBeenCalled() qo'shing — "nima bo'lmasligi kerak" ham tekshiruv.

4. Forma testi

4.1 Foydalanuvchi ssenariysi

Forma testi — foydalanuvchining ikki yo'li: noto'g'ri to'ldirish (xato ko'rinadi, yuborilmaydi) va to'g'ri to'ldirish (ma'lumot ketadi). «Bahor» oshxonasining yetkazib berish formasi:

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { expect, test, vi } from "vitest";

function OrderForm({ onOrder }) {
  const [error, setError] = useState("");
  const nameRef = useRef(null);
  function submit(e) {
    e.preventDefault();
    const data = Object.fromEntries(new FormData(e.currentTarget));
    if (!data.name.trim()) {
      setError("Ismingizni yozing");
      nameRef.current.focus();
      return;
    }
    setError("");
    onOrder(data);
  }
  return (
    <form aria-label="Buyurtma" noValidate onSubmit={submit}>
      <label htmlFor="name">Ism</label>
      <input
        id="name"
        name="name"
        ref={nameRef}
        aria-invalid={error ? "true" : "false"}
        aria-describedby="name-error"
      />
      <p id="name-error" role="alert">{error}</p>
      <label htmlFor="dish">Taom</label>
      <select id="dish" name="dish" defaultValue="Osh">
        <option>Osh</option>
        <option>Lag'mon</option>
        <option>Manti</option>
      </select>
      <button>Buyurtma berish</button>
    </form>
  );
}

test("ism bo'sh — xato, fokus maydonda, yuborilmaydi", async () => {
  const user = userEvent.setup();
  const onOrder = vi.fn();
  render(<OrderForm onOrder={onOrder} />);
  await user.click(
    screen.getByRole("button", { name: "Buyurtma berish" }),
  );
  const name = screen.getByRole("textbox", { name: "Ism" });
  expect(screen.getByRole("alert")).toHaveTextContent(
    "Ismingizni yozing",
  );
  expect(name).toBeInvalid();
  expect(name).toHaveAccessibleDescription("Ismingizni yozing");
  expect(name).toHaveFocus();
  expect(onOrder).not.toHaveBeenCalled();
});

test("to'g'ri to'ldirilgan — ma'lumot ketadi", async () => {
  const user = userEvent.setup();
  const onOrder = vi.fn();
  render(<OrderForm onOrder={onOrder} />);
  await user.type(screen.getByLabelText("Ism"), "Dilshod");
  const dish = screen.getByRole("combobox", { name: "Taom" });
  await user.selectOptions(dish, "Manti");
  await user.click(
    screen.getByRole("button", { name: "Buyurtma berish" }),
  );
  expect(dish).toHaveDisplayValue("Manti");
  expect(onOrder).toHaveBeenCalledExactlyOnceWith({
    name: "Dilshod",
    dish: "Manti",
  });
  console.log(onOrder.mock.calls[0][0]);
});

Konsolda:

text
  ✓ ism bo'sh — xato, fokus maydonda, yuborilmaydi
{name: 'Dilshod', dish: 'Manti'}
  ✓ to'g'ri to'ldirilgan — ma'lumot ketadi
Testlar: 2 o'tdi (jami 2)

Test komponentning ichini bilmaydi: useState bormi, FormData mi yoki React Hook Form mi — farqi yo'q. U faqat foydalanuvchi ko'radiganini tekshiradi: xabar, aria-invalid, fokus, va tashqariga nima chiqdi (onOrder chaqiruvi). Ertaga formani RHF'ga ko'chirsangiz, test o'zgarmaydi — shu uning qiymati.

4.2 Nima tekshiriladi

  • toHaveAccessibleDescription — xato matni maydonga aria-describedby bilan ulangan. Malika maydonga kelganda ekran o'quvchi xatoni o'qiydi. Matn ekranda bor, lekin ulanmagan bo'lsa — bu tekshiruv yiqiladi.
  • noValidate — brauzerning o'z tekshiruvi (required pufakchasi) o'chirilgan, xatoni komponent ko'rsatadi. required qoldirsangiz, Chrome'da forma umuman yuborilmaydi va test boshqa narsani tekshiradi.
  • Tugmani bosish, fireEvent.submit(forma) emas. submit hodisasi tugma disabled bo'lsa ham, tugma umuman bo'lmasa ham ishlaydi. Foydalanuvchi esa tugmani bosadi yoki Enter ni.

RHF formasi darsida va'da qilgan edik: RHF formasi ham oddiy foydalanuvchidek sinaladi — maydonga yozish, tugma bosish, keyin findByText("Ismingizni yozing"). Nega aynan findBy — keyingi bo'limda: RHF tekshiruvi asinxron, xato bir lahzadan keyin paydo bo'ladi.

5. Async UI: findBy, waitFor, waitForElementToBeRemoved

5.1 Uchta kutuvchi

RTL I da findBy ni ko'rdik: element paydo bo'lguncha kutadi. Endi to'liq rasm:

Vosita Nimani kutadi Sukut
await screen.findByRole(…) element paydo bo'lishini 1000 ms, har 50 ms
await waitFor(() => expect(…)) ichidagi tekshiruv o'tishini 1000 ms, har 50 ms
await waitForElementToBeRemoved(el) element yo'qolishini 1000 ms

findBy… = waitFor + getBy…. Element topilishi kerak bo'lsa — findBy. Boshqa narsa (matn, atribut, soxta funksiya chaqiruvi) kerak bo'lsa — waitFor.

sequenceDiagram
  participant T as test
  participant W as waitFor
  participant D as DOM
  T->>W: waitFor(tekshiruv)
  loop har 50 ms (yoki DOM o'zgarganda)
    W->>D: tekshiruvni ishga tushir
    D-->>W: xato — yana kutamiz
  end
  W->>D: tekshiruvni ishga tushir
  D-->>W: o'tdi
  W-->>T: tayyor (1 s o'tsa — oxirgi xato)

waitFor faqat taymer bilan emas, DOM o'zgarishi bilan ham qayta urinadi (MutationObserver) — shuning uchun javob kelishi bilan darhol tugaydi.

5.2 Sekin saqlash

Server o'rnida 200 ms dan keyin javob beradigan soxta funksiya:

jsx
import {
  render,
  screen,
  waitFor,
  waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test, vi } from "vitest";

function SaveDish({ save }) {
  const [status, setStatus] = useState("idle");
  async function handleClick() {
    setStatus("saving");
    await save("Osh");
    setStatus("saved");
  }
  return (
    <div>
      <button disabled={status === "saving"} onClick={handleClick}>
        Saqlash
      </button>
      {status === "saving" && <p role="status">Saqlanmoqda…</p>}
      {status === "saved" && <p>Saqlandi</p>}
    </div>
  );
}

const slowSave = () =>
  new Promise((resolve) => setTimeout(resolve, 200));

test("tugma o'chadi, keyin Saqlandi", async () => {
  const user = userEvent.setup();
  const save = vi.fn(slowSave);
  render(<SaveDish save={save} />);
  const button = screen.getByRole("button", { name: "Saqlash" });
  await user.click(button);
  expect(button).toBeDisabled();
  expect(await screen.findByText("Saqlandi")).toBeInTheDocument();
  expect(button).toBeEnabled();
  expect(save).toHaveBeenCalledExactlyOnceWith("Osh");
});

test("yuklanish yozuvi yo'qoladi", async () => {
  const user = userEvent.setup();
  render(<SaveDish save={slowSave} />);
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
  await waitForElementToBeRemoved(screen.getByRole("status"));
  const gone = screen.queryByRole("status") === null;
  console.log("status yo'qoldi:", gone);
});

test("waitFor — soxta funksiya chaqiruvini kutish", async () => {
  const user = userEvent.setup();
  const save = vi.fn(slowSave);
  render(<SaveDish save={save} />);
  const button = screen.getByRole("button", { name: "Saqlash" });
  await user.dblClick(button);
  await waitFor(() =>
    expect(screen.getByText("Saqlandi")).toBeVisible(),
  );
  expect(save).toHaveBeenCalledTimes(1);
});

Konsolda:

text
  ✓ tugma o'chadi, keyin Saqlandi
status yo'qoldi: true
  ✓ yuklanish yozuvi yo'qoladi
  ✓ waitFor — soxta funksiya chaqiruvini kutish
Testlar: 3 o'tdi (jami 3)

Uchinchi test kichik bir xatoni ham tekshirdi: ikki marta bosish (dblClick) — so'rov bitta. Birinchi bosishdan keyin tugma o'chdi, ikkinchisi o'tmadi. Sabrsiz mijozlar shunday qiladi — test ularning yo'lini ham sinadi.

waitForElementToBeRemoved ga element hozir bor bo'lishi kerak: chaqirilganda u yo'q bo'lsa, darhol xato beradi ("The element(s) given to waitForElementToBeRemoved are already removed").

5.3 Tuzoq: element bor, matn hali yo'q

Ko'p uchraydigan holat — xato yozuvi uchun joy doim bor, faqat ichi bo'sh (aria-live hududlari shunday qilinadi: ekran o'quvchi o'zgarishni sezishi uchun hudud oldindan turadi):

jsx
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test } from "vitest";

function EditDish({ save }) {
  const [error, setError] = useState("");
  async function submit(e) {
    e.preventDefault();
    try {
      await save();
    } catch (err) {
      setError(`Saqlanmadi: ${err.message}`);
    }
  }
  return (
    <form onSubmit={submit}>
      <button>Saqlash</button>
      <p role="alert">{error}</p>
    </form>
  );
}

const failingSave = () =>
  new Promise((_, reject) => {
    setTimeout(() => reject(new Error("Ataylab")), 200);
  });

test("findByRole — bo'sh alert'ni darhol topadi", async () => {
  const user = userEvent.setup();
  render(<EditDish save={failingSave} />);
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
  const alert = await screen.findByRole("alert");
  expect(alert).toHaveTextContent("Saqlanmadi: Ataylab");
});

test("waitFor — matnning o'zini kutadi", async () => {
  const user = userEvent.setup();
  render(<EditDish save={failingSave} />);
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
  await waitFor(() =>
    expect(screen.getByRole("alert")).toHaveTextContent(
      "Saqlanmadi: Ataylab",
    ),
  );
});

Konsolda:

text
  × findByRole — bo'sh alert'ni darhol topadi
    → expected <p> to have text content 'Saqlanmadi: Ataylab'
      Received: ''
  ✓ waitFor — matnning o'zini kutadi
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Haqiqiy Vitest + jsdom'da birinchi test xuddi shunday yiqiladi, xabari jest-dom'niki:

text
 FAIL  sinov/edit.test.jsx > findByRole — bo'sh alert'ni darhol topadi
Error: expect(element).toHaveTextContent()

Expected element to have text content:
  Saqlanmadi: Ataylab
Received:

findByRole("alert") hech narsa kutmadi — <p role="alert"> boshidan bor edi. Tekshiruv bo'sh matnni ko'rdi. waitFor esa matn kelguncha qayta urindi. Muqobil: await screen.findByText("Saqlanmadi: Ataylab") — u ham matnni kutadi.

Eng xavflisi — server tez javob bergan holat: bir zumda javob kelsa, birinchi test ham tasodifan o'tib ketishi mumkin. Kanon vazifalar da aynan shunday yashirin xato bor edi — u MSW darsida, soxta server haqiqiy tarmoqdek kechroq javob bera boshlaganda ochildi.

5.4 waitFor qoidalari

Qilmang Nega O'rniga
waitFor(() => user.click(…)) ichidagi kod har 50 ms qayta ishlaydi — bir necha marta bosadi harakat waitFor dan oldin
waitFor(() => {}) (bo'sh) hech narsani kutmaydi, bir marta "nafas oladi" aniq tekshiruv
bitta waitFor da 5 ta expect birinchisi o'tguncha qolganlari ko'rinmaydi, xato chalkash waitFor da bittasi, qolgani keyin
await waitFor(() => getByRole(…)) ishlaydi, lekin uzun await findByRole(…)
findBy ni await siz [object Promise], tekshiruv yo'q await

Sukutdagi 1 s sekin mashinada (CI, coverage bilan) yetmay qolishi mumkin. Bitta testda: waitFor(fn, { timeout: 3000 }). Hammasi uchun — configure({ asyncUtilTimeout: 3000 }) (@testing-library/react dan). Kanon buni Test strategiyasi darsida qiladi — yuk ostidagi o'lchovdan keyin.

6. act ogohlantirishi

6.1 Xabar

RTL I da act ni ko'rdik: ichidagi yangilanishlar tugaguncha kutadi. RTL render, fireEvent, user-event amallari va waitFor ni act ichida o'zi bajaradi. Yangilanish shulardan tashqarida bo'lsa, React ogohlantiradi.

Sabab ko'pincha bitta: test tugash yoki tekshirish oldidan async ishni kutmagan. Mana shunday test (haqiqiy Vitest, globals: true bilan; komponent — «Sekin saqlash» dagi kabi, holat matni <p role="status"> da):

jsx
test("saqlandi", async () => {
  const user = userEvent.setup();
  render(<SaveButton save={save} />);  // save — 50 ms
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
  await new Promise((r) => setTimeout(r, 100)); // oddiy kutish
  expect(screen.getByRole("status")).toHaveTextContent("Saqlandi");
});
text
stderr | sinov/saqlash.test.jsx > saqlandi
An update to SaveButton inside a test was not wrapped in act(...).

When testing, code that causes React state updates should be wrapped into act(...):

act(() => {
  /* fire events that update state */
});
/* assert on the output */

This ensures that you're testing the behavior the user would see in the browser. Learn more at https://react.dev/link/wrap-tests-with-act

 ✓ sinov/saqlash.test.jsx (1 test) 287ms

Tarjima: "Testda SaveButton yangilanishi act(...) ga o'ralmagan. React holatini o'zgartiradigan kodni act ga o'rang… Shunda brauzerda foydalanuvchi ko'radigan xulqni tekshirasiz."

Test o'tdi — ogohlantirish xato emas. Lekin u aytadi: setStatus("Saqlandi") RTL bilmagan paytda bo'ldi. Bu safar 100 ms yetdi; sekin mashinada yetmaydi. Davosi act ni qo'lda yozish emas — RTL kutuvchisi:

js
await user.click(screen.getByRole("button", { name: "Saqlash" }));
expect(await screen.findByText("Saqlandi")).toBeInTheDocument();

findBy o'zi act ichida kutadi — ogohlantirish yo'qoladi, test ham barqaror.

Ogohlantirish sababi Davosi
tekshiruv async natijadan oldin findBy / waitFor
test tugadi, javob keyin keldi oxirgi natijani ham kuting
holat RTL'dan tashqarida o'zgardi (store, taymer, EventTarget) act(() => …) (RTL I)
soxta taymer o'tkazildi act(() => vi.advanceTimersByTime(…))

Natija oynasida bu ogohlantirish chiqmaydi (sababi pastda). Uni faqat o'z loyihangiz terminalida ko'rasiz.

6.2 Biz topgan jimlik: globals: false da ogohlantirish yo'q

Yuqoridagi testni ikki marta yurgizdik: globals: true bilan — ogohlantirish chiqdi; kanondagidek globals siz (importlar aniq) — jimlik. Sabab: React ogohlantirishni faqat globalThis.IS_REACT_ACT_ENVIRONMENT === true bo'lsa beradi. Testing Library bu bayroqni o'zi yoqadi — lekin global beforeAll bo'lsa. RTL I dagi cleanup hikoyasining egizagi:

text
IS_REACT_ACT_ENVIRONMENT: undefined

Bu qator — globals siz test ichida console.log(globalThis.IS_REACT_ACT_ENVIRONMENT) natijasi (Vitest 5.0.4, RTL 16.3.3). Sozlash fayliga bitta qator qo'shsak, ogohlantirish qaytdi:

ts
// vitest-sozlash.ts — globals siz Testing Library bu bayroqni
// yoqmaydi; React act ogohlantirishlari shusiz jim
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

Kanon vazifalar da globals yo'q va bu qator ham yo'q edi — demak u yerdagi act ogohlantirishlari jim edi. Biz qatorni kanonga qo'shib, o'lchadik (pastdagi «Vazifalar qadami», qo'shimcha qadam):

Testlar act ogohlantirishlari
Bayroqsiz (oldin) 349/349 0 — chunki React jim
Bayroq bilan (keyin) 349/349 0 — testlar haqiqatan toza

Ikkinchi qator muhim: bayroq yoqilgach ham 0. Demak 349 test await user.…, findBy… va waitFor ni to'g'ri ishlatgan. O'lchov ishlashini alohida tekshirdik: ataylab act siz yangilanadigan vaqtinchalik test bayroq bilan 1 ta ogohlantirish berdi, bayroqsiz 0. Shuning uchun "0" ga ishonish mumkin.

Bitta istisno: bayroq faqat jsdom testlari uchun. Vitest Browser Mode da xuddi shu qator 6 ta testda 47 ta foydasiz ogohlantirish berdi. Sababi: u yerdagi userEvent haqiqiy brauzer hodisasini yuboradi, act ga o'ralmaydi. Shuning uchun kanon qatorni faqat jsdom sozlash fayliga qo'ydi.

6.3 Suspense va await act

use(): promise va context'ni o'qish darsida haqiqiy tuzoq bor edi: act kutilmagan edi va React dedi: A component suspended inside an act scope, but the act call was not awaited. Qoida: ichida async ish (Promise, Suspense, transition) bo'lsa — await act(async () => { … }). user-event amallari va findBy buni o'zi qiladi; act ni qo'lda faqat RTL'dan tashqaridagi o'zgarish uchun yozasiz.

7. Soxta soat va user-event

Vitest II da soxta taymerlarni ko'rdik va kanonning saboqini aytdik: soatni render tugagach yoqing, aks holda waitFor abadiy kutadi. user-event bilan ham shunday. Komponent 2 soniya kutadi; vaqtni test boshqarmoqchi:

jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { afterEach, expect, test, vi } from "vitest";

function SaveButton({ save }) {
  const [status, setStatus] = useState("");
  async function handleClick() {
    await save();
    setStatus("Saqlandi");
  }
  return (
    <div>
      <button onClick={handleClick}>Saqlash</button>
      <p role="status">{status}</p>
    </div>
  );
}

// Server o'rnida: 2 soniyadan keyin javob
const save = () => new Promise((r) => setTimeout(r, 2000));

afterEach(() => vi.useRealTimers());

test("soxta soat — faqat advanceTimers", async () => {
  vi.useFakeTimers();
  const user = userEvent.setup({
    advanceTimers: vi.advanceTimersByTime,
  });
  render(<SaveButton save={save} />);
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
});

test("soxta soat — shouldAdvanceTime", async () => {
  vi.useFakeTimers({ shouldAdvanceTime: true });
  const user = userEvent.setup({
    advanceTimers: vi.advanceTimersByTime,
  });
  render(<SaveButton save={save} />);
  await user.click(screen.getByRole("button", { name: "Saqlash" }));
  await vi.advanceTimersByTimeAsync(2000);
  expect(await screen.findByText("Saqlandi")).toBeInTheDocument();
});

Konsolda:

text
  × soxta soat — faqat advanceTimers
    → Test timed out in 5000ms.
  ✓ soxta soat — shouldAdvanceTime
Testlar: 1 yiqildi, 1 o'tdi (jami 2)

Natija 5 soniyadan keyin chiqadi — birinchi test shuncha "o'ylaydi".

Haqiqiy Vitest 5.0.4 da ham xuddi shunday (biz ishga tushirdik):

text
 ❯ sinov/soat.test.jsx (2 tests | 1 failed) 5064ms
   × soxta soat — faqat advanceTimers 5013ms
   ✓ soxta soat — shouldAdvanceTime 48ms

 FAIL  sinov/soat.test.jsx > soxta soat — faqat advanceTimers
Error: Test timed out in 5000ms.
If this is a long-running test, pass a timeout value as the last argument or configure it globally with "testTimeout".

Tarjima: "Test 5000 ms da tugamadi. Uzoq test bo'lsa, oxirgi argument bilan vaqt bering yoki testTimeout ni sozlang." Lekin bu yerda vaqt emas — test qotib qoldi: hech narsa 5 soniya kutmaydi, click ning o'zi tugamadi.

Nega? user-event hujjati advanceTimers ni tavsiya qiladi — u harflar orasidagi kutishni soxta soatga uzatadi. Lekin Testing Library'ning o'zi ham har amaldan keyin setTimeout(…, 0) bilan "nafas oladi". U faqat Jest soxta soatini taniydi. Vitest soatini bilmaydi va to'xtatilgan setTimeout ni abadiy kutadi. shouldAdvanceTime: true — soxta soat haqiqiy vaqt bilan birga yurib turadi: kichik kutishlar o'tib ketadi, katta sakrashni esa advanceTimersByTimeAsync bilan o'zingiz qilasiz. Keyin natijani findByText kutadi — u yangilanishni act ichida kuzatadi.

Ikki test qadamma-qadam:

Qadam 1-test (useFakeTimers()) 2-test (shouldAdvanceTime: true)
user.click boshlandi bosish hodisalari yuborildi bosish hodisalari yuborildi
RTL setTimeout(…, 0) ni kutadi soat to'xtagan — taymer hech qachon ishlamaydi soat o'zi yuradi — taymer ishlaydi
natija click tugamaydi, 5 s dan keyin yiqiladi click tugadi, keyin 2 s ni o'zimiz suramiz

Amaliy qoida: user-event bilan soxta soat kerak bo'lmasa — ishlatmang. Debounce 300 ms bo'lsa, haqiqiy vaqtda findBy kutadi — test 0.3 s sekinlashadi, xolos. Soxta soat 10 soniyalik, daqiqalik taymerlar uchun.

8. Vazifalar qadami: user-event va async (holat 05)

8.1 O'rnatish

bash
git switch -c test/user-event
npm i -D -E @testing-library/user-event@14.6.7

Narx: 1 paket, node_modules ga 1 MB dan kam; ilova bundle'i o'zgarmaydi (JS 206.7 KB gzip) — bu faqat test bog'liqligi. 14.6.7 — kanon tanlagan kunda (2026-10-12 gacha chiqqan) eng yangisi; 14.6.8 keyin chiqdi.

8.2 harakatlar.test.tsx (5 test)

Fayl boshidagi izoh — darsning qisqa xulosasi:

tsx
// harakatlar.test.tsx — 22/#05: user-event — foydalanuvchi
// kabi harakat. fireEvent bitta hodisa yuboradi; userEvent.type
// esa har harf uchun keydown → keypress → beforeinput → input →
// keyup, fokus va tanlovni ham o'zgartiradi (brauzerdagidek).
// Har test: const user = userEvent.setup()

Birinchi test — vazifa qo'shish:

tsx
  test("vazifa qo'shish: yozish + Enter — qator, maydon bo'sh va fokusda", async () => {
    const user = userEvent.setup();
    const sorovlar = await ilovaniOch();
    const maydon = screen.getByRole("textbox", {
      name: "Yangi vazifa",
    });
    await user.type(maydon, "Sut olish{Enter}");
    // findBy* — paydo bo'lguncha kutadi (sukut: 1 s)
    expect(
      await screen.findByRole("checkbox", { name: "Sut olish" }),
    ).toBeInTheDocument();
    expect(maydon).toHaveValue("");
    expect(maydon).toHaveFocus();
    await waitFor(() => expect(sorovlar).toContain("POST "));
  });

Yangi qator findByRole bilan kutiladi; maydon bo'shagani va fokus maydonda qolgani — keyingi vazifani darhol yozish uchun (Bobur uchun muhim). Oxirgi qator — serverga POST ketganini waitFor bilan kutish.

Qolgan to'rttasi:

Test Nima tekshiriladi
bo'sh matn + Enter «Vazifa matnini yozing.» chiqadi, POST yo'q
katakcha bosilishi bilan toBeChecked (optimistik), keyin waitFor — PATCH /1
filtr «Bajarilgan» toBePressed, bitta qator, location.search — ?filtr=bajarilgan
TilTanlov user.tab() → «O'zbekcha» fokusda, {ArrowDown} → keyingi til

8.3 Til tanlash va qisman vi.mock

Vitest II da va'da qilgan edik — mana o'sha qisman soxta:

tsx
// vi.mock — modulning bitta eksportini almashtirish (qolgani asl).
// Vitest uni fayl boshiga "ko'taradi" (hoisting): importlardan oldin
// ishlaydi. tilniAlmashtir location.assign qiladi — jsdom'da
// navigatsiya yo'q ("Not implemented"), bizga faqat "qaysi til
// bilan chaqirildi" kerak
vi.mock("../src/i18n/index.ts", async (asl) => ({
  ...(await asl<typeof import("../src/i18n/index.ts")>()),
  tilniAlmashtir: vi.fn(),
}));

Test esa:

tsx
    await user.tab();
    expect(
      screen.getByRole("radio", { name: "O'zbekcha" }),
    ).toHaveFocus();
    await user.keyboard("{ArrowDown}");
    expect(vi.mocked(tilniAlmashtir)).toHaveBeenCalledExactlyOnceWith(
      "uz-Cyrl",
    );

Radio guruhda strelka — keyingi variant (ARIA patternlar). fireEvent.keyDown bilan buni sinash uchun brauzer xulqini qo'lda takrorlash kerak edi; user-event uni o'zi qiladi.

8.4 Soxta server — hozircha qo'lda

Server javoblari bu holatda fetch ning vi.fn soxtasi bilan beriladi: metod bo'yicha (GET — ro'yxat, POST — yangi vazifa, PATCH — o'zgargani) va har so'rov ro'yxatga yoziladi ("POST ", "PATCH /1"). Bu — 30 qatorlik qo'l mehnati, URL xatosini ham ko'rmaydi. MSW darsida u tarmoq darajasidagi soxta server bilan almashadi.

O'chirish dialogi va Ctrl+K buyruqlar oynasi bu faylda yo'q: jsdom'da showModal yo'q (RTL I). Ular — Browser Mode va Playwright da.

8.5 Natija — va birinchi beqarorlik

Testlar 337/337 (332 + 5), lint, format:check, tip, knip — toza; JS 206.7 KB. Lekin test:qamrov (coverage bilan) 5 urinishdan 3 tasida 1–6 komponent testi yiqildi:

text
 FAIL  |komponent| tekshiruv/harakatlar.test.tsx > foydalanuvchi harakatlari (user-event) > vazifa qo'shish: yozish + Enter — qator, maydon bo'sh va fokusda
 Test Files  1 failed | 52 passed (53)
      Tests  1 failed | 336 passed (337)

Har safar boshqa test (Menyu, Auth, VazifaTahriri …), sabab — Test timed out in 5000ms. O'sha paytda kompyuterda og'ir jarayon (video render) ishlayotgan edi: Duration 10 s dan 32 s gacha o'sdi. Coverage'siz npm test — hammasi o'tdi. Shuning uchun bu holatda qamrov raqami ham yo'q (yiqilgan yurgizish hisobot bermaydi); keyingi holatda u 62.76 % (satr).

Bu — flaky (beqaror) testlarning birinchi belgisi: kod o'zgarmagan, natija o'zgaradi. Kanon uni yashirmadi: xom natijalar saqlandi, tuzatish — Test strategiyasi darsida, sabablarni o'lchab.

bash
git add -A
git commit

Commit (kanon): test: user-event — qo'shish, katak, filtr, bo'sh matn; til tanlash vi.mock bilan.

Qo'shimcha qadam: act muhiti

Bu qadamni kanon qism oxirida, 15-holatdan keyin qo'shdi («act ogohlantirishi» bo'limidagi topilma tufayli). Siz uni hozir qilishingiz mumkin. Branch — test/act-muhiti. tekshiruv/vitest-sozlash.ts ga:

ts
// 22/#05: React'ga "bu test muhiti" deyiladi — act() dan tashqaridagi
// state yangilanishi "not wrapped in act(...)" ogohlantirishini beradi.
// Testing Library buni o'zi faqat global beforeAll bo'lsa (globals: true)
// yoqadi; bizda globals yo'q — aks holda ogohlantirishlar jim qoladi.
// Tur: lib.dom va @types/react'da bu global e'lon qilinmagan
declare global {
  var IS_REACT_ACT_ENVIRONMENT: boolean | undefined;
}
globalThis.IS_REACT_ACT_ENVIRONMENT = true;

declare global nega kerak: usiz tsc -b TS7017 beradi (bu global hech qayerda e'lon qilinmagan). Natija: 349/349, ogohlantirish 0, qamrov 62.72 % (o'zgarmadi), JS 206.7 KB.

Commit (kanon): test: IS_REACT_ACT_ENVIRONMENT — globals: false da act ogohlantirishlari yoqildi.

9. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
user-event amalidan keyin eski holat await yo'q await user.click(…)
Unable to perform pointer interaction … pointer-events: none ustida qatlam yoki pointer-events foydalanuvchi ham bosa olmaydi — UI'ni tuzating
type yozmadi maydon disabled/readOnly xulq to'g'ri; fireEvent.change bilan "aylanib o'tmang"
findByRole o'tdi, matn bo'sh element boshidan bor waitFor + toHaveTextContent yoki findByText
waitFor ichida bosish bir necha marta callback qayta ishlaydi harakatni waitFor dan chiqaring
… was not wrapped in act(...) async natija kutilmagan findBy/waitFor; tashqi o'zgarish — act
ogohlantirish umuman yo'q globals siz IS_REACT_ACT_ENVIRONMENT yoqilmagan sozlash faylida = true
soxta soat bilan Test timed out in 5000ms RTL Vitest soatini tanimaydi shouldAdvanceTime: true + advanceTimers
already removed waitForElementToBeRemoved ga yo'q element avval element borligini tekshiring

10. Mashqlar

Mashqlar kurs/mashqlar/22/05-user-event/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): bo'sh joylarni to'ldiring

  • Har test boshida: const user = userEvent.()
  • Maydonga yozib, yuborish: await user.type(maydon, "Osh")
  • Keyingi fokuslanadigan elementga: await user.()
  • Matn paydo bo'lishini kutish: await (() => expect(el).toHaveTextContent("Tayyor"))
  • Element yo'qolishini kutish: await (yuklanish)
Yechim
  • setup — har test o'z klaviatura/sichqoncha holati bilan.
  • {Enter} — jingalak qavsda tugma nomi; maydonda forma yuboriladi.
  • tab — { shift: true } bilan orqaga.
  • waitFor — tekshiruv o'tguncha qayta urinadi (1 s).
  • waitForElementToBeRemoved — element chaqiruv paytida bor bo'lishi kerak.

2-mashq (o'rta): miqdor tanlagich

Quantity komponenti: «−» va «+» tugmalari (aria-label="Kamaytirish" / "Ko'paytirish"), o'rtada <output aria-live="polite"> da son (boshi 1, oralig'i 1–5). 1 da «−» o'chirilgan, 5 da «+» o'chirilgan. Test yozing: (1) «+» ni to'rt marta bosing — son 5, «+» o'chirilgan; (2) faqat klaviatura: tab bilan «Ko'paytirish» ga boring (birinchi tab — «Kamaytirish» o'chirilgani uchun uni o'tkazib yuboradi!), Enter ikki marta — son 3.

Ishora: o'chirilgan tugma fokus olmaydi; toHaveTextContent("5").

Yechim
jsx
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test } from "vitest";

function Quantity() {
  const [n, setN] = useState(1);
  return (
    <div role="group" aria-label="Miqdor">
      <button
        aria-label="Kamaytirish"
        disabled={n === 1}
        onClick={() => setN(n - 1)}
      >
        −
      </button>
      <output aria-live="polite">{n}</output>
      <button
        aria-label="Ko'paytirish"
        disabled={n === 5}
        onClick={() => setN(n + 1)}
      >
        +
      </button>
    </div>
  );
}

test("sichqoncha: 5 dan oshmaydi", async () => {
  const user = userEvent.setup();
  render(<Quantity />);
  const plus = screen.getByRole("button", { name: "Ko'paytirish" });
  for (let i = 0; i < 4; i++) await user.click(plus);
  expect(screen.getByRole("status")).toHaveTextContent("5");
  expect(plus).toBeDisabled();
});

test("klaviatura: Tab va Enter", async () => {
  const user = userEvent.setup();
  render(<Quantity />);
  await user.tab();
  const plus = screen.getByRole("button", { name: "Ko'paytirish" });
  expect(plus).toHaveFocus();
  await user.keyboard("{Enter}{Enter}");
  console.log(screen.getByRole("status").textContent);
});

Konsolda:

text
  ✓ sichqoncha: 5 dan oshmaydi
3
  ✓ klaviatura: Tab va Enter
Testlar: 2 o'tdi (jami 2)

<output> ning yashirin roli — status. Ikkinchi testda birinchi tab to'g'ri «Ko'paytirish» ga tushdi: o'chirilgan «Kamaytirish» fokus olmaydi. fireEvent bilan bu tekshiruvni yozib bo'lmasdi — u fokusni umuman bilmaydi.

3-mashq (qiyin): kechikkan qidiruv

DishSearch maydonga yozilganda search(query) ni chaqiradi (soxta server, 200 ms), javob kelguncha <p role="status">Qidirilmoqda…</p>, keyin <ul aria-label="Natijalar"> da taomlar. Topilmasa — «Hech narsa topilmadi». Uch test: (1) «osh» yozilganda natijada Osh va Qovurma osh, status yo'qolgan (waitForElementToBeRemoved); (2) «pizza» — «Hech narsa topilmadi»; (3) search oxirgi marta to'liq so'z bilan chaqirilgan (toHaveBeenLastCalledWith). Qidiruv Enter bilan emas, har o'zgarishda — demak search har harfga chaqiriladi; eski javob yangisini bosib ketmasligi kerak.

Ishora: har so'rovga raqam bering va faqat oxirgisining javobini qabul qiling (race condition).

Yechim
jsx
import {
  render,
  screen,
  waitForElementToBeRemoved,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useRef, useState } from "react";
import { expect, test, vi } from "vitest";

const MENU = ["Osh", "Qovurma osh", "Lag'mon", "Manti"];

const fakeSearch = (q) =>
  new Promise((resolve) => {
    const found = MENU.filter((d) =>
      d.toLowerCase().includes(q.toLowerCase()),
    );
    setTimeout(() => resolve(found), 200);
  });

function DishSearch({ search }) {
  const [state, setState] = useState({ status: "idle", items: [] });
  const last = useRef(0);
  async function change(e) {
    const id = ++last.current;
    setState({ status: "loading", items: [] });
    const items = await search(e.target.value);
    if (id === last.current) setState({ status: "done", items });
  }
  return (
    <div>
      <label>
        Taom
        <input onChange={change} />
      </label>
      {state.status === "loading" && (
        <p role="status">Qidirilmoqda…</p>
      )}
      {state.status === "done" && state.items.length === 0 && (
        <p>Hech narsa topilmadi</p>
      )}
      {state.items.length > 0 && (
        <ul aria-label="Natijalar">
          {state.items.map((d) => <li key={d}>{d}</li>)}
        </ul>
      )}
    </div>
  );
}

test("topildi — ikki taom", async () => {
  const user = userEvent.setup();
  render(<DishSearch search={fakeSearch} />);
  await user.type(screen.getByLabelText("Taom"), "osh");
  await waitForElementToBeRemoved(screen.getByRole("status"));
  const list = screen.getByRole("list", { name: "Natijalar" });
  console.log(list.textContent);
  expect(screen.getAllByRole("listitem")).toHaveLength(2);
});

test("topilmadi", async () => {
  const user = userEvent.setup();
  render(<DishSearch search={fakeSearch} />);
  await user.type(screen.getByLabelText("Taom"), "pizza");
  expect(
    await screen.findByText("Hech narsa topilmadi"),
  ).toBeInTheDocument();
});

test("oxirgi so'rov — to'liq so'z", async () => {
  const user = userEvent.setup();
  const search = vi.fn(fakeSearch);
  render(<DishSearch search={search} />);
  await user.type(screen.getByLabelText("Taom"), "manti");
  await screen.findByRole("list", { name: "Natijalar" });
  expect(search).toHaveBeenCalledTimes(5);
  expect(search).toHaveBeenLastCalledWith("manti");
});

Konsolda:

text
OshQovurma osh
  ✓ topildi — ikki taom
  ✓ topilmadi
  ✓ oxirgi so'rov — to'liq so'z
Testlar: 3 o'tdi (jami 3)

user.type har harf uchun onChange chaqirdi — search 5 marta (m, ma, man, mant, manti). Hammasi 200 ms kutadi; id === last.current tekshiruvi bo'lmasa, oldingi so'rovlar javobi ham holatga yozilardi. Real loyihada bunday qidiruvga debounce qo'shiladi (Vitest II) — u holda test soxta soat yoki findBy ning uzunroq timeout i bilan yoziladi.

4-mashq: Vazifalar qadami

test/user-event branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling: @testing-library/user-event@14.6.7 va harakatlar.test.tsx (5 test).

Yechim

Tekshiruv ro'yxati:

  • npm test — 337 test; harakatlar.test.tsx — 5.
  • Vazifa qo'shish testida user.type ni fireEvent.change + fireEvent.submit ga almashtiring — test o'tadimi? Fokus tekshiruvi (toHaveFocus) nima deydi? Qaytaring.
  • vi.mock qatorini vaqtincha o'chiring — TilTanlov testi jsdom'ning Not implemented: navigation xatosiga uchraydi.
  • Sozlash fayliga globalThis.IS_REACT_ACT_ENVIRONMENT = true; qo'shib npm test ni yurgizing — act ogohlantirishlari chiqadimi? Natijani kanon bilan solishtiring (kanonda: 349 test, 0 ogohlantirish). Keyin bitta testdan await ni olib tashlab, ogohlantirish paydo bo'lishini ko'ring va qaytaring.
  • npm run test:qamrov ni bir necha marta yurgizing: hammasi o'tsa — yaxshi; yiqilsa, qaysi test va Duration qancha ekanini yozing (beqarorlik Test strategiyasi da).

11. Real ishda

  • Kod ko'rib chiqishda fireEvent ko'p bo'lsa — "nega user-event emas?" deb so'raladi. eslint-plugin-testing-library ning prefer-user-event qoidasi buni avtomat tekshiradi. fireEvent faqat user-event qila olmaydigan hodisa uchun qoladi (scroll, resize, maxsus CustomEvent).
  • Klaviatura testi — a11y testi. tab + keyboard bilan yozilgan bitta test Bobur yo'lini butunlay tekshiradi; to'liq a11y testlari — Accessibility testlari da.
  • Beqaror testlarning yarmi — noto'g'ri kutish: findBy o'rniga getBy, waitFor o'rniga setTimeout, bo'sh hududni topish. Ular lokal kompyuterda o'tib, CI'da yiqiladi.
  • Intervyu savollari: "fireEvent va user-event farqi?", "findBy va waitFor qachon?", "act ogohlantirishi nimani bildiradi?", "soxta soat bilan user-event nega qotadi?".

Xulosa

  • user-event foydalanuvchi kabi harakat qiladi: to'liq hodisalar tartibi, fokus, disabled va pointer-events tekshiruvi. fireEvent — faqat bitta hodisa.
  • Har test: const user = userEvent.setup(), har amal await. type/keyboard da {Enter}, {Backspace}, {Shift>}…{/Shift}.
  • Forma testi — foydalanuvchining ikki yo'li: xato (matn, aria-invalid, ulangan tavsif, fokus) va yuborish (onSubmit ma'lumoti).
  • findBy — element paydo bo'lishini, waitFor — tekshiruv o'tishini, waitForElementToBeRemoved — yo'qolishini kutadi; doim bor bo'sh hududda findBy yolg'on.
  • act ogohlantirishi — kutilmagan async yangilanish; davosi RTL kutuvchisi. globals siz ogohlantirish jim — IS_REACT_ACT_ENVIRONMENT = true.
  • Soxta soat + user-event: shouldAdvanceTime: true va advanceTimers; kerak bo'lmasa — haqiqiy vaqt. vazifalar: 337 test, coverage ostida birinchi beqarorlik.

Keyingi dars: RTL III: providerlar, router va hooklar bilan test — har testda takrorlanayotgan QueryClient, router va tema o'ramini bitta chiz() yordamchisiga yig'amiz, renderHook bilan hooklarni sinaymiz.

Manbalar

  • user-event 14: Introduction, setup, Options (delay, advanceTimers), Keyboard, Pointer (testing-library.com/docs/user-event/intro)
  • Testing Library: Async Methods — findBy, waitFor, waitForElementToBeRemoved (testing-library.com/docs/dom-testing-library/api-async)
  • React: act va IS_REACT_ACT_ENVIRONMENT (react.dev/reference/react/act)
  • Kent C. Dodds, "Common mistakes with React Testing Library" — waitFor qoidalari (kentcdodds.com)
  • Haqiqiy chiqishlar: Vitest 5.0.4, RTL 16.3.3, user-event 14.6.7, jsdom 30.1.2, Node 24.21.0 (2026-10-20); natija oynasi — Chrome 155
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
React Testing Library II: user-event, findBy, waitFor va forma testlari — IlmHamroh