Mundarija (42)
- Bu darsda
- 1. Nega bu kerak?
- 2. Bir marta kirish: storageState
- 2.1 G'oya
- 2.2 Setup fayli
- 2.3 Loyihalar va bog'liqlik
- 2.4 Faylda nima bor
- 2.5 Kirmagan foydalanuvchi
- 3. Fixture'lar
- 3.1 Tayyor fixture'lar
- 3.2 O'z fixture'ingiz: test.extend
- 4. Tarmoqni boshqarish: page.route chuqur
- 4.1 To'rt javob usuli
- 4.2 Server 500
- 4.3 Uzilish va sekin server
- 4.4 Kuzatuv: 500 va uzilish — bir xil matn
- 4.5 Yana uch imkoniyat (hujjat bo'yicha)
- 5. Qayta urinish (retries) va beqaror testlar
- 6. Trace viewer: yiqilgan testning "qora qutisi"
- 6.1 Trace nima
- 6.2 Narxi va ochish
- 7. Parallel ishlash va sharding
- 8. Page Object Model
- 8.1 Muammo va g'oya
- 8.2 Playwright'da va tuzoqlari
- 9. GitHub Actions: test.yml
- 9.1 Fayl
- 9.2 Qatorma-qator
- 9.3 Tekshiruv: actionlint — va halol eslatma
- 10. Hujumchi nigohi
- 11. Vazifalar qadami: auth, tarmoq, trace, CI (holat 10)
- 11.1 Branch va o'zgarishlar
- 11.2 Natija va commit
- 12. Ko'p uchraydigan xatolar
- 13. Mashqlar
- 1-mashq (oson): bo'sh joylarni to'ldiring
- 2-mashq (o'rta): yozuv xatosi
- 3-mashq (qiyin): Page Object Model + fixture
- 4-mashq: Vazifalar qadami
- 14. Real ishda
- Xulosa
- Manbalar
Playwright II: auth, tarmoq, trace va CI — storageState, page.route, retries va GitHub Actions
Qisqacha: Har testda login qilish o'rniga bir marta kiriladi va brauzer holati faylga yoziladi (
storageState) — qolgan testlar "kirgan" bo'lib boshlanadi.page.routeserver xatosi (route.fulfill({ status: 500 })), uzilish (route.abort()) va sekin javobni sinashga imkon beradi. CI'daretries: 2vatrace: "on-first-retry": yiqilgan test qayta yurgiziladi va uning trace'i — har qadam, DOM surati, tarmoq — yoziladi,npx playwright show-tracebilan ochiladi. Fixture (test.extend) va Page Object Model testlarni qisqa qiladi. GitHub Actions'da testlar parallel job'larda ishlaydi, hisobot artifact bo'lib saqlanadi.
Bu darsda
storageStateva setup loyihasi bilan bir marta kirib, holatni barcha testlarga berasiz.- O'z fixture'ingizni (
test.extend) va Page Object Model'ni yozasiz. page.routebilan server xatosi, uzilish va sekin javobni sinaysiz (fulfill,fallback,abort).retries, trace viewer va sharding'ni sozlab, natijasini o'qiysiz.- GitHub Actions workflow'ini yozib, actionlint bilan tekshirasiz va xavfsizlik tuzoqlarini bilasiz.
Oldin bilishingiz kerak: Playwright I, GitHub Actions: birinchi CI, MSW, useOptimistic.
1. Nega bu kerak?
O'tgan darsdagi 5 ta e2e test ishlaydi. Lekin to'rtta dard qoldi:
- Har test kirish formasini to'ldiradi.
beforeEachda — 5 marta bir xil ish. Testlar 50 ta bo'lsa — 50 marta. - "Yomon kun" sinalmagan. Server 500 qaytarsa, internet uzilsa, javob 2 soniya kechiksa — ilova nima ko'rsatadi? Haqiqiy serverni ataylab buzib bo'lmaydi.
- CI'da yiqilgan testni ko'rib bo'lmaydi. Lokal kompyuterda UI mode bor. CI serverida esa ekran yo'q, faqat log.
- Testlarni kim yurgizadi? Hozir — siz, qo'lda. Esdan chiqsa, buzilgan kod
mainga kiradi.
Bu darsda to'rttasiga ham yechim: storageState, page.route, trace va GitHub Actions.
2. Bir marta kirish: storageState
2.1 G'oya
Kirgandan keyin brauzer "eslab qoladi": cookie'lar va localStorage da sessiya bor. Playwright shu holatni faylga yoza oladi — storageState (saqlash holati). Keyingi test yangi kontekstni shu fayldan boshlaydi: forma yo'q, sahifa darhol "kirgan".
O'xshatish: mehmonxonadagi kalit-karta. Har safar resepshnda pasport ko'rsatmaysiz — bir marta ro'yxatdan o'tasiz, karta beriladi.
flowchart LR
S["kirish loyihasi: kirish.setup.ts"] --> F["e2e/.auth/foydalanuvchi.json"]
F --> A["chrome: test 1"]
F --> B["chrome: test 2"]
F --> C["chrome: test …"]Avval kirish loyihasi bir marta ishlaydi va faylni yozadi. Keyin chrome loyihasining hamma testlari shu fayldan boshlanadi.
2.2 Setup fayli
Kanondagi e2e/kirish.setup.ts:
import { expect, test as setup } from "@playwright/test";
import { soxtaApi } from "./soxta-api.ts";
const HOLAT = "e2e/.auth/foydalanuvchi.json";
setup("kirish", async ({ page }) => {
await soxtaApi(page);
await page.goto("/kirish");
await page.getByLabel("Ismingiz").fill("Sinovchi");
await page.getByRole("button", { name: "Kirish" }).click();
await expect(page).toHaveURL("/");
await expect(
page.getByRole("textbox", { name: "Yangi vazifa" }),
).toBeVisible();
await page.context().storageState({ path: HOLAT });
});test as setup — o'sha test, faqat o'qish uchun boshqa nom. Muhim joy — storageState dan oldingi ikki expect: kirish tugaganini kutamiz. Shoshilsak, faylga yarim holat yoziladi.
Bizdagi kirish — o'quv kirishi (soxta auth): parol yo'q, ism localStorage da. Haqiqiy login (Ro'yxat va login oqimi) bo'lsa ham usul shu: forma → kutish → storageState.
2.3 Loyihalar va bog'liqlik
playwright.config.ts dagi projects (holat 10):
projects: [
// 1) Kirish bir marta: holat (localStorage) faylga — storageState
{ name: "kirish", testMatch: /kirish\.setup\.ts/ },
// 2) Qolgan testlar shu holat bilan "kirgan" bo'lib boshlanadi
{
name: "chrome",
use: {
...devices["Desktop Chrome"],
channel: "chrome",
storageState: "e2e/.auth/foydalanuvchi.json",
},
dependencies: ["kirish"],
},
],dependencies: ["kirish"] — "chrome loyihasidan oldin kirish ni yurgiz". asosiy.spec.ts dagi beforeEach (forma to'ldirish) olib tashlandi. Terminalda kirish birinchi chiqadi:
Running 10 tests using 8 workers
ok 1 [kirish] › e2e\kirish.setup.ts:11:1 › kirish (523ms)
ok 6 [chrome] › e2e\asosiy.spec.ts:120:3 › kirmagan foydalanuvchi › himoyalangan sahifa → /kirish, kirgach — qaytadi (2.9s)
ok 3 [chrome] › e2e\asosiy.spec.ts:46:1 › oflayn: o'zgarish navbatda, internet qaytganda yuboriladi (3.1s)
…
10 passed (15.0s)2.4 Faylda nima bor
Biz yaratilgan faylni ochdik (qisqartirilgan):
{
"cookies": [],
"origins": [
{
"origin": "http://localhost:4173",
"localStorage": [
{
"name": "vazifalar:foydalanuvchi",
"value": "{\"ism\":\"Sinovchi\"}"
},
{ "name": "vazifalar:kalit", "value": "…" },
{ "name": "vazifalar:v3", "value": "{ … ro'yxat … }" }
]
}
]
}Ikki narsaga diqqat qiling:
vazifalar:v3— ro'yxat nusxasi ham saqlangan.storageStatefaqat "kirish"ni emas, hammalocalStorageni oladi. Har test shu ro'yxat bilan boshlanadi. Bizda zarari yo'q (soxta server ro'yxati bir xil), lekin bilib qo'ying: setup'da qoldirilgan har narsa hamma testga tarqaladi.- Haqiqiy loyihada bu yerda sessiya cookie'si yoki token bo'ladi — u parolga teng. Shuning uchun
e2e/.auth/—.gitignoreda (Playwright I dan). Playwright hujjati ham aynan shuni tavsiya qiladi.
2.5 Kirmagan foydalanuvchi
Himoyalangan sahifani (himoyalangan yo'llar) kirmagan holda ham sinash kerak. Bitta guruh uchun holatni bo'shatamiz:
test.describe("kirmagan foydalanuvchi", () => {
// Bu guruhda storageState bo'sh — "yangi brauzer"
test.use({ storageState: { cookies: [], origins: [] } });
test("himoyalangan sahifa → /kirish, kirgach — qaytadi", async ({
page,
}) => {
await soxtaApi(page);
await page.goto("/statistika");
await expect(page).toHaveURL("/kirish?keyin=%2Fstatistika");
await page.getByLabel("Ismingiz").fill("Mehmon");
await page.getByRole("button", { name: "Kirish" }).click();
await expect(page).toHaveURL("/statistika");test.use({...}) — shu guruh uchun sozlamani almashtirish. ?keyin=%2Fstatistika — himoyalangan yo'llar darsidagi qaytish manzili; %2F — / ning URL kodi.
Vaqt: holat 09 da 5 test (har birida forma) — 12.1 s. storageState bilan 7 test — 13.5–15.4 s. Test soni ko'paydi, vaqt deyarli o'zgarmadi. Kirish og'ir bo'lgan ilovada (haqiqiy server, yo'naltirishlar) farq ancha katta bo'ladi.
Tekshirib ko'ring: Kimdir
e2e/.auth/foydalanuvchi.jsonni git'ga qo'shib, ommaviy repoga push qildi. Haqiqiy loyihada bu nega xavfli?
Javob
Faylda sessiya cookie'si yoki token bo'ladi. Uni olgan odam parolsiz o'sha hisobga kira oladi — sessiya tugaguncha. Shuning uchun .auth/ — .gitignore da, test uchun esa alohida test hisobi ishlatiladi (haqiqiy foydalanuvchi emas). Push qilingan bo'lsa — sessiyani bekor qilish (chiqish, tokenni almashtirish) kerak: git tarixidan o'chirish yetmaydi.
3. Fixture'lar
3.1 Tayyor fixture'lar
O'tgan darsda aytgan edik: async ({ page }) dagi page — fixture (tayyorlab beriladigan narsa). Test nimani so'rasa, Playwright o'shani tayyorlaydi va test tugagach tozalaydi. O'xshatish: restoranda stol — siz o'tirishingizdan oldin dasturxon yoziladi, ketganingizdan keyin yig'iladi.
| Fixture | Nima |
|---|---|
page |
yangi kontekstdagi sahifa |
context |
brauzer konteksti (setOffline, cookie'lar) |
browser |
brauzerning o'zi (yangi kontekst ochish uchun) |
request |
brauzersiz HTTP so'rov (API'ni to'g'ridan chaqirish) |
browserName |
"chromium", "firefox" yoki "webkit" |
Fixture dangasa (lazy): test so'ramasa, yaratilmaydi. context ni so'ramagan test uchun alohida ish qilinmaydi.
3.2 O'z fixture'ingiz: test.extend
Kanonda har test const server = await soxtaApi(page) bilan boshlanadi. Buni fixture qilsa bo'ladi — biz yozib, sinab ko'rdik:
import { expect, test as asos } from "@playwright/test";
import { soxtaApi, type SoxtaServer } from "./soxta-api.ts";
const test = asos.extend<{ server: SoxtaServer }>({
server: async ({ page }, use) => {
const server = await soxtaApi(page);
await use(server);
// use() dan keyingi qism — test tugagach (tozalash)
},
});
test("qo'shish — server fixture bilan", async ({ page, server }) => {
await page.goto("/");
const maydon = page.getByRole("textbox", { name: "Yangi vazifa" });
await maydon.fill("Choy damlash");
await maydon.press("Enter");
await expect.poll(() => server.sorovlar).toContain("POST");
});asos.extend<{ server: SoxtaServer }>— yangitest: eskisining hamma fixture'lari +server.- Fixture funksiyasi boshqa fixture'ni so'ray oladi (
{ page }). await use(server)— "mana, testga ber". Test shu yerda ishlaydi.usedan keyingi qatorlar — test tugagach (tozalash).
Natija: test faqat "nima" ni yozadi, tayyorgarlik bir joyda. Bunday fixture'lar odatda e2e/fixtures.ts da turadi va testlar test ni o'sha fayldan import qiladi.
4. Tarmoqni boshqarish: page.route chuqur
4.1 To'rt javob usuli
Playwright I da route.fulfill bilan soxta server qurgan edik. Handler so'rov bilan to'rt xil ish qila oladi:
| Metod | Nima qiladi |
|---|---|
route.fulfill({ status, json }) |
serverga bormasdan javob beradi |
route.fallback() |
shu naqshdagi oldingi handlerga uzatadi |
route.continue() |
handlerlarni o'tkazib, haqiqiy tarmoqqa yuboradi |
route.abort("internetdisconnected") |
so'rovni uzadi — tarmoq xatosi |
Bir manzilga bir nechta page.route bo'lsa, ular teskari tartibda ishlaydi: oxirgi qo'shilgani birinchi. Shuning uchun testda umumiy soxta server ustidan "bugungi buzilish" ni qo'shish oson.
4.2 Server 500
Kanondagi e2e/tarmoq.spec.ts ning birinchi testi:
const server = await soxtaApi(page);
// Keyin qo'shilgan route birinchi tekshiriladi: GET — 500,
// qolgani route.fallback() bilan oldingi (soxtaApi) handlerga
let buzuq = true;
await page.route(API, (route) =>
buzuq && route.request().method() === "GET"
? route.fulfill({ status: 500, json: { xato: "Ataylab" } })
: route.fallback(),
);
await page.goto("/");
// Ilova sabab bo'yicha ajratmaydi (500 ham, uzilish ham —
// bir xil matn)
await expect(page.locator("#aloqa")).toHaveText(
"Serverga ulanib bo'lmadi. Ro'yxat — shu brauzerdagi nusxa.",
);
buzuq = false;
await page.getByRole("button", { name: "Qayta urinish" }).click();
await expect(page.locator("#aloqa")).toHaveText("");buzuq o'zgaruvchisi — server "tuzaldi" degan belgi. Qadamma-qadam:
| Qadam | buzuq |
GET kimga boradi | #aloqa da |
|---|---|---|---|
page.goto("/") |
true |
yangi handler → 500 | «Serverga ulanib bo'lmadi…» + «Qayta urinish» |
buzuq = false |
false |
— | o'zgarmaydi |
| «Qayta urinish» bosildi | false |
fallback() → soxta server |
bo'sh |
4.3 Uzilish va sekin server
await page.route(API, (route) => route.abort("internetdisconnected"));abort — fetch TypeError bilan yiqiladi, xuddi Wi-Fi o'chgandek. Sekin server — handler ichida kutish, keyin fallback():
await page.route(API, async (route) => {
await new Promise((tayyor) => setTimeout(tayyor, 1500));
await route.fallback();
});Test avval yuklanish holatini (#aloqa bo'sh emas), keyin ro'yxatni kutadi. 1.5 s — web-first assertion'ning 5 s chegarasidan ancha kam.
4.4 Kuzatuv: 500 va uzilish — bir xil matn
Kanon testi birinchi o'tishda yiqildi: test server xabarini kutgan, ilova esa 500 da ham "Serverga ulanib bo'lmadi" dedi. Bu xato emas — UX qarori: foydalanuvchi uchun natija bir xil (yangi ma'lumot yo'q, nusxa ko'rsatiladi). Lekin uni ongli qilish kerak edi. Test shu qarorni yozma qildi.
Farqlash kerak bo'lsa — ilova fetch xatosi turini ajratadi. G'oyani natija oynasida ko'ramiz: bu yerda page.route yo'q, uning o'rniga loadMenu funksiyasini tashqaridan beramiz (bog'liqlikni tashqaridan berish). «Ishga tushir» ni bosing:
import { render, screen } from "@testing-library/react";
import { useEffect, useState } from "react";
import { expect, test, vi } from "vitest";
function Menu({ loadMenu }) {
const [state, setState] = useState("yuklanmoqda");
useEffect(() => {
loadMenu().then(
(res) => setState(res.ok ? "tayyor" : `server ${res.status}`),
() => setState("internet yo'q"),
);
}, [loadMenu]);
return <p role="status">{state}</p>;
}
test.each([
["server 500", () => Promise.resolve({ ok: false, status: 500 })],
["internet yo'q", () => Promise.reject(new TypeError("fetch"))],
["tayyor", () => Promise.resolve({ ok: true, status: 200 })],
])("%s", async (text, load) => {
render(<Menu loadMenu={vi.fn(load)} />);
const status = await screen.findByText(text);
expect(status).toHaveAttribute("role", "status");
});Konsolda:
✓ server 500
✓ internet yo'q
✓ tayyor
Testlar: 3 o'tdi (jami 3)Promise.resolve({ ok: false }) — server javob berdi, lekin xato bilan (fetch xatolari). Promise.reject(TypeError) — javobning o'zi kelmadi. page.route da birinchisi — fulfill({ status: 500 }), ikkinchisi — abort().
4.5 Yana uch imkoniyat (hujjat bo'yicha)
context.route(...)— sahifa emas, butun kontekst uchun (yangi oynalar ham).page.routeFromHAR("fayl.har")— oldin yozib olingan haqiqiy tarmoq javoblarini qayta o'ynatish.requestfixture — brauzersiz API chaqiruvi: masalan, testdan oldin server orqali ma'lumot tayyorlash.
Kanonda ular ishlatilmagan.
5. Qayta urinish (retries) va beqaror testlar
Kanon: retries: CI ? 2 : 0. Lokal kompyuterda qayta urinish yo'q — beqaror test darhol ko'rinsin. CI'da esa ikki marta qayta urinadi. Biz ataylab birinchi urinishda yiqiladigan test yozib, CI=1 bilan yurgizdik:
test("beqaror: birinchi urinishda yiqiladi", async (
{ page },
info,
) => {
await soxtaApi(page);
await page.goto("/");
expect(info.retry).toBeGreaterThan(0);
});info.retry (test ma'lumoti — testInfo) — nechanchi urinish (0 — birinchisi). Natija (qisqartirilgan) — ikkinchi qatordagi uch belgiga va oxiridagi 1 flaky ga qarang:
Running 2 tests using 1 worker
·×±
::error file=e2e\tajriba10.spec.ts,title=[chrome] › e2e\tajriba10.spec.ts:4:1 › beqaror: birinchi urinishda yiqiladi,line=7,col=26:: 1) [chrome] › …
…
1 flaky
[chrome] › e2e\tajriba10.spec.ts:4:1 › beqaror: birinchi urinishda yiqiladi ────────────────────
1 passed (11.0s)·×±—dothisobotchisining belgilari:·— o'tdi (kirish),×— yiqildi (qayta urinadi),±— qayta urinishda o'tdi. Konfiguratsiyadadotyo'q-ku?githubvahtmlterminalga o'zi yozmaydi — shunda Playwright CI rejimidadotni o'zi qo'shadi (lokalda —line). Buni Playwright 1.64.0 manbasida tekshirdik.::error file=…,line=7— GitHub Actions uchun maxsus qator. Actions uni PR'da shu fayl, shu qator yonida izoh qilib ko'rsatadi.1 flaky— test qayta urinishda o'tdi. Butun run "yashil", lekin Playwright yashirmaydi: hisobotda alohida.
Ehtiyot bo'ling: retries beqarorlikni davolamaydi, faqat CI'ni to'xtatmaydi. Har flaky — tekshirish kerak bo'lgan signal. Sabablarini va davolashni Test strategiyasi va flaky testlar darsida ko'ramiz.
6. Trace viewer: yiqilgan testning "qora qutisi"
6.1 Trace nima
Trace (iz yozuvi) — test davomida bo'lgan hamma narsaning yozuvi: har harakat va uning vaqti, har qadamdan oldin va keyin DOM surati, tarmoq so'rovlari, konsol, test kodi. O'xshatish: samolyotning qora qutisi — avariyadan keyin nima bo'lganini qadamma-qadam tiklaydi.
use: {
// Trace (DOM suratlari, tarmoq, konsol) — faqat qayta urinishda:
// har testda yozilsa sekin va hajmli.
// Ko'rish: npx playwright show-trace
trace: "on-first-retry",| Qiymat | Qachon yoziladi |
|---|---|
"off" |
hech qachon |
"on" |
har testda |
"retain-on-failure" |
har testda, o'tganlariniki o'chiriladi |
"on-first-retry" |
faqat birinchi qayta urinishda |
Yuqoridagi beqaror testdan keyin test-results/ ga qaradik: trace faqat …-chrome-retry1/trace.zip da (20 KB). Birinchi urinish papkasida faqat error-context.md.
6.2 Narxi va ochish
Kanon o'lchovi: hamma testni --trace on bilan yurgizish — 15.6 s; 7 ta trace.zip jami 1 836 KB, playwright-report/ — 3.7 MB. Shuning uchun har testda emas.
npx playwright show-trace test-results/<test-papka>/trace.zipTrace viewer oynasi ochiladi: chapda harakatlar ro'yxati, tepada vaqt chizig'i, o'rtada tanlangan qadamdagi sahifa. «Network» tabida har so'rov va javob. HTML hisobotda ham har yiqilgan test yonida trace havolasi bor. trace.playwright.dev sayti ham ochadi — hujjatga ko'ra, fayl hech qayerga yuklanmaydi, brauzerning o'zida qayta ishlanadi.
Tekshirib ko'ring: CI'da test yiqildi, lekin lokal kompyuteringizda o'tadi. Trace'ni qayerdan olasiz va unda birinchi navbatda nimaga qaraysiz?
Javob
Trace va hisobot CI'dagi artifact'da (playwright-report). Uni Actions sahifasidan yuklab olasiz («GitHub Actions» bo'limi). Keyin show-trace bilan ochib, yiqilgan qadamni topasiz: o'sha paytdagi DOM surati (element bormidi, ustini nima yopgan), Network (API qanday javob berdi, qancha vaqtda) va konsol xatolari. Ko'pincha farq vaqtda: CI sekinroq.
7. Parallel ishlash va sharding
fullyParallel: true — hamma testlar ishchilar orasida bo'linadi. Ishchilar soni sukut bo'yicha protsessor yadrolarining yarmi. Bizning mashinada CI rejimida 8 ta ishchi edi. npm init playwright shabloni CI'da workers: 1 yozadi — kanon buni qilmadi: testlar mustaqil, parallel xavfsiz.
Testlar juda ko'p bo'lsa, ularni bir nechta mashinaga bo'lish mumkin — sharding (bo'laklash). Biz bitta kompyuterda ikki bo'lakni ketma-ket yurgizib ko'rdik:
npx playwright test --shard=1/2
npx playwright test --shard=2/2Running 6 tests using 5 workers, shard 1 of 2
ok 1 [kirish] › e2e\kirish.setup.ts:11:1 › kirish (538ms)
ok 3 [chrome] › e2e\asosiy.spec.ts:9:1 › qo'shish: Enter — qator paydo bo'ladi, POST ketadi (1.8s)
…
6 passed (11.0s)
Running 5 tests using 4 workers, shard 2 of 2
ok 1 [kirish] › e2e\kirish.setup.ts:11:1 › kirish (491ms)
…
5 passed (11.5s)9 ta test ikkiga bo'lindi (5 + 4), kirish esa ikkala bo'lakda ishladi — har bo'lak o'z bog'liqligini o'zi yurgizadi. CI'da bo'laklar parallel mashinalarda ishlaydi (job matritsasi), hisobotlar esa blob hisobotchisi va npx playwright merge-reports bilan bitta HTML'ga birlashtiriladi. 10 ta testda bu kerak emas — kanonda sharding yo'q. Yuzlab testli loyihada esa vaqtni bo'laklar soniga bo'ladi.
8. Page Object Model
8.1 Muammo va g'oya
Ro'yxat sahifasidagi lokatorlar (getByRole("textbox", { name: "Yangi vazifa" })) o'nta testda takrorlanadi. Dizayner maydon nomini «Vazifa matni» ga o'zgartirsa — o'nta joyni tuzatish kerak.
Page Object Model (POM, sahifa obyekti) — sahifa bilan ishlashni bitta klassga yig'ish. Klass "qanday" ni biladi (lokatorlar, qadamlar), test "nima" ni yozadi: "vazifa qo'sh", "o'chir". O'xshatish: televizor pulti — siz «ovoz +» ni bosasiz, ichidagi sxemani bilish shart emas.
G'oya Playwright'ga bog'liq emas — RTL testida ham ishlaydi. «Ishga tushir» ni bosing:
import { render, screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { useState } from "react";
import { expect, test } from "vitest";
function Orders() {
const [items, setItems] = useState([]);
const [text, setText] = useState("");
return (
<form
onSubmit={(e) => {
e.preventDefault();
setItems([...items, text]);
setText("");
}}
>
<input
aria-label="Taom"
value={text}
onChange={(e) => setText(e.target.value)}
/>
<ul aria-label="Buyurtmalar">
{items.map((it, i) => (
<li key={i}>{it}</li>
))}
</ul>
</form>
);
}
class OrdersPage {
user = userEvent.setup();
input = () => screen.getByRole("textbox", { name: "Taom" });
rows = () => screen.queryAllByRole("listitem");
async add(name) {
await this.user.type(this.input(), `${name}{Enter}`);
}
}
test("ikki taom — ikki qator", async () => {
render(<Orders />);
const orders = new OrdersPage();
await orders.add("Osh");
await orders.add("Manti");
const names = orders.rows().map((li) => li.textContent);
console.log(names.join(", "));
expect(orders.rows()).toHaveLength(2);
expect(orders.input()).toHaveValue("");
});Konsolda:
Osh, Manti
✓ ikki taom — ikki qator
Testlar: 1 o'tdi (jami 1)Test o'qiladi: "ochdik, Osh qo'shdik, Manti qo'shdik, 2 qator". Maydon nomi o'zgarsa — faqat OrdersPage.input tuzatiladi.
8.2 Playwright'da va tuzoqlari
Playwright versiyasi — 3-mashqda (RoyxatSahifasi klassi + fixture). Uni yozayotib ikki tuzoqqa duch keldik:
constructor(private readonly page: Page)— TS1294. Hujjatlardagi qisqa yozuverasableSyntaxOnlybilan taqiqlangan (15-qism qarori). Maydon va qiymat alohida yoziladi.npm run buildtestni ham tekshiradi. Tip xatosi tufayliwebServerishga tushmadi:Error: Process from config.webServer was not able to start. Exit code: 1. Xabar test haqida emas, build haqida —npm run buildni alohida yurgizib, sababini ko'rasiz.
POM'ni oshirib yubormang: har tugma uchun metod — ortiqcha qatlam. Kanon POM'siz: 10 ta testda lokatorlar takrori kam. Odatda u 20–30 testdan keyin o'zini oqlaydi.
9. GitHub Actions: test.yml
9.1 Fayl
GitHub Actions darsida check.yml yozgan edik, Pages darsida — pages.yml. Kanon uchinchisini qo'shdi — .github/workflows/test.yml (izohlar qisqartirilgan):
name: Testlar
on:
push:
branches: [main]
pull_request:
workflow_dispatch:
permissions:
contents: read
env:
HUSKY: 0
# Bir branch'ga ketma-ket push — eskisi bekor qilinadi
concurrency:
group: test-${{ github.ref }}
cancel-in-progress: true
jobs:
unit:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
# Vitest: mantiq + komponent; chegara tushsa — yiqiladi
- run: npm run test:qamrov
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: qamrov
path: coverage/
retention-days: 7
brauzer:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
# ubuntu-latest da Google Chrome bor — channel: "chrome".
# Bo'lmasa: npx playwright install --with-deps chrome
- run: npm run test:brauzer
e2e:
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v7
- uses: actions/setup-node@v7
with:
node-version-file: .nvmrc
cache: npm
- run: npm ci
# CI=true (GitHub o'zi qo'yadi): retries 2, trace, forbidOnly,
# "github" reporter — xatolar PR'da izoh bo'lib chiqadi
- run: npm run test:e2e
- uses: actions/upload-artifact@v7
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 79.2 Qatorma-qator
| Joy | Nima |
|---|---|
on: push / pull_request / workflow_dispatch |
main ga push, har PR va qo'lda ishga tushirish |
permissions: contents: read |
token faqat o'qiy oladi (GitHub Actions) |
HUSKY: 0 |
npm ci dagi git hook o'rnatish o'chiriladi (Husky) |
concurrency |
yangi push kelsa, eski run bekor bo'ladi |
uchta jobs |
unit, brauzer, e2e — parallel, alohida mashinalarda |
timeout-minutes: 15 |
e2e osilib qolsa, 15 daqiqada to'xtaydi (sukut — 6 soat) |
upload-artifact |
hisobot va coverage'ni run sahifasiga saqlaydi, 7 kun |
if: ${{ !cancelled() }} |
test yiqilsa ham yuklaydi — aynan o'shanda kerak |
Uchta yangi narsa: concurrency, artifact va !cancelled(). Bir gap bilan: artifact — run tugagach saqlanadigan fayl (bu yerda — HTML hisobot va trace'lar); Actions sahifasidan zip qilib yuklab olinadi. Chuqur (kesh, artifact narxi, qayta ishlatiladigan workflow) — Actions tezligi darsida.
Nega job'lar alohida? Biri yiqilsa, qolganlari baribir natija beradi: "unit yashil, e2e qizil" — xato qayerdaligi darhol ko'rinadi. Narxi — har job o'z npm ci sini qiladi (kesh yordam beradi).
ubuntu-latest da Google Chrome oldindan o'rnatilgan (GitHub runner rasmi tarkibida) — shuning uchun brauzer yuklanmaydi. Rasm o'zgarsa — izohdagi zaxira buyruq.
9.3 Tekshiruv: actionlint — va halol eslatma
YAML'dagi xato push'dan keyin bilinadi — workflow umuman ishlamaydi. Kanon faylni actionlint (workflow fayllarini tekshiradigan vosita) bilan tekshirdi. actionlint 1.7.12 rasmiy relizidan, o'rnatmasdan:
actionlint .github/workflows/pages.yml .github/workflows/test.ymlexit 0Chiqish yo'q, kod 0 — xato topilmadi. Lokal "CI rejimi" ham sinaldi: CI=1 npx playwright test — 10/10, 16.6 s (retries 2, github hisobotchi, server qayta build).
Diqqat: kanondagi
test.ymlpush qilinmagan va GitHub Actions'da ishga tushirilmagan. U actionlint'dan o'tgan va lokal CI rejimida sinalgan — xolos. Birinchi haqiqiy run'ni siz o'z repongizda ko'rasiz. Kutilmagan farqlar bo'lishi mumkin (runner'dagi Chrome versiyasi, Linux shriftlari).
10. Hujumchi nigohi
E2E testlar haqiqiy sessiya, haqiqiy so'rov va haqiqiy sahifa bilan ishlaydi — ular ham sizib chiqish manbai bo'lishi mumkin.
storageStatefayli = kalit. Repo'ga tushsa, sessiya o'g'irlanadi. Himoya:.gitignore(kanonda bor), alohida test hisobi, qisqa sessiya.- Trace va hisobot — hamma narsaning nusxasi. Ularda DOM suratlari, so'rov va javob tanalari, cookie'lar bo'lishi mumkin. Ommaviy repoda Actions artifact'ini GitHub'ga kirgan istalgan foydalanuvchi yuklab oladi. Himoya: testlarda haqiqiy ma'lumot va haqiqiy hisob yo'q (bizda — soxta server),
retention-days: 7. - Test paroli koddami? Haqiqiy login sinalsa, parol
${{ secrets.TEST_PAROL }}dan — kodda emas. Fork'dan kelgan PR'larga GitHub secret'larni bermaydi — begona kod ularni o'qiy olmaydi. Batafsil — Actions xavfsizligi. permissions: contents: read— testlar repo'ga yoza olmaydi. Zararli bog'liqlik CI'da ishga tushsa ham, kodni o'zgartira olmaydi.
11. Vazifalar qadami: auth, tarmoq, trace, CI (holat 10)
11.1 Branch va o'zgarishlar
git switch -c ci/testlarYangi paket yo'q. O'zgarishlar:
| Fayl | Nima |
|---|---|
e2e/kirish.setup.ts |
bir marta kirish → storageState |
playwright.config.ts |
kirish loyihasi, dependencies, retries, trace, CI hisobotchi |
e2e/asosiy.spec.ts |
beforeEach olib tashlandi; kirmagan foydalanuvchi testi |
e2e/tarmoq.spec.ts |
3 test: 500, uzilish, sekin server |
.github/workflows/test.yml |
uchta parallel job |
knip.json |
e2e/**/*.setup.ts — entry ga |
knip birinchi o'tishda kirish.setup.ts ni "Unused file" dedi: uni hech kim import qilmaydi, Playwright testMatch bilan topadi. Shuning uchun entry ga qo'shildi.
11.2 Natija va commit
- E2E: 10 test (kirish + 6 asosiy + 3 tarmoq), 17.0 s devor soati (Playwright 15.3 s).
- Vitest 348, brauzer 6 — o'zgarmadi. Coverage 62.72 % — o'zgarmadi.
- Ilova JS — 206.7 KB gzip, o'zgarmadi. actionlint — 0; lint,
format:check,tip, knip — toza. test.ymlpush qilinmagan (yuqoridagi eslatma).
git add -A
git commitCommit (kanon): ci: test.yml — Vitest, Browser Mode va Playwright; storageState, page.route xatolari, trace.
12. Ko'p uchraydigan xatolar
| Belgi | Sabab | Davosi |
|---|---|---|
| testlar kirish sahifasida qoladi | storageState yo'li yoki dependencies yo'q |
setup va loyiha bog'liqligini tekshiring |
| setup "o'tdi", lekin testlar kirmagan | kirish tugamasdan storageState yozilgan |
oldin expect bilan yakuniy holatni kuting |
route.fallback() haqiqiy tarmoqqa ketdi |
oldingi handler yo'q | umumiy soxta serverni birinchi qo'shing |
1 flaky har run'da |
beqaror test | trace'ni oching; davolash — Test strategiyasi |
| artifact yo'q | if: ${{ !cancelled() }} yo'q — test yiqilgach qadam o'tkazildi |
if ni qo'shing |
Process from config.webServer was not able to start |
build yiqildi (masalan TS1294) | npm run build ni alohida yurgizing |
| workflow umuman ishlamadi | YAML xatosi | push'dan oldin actionlint |
13. Mashqlar
Mashqlar kurs/mashqlar/22/10-playwright-ci/ da. Testlarni vazifalar (holat 10) ning e2e/ papkasiga qo'yib, npx playwright test bilan yurgizing.
1-mashq (oson): bo'sh joylarni to'ldiring
- Holatni faylga yozish:
await page.context().({ path: HOLAT }) - Avval setup yursin:
: ["kirish"] - Oldingi handlerga uzatish:
route.() - Trace faqat qayta urinishda:
trace: "" - Test yiqilsa ham artifact yuklansin:
if: ${{ !() }}
Yechim
- storageState — cookie'lar va
localStorageJSON faylga. - dependencies — loyiha boshqasidan keyin ishlaydi.
- fallback —
continuedan farqi: oldingi handlerlar ham ishlaydi. - on-first-retry — hajm va vaqtni tejaydi; yiqilgan testning ikkinchi urinishi yoziladi.
- cancelled — faqat run bekor qilinganda o'tkaziladi; yiqilganda — yuklanadi.
2-mashq (o'rta): yozuv xatosi
Kanonda GET 500 sinalgan. Endi teskarisi: GET ishlaydi, POST 500 qaytaradi (json: { xato: "Bazaga yozib bo'lmadi" }). Test: «Choy damlash» qo'shing va tekshiring — (1) «Saqlanmadi: Bazaga yozib bo'lmadi» matni ko'rinadi; (2) «Choy damlash» qatori yo'qoladi; (3) «Non olish #bozor» joyida; (4) soxta server POST'ni ko'rmagan.
Ishora: «Server 500» bo'limidagi kabi page.route + route.fallback(); qator nega yo'qoladi — useOptimistic ni eslang.
Yechim
import { expect, test } from "@playwright/test";
import { soxtaApi } from "./soxta-api.ts";
const API = "**/api/mashq/vazifalar/**";
test("POST 500 — qator qaytariladi, «Saqlanmadi» xabari", async ({
page,
}) => {
const server = await soxtaApi(page);
// Keyin qo'shilgan route birinchi ishlaydi: POST — 500, qolgani
// (GET) — route.fallback() bilan soxtaApi ga
await page.route(API, (route) =>
route.request().method() === "POST"
? route.fulfill({
status: 500,
json: { xato: "Bazaga yozib bo'lmadi" },
})
: route.fallback(),
);
await page.goto("/");
const maydon = page.getByRole("textbox", { name: "Yangi vazifa" });
await maydon.fill("Choy damlash");
await maydon.press("Enter");
await expect(
page.getByText("Saqlanmadi: Bazaga yozib bo'lmadi"),
).toBeVisible();
await expect(
page.getByRole("checkbox", { name: "Choy damlash" }),
).toBeHidden();
await expect(
page.getByRole("checkbox", { name: "Non olish #bozor" }),
).toBeVisible();
// POST soxtaApi gacha yetib bormadi — u faqat GET ni ko'rdi
expect(server.sorovlar).not.toContain("POST");
});Biz yurgizdik — o'tdi (1.9 s). Qator avval darhol paydo bo'ladi (optimistik), server rad etgach olib tashlanadi — 18-qismdagi xulq endi butun ilova darajasida tasdiqlandi. Xabar server yuborgan xato matnini ko'rsatadi: haqiqiy loyihada server matni foydalanuvchiga to'g'ridan chiqishi kerakmi — o'ylab ko'ring (ichki xato tafsiloti sizib chiqmasin).
3-mashq (qiyin): Page Object Model + fixture
RoyxatSahifasi klassini yozing: och(), qosh(matn), ochir(matn) va qator(matn) lokatori. Uni royxat fixture'i qilib bering (u server fixture'iga bog'liq bo'lsin). Ikki test: (1) «Choy damlash» qo'shib, o'chiring — soxta serverga DELETE /4 ketgan; (2) «Non olish» va «Sabzi olish» qo'shing — ikkita POST.
Ishora: «Page Object Model» bo'limidagi ikki tuzoq; ro'yxatda «Non olish #bozor» ham bor — qator() da exact: true.
Yechim
import { expect, test as asos, type Page } from "@playwright/test";
import { soxtaApi, type SoxtaServer } from "./soxta-api.ts";
class RoyxatSahifasi {
// "constructor(private page)" emas: erasableSyntaxOnly
// (15-qism) uni taqiqlaydi (TS1294) — maydon va qiymat alohida
readonly page: Page;
constructor(page: Page) {
this.page = page;
}
maydon = () =>
this.page.getByRole("textbox", { name: "Yangi vazifa" });
// exact: true — name sukut bo'yicha QISM bo'yicha mos keladi
qator = (matn: string) =>
this.page.getByRole("checkbox", { name: matn, exact: true });
async och() {
await this.page.goto("/");
await expect(this.maydon()).toBeVisible();
}
async qosh(matn: string) {
await this.maydon().fill(matn);
await this.maydon().press("Enter");
await expect(this.qator(matn)).toBeVisible();
}
async ochir(matn: string) {
await this.page
.getByRole("button", { name: `O'chirish: ${matn}` })
.click();
await this.page
.getByRole("dialog", { name: "Vazifa o'chirilsinmi?" })
.getByRole("button", { name: "O'chirish", exact: true })
.click();
await expect(this.qator(matn)).toBeHidden();
}
}
const test = asos.extend<{
server: SoxtaServer;
royxat: RoyxatSahifasi;
}>({
server: async ({ page }, use) => {
await use(await soxtaApi(page));
},
// royxat server'ga bog'liq — avval soxta API, keyin sahifa
royxat: async ({ page, server }, use) => {
void server;
const royxat = new RoyxatSahifasi(page);
await royxat.och();
await use(royxat);
},
});
test("qo'shib, keyin o'chirish", async ({ royxat, server }) => {
await royxat.qosh("Choy damlash");
await royxat.ochir("Choy damlash");
await expect.poll(() => server.sorovlar).toContain("DELETE /4");
});
test("ikki vazifa ketma-ket", async ({ royxat, server }) => {
await royxat.qosh("Non olish");
await royxat.qosh("Sabzi olish");
await expect
.poll(() => server.sorovlar.filter((s) => s === "POST").length)
.toBe(2);
});Biz yurgizdik — ikkalasi o'tdi. Birinchi variantda exact yo'q edi: «Non olish» qo'shilgach strict mode violation … resolved to 2 elements — «Non olish #bozor» ham mos keldi. void server — royxat fixture'i server ni ishlatmaydi, lekin so'raydi: shunda Playwright avval soxta serverni ulaydi, keyin sahifani ochadi. Tartib muhim — aks holda birinchi GET haqiqiy tarmoqqa ketardi.
4-mashq: Vazifalar qadami
ci/testlar branch'ida «Vazifalar qadami» dagi o'zgarishlarni qiling. Keyin: (1) CI=1 npx playwright test bilan lokal "CI rejimi"ni yurgizing; (2) actionlint'ni o'rnatmasdan (rasmiy relizdan) yurgizib, test.yml ni tekshiring; (3) o'z GitHub repongizga push qilib, birinchi run'ni kuzating.
Yechim
Tekshiruv ro'yxati:
CI=1 npx playwright test— 10 passed; chiqishda·belgilari (githubhisobotchi),playwright-report/yaratiladi. PowerShell'da:$env:CI=1; npx playwright test, keyinRemove-Item Env:CI.- actionlint:
exit 0. Ataylabruns-onniruns_onqiling — actionlint xato beradi. Qaytaring. - Push'dan keyin Actions sahifasida uchta job parallel ko'rinadi. Biror testni ataylab buzing: e2e job qizil,
playwright-reportartifact'i bor, PR'da xato qatori yonida izoh. - Push — o'zingiz qiladigan qadam: kanonda u qilinmagan, shuning uchun bu natijani biz sizga ko'rsata olmaymiz.
14. Real ishda
- Har PR'da e2e — jamoalarda odatiy talab: ruleset bilan
e2ejob'ini majburiy qilish. - Test hisoblari va ma'lumot tayyorlash — katta loyihalarda har ishchiga alohida hisob, ma'lumot esa API orqali (
requestfixture) tayyorlanadi; UI faqat sinaladigan qismda. - Trace — jamoaviy til: "trace'ni yubor" — CI'da yiqilgan testni muhokama qilishning standart yo'li.
- CI narxi: ommaviy repoda Actions daqiqalari bepul, yopiq repoda — limit bilan. Byudjetsiz CI yo'llari — CI narxi darsida.
- Intervyu savollari: "testlarda login'ni qanday tezlashtirasiz?", "flaky test bilan nima qilasiz?", "trace nima?", "POM qachon foydali?".
Xulosa
storageState+ setup loyihasi +dependencies— bir marta kirish; fayl kalitga teng,.gitignoreda.- Fixture (
test.extend) tayyorgarlikni bir joyga yig'adi;use()dan keyin — tozalash. page.route:fulfill(javob),fallback(oldingi handler),abort(uzilish); oxirgi qo'shilgani birinchi ishlaydi.retries: 2CI'ni to'xtatmaydi, lekinflakyni ko'rsatadi;trace: "on-first-retry"— qora quti.- Sharding testlarni mashinalarga bo'ladi; POM lokatorlarni bitta klassga yig'adi.
test.yml: uchta parallel job, artifact, actionlint 0 — push qilinmagan.vazifalar: 10 e2e test, ilova 206.7 KB.
Keyingi dars: Storybook I: komponentlarni izolyatsiyada ishlab chiqish — komponentni ilovadan alohida, har holatida ko'rish va hujjatlash.
Manbalar
- Playwright: "Authentication" (setup project,
storageState,.authni.gitignorega), "Fixtures", "Mock APIs" vaRoute(fulfill,fallback,continue,abort) (playwright.dev/docs) - Playwright: "Retries", "Trace viewer", "Sharding" va
merge-reports, "Page object models", "Continuous Integration" - GitHub Docs: "Storing workflow data as artifacts", "Using concurrency",
actions/upload-artifact - actionlint 1.7.12 — github.com/rhysd/actionlint
- Haqiqiy chiqishlar: Playwright 1.64.0, Node 24.21.0, Chrome (channel "chrome", headless), Windows 11 (2026-10-20)
Izohlar (0)
Izoh yozish uchun kiring.
- Hozircha izoh yo'q. Birinchi bo'ling!