IlmHamroh
JavaScript Full-stack/22-qism. Frontend testlash10/16-dars26 daqiqa
Mundarija (42)

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.route server xatosi (route.fulfill({ status: 500 })), uzilish (route.abort()) va sekin javobni sinashga imkon beradi. CI'da retries: 2 va trace: "on-first-retry": yiqilgan test qayta yurgiziladi va uning trace'i — har qadam, DOM surati, tarmoq — yoziladi, npx playwright show-trace bilan 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

  • storageState va setup loyihasi bilan bir marta kirib, holatni barcha testlarga berasiz.
  • O'z fixture'ingizni (test.extend) va Page Object Model'ni yozasiz.
  • page.route bilan 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:

  1. Har test kirish formasini to'ldiradi. beforeEach da — 5 marta bir xil ish. Testlar 50 ta bo'lsa — 50 marta.
  2. "Yomon kun" sinalmagan. Server 500 qaytarsa, internet uzilsa, javob 2 soniya kechiksa — ilova nima ko'rsatadi? Haqiqiy serverni ataylab buzib bo'lmaydi.
  3. CI'da yiqilgan testni ko'rib bo'lmaydi. Lokal kompyuterda UI mode bor. CI serverida esa ekran yo'q, faqat log.
  4. Testlarni kim yurgizadi? Hozir — siz, qo'lda. Esdan chiqsa, buzilgan kod main ga 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:

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

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

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

json
{
  "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. storageState faqat "kirish"ni emas, hamma localStorage ni 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/ — .gitignore da (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:

ts
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.json ni 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:

ts
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 }> — yangi test: eskisining hamma fixture'lari + server.
  • Fixture funksiyasi boshqa fixture'ni so'ray oladi ({ page }).
  • await use(server) — "mana, testga ber". Test shu yerda ishlaydi. use dan 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:

ts
  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

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

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

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

text
  ✓ 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.
  • request fixture — 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:

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

text
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)
  • ·×± — dot hisobotchisining belgilari: · — o'tdi (kirish), × — yiqildi (qayta urinadi), ± — qayta urinishda o'tdi. Konfiguratsiyada dot yo'q-ku? github va html terminalga o'zi yozmaydi — shunda Playwright CI rejimida dot ni 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.

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

bash
npx playwright show-trace test-results/<test-papka>/trace.zip

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

bash
npx playwright test --shard=1/2
npx playwright test --shard=2/2
text
Running 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:

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

text
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 yozuv erasableSyntaxOnly bilan taqiqlangan (15-qism qarori). Maydon va qiymat alohida yoziladi.
  • npm run build testni ham tekshiradi. Tip xatosi tufayli webServer ishga tushmadi: Error: Process from config.webServer was not able to start. Exit code: 1. Xabar test haqida emas, build haqida — npm run build ni 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):

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

9.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:

bash
actionlint .github/workflows/pages.yml .github/workflows/test.yml
text
exit 0

Chiqish 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.yml push 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.

  • storageState fayli = 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

bash
git switch -c ci/testlar

Yangi 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.yml push qilinmagan (yuqoridagi eslatma).
bash
git add -A
git commit

Commit (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 localStorage JSON faylga.
  • dependencies — loyiha boshqasidan keyin ishlaydi.
  • fallback — continue dan 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
ts
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
ts
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 (github hisobotchi), playwright-report/ yaratiladi. PowerShell'da: $env:CI=1; npx playwright test, keyin Remove-Item Env:CI.
  • actionlint: exit 0. Ataylab runs-on ni runs_on qiling — actionlint xato beradi. Qaytaring.
  • Push'dan keyin Actions sahifasida uchta job parallel ko'rinadi. Biror testni ataylab buzing: e2e job qizil, playwright-report artifact'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 e2e job'ini majburiy qilish.
  • Test hisoblari va ma'lumot tayyorlash — katta loyihalarda har ishchiga alohida hisob, ma'lumot esa API orqali (request fixture) 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, .gitignore da.
  • 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: 2 CI'ni to'xtatmaydi, lekin flaky ni 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, .auth ni .gitignore ga), "Fixtures", "Mock APIs" va Route (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)
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
Playwright II: auth, tarmoq, trace va CI — storageState, page.route, retries va GitHub Actions — IlmHamroh