IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash8/16-dars25 daqiqa
Mundarija (37)

Vitest Browser Mode: komponent testini haqiqiy Chrome'da yurgizish — lokatorlar, expect.element va jsdom farqi

Qisqacha: Vitest Browser Mode test faylini jsdom'da emas, haqiqiy brauzerda (Chrome, Firefox, WebKit) ishga tushiradi. Brauzerni provayder ochadi — odatda Playwright: browser: { enabled: true, provider: playwright(), instances: [{ browser: "chromium" }] }. Elementlar page.getByRole(...) lokatori bilan topiladi, tekshiruv await expect.element(lokator).toBeVisible() — shart bajarilguncha qayta uriniladi. userEvent (vitest/browser) haqiqiy klaviatura va sichqoncha hodisalarini yuboradi. Narxi — tezlik: bitta test jsdom'dagidan taxminan 6 baravar sekin (holat 08 o'lchovi). Shuning uchun Browser Mode'ga faqat jsdom ko'rmaydigan narsalar o'tadi: modal dialog, fokus, o'lcham, CSS.

Bu darsda

  • Browser Mode qanday ishlashini (Vitest, Vite, provayder, brauzer) tushuntira olasiz.
  • @vitest/browser-playwright ni o'rnatib, alohida brauzer loyihasini sozlaysiz.
  • Lokator, expect.element va vitest/browser dagi userEvent bilan test yozasiz.
  • jsdom va haqiqiy brauzer farqini o'lchov bilan ko'rsatasiz va qaysi test qayerga borishini tanlaysiz.
  • vitest-browser-react va toMatchScreenshot nima ekanini bilasiz.

Oldin bilishingiz kerak: RTL II: foydalanuvchi harakatlari, RTL III: providerlar, MSW, dialog JS'dan.

1. Nega bu kerak?

React Testing Library I darsida bir topilma bo'lgan edi: jsdom 30.1.2 da <dialog> ning showModal() metodi yo'q. Kanon o'chirish dialogini testda open atributi bilan ochdi. Dialog ko'rindi — lekin bu haqiqiy modal emas edi. Fokus qayerga tushadi, Esc yopadimi, orqa fondagi tugma bosiladimi — hech biri tekshirilmadi.

Coverage hisobotida ham shu bo'shliq ko'ringan edi: buyruqlar oynasi (Ctrl+K) — 0 %. Uni jsdom'da ochib bo'lmaydi.

Kanon bir tajriba qildi. Bu darsdagi 6 ta brauzer testini olib, jsdom'da yurgizdi. Natija — 6 tadan 6 tasi yiqildi, ulardan 4 tasi shu xato bilan:

text
TypeError: ref.current?.showModal is not a function

Tarjima: "showModal funksiya emas". Brauzerda bor metod jsdom'da yo'q. Demak, jsdom'dagi "yashil" test ba'zan yolg'on — u foydalanuvchi ko'radigan narsani tekshirmagan.

O'xshatish: haydovchilik maktabidagi trenajor. U arzon va tez — har kuni o'nlab mashq. Lekin unda yo'l chuqurlari, yomg'ir, haqiqiy tormoz yo'q. Imtihon esa haqiqiy yo'lda. jsdom — trenajor, Browser Mode — haqiqiy yo'l.

flowchart LR
  T["test fayli"] --> J["jsdom (Node ichida)"]
  T --> B["haqiqiy Chrome"]
  J --> J1["tez: ~0.1 s/test"]
  J --> J2["showModal yo'q, o'lcham 0"]
  B --> B1["sekin: ~0.64 s/test"]
  B --> B2["modal, fokus, CSS — haqiqiy"]

Rasmga qarang: bitta test fayli ikki muhitda ishlashi mumkin. Chap yo'l tez, lekin brauzerning bir qismi yo'q. O'ng yo'l sekin, lekin hamma narsa haqiqiy.

2. Browser Mode qanday ishlaydi

2.1 To'rt ishtirokchi

Odatdagi Vitest testi Node ichida ishlaydi, document ni jsdom beradi (Vitest I). Browser Mode'da test kodi brauzer sahifasida bajariladi:

  1. Vitest (Node) — testlarni topadi, natijani terminalga chiqaradi.
  2. Vite server — test faylini va uning importlarini (React, komponentlar) brauzerga beradi — xuddi npm run dev dagidek.
  3. Provayder (provider) — brauzerni ochadigan va boshqaradigan vosita. Bizda — Playwright. Xuddi televizor pulti: televizor (brauzer) o'zi ko'rsatadi, pult esa tugmalarni bosadi.
  4. Brauzer — Chrome. Test fayli shu yerda, oddiy sahifa kabi ishlaydi.

Provayder brauzerga buyruqni CDP (Chrome DevTools Protocol) orqali yuboradi. CDP — Chrome'ni tashqaridan boshqarish kanali: DevTools ham Chrome bilan aynan shu kanal orqali gaplashadi.

sequenceDiagram
  participant V as Vitest (Node)
  participant S as Vite server
  participant P as Playwright
  participant C as Chrome
  V->>P: brauzerni och (headless)
  P->>C: ishga tushirish
  C->>S: test faylini so'raydi
  S-->>C: test + React + komponent
  C->>V: userEvent.click(...)
  V->>P: bosish buyrug'i
  P->>C: CDP: haqiqiy sichqoncha hodisasi
  C-->>V: natija: ✓ yoki ×

Diagrammadagi muhim qator — oxiridan ikkinchisi. Bosish JavaScript'dan "taqlid" qilinmaydi: Playwright Chrome'ga "shu nuqtada sichqoncha bosildi" deydi. Chrome uni xuddi odam bosgandek qabul qiladi.

2.2 Uch provayder

Provayder Paket Hodisalar Qachon
playwright @vitest/browser-playwright CDP — haqiqiy asosiy tanlov, CI ham
webdriverio @vitest/browser-webdriverio WebDriver — haqiqiy jamoa WebdriverIO'da bo'lsa
preview @vitest/browser-preview @testing-library/user-event taqlidi faqat lokal ko'rib chiqish

Vitest hujjatiga ko'ra preview headless rejimni qo'llamaydi (oyna doim ochiladi) va uning userEvent i oddiy @testing-library/user-event ning o'zi. Shuning uchun CI uchun yaramaydi. Kanon — playwright.

Tekshirib ko'ring: Browser Mode'da test kodi qayerda bajariladi — Node'dami yoki brauzerdami? console.log natijasi qayerga chiqadi?

Javob

Test kodi brauzer sahifasida bajariladi — shuning uchun document, showModal, CSS haqiqiy. console.log esa terminalda ko'rinadi: Vitest brauzer konsolini ushlab, Node'dagi hisobotga yuboradi (pastdagi chiqishlarda stdout | … qatorlari shundan).

3. O'rnatish va sozlash

3.1 Ikki paket

bash
npm i -D -E @vitest/browser-playwright@5.0.4 playwright@1.64.0

Kanondagi o'lchov: +16 paket, +22.1 MB node_modules, o'rnatish 16 s. @vitest/browser-playwright — Vitest bilan bir xil versiya bo'lishi shart (5.0.4). playwright — brauzerni boshqaradigan kutubxona; keyingi darsdagi e2e testlar ham shunga tayanadi.

Brauzerning o'zi-chi? Ikki yo'l bor:

  • Playwright o'z brauzerini yuklaydi: npx playwright install chromium. U Playwright'ning shu versiyasiga moslangan "Chrome for Testing" ni foydalanuvchi papkasidagi ms-playwright keshiga tushiradi.
  • Kompyuterdagi Chrome: channel: "chrome". Hech narsa yuklanmaydi — o'rnatilgan Google Chrome ishlatiladi.

Kanon ikkinchisini tanladi: brauzer yuklanmadi. Bu disk va vaqtni tejaydi. Narxi: testlar Chrome versiyasiga bog'liq bo'lib qoladi — Chrome yangilansa, natija ham o'zgarishi mumkin.

3.2 vitest.config.ts — to'rtinchi loyiha

Vitest I da projects — bitta konfiguratsiyada bir nechta "loyiha" (har biri o'z muhiti bilan) — ni ko'rgan edik: mantiq (node) va komponent (jsdom). Kanon yana bittasini qo'shdi:

ts
      {
        // 22/#08: Browser Mode — HAQIQIY Chrome (headless,
        // Playwright orqali, tizimdagi Chrome: channel "chrome" —
        // brauzer yuklanmaydi). jsdom'da yo'q narsalar uchun:
        // <dialog>.showModal, fokus, :modal, lazy() chunk.
        // Sekinroq — shuning uchun alohida: npm run test:brauzer
        extends: true,
        // MSW service worker (mockServiceWorker.js) shu
        // papkadan "/" ga
        publicDir: "tekshiruv/msw/ommaviy",
        test: {
          name: "brauzer",
          include: ["tekshiruv/**/*.brauzer.tsx"],
          setupFiles: ["tekshiruv/brauzer-sozlash.ts"],
          browser: {
            enabled: true,
            headless: true,
            provider: playwright({
              launchOptions: { channel: "chrome" },
            }),
            instances: [{ browser: "chromium" }],
          },
        },
      },

Fayl boshida import: import { playwright } from "@vitest/browser-playwright";.

Kalit Nima qiladi
include faqat *.brauzer.tsx — jsdom testlari bilan aralashmaydi
browser.enabled shu loyiha brauzerda ishlaydi
headless: true oynasiz — ekranda hech narsa ochilmaydi
provider Playwright; channel: "chrome" — tizimdagi Chrome
instances qaysi brauzer(lar): chromium, firefox, webkit
publicDir MSW fayli shu papkadan beriladi (pastda)

Headless (oynasiz) — brauzer ishlaydi, lekin ekranda oyna chizmaydi. CI serverida ekran yo'q, shuning uchun u yerda doim headless.

instances ga uchta brauzer yozilsa, har test uch marta — har brauzerda bir marta ishlaydi. Kanon faqat chromium bilan qoldi.

3.3 Nega npm test ga kirmadi

package.json ga alohida skript: "test:brauzer": "vitest run --project brauzer". Asosiy npm test esa avvalgidek faqat mantiq va komponent. Sabab ikkita: brauzer testlari sekinroq va ular uchun kompyuterda Chrome bo'lishi kerak. Har saqlashda ishlaydigan tez testlar tez qolsin.

4. Birinchi brauzer testi

Kanondagi dialoglar.brauzer.tsx ning birinchi testi. Avval yordamchi — dialogni chizib, haqiqiy showModal() bilan ochadi:

tsx
function dialogniOch(onJavob = vi.fn()) {
  const ref = createRef<HTMLDialogElement>();
  render(
    <>
      <button type="button">Orqadagi tugma</button>
      <OchirishDialog ref={ref} matn="Non olish" onJavob={onJavob} />
    </>,
  );
  ref.current?.showModal();
  return { dialog: ref.current, onJavob };
}

render — o'sha RTL dan: React Testing Library brauzerda ham ishlaydi. Keyin test:

tsx
  test("modal: nomi, tavsifi, fokus «Bekor qilish»da", async () => {
    const { dialog } = dialogniOch();
    const oyna = page.getByRole("dialog", {
      name: "Vazifa o'chirilsinmi?",
    });
    await expect.element(oyna).toBeVisible();
    await expect
      .element(oyna)
      .toHaveAccessibleDescription(
        "«Non olish» ro'yxatdan o'chiriladi.",
      );
    expect(dialog?.matches(":modal")).toBe(true);
    await expect
      .element(page.getByRole("button", { name: "Bekor qilish" }))
      .toHaveFocus();
  });

Uch yangi narsa bor: page va expect.element (keyingi bo'lim) hamda :modal. :modal — CSS psevdo-klassi: element hozir modal holatdami (orqa fon bloklanganmi). jsdom buni bilmaydi, Chrome biladi. Oxirgi qator — showModal fokusni birinchi tugmaga qo'yganini tekshiradi: Malika uchun bu "dialog ochildi, men qayerdaman?" savolining javobi.

Biz kanon testlarini terminalda yurgizdik (Vitest 5.0.4, Chrome headless):

bash
npx vitest run --project brauzer --reporter=verbose

Qatorlar uzun — har birining boshiga qarang: ✓, keyin |brauzer (chromium)|, oxirida vaqt.

text
 RUN  v5.0.4 C:/Users/…/vazifalar

 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > OchirishDialog — native <dialog> (showModal) > modal: nomi, tavsifi, fokus «Bekor qilish»da 37ms
 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > OchirishDialog — native <dialog> (showModal) > Esc — yopiladi, javob false 17ms
 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > OchirishDialog — native <dialog> (showModal) > «O'chirish» — form method=dialog, returnValue → javob true 105ms
 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > OchirishDialog — native <dialog> (showModal) > orqa fon inert: Tab dialogdan tashqaridagi tugmaga o'tmaydi 31ms
 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > BuyruqlarOynasi — Ctrl+K, lazy chunk, fokus qaytishi > Ctrl+K — oyna, qidiruv maydoni fokusda; Esc — fokus qaytadi 499ms
 ✓ |brauzer (chromium)| tekshiruv/dialoglar.brauzer.tsx > App — o'chirish oqimi (MSW service worker bilan) > «O'chirish» → dialog → tasdiq: qator yo'q, DELETE /1 ketdi 311ms

 Test Files  1 passed (1)
      Tests  6 passed (6)
   Duration  3.66s (tests 53%, import 18%, setup 18%, worker 11%)

Yangi belgi — |brauzer (chromium)|: loyiha nomi va brauzer. Qolgani tanish. Sizda vaqtlar boshqacha chiqadi.

5. Lokatorlar va expect.element

5.1 Lokator — element emas, "qidiruv yo'riqnomasi"

RTL'dagi screen.getByRole(...) elementni hozir qidiradi va DOM elementini qaytaradi. Topmasa — darhol xato.

Lokator (locator) boshqacha. page.getByRole("button", { name: "Saqlash" }) hech narsa qidirmaydi — u "qanday topish kerak" degan yo'riqnomani saqlaydi. Qidiruv keyin bo'ladi: bosganda (.click()) yoki tekshirganda (expect.element). O'xshatish: taksiga "Chorsu bozori, asosiy darvoza" deysiz — bu manzil, hali yetib borganingiz yo'q. Haydovchi manzilni kerak paytda topadi.

RTL screen.getByRole Vitest page.getByRole
Qaytaradi DOM element lokator
Qachon qidiradi darhol harakat yoki tekshiruv paytida
Topilmasa darhol xato qayta urinadi
Kutish findBy… alohida o'zi kutadi
Bosish user.click(element) await lokator.click()

Lokatorlar oilasi RTL so'rovlariga juda o'xshaydi: getByRole, getByLabelText, getByPlaceholder, getByText, getByAltText, getByTitle, getByTestId. So'rovlar ustuvorligi ham o'sha: avval rol.

5.2 expect.element — qayta urinadigan tekshiruv

await expect.element(lokator).toBeVisible() — "element paydo bo'lib, ko'rinmaguncha qayta tekshir". Bu RTL'dagi await waitFor(() => expect(...)) ning qisqa shakli: waitFor yozish shart emas. Matcherlar — o'sha jest-dom: toBeVisible, toHaveFocus, toHaveAccessibleName va boshqalar.

await ni unutmang. expect.element — Promise: await siz test tekshiruvni kutmasdan tugaydi.

Element umuman topilmasa nima bo'ladi? Biz ataylab yo'q nomli tugmani qidirdik — sahifada faqat «Qo'shish» bor:

tsx
test("yo'q tugma", async () => {
  render(<button>Qo'shish</button>);
  await expect
    .element(page.getByRole("button", { name: "Saqlash" }))
    .toBeVisible();
});
text
 ❯ |brauzer (chromium)| tekshiruv/tajriba2.brauzer.tsx (1 test | 1 failed) 15100ms
   × yo'q tugma 15093ms

 FAIL  |brauzer (chromium)| tekshiruv/tajriba2.brauzer.tsx > yo'q tugma
VitestBrowserElementError: Cannot find element with locator: getByRole('button', { name: 'Saqlash' })

ARIA tree:
- button "Qo'shish"

Failure screenshot:
  - .vitest/attachments/failure-screenshots/tajriba2.brauzer.tsx/yo-q-tugma.png

Tarjima: "Lokator bo'yicha element topilmadi". Uch foydali narsa bor:

  • ARIA tree — sahifadagi rollar va nomlar daraxti. Darhol ko'rinadi: tugma bor, nomi boshqa.
  • Failure screenshot — yiqilgan paytdagi sahifa rasmi. .vitest/ papkasiga tushadi.
  • 15 093 ms — test 15 soniya kutdi. Browser Mode'da test vaqti chegarasi sukut bo'yicha 15 s (Node'da — 5 s). Nomdagi bitta harf xatosi testni 15 soniyaga "osib" qo'yadi.

5.3 Yashirin elementlar

Lokator RTL kabi yashirin elementni o'tkazib yuboradi: display: none, yopiq <dialog>, hidden. Bu to'g'ri — Malika uchun ular yo'q (RTL I dagi hidden sozlamasi). Lekin bir tuzoq bor. Biz mashq yechimini yozayotganda shunday test yozdik:

tsx
await expect
  .element(page.getByRole("dialog", { name: "Bron" }))
  .not.toBeVisible();

Dialog yopildi — va test 15 soniya kutib yiqildi: Cannot find element with locator. Yopiq dialog yashirin, lokator uni topmaydi, not.toBeVisible esa "avval top, keyin tekshir" deb kutaveradi. Ikki to'g'ri yo'l:

  • getByRole("dialog", { name: "Bron", includeHidden: true }) — yashirinni ham top, keyin not.toBeVisible().
  • .not.toBeInTheDocument() — bu matcher lokatorni kutmaydi, "topilmadi" ni o'zi muvaffaqiyat deb oladi. Kanonning "Esc — yopiladi" testi shunday yozilgan.

Ikkinchisining nozikligi: yopiq dialog DOM'da turibdi, lekin test "hujjatda yo'q" deydi. Lokator uchun "hujjatda" — "foydalanuvchi ko'radigan daraxtda" degani.

Tekshirib ko'ring: Nega page.getByRole(...) ni test boshida o'zgaruvchiga saqlab, element hali chizilmagan bo'lsa ham keyin ishlatish mumkin, screen.getByRole(...) ni esa yo'q?

Javob

Lokator hech narsani darhol qidirmaydi — u faqat "qanday topish kerak" ni eslab qoladi. Har click() yoki expect.element da qidiruv yangidan bo'ladi. Shuning uchun element keyin paydo bo'lsa yoki qayta chizilsa ham topiladi. screen.getByRole esa shu zahoti qidiradi. Element yo'q bo'lsa — xato, bor bo'lsa — aynan o'sha DOM tugunini qaytaradi. React uni almashtirsa, eski tugun qo'lingizda qoladi.

6. userEvent: haqiqiy hodisalar

RTL II dagi @testing-library/user-event foydalanuvchini JavaScript bilan taqlid qiladi: pointerdown, mousedown, focus, click hodisalarini ketma-ket o'zi yaratib yuboradi. Bu juda yaxshi taqlid, lekin baribir taqlid.

Brauzerda har hodisaning isTrusted belgisi bor — "bu hodisani foydalanuvchining haqiqiy harakati yaratdimi?" JavaScript yaratgan hodisada u false. «Ishga tushir» ni bosing — natija oynasi haqiqiy Chrome, lekin hodisani user-event yaratadi:

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

function OrderButton({ onPress }) {
  return (
    <button onClick={(e) => onPress(e.isTrusted)}>
      Buyurtma berish
    </button>
  );
}

test("user-event — hodisa JavaScript'dan", async () => {
  const seen = [];
  render(<OrderButton onPress={(t) => seen.push(t)} />);
  const user = userEvent.setup();
  await user.click(screen.getByRole("button"));
  console.log(seen);
  expect(seen).toEqual([false]);
});

Konsolda:

text
(1) [false]
  ✓ user-event — hodisa JavaScript'dan
Testlar: 1 o'tdi (jami 1)

Xuddi shu komponentni Browser Mode'da ikki xil bosib ko'rdik — vitest/browser dagi userEvent va user-event kutubxonasi bilan. Terminal:

text
stdout | tekshiruv/tajriba.brauzer.tsx > isTrusted: vitest/browser userEvent
vitest/browser: [
  true,
]
stdout | tekshiruv/tajriba.brauzer.tsx > isTrusted: @testing-library/user-event
user-event: [
  false,
]

vitest/browser ning userEvent i Playwright orqali Chrome'ga haqiqiy sichqoncha hodisasini yuboradi — true. Bu nega muhim? Brauzerning ba'zi imkoniyatlari faqat haqiqiy harakatdan keyin ochiladi: to'liq ekran (requestFullscreen), yangi oyna, buferga yozish. Taqlid hodisa ularni ochmaydi. Bundan tashqari, haqiqiy bosish element ustida boshqa narsa (masalan, toast) turgan bo'lsa, o'sha narsaga tegadi — xuddi foydalanuvchidagidek.

API deyarli bir xil — RTL II dagi yozuvlar ishlaydi:

tsx
import { page, userEvent } from "vitest/browser";

await userEvent.keyboard("{Escape}");
await userEvent.keyboard("{Control>}k{/Control}"); // Ctrl+K
await page.getByRole("button", { name: "O'chirish" }).click();
await page.getByLabelText("Matn").fill("Non olish");

setup() shart emas. Lokatorning o'z metodlari ham bor: click(), fill(), hover(), selectOptions().

7. jsdom va haqiqiy brauzer: nima farq qiladi

7.1 O'lchov va joylashuv

jsdom sahifani chizmaydi — joylashuvni (layout) hisoblamaydi. Shuning uchun hamma o'lchamlar 0. Natija oynasi esa haqiqiy Chrome. Bu blokni natija oynasida ishga tushiring:

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

test("tugma kengligi — 160 px", () => {
  render(<button style={{ width: 160 }}>Buyurtma berish</button>);
  const button = screen.getByRole("button");
  const width = button.getBoundingClientRect().width;
  console.log(width);
  expect(width).toBe(160);
});

Chrome'da (natija oynasi) konsol: 160, ✓ tugma kengligi — 160 px, Testlar: 1 o'tdi (jami 1). Xuddi shu testni Vitest + jsdom'da yurgizdik:

text
 FAIL  |komponent| tekshiruv/tajriba-layout.test.tsx > layout: kenglik
AssertionError: expected +0 to be 160 // Object.is equality

- Expected
+ Received

- 160
+ 0

jsdom'da kenglik — 0. Tooltip qayerda chiqishi, matn qatorga sig'ishi, sticky sarlavha — bularning hammasi o'lchamga tayanadi va jsdom'da tekshirilmaydi.

7.2 Farqlar jadvali

Narsa jsdom 30.1.2 Chrome (Browser Mode)
dialog.showModal() yo'q (TypeError) bor, :modal, Esc
O'lcham (getBoundingClientRect) doim 0 haqiqiy
Oyna kengligi doim 1024, o'zgarmaydi page.viewport(375, 700)
matchMedia yo'q (kanon soxtasini qo'ygan) haqiqiy media query
isTrusted hodisa faqat taqlid haqiqiy (CDP)
Service Worker yo'q bor (MSW setupWorker)
Bitta test (Duration / test soni, holat 08) ~0.1 s ~0.64 s

Oyna kengligi va matchMedia ni biz tekshirdik: jsdom'da window.innerWidth — 1024. Browser Mode'da page.viewport(375, 700) dan keyin innerWidth — 375, media query ishlaydi (3-mashq).

7.3 Narx: vaqt

Kanon o'lchovi (dialoglar.brauzer.tsx, 6 test, i5-12500H) — grafikda devor soati vaqti (testdan boshlab terminal qaytguncha):

6 ta brauzer testi: sovuq va issiq start
  • Sovuq (1-marta)Vite deps + Chrome ishga tushishi24,3 s
  • Issiq 16,64 s
  • Issiq 28 s
  • Issiq 36,94 s

Manba: O'lchandi: vazifalar kanoni (holat 08), i5-12500H, Windows 11, Node 24.21.0, Vitest 5.0.4, Playwright 1.64.0, Chrome headless, 2026-10-20

Birinchi ishga tushirish 24 soniya — Vite bog'liqliklarni tayyorlaydi va Chrome ochiladi. Keyin 7–8 s. Taqqoslash uchun: 82 ta jsdom komponent testi taxminan shuncha vaqt oladi.

Bitta testga qancha tushadi? Bu qismda hamma joyda bir usul: Vitest hisobotidagi Duration ni testlar soniga bo'lamiz. Holat 08 ning yakuniy yurgizishida Duration 3.84s, 6 test — ≈ 0.64 s bitta brauzer testiga. jsdom komponent loyihasi: 82 test, Duration 7–10 s — ≈ 0.1 s. Brauzer ishga tushishi ham shu vaqt ichida, shuning uchun testlar ko'paygan sari bittasiga kamroq tushadi (keyingi holatlarda raqam boshqacha chiqishi tabiiy).

Qoida shundan kelib chiqadi: Browser Mode'ga faqat jsdom ko'ra olmaydigan narsa o'tadi. Modal, fokus tuzog'i, o'lcham, CSS, haqiqiy hodisa. Oddiy "tugma bosilsa matn chiqadi" — jsdom'da qoladi.

Tekshirib ko'ring: Uch test bor: (a) narxni formatlash funksiyasi; (b) filtr tugmasi bosilsa ro'yxat kamayadi; (c) mobil ekranda (375 px) menyu tugmasi ko'rinadi. Har birini qaysi loyihaga (mantiq, komponent, brauzer) qo'yasiz?

Javob

(a) mantiq — DOM kerak emas, node muhiti eng tez. (b) komponent — DOM kerak, lekin o'lcham yoki modal kerak emas: jsdom yetadi. (c) brauzer — media query va ekran kengligi faqat haqiqiy brauzerda ishlaydi; jsdom'da oyna doim 1024 px.

8. vitest-browser-react va MSW brauzerda

8.1 vitest-browser-react

Vitest hamjamiyati (vitest-community) React uchun alohida kichik paket chiqaradi — vitest-browser-react. Uning render i lokatorlar bilan ishlaydi:

tsx
import { render } from "vitest-browser-react";

test("buyurtma tugmasi", async () => {
  const screen = await render(<OrderButton onPress={() => {}} />);
  await screen.getByRole("button").click();
  const done = screen.getByText("Qabul qilindi");
  await expect.element(done).toBeVisible();
});

Farqlari: render — await bilan (Promise qaytaradi), qaytgan screen dagi so'rovlar — lokatorlar, tozalash testdan oldin bo'ladi. Shuning uchun yiqilgan testning komponenti brauzerda ko'rinib turadi. Biz sinab ko'rdik: vitest-browser-react 2.3.0 Vitest 5.0.4 bilan ishladi, hajmi 36 KB.

Kanon uni qo'shmadi. Sabab: RTL'ning render i brauzerda ham ishlaydi, providerli chiz() yordamchisi (RTL III) o'zgarishsiz qoldi. Bitta paket kam, bitta usul kam. Yangi loyihada, Browser Mode'dan boshlasangiz, vitest-browser-react — tabiiy tanlov.

8.2 MSW: endi haqiqiy service worker

MSW darsida Node'da setupServer ishlatgan edik — u fetch ni ichkaridan o'raydi. Brauzerda MSW boshqacha ushlaydi: haqiqiy service worker orqali (Service Worker). Handlerlar o'sha — faqat ulash boshqa:

ts
import { setupWorker } from "msw/browser";
import { handlerlar } from "./soxta-api.ts";

export const worker = setupWorker(...handlerlar);

Sozlash faylida (brauzer-sozlash.ts):

ts
// Top-level await: SW ro'yxatdan o'tib, sahifani boshqarguncha
// kutadi — aks holda birinchi so'rovlar ushlanmay tarmoqqa ketadi.
// Faqat API so'rovlari xato: Vite modul so'rovlari ham SW orqali
// o'tadi
await worker.start({
  quiet: true,
  onUnhandledFrame: "bypass",
});

Ikki nozik joy:

  • mockServiceWorker.js (MSW'ning worker fayli, npx msw init yaratadi) ilovaning public/ iga emas, tekshiruv/msw/ommaviy/ ga qo'yildi. public/ da ilovaning o'z sw.js i bor — ikki worker bir-biriga xalaqit bermasin. Shuning uchun konfiguratsiyada publicDir.
  • onUnhandledFrame: "bypass" — handleri yo'q so'rov tarmoqqa o'tadi. Node'da kanon "error" qo'ygan edi. Brauzerda esa Vite'ning modul so'rovlari ham worker orqali o'tadi — "error" ularni ham yiqitardi.

9. Vizual tekshiruv: toMatchScreenshot (qisqa)

Browser Mode haqiqiy chizadi — demak, rasmga olish mumkin. toMatchScreenshot elementni rasmga oladi va oldingi "oltin" rasm bilan solishtiradi. Biz sinab ko'rdik:

tsx
test("tugma ko'rinishi", async () => {
  render(<button style={{ padding: 8 }}>Buyurtma berish</button>);
  await expect
    .element(page.getByRole("button"))
    .toMatchScreenshot("buyurtma-tugmasi");
});

Birinchi ishga tushirish:

text
Error: expect(element).toMatchScreenshot()

No existing reference screenshot found; a new one was created. Review it before running tests again.

Reference screenshot:
  C:/Users/…/tekshiruv/__screenshots__/tajriba3.brauzer.tsx/buyurtma-tugmasi-chromium-win32.png

Tarjima: "Oltin rasm topilmadi; yangisi yaratildi. Qayta yurgizishdan oldin uni ko'rib chiqing." Birinchi marta test ataylab yiqiladi — siz rasmni ko'rib, to'g'riligini tasdiqlashingiz kerak. Ikkinchi ishga tushirishda — ✓.

Fayl nomiga qarang: -chromium-win32. Rasm brauzerga va operatsion tizimga bog'langan: Linux'dagi CI boshqa shrift bilan boshqa rasm chizadi. Bu muammo va uning yechimlari — Visual regression testlar darsida.

10. Vazifalar qadami: Browser Mode (holat 08)

10.1 Branch va o'rnatish

bash
git switch -c test/brauzer-rejimi
npm i -D -E @vitest/browser-playwright@5.0.4 playwright@1.64.0

Kanon: +16 paket, +22.1 MB, 16 s. Brauzer yuklanmadi (channel: "chrome"). vitest.config.ts ga «vitest.config.ts — to'rtinchi loyiha» bo'limidagi blok, package.json ga test:brauzer skripti.

10.2 Testlar — tekshiruv/dialoglar.brauzer.tsx (6)

Guruh Testlar
OchirishDialog modal: nom, tavsif, fokus; Esc → false; «O'chirish» → true; Tab orqa fonga chiqmaydi
BuyruqlarOynasi Ctrl+K → oyna, qidiruv maydoni fokusda; Esc → fokus joyiga qaytadi
App o'chirish oqimi: dialog → tasdiq → qator yo'q, DELETE /1 (MSW worker)

Buyruqlar oynasi testi — jsdom'da umuman yozib bo'lmaydigan oqim:

tsx
    await boshqa.click();
    await userEvent.keyboard("{Control>}k{/Control}");
    await expect
      .element(page.getByRole("dialog", { name: "Buyruqlar" }))
      .toBeVisible();
    // Birinchi ochilishda BuyruqlarRoyxati chunk'i (cmdk)
    // yuklanadi — maydon kelgach fokusni o'zi oladi (20/#11)
    await expect.element(page.getByRole("combobox")).toHaveFocus();
    await userEvent.keyboard("{Escape}");

lazy() bilan ajratilgan qism (code splitting) haqiqatan tarmoqdan yuklanadi, expect.element uni kutadi. Bobur (faqat klaviatura) uchun eng muhim narsa — oxirgi tekshiruv: yopilgach fokus avvalgi maydonga qaytadi.

Kanon bir topilmani izohga yozdi: orqa fon inert testida Vitest lokatori includeHidden: true bilan orqadagi tugmani topadi — shuning uchun tekshiruv "topildimi" emas, "fokus unga o'tmadimi" orqali (Tab uch marta, har safar not.toHaveFocus()).

10.3 Uch tur tuzog'i (o'lchandi)

npm run tip (tsc -b) birinchi o'tishda yiqildi — uch sabab bilan:

  1. TS2320 × 16. @vitest/browser jest-dom matcherlarining o'z tur nusxasini olib keladi. RTL I dagi o'zimizning declare module "vitest" e'lonimiz u bilan to'qnashdi. Yechim — jest-dom.ts birinchi qatorida bitta manba: /// <reference types="@vitest/browser/matchers" />. Vitest nusxasida hali yo'q ikki matcher (toBePressed, toBePartiallyPressed) qo'lda qo'shildi.
  2. 190+ TS2304. Playwright turlari DOM'ga (HTMLElement, Node) tayanadi. vitest.config.ts esa tsconfig.node.json da edi — u yerda DOM ataylab yo'q (tsconfig app/node). Yechim — vitest.config.ts uchun alohida tsconfig.vitest.json (DOM bilan).
  3. BufferEncoding topilmadi. vitest/browser turlari Node'ning BufferEncoding turini so'raydi. TS 6 da types sukut bo'yicha bo'sh — @types/node o'zi ulanmaydi. Yechim — tsconfig.test.json da "types": ["node"].
jsonc
{
  // 22/#08: vitest.config.ts alohida — u
  // @vitest/browser-playwright ni import qiladi, Playwright turlari
  // esa DOM'ga (HTMLElement, Node) tayanadi. tsconfig.node.json da
  // DOM ataylab yo'q (16/#42) — uni buzmaslik uchun faqat shu fayl
  // DOM lib bilan tekshiriladi
  "extends": "./tsconfig.node.json",
  "compilerOptions": {
    "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.vitest.tsbuildinfo",
    "lib": ["es2024", "DOM"]
  },
  "include": ["vitest.config.ts"]
}

.vitest/ (yiqilgan test rasmlari) — .gitignore, ESLint va Prettier e'tiboridan chiqarildi; knip'ga MSW worker fayli ignore ga.

10.4 Natija va commit

  • Testlar: Vitest 348 (o'zgarmadi) + brauzer 6. npm run test:brauzer — Duration 3.84 s; sovuq start 24.3 s.
  • Coverage: satrlar 62.72 % — o'zgarmadi. test:qamrov faqat mantiq va komponent ni o'lchaydi; brauzer testlari raqamga kirmaydi (Browser Mode'da coverage yig'ish mumkin, kanonda o'lchanmagan).
  • Ilova JS — 206.7 KB gzip, o'zgarmadi: test paketlari faqat devDependencies da.
  • Lint, format:check, tip, knip — toza. Zavod e2e: 159, 72, 100, 77, 81; axe — 0.
bash
git add -A
git commit

Commit (kanon): test: Vitest Browser Mode (Playwright, tizim Chrome) — dialog, Ctrl+K, o'chirish oqimi.

11. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
test 15 s osilib, Cannot find element with locator nom xato yoki element yashirin ARIA tree ga qarang; yashirin — includeHidden: true
not.toBeVisible() 15 s kutadi yashirin elementni lokator topmaydi includeHidden: true yoki not.toBeInTheDocument()
tekshiruv "o'tdi", lekin hech narsa tekshirilmadi expect.element oldida await yo'q doim await expect.element(...)
Executable doesn't exist … npx playwright install Playwright brauzeri yo'q npx playwright install chromium yoki channel: "chrome"
TS2320 (jest-dom turlari) ikki tur nusxasi /// <reference types="@vitest/browser/matchers" />
MSW so'rovni ushlamaydi worker ishga tushguncha so'rov ketgan await worker.start() sozlash faylida
hamma test sekinlashdi jsdom testlari ham brauzerga ko'chgan faqat jsdom ko'rmaydiganlar — *.brauzer.tsx

12. Mashqlar

Mashqlar kurs/mashqlar/22/08-brauzer-rejimi/ da. Brauzer testlari natija oynasida ishlamaydi — ularni vazifalar loyihangizda (holat 08 dan) tekshiruv/ ga qo'yib, npx vitest run --project brauzer bilan yurgizing.

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

  • Brauzer loyihasini yoqadigan kalit: browser: { : true }
  • Qayta urinadigan tekshiruv: await expect.(lokator).toBeVisible()
  • Haqiqiy hodisalar uchun import: import { page, userEvent } from ""
  • Oynasiz rejim: : true
  • Ekran kengligini o'zgartirish: await page.(375, 700)
Yechim
  • enabled — shu loyiha brauzerda ishlaydi.
  • element — expect.element lokatorni shart bajarilguncha qayta tekshiradi.
  • vitest/browser — page (lokatorlar) va userEvent (CDP orqali haqiqiy hodisalar) shu yerda.
  • headless — CI'da ekran yo'q, oyna chizilmaydi.
  • viewport — media query'larni sinash uchun; jsdom'da kenglik doim 1024.

2-mashq (o'rta): fokus qaytadimi?

«Bahor» saytida «Stol bron qilish» tugmasi <dialog> ni showModal() bilan ochadi. Dialog ichida «Mehmonlar soni» maydoni va <form method="dialog"> ichida «Yopish» tugmasi. Ikki brauzer testi yozing: (1) ochilganda fokus maydonda; Esc — dialog yopiladi va fokus «Stol bron qilish» tugmasiga qaytadi; (2) «Yopish» bosilsa ham fokus tugmaga qaytadi.

Ishora: yopiq dialogni tekshirish — «Yashirin elementlar» bo'limidagi tuzoqni eslang. type="number" ning roli — spinbutton (RTL I).

Yechim
tsx
import { useRef } from "react";
import { expect, test } from "vitest";
import { page, userEvent } from "vitest/browser";
import { render } from "@testing-library/react";

function BookingDialog() {
  const ref = useRef<HTMLDialogElement>(null);
  return (
    <>
      <button type="button" onClick={() => ref.current?.showModal()}>
        Stol bron qilish
      </button>
      <dialog ref={ref} aria-labelledby="bron-sarlavha">
        <h2 id="bron-sarlavha">Bron</h2>
        <label>
          Mehmonlar soni
          <input type="number" defaultValue={2} />
        </label>
        <form method="dialog">
          <button>Yopish</button>
        </form>
      </dialog>
    </>
  );
}

// Yopiq dialog a11y daraxtida yo'q — lokator uni topishi uchun
// includeHidden: true (aks holda not.toBeVisible 15 s kutib yiqiladi)
const dialog = () =>
  page.getByRole("dialog", { name: "Bron", includeHidden: true });

test("Esc — dialog yopiladi, fokus tugmaga qaytadi", async () => {
  render(<BookingDialog />);
  const open = page.getByRole("button", { name: "Stol bron qilish" });
  await open.click();
  await expect.element(dialog()).toBeVisible();
  // showModal birinchi fokuslanadigan elementga fokus beradi
  await expect
    .element(page.getByRole("spinbutton", { name: "Mehmonlar soni" }))
    .toHaveFocus();
  await userEvent.keyboard("{Escape}");
  await expect.element(dialog()).not.toBeVisible();
  await expect.element(open).toHaveFocus();
});

test("«Yopish» — fokus yana tugmada", async () => {
  render(<BookingDialog />);
  const open = page.getByRole("button", { name: "Stol bron qilish" });
  await open.click();
  await page.getByRole("button", { name: "Yopish" }).click();
  await expect.element(dialog()).not.toBeVisible();
  await expect.element(open).toHaveFocus();
});

Biz yurgizdik — ikkalasi o'tdi (133 ms va 147 ms). Fokusni ochgan tugmaga qaytarishni brauzer o'zi qiladi — kodda hech narsa yozilmagan. jsdom'da bu testni yozib ham bo'lmasdi: showModal yo'q.

3-mashq (qiyin): mobil menyu

«Bahor» sarlavhasida «Menyu» tugmasi va «Taomlar», «Bron» havolalari bor. CSS: 600 px dan tor ekranda tugma ko'rinadi, havolalar yashirinadi; kengda — aksincha. Ikki test yozing: 375 px va 1280 px. Har testdan keyin kenglikni 1280 ga qaytaring.

Ishora: CSS'ni <style> matni bilan komponent ichida bering; page.viewport(kenglik, balandlik) Promise qaytaradi; yashirin element — includeHidden: true.

Yechim
tsx
import { afterEach, expect, test } from "vitest";
import { page } from "vitest/browser";
import { render } from "@testing-library/react";

const css = `
  .burger { display: none; }
  @media (max-width: 600px) {
    .burger { display: inline-block; }
    .links { display: none; }
  }
`;

function Header() {
  return (
    <header>
      <style>{css}</style>
      <button className="burger">Menyu</button>
      <nav className="links" aria-label="Asosiy">
        <a href="#menyu">Taomlar</a> <a href="#bron">Bron</a>
      </nav>
    </header>
  );
}

const burger = () =>
  page.getByRole("button", { name: "Menyu", includeHidden: true });
const link = () =>
  page.getByRole("link", { name: "Taomlar", includeHidden: true });

afterEach(async () => {
  await page.viewport(1280, 720);
});

test("telefon (375 px): faqat «Menyu»", async () => {
  await page.viewport(375, 700);
  render(<Header />);
  await expect.element(burger()).toBeVisible();
  await expect.element(link()).not.toBeVisible();
  console.log("kenglik:", window.innerWidth);
});

test("kompyuter (1280 px): havolalar, «Menyu» yo'q", async () => {
  await page.viewport(1280, 720);
  render(<Header />);
  await expect.element(link()).toBeVisible();
  await expect.element(burger()).not.toBeVisible();
  console.log("kenglik:", window.innerWidth);
});

Terminalda (biz yurgizdik):

text
stdout | tekshiruv/mashq22-3-mobil-menyu.brauzer.tsx > telefon (375 px): faqat «Menyu»
kenglik: 375
stdout | tekshiruv/mashq22-3-mobil-menyu.brauzer.tsx > kompyuter (1280 px): havolalar, «Menyu» yo'q
kenglik: 1280

afterEach muhim: viewport butun brauzer sahifasiniki. Qaytarilmasa, keyingi test faylidagi testlar ham 375 px da ishlab, tasodifan yiqilishi mumkin. jsdom'da esa innerWidth doim 1024 — media query hech qachon ishlamaydi.

4-mashq: Vazifalar qadami

test/brauzer-rejimi branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling. Avval jest-dom.ts ni o'zgartirmasdan npm run tip ni yurgizib, TS2320 xatolarini o'zingiz ko'ring.

Yechim

Tekshiruv ro'yxati:

  • npm run test:brauzer — 6/6; birinchi marta sekin (20 s atrofida), keyin 7–8 s.
  • npm test — 348 (brauzer testlari bunga kirmaydi).
  • dialoglar.brauzer.tsx ni nusxalab .test.tsx qilib jsdom'da yurgizing: showModal is not a function — jsdom chegarasini o'z ko'zingiz bilan ko'rasiz. Nusxani o'chiring.
  • Bitta lokator nomini ataylab buzing: test 15 s kutadi, .vitest/attachments/ da rasm paydo bo'ladi. Qaytaring.
  • npm run tip, lint, knip — toza.

13. Real ishda

  • Dizayn tizimlari va UI kutubxonalari (Radix, React Aria kabi) fokus, klaviatura va joylashuvni haqiqiy brauzerda sinaydi — jsdom bunday komponentlar uchun yetmaydi.
  • Odatdagi loyihada ko'pchilik komponent testi jsdom'da qoladi, Browser Mode — bir necha o'nta "og'ir" test uchun. Storybook testlari ham Browser Mode ustida ishlaydi — u yerda yana ko'ramiz.
  • CI'da Browser Mode uchun brauzer kerak: GitHub'ning Ubuntu runner'ida Chrome bor, yo'q bo'lsa npx playwright install --with-deps chromium (Playwright II).
  • Intervyu savollari: "jsdom nimani qila olmaydi?", "lokator va element farqi?", "nega hamma testni brauzerda yurgizmaymiz?".

Xulosa

  • Browser Mode test faylini haqiqiy brauzerda yurgizadi; brauzerni provayder (Playwright) CDP orqali boshqaradi.
  • Sozlash — alohida loyiha: browser.enabled, provider: playwright(), instances; kanonda tizim Chrome'i (channel: "chrome").
  • page.getByRole — lokator: qidiruv harakat paytida; await expect.element(...) o'zi qayta uradi. Yashirin elementga includeHidden.
  • vitest/browser dagi userEvent — haqiqiy hodisa (isTrusted: true), user-event — taqlid.
  • jsdom'da o'lcham 0, showModal yo'q, kenglik 1024 — bularni faqat brauzer tekshiradi; narxi ~6 baravar sekinlik.
  • vazifalar: 348 + 6 brauzer testi, +22.1 MB, ilova 206.7 KB — o'zgarmadi.

Keyingi dars: Playwright I: e2e testlar asoslari — endi bitta komponent emas, butun ilova: production build, router, tarmoq va foydalanuvchi yo'li boshidan oxirigacha.

Manbalar

  • Vitest: Browser Mode — "Why Browser Mode", "Configuring Playwright", "Locators", "Assertions" (expect.element), "Interactivity API" (vitest.dev/guide/browser)
  • Vitest: testTimeout — Node'da 5 000, Browser Mode'da 15 000 ms (vitest.dev/config/testtimeout)
  • Vitest: Preview provider cheklovlari (vitest.dev/config/browser/preview)
  • vitest-browser-react — github.com/vitest-community/vitest-browser-react
  • MSW: Browser integration — setupWorker, npx msw init (mswjs.io/docs/integrations/browser)
  • MDN: Event.isTrusted, HTMLDialogElement.showModal(), :modal
  • Haqiqiy chiqishlar: Vitest 5.0.4, @vitest/browser-playwright 5.0.4, Playwright 1.64.0, jsdom 30.1.2, Node 24.21.0, Chrome headless (2026-10-20)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Vitest Browser Mode: komponent testini haqiqiy Chrome'da yurgizish — lokatorlar, expect.element va jsdom farqi — IlmHamroh