Azimjon Abdurasulov

QA 19: Playwright (2) — Page Object Model, fixtures, auth state, network mocking va visual testlar

🧪 QA Engineer — Zero to Hero

QA 19: Playwright (2) — Page Object Model, fixtures, auth state, network mocking va visual testlar

Holat: 2026-yil oktabr. Dars Playwright 1.64 (@playwright/test), Node.js 24 LTS va TypeScript asosida yozilgan. Barcha kodlar www.saucedemo.com va practicesoftwaretesting.com (Toolshop) saytlarida Chromium'da ishga tushirib tekshirilgan.

Bu darsdan keyin siz: QA 18dagi "ishlaydigan" testlarni katta jamoada yillab yashay oladigan framework'ga aylantirasiz.

  • Page Object Model (POM) bilan locator va harakatlarni bitta joyga yig'asiz;
  • test.extend() bilan o'z fixture'laringizni yozasiz (test, worker va auto fixture);
  • login'ni bir marta qilib, storageState bilan barcha testlarda qayta ishlatasiz;
  • tarmoq so'rovlarini mock qilasiz, o'zgartirasiz va HAR fayldan qayta o'ynatasiz;
  • vizual regressiyani toHaveScreenshot() bilan ushlaysiz, projects va reporter'larni sozlaysiz.

Mundarija

  1. Muammo: nusxa-ko'chirilgan testlar
  2. Page Object Model (POM)
  3. Fixtures: test.extend() bilan toza testlar
  4. Autentifikatsiya: storageState va setup project
  5. Data-driven testlar va muhit sozlamalari
  6. Tarmoqni boshqarish: route, waitForResponse, HAR
  7. Vizual testlar: toHaveScreenshot()
  8. Projects, retries, serial rejim va reporter'lar
  9. Best practices va ESLint
  10. Amaliy mashqlar
  11. Xulosa
  12. Manbalar

1. Muammo: nusxa-ko'chirilgan testlar {#muammo}

QA 18'da biz saucedemo uchun to'g'ridan-to'g'ri locator'lar bilan testlar yozdik. 3–5 ta test bo'lganda bu juda qulay. Endi tasavvur qiling: loyihada 300 ta UI test bor va ularning har biri shunday boshlanadi:

// tests/ui/cart.spec.ts — "oldingi" uslub
test("savatchaga mahsulot qo'shish", async ({ page }) => {
  await page.goto("/");
  await page.getByTestId("username").fill("standard_user");
  await page.getByTestId("password").fill("secret_sauce");
  await page.getByTestId("login-button").click();

  await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
  await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");
});

// tests/ui/checkout.spec.ts — yana o'sha 4 qator login
test("buyurtma berish", async ({ page }) => {
  await page.goto("/");
  await page.getByTestId("username").fill("standard_user");
  await page.getByTestId("password").fill("secret_sauce");
  await page.getByTestId("login-button").click();
  // ...
});

Bu yondashuvning muammolari:

MuammoReal oqibat
TakrorlanishDasturchi data-test="login-button"ni o'zgartirsa, 300 ta faylni tuzatish kerak
O'qilishi qiyinTest biznes ssenariyni emas, "qaysi tugma bosildi"ni hikoya qiladi
SekinHar bir test UI orqali login qiladi — 300 test × bir necha soniya
Mo'rt (brittle)Locator'lar fayllar bo'ylab sochilgan, bittasini yangilab, ikkinchisini unutasiz

Bu darsda shu testlarni bosqichma-bosqich yaxshilaymiz. Oxirida loyiha shunday ko'rinishda bo'ladi:

qa-demo-tests/
├── package.json
├── playwright.config.ts        # projects, baseURL, testIdAttribute: 'data-test'
├── .env.example                # BASE_URL, SAUCE_USERNAME, SAUCE_PASSWORD ...
├── pages/                      # POM: LoginPage, InventoryPage, CartPage, CheckoutPage, Header
├── fixtures/test.ts            # test.extend — page object'lar fixture sifatida
├── data/checkout-customers.json
├── tests/
│   ├── auth.setup.ts           # bir marta login -> playwright/.auth/*.json
│   └── ui/
│       ├── login.spec.ts
│       ├── cart.spec.ts
│       ├── checkout.spec.ts
│       ├── network.spec.ts     # Toolshop: route, HAR
│       └── visual.spec.ts
└── playwright/.auth/           # .gitignore'da! (sessiya ma'lumoti)

.gitignore fayliga albatta qo'shing:

node_modules/
test-results/
playwright-report/
blob-report/
playwright/.auth/
.env

⚠️ Diqqat: playwright/.auth/*.json ichida haqiqiy sessiya cookie'lari bo'ladi. Uni git'ga commit qilish — parolni commit qilish bilan barobar.


2. Page Object Model (POM) {#pom}

Page Object Model — har bir sahifa (yoki sahifa qismi) uchun alohida class yaratish pattern'i. Class ichida ikki narsa yashaydi:

  1. Locator'lar — sahifadagi elementlar (readonly property sifatida, constructor'da bir marta e'lon qilinadi);
  2. Harakatlar (methods) — foydalanuvchi tilida: login(), addToCart(), checkout().

Natijada test "nima qilinyapti"ni aytadi, page object esa "qanday qilinadi"ni yashiradi. UI o'zgarsa — faqat bitta class tuzatiladi.

Page Object Model: testlar, fixture'lar, page object'lar va locator'lar qatlami

2.1. LoginPage

// pages/LoginPage.ts
import type { Locator, Page } from "@playwright/test";

export class LoginPage {
  readonly page: Page;
  readonly usernameInput: Locator;
  readonly passwordInput: Locator;
  readonly loginButton: Locator;
  readonly errorMessage: Locator;

  constructor(page: Page) {
    this.page = page;
    this.usernameInput = page.getByTestId("username");
    this.passwordInput = page.getByTestId("password");
    this.loginButton = page.getByTestId("login-button");
    this.errorMessage = page.getByTestId("error");
  }

  async goto() {
    await this.page.goto("/");
  }

  async login(username: string, password: string) {
    await this.usernameInput.fill(username);
    await this.passwordInput.fill(password);
    await this.loginButton.click();
  }
}

🧠 Esda tuting: Locator — bu "qidiruv retsepti", element emas. page.getByTestId('username') constructor'da yaratilganda hali hech narsa qidirilmaydi. Qidiruv faqat fill(), click() yoki expect() paytida bo'ladi. Shuning uchun locator'larni constructor'da e'lon qilish xavfsiz.

2.2. Header — takrorlanuvchi qism uchun "component object"

Savatcha ikonkasi va menyu har bir sahifada bor. Uni har bir page class'ga ko'chirish o'rniga alohida komponent qilamiz va composition orqali ishlatamiz:

// pages/Header.ts
import type { Locator, Page } from "@playwright/test";

// Har sahifada takrorlanadigan qism — alohida "component object"
export class Header {
  readonly cartLink: Locator;
  readonly cartBadge: Locator;
  readonly menuButton: Locator;
  readonly logoutLink: Locator;

  constructor(page: Page) {
    this.cartLink = page.getByTestId("shopping-cart-link");
    this.cartBadge = page.getByTestId("shopping-cart-badge");
    this.menuButton = page.getByRole("button", { name: "Open Menu" });
    this.logoutLink = page.getByTestId("logout-sidebar-link");
  }

  async openCart() {
    await this.cartLink.click();
  }

  async logout() {
    await this.menuButton.click();
    await this.logoutLink.click();
  }
}

2.3. InventoryPage — filter va chaining bilan

Mahsulot tugmalarining data-test qiymatlari nomdan yasaladi (add-to-cart-sauce-labs-backpack). Bunga bog'lanib qolish o'rniga, kartani nomi bo'yicha topib, uning ichidagi tugmani bosamiz — bu ancha barqaror:

// pages/InventoryPage.ts
import type { Locator, Page } from "@playwright/test";
import { Header } from "./Header";

export type SortOption = "az" | "za" | "lohi" | "hilo";

export class InventoryPage {
  readonly page: Page;
  readonly header: Header;
  readonly title: Locator;
  readonly items: Locator;
  readonly itemNames: Locator;
  readonly itemPrices: Locator;
  readonly sortSelect: Locator;

  constructor(page: Page) {
    this.page = page;
    this.header = new Header(page);
    this.title = page.getByTestId("title");
    this.items = page.getByTestId("inventory-item");
    this.itemNames = page.getByTestId("inventory-item-name");
    this.itemPrices = page.getByTestId("inventory-item-price");
    this.sortSelect = page.getByTestId("product-sort-container");
  }

  async goto() {
    await this.page.goto("/inventory.html");
  }

  // Mahsulot kartasini nomi bo'yicha topamiz
  productCard(name: string): Locator {
    return this.items.filter({ hasText: name });
  }

  async addToCart(name: string) {
    await this.productCard(name)
      .getByRole("button", { name: "Add to cart" })
      .click();
  }

  async removeFromCart(name: string) {
    await this.productCard(name)
      .getByRole("button", { name: "Remove" })
      .click();
  }

  async sortBy(option: SortOption) {
    await this.sortSelect.selectOption(option);
  }

  // "$29.99" -> 29.99
  async prices(): Promise<number[]> {
    const texts = await this.itemPrices.allTextContents();
    return texts.map((text) => Number(text.replace("$", "")));
  }
}

SortOption turi saucedemo'dagi <select> qiymatlariga mos: az, za, lohi, hilo. TypeScript endi sortBy('lowhigh') kabi xatoni test ishga tushmasdan oldin ushlaydi.

2.4. CartPage va CheckoutPage

// pages/CartPage.ts
import type { Locator, Page } from "@playwright/test";

export class CartPage {
  readonly page: Page;
  readonly title: Locator;
  readonly itemNames: Locator;
  readonly checkoutButton: Locator;
  readonly continueShoppingButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.title = page.getByTestId("title");
    this.itemNames = page.getByTestId("inventory-item-name");
    this.checkoutButton = page.getByTestId("checkout");
    this.continueShoppingButton = page.getByTestId("continue-shopping");
  }

  async goto() {
    await this.page.goto("/cart.html");
  }

  async checkout() {
    await this.checkoutButton.click();
  }
}
// pages/CheckoutPage.ts
import type { Locator, Page } from "@playwright/test";

export interface CustomerInfo {
  firstName: string;
  lastName: string;
  postalCode: string;
}

export class CheckoutPage {
  readonly page: Page;
  readonly firstNameInput: Locator;
  readonly lastNameInput: Locator;
  readonly postalCodeInput: Locator;
  readonly continueButton: Locator;
  readonly finishButton: Locator;
  readonly errorMessage: Locator;
  readonly subtotalLabel: Locator;
  readonly taxLabel: Locator;
  readonly totalLabel: Locator;
  readonly completeHeader: Locator;

  constructor(page: Page) {
    this.page = page;
    this.firstNameInput = page.getByTestId("firstName");
    this.lastNameInput = page.getByTestId("lastName");
    this.postalCodeInput = page.getByTestId("postalCode");
    this.continueButton = page.getByTestId("continue");
    this.finishButton = page.getByTestId("finish");
    this.errorMessage = page.getByTestId("error");
    this.subtotalLabel = page.getByTestId("subtotal-label");
    this.taxLabel = page.getByTestId("tax-label");
    this.totalLabel = page.getByTestId("total-label");
    this.completeHeader = page.getByTestId("complete-header");
  }

  async fillInformation(info: CustomerInfo) {
    await this.firstNameInput.fill(info.firstName);
    await this.lastNameInput.fill(info.lastName);
    await this.postalCodeInput.fill(info.postalCode);
  }

  async continue() {
    await this.continueButton.click();
  }

  async finish() {
    await this.finishButton.click();
  }
}

2.5. Assertion'lar qayerda yashaydi?

Bu bahsli mavzu. Playwright hujjatidagi POM misolida ham page object ichida expect bor. Bizning qoidamiz:

QayerdaNimaNega
Page objectlocator'lar, harakatlar, kerak bo'lsa "sahifa ochildi" kabi navigatsiya kutishlariqayta ishlatiladigan "qanday qilinadi"
Testbiznes natijani tekshirish (toHaveText('2'), toHaveURL(...))xato bo'lsa, hisobotda aynan qaysi talab buzilgani ko'rinadi

Shunda test o'qilganda "nima kutilyapti" darhol ko'rinadi, page object esa testlar orasida neytral qoladi.

2.6. Ko'p uchraydigan xatolar

  • **"God object" BasePage** — 40 ta umumiy metodli ulkan ota-class. Merosdan ko'ra composition (Header kabi kichik komponentlar) yaxshiroq.
  • **Metod ichida awaitni unutish** — this.loginButton.click() oldida await bo'lmasa, test keyingi qatorga o'tib ketadi va tasodifiy yiqiladi.
  • **Locator o'rniga ElementHandle saqlash** (page.$('...')) — eskirgan va DOM yangilanganda "stale" bo'ladi. Faqat Locator ishlating.
  • Har sahifaga 50 ta metod — faqat testlarda haqiqatan ishlatiladiganlarini yozing (YAGNI).

🎯 Intervyuda: "Page Object Model nima va nima uchun kerak?" — Har sahifa uchun locator va foydalanuvchi harakatlarini o'z ichiga olgan class. Takrorlanishni yo'qotadi, UI o'zgarganda tuzatishni bitta joyga jamlaydi va testlarni biznes tilida o'qiladigan qiladi.


3. Fixtures: test.extend() bilan toza testlar {#fixtures}

POM bilan ham har bir testda const loginPage = new LoginPage(page) yozish kerak bo'ladi. Fixture bu ishni ham yo'qotadi.

Fixture — test uchun kerakli "tayyor narsa": Playwright uni testdan oldin yaratadi, testga beradi va testdan keyin tozalaydi. Siz allaqachon built-in fixture'larni ishlatgansiz:

FixtureNima beradiDoirasi (scope)
pagehar test uchun yangi, izolyatsiyalangan sahifatest
contextpage tegishli bo'lgan BrowserContext (cookie, storage)test
browserishga tushgan brauzerworker
browserName'chromium', 'firefox' yoki 'webkit'worker
requestbrauzersiz HTTP so'rovlar uchun APIRequestContext (QA 20)test

Fixture'lar dangasa (lazy): test so'ramagan fixture umuman yaratilmaydi.

3.1. O'z fixture'larimiz

// fixtures/test.ts
import { test as base, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
import { InventoryPage } from "../pages/InventoryPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";

type Pages = {
  loginPage: LoginPage;
  inventoryPage: InventoryPage;
  cartPage: CartPage;
  checkoutPage: CheckoutPage;
  consoleErrors: string[];
};

export const test = base.extend<Pages>({
  loginPage: async ({ page }, use) => {
    await use(new LoginPage(page));
  },
  inventoryPage: async ({ page }, use) => {
    await use(new InventoryPage(page));
  },
  cartPage: async ({ page }, use) => {
    await use(new CartPage(page));
  },
  checkoutPage: async ({ page }, use) => {
    await use(new CheckoutPage(page));
  },
  // auto: true — har bir testda o'zi ishga tushadi, testda so'rash shart emas
  consoleErrors: [
    async ({ page }, use, testInfo) => {
      const errors: string[] = [];
      page.on("console", (message) => {
        if (message.type() === "error") errors.push(message.text());
      });
      await use(errors);
      if (errors.length > 0) {
        await testInfo.attach("console-errors.txt", {
          body: errors.join("\n"),
          contentType: "text/plain",
        });
      }
    },
    { auto: true },
  ],
});

export { expect };

Fixture tanasini uch qismga bo'lib o'qing:

async ({ page }, use, testInfo) => {
   ... SETUP     — testdan oldin (obyekt yaratish, ma'lumot tayyorlash)
   await use(x)  — test shu yerda ishlaydi va x'ni oladi
   ... TEARDOWN  — testdan keyin (tozalash, hisobotga fayl biriktirish)
}

consoleErrors — auto fixture: hech bir test uni so'ramaydi, lekin u har bir testda ishlaydi va brauzer konsolidagi xatolarni HTML hisobotga console-errors.txt qilib biriktiradi. Bug report uchun tayyor dalil (QA 8).

3.2. Refactor qilingan test

Endi testlar @playwright/testdan emas, o'zimizning fixtures/test.tsdan import qiladi:

// tests/ui/cart.spec.ts
import { test, expect } from "../../fixtures/test";

test.describe("Savatcha", () => {
  test.beforeEach(async ({ inventoryPage }) => {
    // login yo'q — storageState tufayli darhol ichkaridamiz (4-bo'lim)
    await inventoryPage.goto();
  });

  test(
    "2 ta mahsulot qo'shish va bittasini olib tashlash",
    { tag: ["@smoke", "@mobile"] },
    async ({ inventoryPage, cartPage }) => {
      await inventoryPage.addToCart("Sauce Labs Backpack");
      await inventoryPage.addToCart("Sauce Labs Bike Light");
      await expect(inventoryPage.header.cartBadge).toHaveText("2");

      await inventoryPage.removeFromCart("Sauce Labs Backpack");
      await expect(inventoryPage.header.cartBadge).toHaveText("1");

      await inventoryPage.header.openCart();
      await expect(cartPage.title).toHaveText("Your Cart");
      await expect(cartPage.itemNames).toHaveText(["Sauce Labs Bike Light"]);
    },
  );

  test("narx bo'yicha saralash (arzondan qimmatga)", async ({
    inventoryPage,
  }) => {
    await inventoryPage.sortBy("lohi");
    const prices = await inventoryPage.prices();
    const sorted = [...prices].sort((a, b) => a - b);
    expect(prices).toEqual(sorted);
  });
});

Solishtiring: 1-bo'limdagi testda locator'lar va login qatorlari bor edi, bu yerda esa faqat biznes qadamlari qoldi.

3.3. Fixture doiralari: test, worker, auto

TurQachon yaratiladiMisol
test (default)har bir test uchun yangidanpage object'lar, test ma'lumoti
worker ({ scope: 'worker' })har bir worker jarayoni uchun bir martaAPI token, test bazasiga ulanish
auto ({ auto: true })hech kim so'ramasa ham har testdakonsol xatolarini yig'ish, log biriktirish

Worker fixture'ning to'liq real misolini (restful-booker uchun API token) QA 20da yozamiz. Qisqa ko'rinishi:

export const test = base.extend<{}, { authToken: string }>({
  authToken: [
    async ({ playwright }, use) => {
      const token = "..."; // worker boshida bir marta olinadi
      await use(token);
    },
    { scope: "worker" },
  ],
});

💡 Maslahat: Fixture'lar bir-biriga bog'lanishi mumkin: cartPage fixture'i pageni, newBooking esa bookingClientni so'raydi. Playwright tartibni o'zi hal qiladi — xuddi DI (dependency injection) konteyneri kabi.

🎯 Intervyuda: "beforeEach va fixture farqi nima?" — beforeEach describe ichidagi barcha testlar uchun ishlaydi va natijani global o'zgaruvchiga yozishga majbur qiladi. Fixture esa faqat so'ragan testda yaratiladi, tipizatsiyalangan, qayta ishlatiladi va o'z teardown'iga ega.


4. Autentifikatsiya: storageState va setup project {#auth-state}

Login — eng ko'p takrorlanadigan qadam. Har testda UI orqali login qilish ham sekin, ham login sahifasidagi har bir muammo 300 ta testni qizartiradi. Yechim: bir marta login qilib, sessiyani faylga saqlash va barcha testlarni shu holatdan boshlash.

storageState brauzer kontekstining cookie va localStorage'ini JSON faylga yozadi (Playwright 1.51+ da indexedDB: true opsiyasi bilan IndexedDB'ni ham). Saucedemo login holatini session-username cookie'sida saqlaydi, shuning uchun bu usul unga to'liq mos keladi.

4.1. Setup fayli (ikki rol bilan)

// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";

const users = ["standard_user", "problem_user"] as const;

for (const username of users) {
  setup(`${username} sifatida login`, async ({ page }) => {
    await page.goto("/");
    await page.getByTestId("username").fill(username);
    await page
      .getByTestId("password")
      .fill(process.env.SAUCE_PASSWORD ?? "secret_sauce");
    await page.getByTestId("login-button").click();
    await expect(page).toHaveURL(/inventory\.html/);

    // cookie va localStorage'ni faylga yozamiz
    await page
      .context()
      .storageState({ path: `playwright/.auth/${username}.json` });
  });
}

4.2. Config: setup project va dependencies

// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "node:path";

// .env faylidan BASE_URL, login va parollarni o'qiymiz
dotenv.config({ path: path.resolve(__dirname, ".env"), quiet: true });

const AUTH_FILE = "playwright/.auth/standard_user.json";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: [["list"], ["html", { open: "never" }]],
  use: {
    baseURL: process.env.BASE_URL ?? "https://www.saucedemo.com",
    testIdAttribute: "data-test",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
  },
  projects: [
    // 1) avval login qilib, holatni faylga saqlaydi
    { name: "setup", testMatch: /.*\.setup\.ts/ },
    // 2) UI testlar tayyor login holati bilan boshlanadi
    {
      name: "chromium",
      testDir: "./tests/ui",
      use: { ...devices["Desktop Chrome"], storageState: AUTH_FILE },
      dependencies: ["setup"],
    },
    // 3) mobil emulyatsiya: faqat @mobile tegli testlar
    {
      name: "mobile-chrome",
      testDir: "./tests/ui",
      grep: /@mobile/,
      use: { ...devices["Pixel 7"], storageState: AUTH_FILE },
      dependencies: ["setup"],
    },
    // 4) API testlar — brauzer kerak emas (QA 20)
    {
      name: "api",
      testDir: "./tests/api",
      use: {
        baseURL:
          process.env.API_BASE_URL ?? "https://restful-booker.herokuapp.com",
        extraHTTPHeaders: { Accept: "application/json" },
      },
    },
  ],
});

Ishga tushirish tartibi:

npx playwright test
   │
   ├─ [setup]    auth.setup.ts  → playwright/.auth/standard_user.json, problem_user.json
   │
   ├─ [chromium]      ui/*.spec.ts    (storageState: standard_user.json)  ← dependencies: setup
   ├─ [mobile-chrome] ui/*.spec.ts @mobile                                  ← dependencies: setup
   └─ [api]           api/*.spec.ts   (brauzersiz, setup'ga bog'liq emas)

--project=chromium deb ishga tushirsangiz ham Playwright avval setupni bajaradi — chunki u dependency. Buni o'chirish uchun --no-deps flag'i bor.

4.3. Login'ni tekshiradigan test sessiyasiz bo'lishi shart

login.spec.ts login formasini tekshiradi, unga tayyor sessiya xalaqit beradi. Fayl boshida bo'sh holat beramiz:

// tests/ui/login.spec.ts (boshi)
import { test, expect } from "../../fixtures/test";

// Bu fayl login formasini tekshiradi — saqlangan sessiya kerak emas
test.use({ storageState: { cookies: [], origins: [] } });

4.4. Boshqa rol bilan test

// tests/ui/problem-user.spec.ts
import { test, expect } from "../../fixtures/test";

// Boshqa rol: problem_user sessiyasi bilan ishlaymiz
test.use({ storageState: "playwright/.auth/problem_user.json" });

test("problem_user: har mahsulotning rasmi har xil bo'lishi kerak", async ({
  inventoryPage,
}) => {
  test.fail(true, "Ma'lum bug: problem_user uchun barcha rasmlar bir xil");

  await inventoryPage.goto();
  const sources = await inventoryPage.items
    .locator("img")
    .evaluateAll((images) => images.map((image) => image.getAttribute("src")));
  expect(new Set(sources).size).toBe(sources.length);
});

problem_user — saucedemo'dagi ataylab "buzilgan" foydalanuvchi: unda 6 ta mahsulotning hammasida bir xil rasm chiqadi. Test to'g'ri kutilgan xulq-atvorni tekshiradi (rasmlar unikal bo'lishi kerak) va test.fail() bilan "bu ma'lum bug, hozircha yiqilishi kutiladi" deb belgilanadi. Bug tuzatilgan kuni test kutilmaganda o'tib ketadi va Playwright buni xato sifatida ko'rsatadi — test.fail()ni olib tashlash vaqti keldi degani.

⚠️ Diqqat: Saqlangan sessiyaning ham muddati bor. Bizning tekshiruvimizda saucedemo'ning session-username cookie'si login'dan keyin taxminan 10 daqiqa amal qiladigan expires bilan o'rnatildi (buni .auth/*.json faylidagi expires maydonida ko'rasiz). Setup project har npx playwright testda qayta ishlagani uchun bu muammo emas, lekin eski .auth faylini qo'lda qayta ishlatsangiz, testlar login sahifasiga "qulab" tushadi.

🎯 Intervyuda: "UI testlarda login'ni qanday optimallashtirasiz?" — Setup project'da bir marta login qilib storageStateni faylga saqlayman, test project'lar uni use.storageState va dependencies orqali qayta ishlatadi. Login formasining o'zi esa alohida, sessiyasiz testlar bilan tekshiriladi. Ilova API'si bo'lsa, login'ni API orqali qilish yanada tezroq (QA 20).


5. Data-driven testlar va muhit sozlamalari {#data-driven}

5.1. Massiv ustida sikl

Bitta ssenariyni turli ma'lumotlar bilan tekshirish uchun testni sikl ichida yaratamiz:

// tests/ui/login.spec.ts (davomi)
const invalidLogins = [
  {
    username: "locked_out_user",
    error: "Epic sadface: Sorry, this user has been locked out.",
  },
  {
    username: "unknown_user",
    error:
      "Epic sadface: Username and password do not match any user in this service",
  },
  { username: "", error: "Epic sadface: Username is required" },
];

test.describe("Login", () => {
  test.beforeEach(async ({ loginPage }) => {
    await loginPage.goto();
  });

  test(
    "standard_user tizimga kiradi",
    { tag: "@smoke" },
    async ({ loginPage, inventoryPage, page }) => {
      await loginPage.login("standard_user", "secret_sauce");

      await expect(page).toHaveURL(/inventory\.html/);
      await expect(inventoryPage.title).toHaveText("Products");
    },
  );

  for (const { username, error } of invalidLogins) {
    test(`kira olmaydi: "${username}"`, async ({ loginPage, page }) => {
      await loginPage.login(username, "secret_sauce");

      await expect(loginPage.errorMessage).toHaveText(error);
      await expect(page).toHaveURL("/");
    });
  }
});

Ikki muhim qoida:

  1. Test nomlari unikal bo'lsin — shuning uchun nomga ${username} qo'shdik. Bir faylda ikkita bir xil nomli test bo'lsa, Playwright xato beradi.
  2. **Test ichida if/else yozmang.** Muvaffaqiyatli va muvaffaqiyatsiz login'ni bitta siklda if (error) bilan ajratsangiz, test ikki xil narsani tekshiradi va o'qish qiyinlashadi. Biz ularni ikki xil testga ajratdik (9-bo'limda ESLint ham buni talab qilishini ko'rasiz).

5.2. JSON fayldan ma'lumot

[
  { "firstName": "Aziz", "lastName": "Karimov", "postalCode": "100000" },
  { "firstName": "Madina", "lastName": "Yusupova", "postalCode": "140100" },
  { "firstName": "Jasur", "lastName": "To'xtayev", "postalCode": "200100" }
]
// tests/ui/checkout.spec.ts
import { test, expect } from "../../fixtures/test";
import customers from "../../data/checkout-customers.json";

test.describe("Checkout", () => {
  test.beforeEach(async ({ inventoryPage }) => {
    await inventoryPage.goto();
    await inventoryPage.addToCart("Sauce Labs Backpack");
    await inventoryPage.addToCart("Sauce Labs Bike Light");
    await inventoryPage.header.openCart();
  });

  for (const customer of customers) {
    test(`buyurtma: ${customer.firstName} ${customer.lastName}`, async ({
      cartPage,
      checkoutPage,
    }) => {
      await cartPage.checkout();
      await checkoutPage.fillInformation(customer);
      await checkoutPage.continue();

      await expect(checkoutPage.subtotalLabel).toHaveText("Item total: $39.98");
      await expect(checkoutPage.taxLabel).toHaveText("Tax: $3.20");
      await expect(checkoutPage.totalLabel).toHaveText("Total: $43.18");

      await checkoutPage.finish();
      await expect(checkoutPage.completeHeader).toHaveText(
        "Thank you for your order!",
      );
    });
  }

  test("ism kiritilmasa xato chiqadi", async ({ cartPage, checkoutPage }) => {
    await cartPage.checkout();
    await checkoutPage.continue();
    await expect(checkoutPage.errorMessage).toHaveText(
      "Error: First Name is required",
    );
  });
});

JSON'ni import qilish uchun tsconfig.jsonda "resolveJsonModule": true bo'lishi kerak. Tasodifiy, lekin realistik ma'lumot kerak bo'lsa — @faker-js/faker (QA 17'da tanishgansiz): faker.person.firstName(). Uning amaliy ishlatilishini QA 20'dagi buildBooking()da ko'rasiz.

💡 Maslahat: Data-driven testda ekvivalent sinflar va chegaraviy qiymatlardan foydalaning (QA 5). 50 ta tasodifiy ism emas, har bir sinfdan bittadan vakil — testlar kam, qamrov yuqori.

5.3. Muhit sozlamalari: .env va baseURL

Bir xil testlar dev, staging va production-like muhitlarda ishlashi kerak. URL va login'larni kodga yozmaymiz:

# .env.example — git'ga commit qilinadi (namuna)
BASE_URL=https://www.saucedemo.com
API_BASE_URL=https://restful-booker.herokuapp.com
SAUCE_USERNAME=standard_user
SAUCE_PASSWORD=secret_sauce
BOOKER_USERNAME=admin
BOOKER_PASSWORD=password123

.env.exampledan nusxa olib .env yaratasiz (u .gitignoreda). Config dotenv orqali uni o'qiydi (4.2-bo'limdagi dotenv.config(...)). quiet: true — dotenv 17+ versiyalarida har ishga tushganda chiqadigan "injected env" log qatorini o'chiradi. Muhitni bir martalik almashtirish:

BASE_URL=https://staging.demobozor.example npx playwright test --project=chromium

(staging.demobozor.example — kursimizdagi xayoliy DemoBozor misoli.)

⚠️ Diqqat: O'zgaruvchiga USERNAME deb nom bermang. Windows'da USERNAME tizim o'zgaruvchisi allaqachon mavjud, dotenv esa sukut bo'yicha mavjud o'zgaruvchini ustidan yozmaydi — testlaringiz Windows foydalanuvchi nomi bilan login qilishga urinadi. Shuning uchun SAUCE_USERNAME kabi prefiksli nom ishlatamiz.


6. Tarmoqni boshqarish: route, waitForResponse, HAR {#network}

UI testning eng og'ir savollari tarmoq bilan bog'liq: "server bo'sh ro'yxat qaytarsa nima bo'ladi?", "500 xatoda UI nima ko'rsatadi?", "narx 0 bo'lsa-chi?". Bunday holatlarni haqiqiy backend'da yaratish qiyin. Playwright brauzer so'rovlarini ushlab olib o'zgartirishga imkon beradi.

Bu bo'limda Toolshop'dan foydalanamiz (practicesoftwaretesting.com) — u mahsulotlarni haqiqatan ham API'dan oladi. Qiziq fakt: Toolshop ro'yxatni oddiy GET emas, yangi QUERY HTTP metodi bilan so'raydi (body ichida filtrlar bilan). page.route() metoddan qat'i nazar URL bo'yicha ushlaydi, shuning uchun bu bizga xalaqit bermaydi.

MetodNima qiladiQachon
route.fulfill({...})serverga bormasdan javobni o'zimiz beramizto'liq mock: 500, bo'sh ro'yxat
route.fetch() + route.fulfill({ response, json })haqiqiy javobni olib, bir qismini o'zgartiramizbitta maydonni almashtirish
route.abort()so'rovni bekor qilamizrasm/analitika/CDN ishlamasa
route.continue({ headers })so'rovni o'zgartirib serverga yuboramizheader qo'shish

6.1. Javobni o'zgartirish (modify)

// tests/ui/network.spec.ts
import { test, expect } from '@playwright/test';

const SHOP = 'https://practicesoftwaretesting.com';
const API = 'https://api.practicesoftwaretesting.com';

test.describe('Tarmoqni boshqarish (Toolshop)', () => {
  test('javobni o\'zgartirish: birinchi mahsulot nomi', async ({ page }) => {
    await page.route(`${API}/products*`, async (route) => {
      const response = await route.fetch(); // haqiqiy serverdan olamiz
      const json = await response.json();
      json.data[0].name = 'Choynak (mock)'; // bitta maydonni almashtiramiz
      await route.fulfill({ response, json });
    });

    await page.goto(SHOP);
    await expect(page.getByTestId('product-name').first()).toHaveText('Choynak (mock)');
  });

6.2. Bo'sh holat va server xatosi

test("bo'sh natija holati", async ({ page }) => {
  await page.route(`${API}/products*`, async (route) => {
    const response = await route.fetch();
    const json = await response.json();
    await route.fulfill({ response, json: { ...json, data: [], total: 0 } });
  });

  await page.goto(SHOP);
  await expect(page.getByTestId("no-results")).toHaveText(
    "There are no products found.",
  );
});

test("server 500 qaytarsa UI nima qiladi", async ({ page }) => {
  await page.route(`${API}/products*`, (route) =>
    route.fulfill({
      status: 500,
      contentType: "application/json",
      body: JSON.stringify({ message: "Server Error" }),
    }),
  );

  await page.goto(SHOP);
  await expect(page.getByTestId("sort")).toBeVisible();
  // Kuzatuv: mahsulot ham, xato xabari ham ko'rinmaydi -> UX topilma
  await expect(page.getByTestId("product-name")).toHaveCount(0);
  await expect(page.getByTestId("no-results")).toHaveCount(0);
});

Uchinchi test qiziq natija berdi: 500 xatoda Toolshop foydalanuvchiga hech narsa demaydi — na mahsulot, na "Xatolik yuz berdi, qayta urinib ko'ring" xabari. Bo'sh ro'yxatdagi "There are no products found." ham chiqmaydi. Bu — mock qilish orqali topilgan haqiqiy UX muammo: QA sifatida unga bug report yozasiz (QA 8), kutilgan natija sifatida esa "xato holati uchun tushunarli xabar va qayta urinish tugmasi"ni ko'rsatasiz.

6.3. So'rovni bekor qilish (abort)

test("rasmlarni bloklash (abort)", async ({ page }) => {
  await page.route(/\.(png|jpe?g|webp|avif)$/, (route) => route.abort());

  await page.goto(SHOP);
  await expect(page.getByTestId("product-name").first()).toBeVisible();
});

CDN ishlamay qolsa ham sahifa ishlashi kerak. Bu test aynan shuni tekshiradi (va testlarni biroz tezlashtiradi ham).

6.4. Javobni kutish: waitForResponse

test("saralash so'rovini kutish (waitForResponse)", async ({ page }) => {
  await page.goto(SHOP);
  await expect(page.getByTestId("product-name").first()).toBeVisible();

  const responsePromise = page.waitForResponse(
    (response) =>
      response.url().startsWith(`${API}/products`) &&
      (response.request().postData() ?? "").includes("price,asc"),
  );
  await page.getByTestId("sort").selectOption("price,asc");
  const response = await responsePromise;

  expect(response.status()).toBe(200);
  const body = await response.json();
  await expect(page.getByTestId("product-name").first()).toHaveText(
    body.data[0].name,
  );
});

E'tibor bering: waitForResponse harakatdan oldin chaqiriladi (promise'ni saqlab qo'yamiz), keyin selectOption, keyin await. Agar avval bosib, keyin kutsangiz — javob allaqachon kelib bo'lgan bo'lishi mumkin va test 30 soniya kutib yiqiladi. Shuningdek, birinchi yuklanishdagi so'rov bilan adashmaslik uchun body'dagi price,asc bo'yicha filtrlaymiz.

6.5. HAR: yozib olish va qayta o'ynatish

HAR (HTTP Archive) — tarmoq trafigining JSON yozuvi. Playwright uni ikki rejimda ishlatadi:

  test('HAR: yozib olish va qayta o\'ynatish', async ({ page }) => {
    // UPDATE_HAR=1 bo'lsa — haqiqiy javoblar hars/ fayliga yoziladi
    await page.routeFromHAR('hars/toolshop-api.har', {
      url: `${API}/**`,
      update: !!process.env.UPDATE_HAR,
    });

    await page.goto(SHOP);
    await expect(page.getByTestId('product-name').first()).toBeVisible();
  });
});
# 1) yozib olish: haqiqiy serverga borib, javoblarni saqlaydi
UPDATE_HAR=1 npx playwright test -g "HAR"

# 2) qayta o'ynatish: API'ga bormaydi, javoblar HAR fayldan
npx playwright test -g "HAR"

Biz yozib olganimizda hars/ papkasida toolshop-api.har va uning yonida javob body'lari alohida .json fayllar bo'lib saqlandi. Eng foydali tomoni: o'sha .json faylda birinchi mahsulot nomini Choynak (HAR) ga o'zgartirib, testni qayta ishga tushirganimizda UI aynan shu nomni ko'rsatdi. Ya'ni HAR — backend'siz, barqaror va qo'lda tahrirlanadigan test ma'lumoti.

💡 Maslahat: page.route() faqat test ochgan brauzer ichida ishlaydi. Mobil ilova yoki boshqa qurilma trafigini o'zgartirish uchun Proxymanning Map Local va Breakpoint vositalari kerak bo'ladi — g'oya bir xil, faqat "tarmoq darajasida".

🎯 Intervyuda: "Mock qilingan testlar real bug'ni o'tkazib yubormaydimi?" — Yuboradi, shuning uchun mock'lar real integratsiya testlarining o'rnini bosmaydi. Ular backend'da yaratish qiyin bo'lgan chekka holatlarni (500, bo'sh ro'yxat, g'alati qiymatlar) tez va barqaror tekshirish uchun. Asosiy oqimlar real backend bilan alohida tekshiriladi.


7. Vizual testlar: toHaveScreenshot() {#visual}

Funksional test "tugma bosiladimi?" deb so'raydi. Vizual test esa "tugma joyida turibdimi, rangi to'g'rimi, matn ustma-ust tushmadimi?" deb so'raydi. Playwright buni skrinshotlarni piksellab solishtirish orqali qiladi.

// tests/ui/visual.spec.ts
import { test, expect } from "../../fixtures/test";

const screenshotOptions = {
  fullPage: true,
  animations: "disabled" as const,
  maxDiffPixelRatio: 0.01, // 1% gacha farqqa ruxsat
};

test.describe("Vizual regressiya", { tag: "@visual" }, () => {
  test("inventory sahifasi", async ({ inventoryPage, page }) => {
    await inventoryPage.goto();
    await expect(page).toHaveScreenshot("inventory.png", {
      ...screenshotOptions,
      mask: [page.getByTestId("footer-copy")], // o'zgaruvchan matnni yopamiz
    });
  });

  test("bitta mahsulot kartasi", async ({ inventoryPage }) => {
    await inventoryPage.goto();
    await expect(
      inventoryPage.productCard("Sauce Labs Backpack"),
    ).toHaveScreenshot("backpack-card.png");
  });

  test.describe("visual_user", () => {
    test.use({ storageState: { cookies: [], origins: [] } });

    test("xuddi shu sahifa visual_user uchun", async ({ loginPage, page }) => {
      test.fail(
        true,
        "visual_user ataylab vizual buglarga ega — test ularni ushlashi kerak",
      );

      await loginPage.goto();
      await loginPage.login("visual_user", "secret_sauce");
      await expect(page).toHaveScreenshot("inventory.png", {
        ...screenshotOptions,
        mask: [page.getByTestId("footer-copy")],
      });
    });
  });
});

7.1. Birinchi ishga tushirish: baseline

Etalon (baseline) rasm hali yo'q bo'lgani uchun birinchi ishga tushirish yiqiladi va rasmni yozib qo'yadi:

Error: A snapshot doesn't exist at .../tests/ui/visual.spec.ts-snapshots/inventory-chromium-darwin.png, writing actual.

Ikkinchi ishga tushirishdan boshlab test har safar yangi skrinshotni shu etalon bilan solishtiradi. Fayl nomiga e'tibor bering: inventory + project (chromium) + platforma (darwin = macOS). Linux CI'da xuddi shu test inventory-chromium-linux.pngni qidiradi.

UI ataylab o'zgargan bo'lsa, etalonlarni yangilaysiz:

npx playwright test --update-snapshots          # yoki -u
npx playwright test -u --grep @visual           # faqat vizual testlar

7.2. Vizual test bug'ni ushlaydi

Saucedemo'dagi visual_user — ataylab vizual buglarga ega foydalanuvchi. Yuqoridagi uchinchi test u bilan xuddi shu inventory.png etalonini solishtiradi. Natijada test-results/ papkasida uchta fayl paydo bo'ldi:

  • inventory-expected.png — etalon (standard_user);
  • inventory-actual.png — visual_user'da ko'ringani;
  • inventory-diff.png — farqli piksellar qizil/sariq rangda.

Diff'da biz ko'rgan farqlar: birinchi mahsulot rasmi boshqa, savatcha ikonkasi joyidan siljigan, ba'zi narx va nomlar o'zgargan, oxirgi kartadagi "Add to cart" tugmasi o'ngga surilib ketgan. Funksional testlar bularning hech birini sezmasdi — tugmalar baribir bosiladi.

7.3. Barqaror vizual testlar qoidalari

MuammoYechim
Animatsiya, kursor miltillashianimations: 'disabled' (CSS animatsiya va transition'lar to'xtatiladi)
Sana, vaqt, reklama, tasodifiy ma'lumotmask: [locator] bilan yopish yoki ma'lumotni mock qilish (6-bo'lim)
Shrift va anti-aliasing farqlarietalonni testlar ishlaydigan muhitda yaratish (Docker/CI, QA 21)
Kichik piksel shovqinimaxDiffPixels yoki maxDiffPixelRatio bilan kichik chegara
Butun sahifa juda kattamuhim komponentlarning alohida skrinshoti (locator.toHaveScreenshot())

⚠️ Diqqat: macOS'da yaratilgan etalon Linux CI'da baribir mos kelmaydi — shriftlar boshqacha render qilinadi. Shuning uchun jamoalar etalonlarni odatda CI bilan bir xil Docker image'da yaratadi. Batafsil — QA 21.

Vizual testlash bo'yicha chuqurroq mavzular (Percy, Applitools, responsive tekshiruv) — QA 25da.


8. Projects, retries, serial rejim va reporter'lar {#projects-reporters}

8.1. Projects: bir xil testlar, turli sharoitlar

Project — testlar to'plami + sozlamalar (brauzer, qurilma, baseURL, storageState). 4.2-bo'limdagi config'da to'rtta project bor: setup, chromium, mobile-chrome, api.

Mobil emulyatsiya devices lug'atidan olinadi. devices['Pixel 7'] — Chromium asosidagi profil: ekran 412×839, isMobile: true, hasTouch: true, Android user-agent. Shu project'da @mobile tegli savatcha testi telefon o'lchamida ham o'tdi.

To'liq cross-browser to'plam esa shunday qo'shiladi:

projects: [
  { name: 'setup', testMatch: /.*\.setup\.ts/ },
  { name: 'chromium', use: { ...devices['Desktop Chrome'] }, dependencies: ['setup'] },
  { name: 'firefox', use: { ...devices['Desktop Firefox'] }, dependencies: ['setup'] },
  { name: 'webkit', use: { ...devices['Desktop Safari'] }, dependencies: ['setup'] },
  { name: 'iphone', use: { ...devices['iPhone 15'] }, dependencies: ['setup'] },
],

⚠️ Diqqat: devices['iPhone 15'] va Desktop Safari WebKit dvigatelida ishlaydi, Desktop Firefox esa Firefox'da. Ular uchun brauzerlar o'rnatilgan bo'lishi kerak: npx playwright install (hammasi) yoki npx playwright install webkit firefox. Emulyatsiya haqiqiy iPhone emas — Safari'ning o'ziga xos xatolarini to'liq tekshirish uchun real qurilma yoki device farm kerak (QA 14).

Faqat bitta project'ni ishga tushirish: npx playwright test --project=chromium.

8.2. Retries va flaky testlar

retries: process.env.CI ? 2 : 0,
use: { trace: 'on-first-retry' },

Playwright natijalarni uch toifaga ajratadi: passed (birinchi urinishda o'tdi), flaky (birinchi urinishda yiqildi, qayta urinishda o'tdi), failed (barcha urinishlarda yiqildi). trace: 'on-first-retry' — test qayta urinilganda trace yoziladi, ya'ni beqaror test uchun tayyor "qora quti" bo'ladi. Test ichida qaysi urinish ekanini testInfo.retry bilan bilish mumkin.

🧠 Esda tuting: Retry — kasallikni emas, simptomni yashiradi. Flaky test topilsa, uni tuzatish kerak. Flaky testlar bilan tizimli ishlash — QA 21da.

8.3. Parallel va serial rejim

fullyParallel: true — hatto bitta fayl ichidagi testlar ham parallel ishlaydi. Shuning uchun har bir test mustaqil bo'lishi shart. Ba'zan (masalan, bitta uzun wizard'ni bosqichma-bosqich tekshirganda) testlar ketma-ketligi muhim bo'ladi:

test.describe.configure({ mode: "serial" });

Serial guruhda bitta test yiqilsa, qolganlari o'tkazib yuboriladi va retry'da butun guruh qaytadan ishlaydi. Playwright hujjatlari ham testlarni izolyatsiyalangan qilishni afzal ko'radi — serial rejim istisno bo'lishi kerak, qoida emas.

Testlarni bir nechta mashinaga bo'lish (--shard=1/4) — QA 21 mavzusi.

8.4. Reporter'lar

ReporterNatijaQachon
listhar test bir qatordalokal ishlash
line, dotixcham chiqishko'p testli CI loglari
htmlinteraktiv sayt: trace, skrinshot, video, biriktirmalarasosiy hisobot
jsonmashina o'qiydigan natijao'z dashboard'ingiz uchun
junitXMLJenkins, GitLab, Azure DevOps test tab'lari
blobsharding uchun "xom" hisobotkeyin merge-reports bilan birlashtiriladi
githubGitHub Actions'da xatolarga annotatsiyaGitHub CI

Bir vaqtning o'zida bir nechtasi:

reporter: [
  ['list'],
  ['html', { open: 'never' }],
  ['json', { outputFile: 'test-results/results.json' }],
  ['junit', { outputFile: 'test-results/junit.xml' }],
  ['allure-playwright', { resultsDir: 'allure-results' }],
],

Allure — jamoalarda mashhur, chiroyli tarixiy hisobot. allure-playwright (3.x) reporter natijalarni allure-results/ papkasiga yozadi, hisobotni esa Allure 3 CLI (allure npm paketi, Java talab qilmaydi) yasaydi:

npm i -D allure-playwright allure
npx playwright test
npx allure generate ./allure-results     # -> allure-report/
npx allure open                          # hisobotni brauzerda ochish

9. Best practices va ESLint {#best-practices}

9.1. Qisqa qoidalar ro'yxati

  • Har test mustaqil: o'z ma'lumotini o'zi yaratadi, boshqa testning natijasiga tayanmaydi.
  • Barqaror locator'lar: getByRole, getByLabel, getByTestId. Dasturchilardan data-testid/data-test atributlarini so'rang — bu QA va dev hamkorligining eng arzon investitsiyasi.
  • **Hech qachon waitForTimeout()** — web-first assertion'lar (toBeVisible, toHaveText) o'zi kutadi.
  • Bitta test — bitta biznes ssenariy, nomi esa natijani aytsin: "ism kiritilmasa xato chiqadi".
  • Test kodini production kod kabi review qiling: PR, lint, typecheck.
  • Ma'lum bug'lar uchun to'g'ri annotatsiya:
AnnotatsiyaMa'nosiQachon
test.fail()"bu test hozir yiqilishi kutiladi" (ma'lum bug)bug ochilgan, test to'g'ri xulqni tekshiradi
test.fixme()"test buzuq, ishga tushirilmaydi"testning o'zi tuzatilishi kerak
test.skip()"bu sharoitda ma'nosi yo'q"masalan, faqat WebKit'ga tegishli test
test.slow()timeout uch barobarog'ir, lekin to'g'ri test

9.2. ESLint: xatolarni kod review'dan oldin ushlash

eslint-plugin-playwright (2.x) Playwright'ga xos xatolarni topadi. TypeScript bilan flat config:

npm i -D eslint typescript-eslint eslint-plugin-playwright
// eslint.config.mjs
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import playwright from "eslint-plugin-playwright";

export default defineConfig([
  {
    files: ["**/*.ts"],
    extends: [tseslint.configs.base],
    languageOptions: { parserOptions: { projectService: true } },
    rules: { "@typescript-eslint/no-floating-promises": "error" },
  },
  {
    files: ["tests/**/*.ts"],
    extends: [playwright.configs["flat/recommended"]],
  },
]);

Mana "yomon odatlar" to'plami:

import { test, expect } from "@playwright/test";

test("yomon odatlar", async ({ page }) => {
  await page.goto("/");
  page.getByTestId("username").fill("standard_user"); // await yo'q
  await page.waitForTimeout(3000); // qattiq kutish
  expect(await page.getByTestId("error").isVisible()).toBe(true);
});

npx eslint . haqiqiy natijasi:

  5:3  error    Promises must be awaited, end with a call to .catch, ...   @typescript-eslint/no-floating-promises
  6:9  warning  Unexpected use of page.waitForTimeout()                    playwright/no-wait-for-timeout
  7:3  error    Replace isVisible() with toBeVisible()                     playwright/prefer-web-first-assertions

Bizning o'z loyihamizda ham lint foydali chiqdi: login testlarining birinchi variantida muvaffaqiyatli va muvaffaqiyatsiz holatlar bitta siklda if (error) {...} else {...} bilan yozilgan edi. ESLint playwright/no-conditional-in-test va playwright/no-conditional-expect ogohlantirishlarini berdi — shundan keyin ularni 5.1-bo'limdagidek ikki xil testga ajratdik.

💡 Maslahat: npx tsc --noEmit va npx eslint . ni CI'da testlardan oldin ishga tushiring — ular soniyalarda tugaydi va bir soatlik test yurishidan oldin oddiy xatolarni ushlaydi.


10. Amaliy mashqlar {#mashqlar}

Mashq 1: Qimmatdan arzonga saralash

InventoryPage yordamida narxlar qimmatdan arzonga saralanishini tekshiradigan test yozing.

<details> <summary>💡 Javobni ko'rish</summary>

import { test, expect } from "../../fixtures/test";

test("qimmatdan arzonga saralash", async ({ inventoryPage }) => {
  await inventoryPage.goto();
  await inventoryPage.sortBy("hilo");
  const prices = await inventoryPage.prices();
  expect(prices).toEqual([...prices].sort((a, b) => b - a));
});

[...prices] — massiv nusxasi. sort() massivni joyida o'zgartiradi; nusxasiz yozsangiz, prices va "kutilgan" massiv bir xil obyekt bo'lib qoladi va test hech qachon yiqilmaydi.

</details>

Mashq 2: ProductDetailsPage

Saucedemo'da mahsulot sahifasi /inventory-item.html?id=4 (Backpack). Unda data-test qiymatlari: inventory-item-name, inventory-item-price, add-to-cart, back-to-products. Shu sahifa uchun page object yozing va "nomi, narxi to'g'ri; savatchaga qo'shilganda badge 1" testini yozing.

<details> <summary>💡 Javobni ko'rish</summary>

// pages/ProductDetailsPage.ts
import type { Locator, Page } from "@playwright/test";

export class ProductDetailsPage {
  readonly page: Page;
  readonly name: Locator;
  readonly price: Locator;
  readonly addToCartButton: Locator;
  readonly backButton: Locator;

  constructor(page: Page) {
    this.page = page;
    this.name = page.getByTestId("inventory-item-name");
    this.price = page.getByTestId("inventory-item-price");
    this.addToCartButton = page.getByTestId("add-to-cart");
    this.backButton = page.getByTestId("back-to-products");
  }

  async goto(id: number) {
    await this.page.goto(`/inventory-item.html?id=${id}`);
  }
}
import { test, expect } from "../../fixtures/test";
import { ProductDetailsPage } from "../../pages/ProductDetailsPage";

test("mahsulot sahifasi: Backpack", async ({ page, inventoryPage }) => {
  const details = new ProductDetailsPage(page);
  await details.goto(4);
  await expect(details.name).toHaveText("Sauce Labs Backpack");
  await expect(details.price).toHaveText("$29.99");
  await details.addToCartButton.click();
  await expect(inventoryPage.header.cartBadge).toHaveText("1");
});

Header komponenti mahsulot sahifasida ham ishladi — composition'ning foydasi shu. Keyingi qadam: ProductDetailsPageni ham fixtures/test.tsga fixture qilib qo'shish.

</details>

Mashq 3: "Savatda 2 ta mahsulot" fixture'i

Checkout testlari har safar savatchaga 2 ta mahsulot qo'shishdan boshlanadi. Buni cartWithTwoItems fixture'iga aylantiring: fixture mahsulotlarni qo'shsin va ularning nomlari massivini testga bersin.

<details> <summary>💡 Javobni ko'rish</summary>

// fixtures/cart.ts
import { test as base, expect } from "./test";

type CartFixtures = { cartWithTwoItems: string[] };

export const test = base.extend<CartFixtures>({
  cartWithTwoItems: async ({ inventoryPage }, use) => {
    const products = ["Sauce Labs Backpack", "Sauce Labs Bike Light"];
    await inventoryPage.goto();
    for (const product of products) {
      await inventoryPage.addToCart(product);
    }
    await use(products);
  },
});

export { expect };
import { test, expect } from "../../fixtures/cart";

test("fixture: savatda 2 ta mahsulot tayyor", async ({
  cartWithTwoItems,
  inventoryPage,
  cartPage,
}) => {
  await inventoryPage.header.openCart();
  await expect(cartPage.itemNames).toHaveText(cartWithTwoItems);
});

E'tibor bering: fixtures/cart.ts bizning ./testni kengaytiryapti — fixture'lar qatlam-qatlam o'sishi mumkin.

</details>

Mashq 4: Chegaraviy narxlar (mock)

Toolshop'da birinchi mahsulot narxini 0, ikkinchisini 1234567.5 qilib mock qiling. UI ularni qanday ko'rsatadi? Natijani testga aylantiring va QA nuqtai nazaridan baholang.

<details> <summary>💡 Javobni ko'rish</summary>

test("Toolshop: narx 0 va katta son qanday ko'rinadi", async ({ page }) => {
  await page.route(
    "https://api.practicesoftwaretesting.com/products*",
    async (route) => {
      const response = await route.fetch();
      const json = await response.json();
      json.data[0].price = 0;
      json.data[1].price = 1234567.5;
      await route.fulfill({ response, json });
    },
  );
  await page.goto("https://practicesoftwaretesting.com/");
  const prices = page.getByTestId("product-price");
  await expect(prices.nth(0)).toHaveText("$0.00");
  await expect(prices.nth(1)).toHaveText("$1234567.50");
});

Kuzatuvlar:

  • $0.00 — texnik jihatdan to'g'ri, lekin biznes savol tug'iladi: "bepul" mahsulot do'konda ko'rinishi kerakmi? Bu talablar bo'yicha savol.
  • $1234567.50 — minglik ajratuvchi yo'q ($1,234,567.50 kutilgan bo'lardi). Katta summalarda o'qish qiyin; bu lokalizatsiya/UX topilmasi (QA 25). DemoBozor'da so'm summalari 1 234 567 so'm ko'rinishida bo'lishi kerak edi.

</details>

Mashq 5: Vizual diff'ni tahlil qilish

7.2-bo'limdagi visual_user testi diff'ida 5 xil farq topildi. Ularning har birini bug report uchun jiddiylik (severity) bo'yicha baholang.

<details> <summary>💡 Javobni ko'rish</summary>

FarqTaklif etilgan severityIzoh
Narxlar o'zgarganCriticalFoydalanuvchi bir narxni ko'rib, boshqasini to'lashi — moliyaviy va huquqiy xavf
Mahsulot nomlari o'zgarganMajorNoto'g'ri mahsulot buyurtma qilinishi mumkin
Birinchi rasm boshqa mahsulotnikiMajorXaridorni chalg'itadi
"Add to cart" tugmasi siljiganMinorIshlaydi, lekin dizayn buzilgan; kichik ekranda bosish qiyinlashishi mumkin
Savatcha ikonkasi siljiganMinorKosmetik, lekin brend sifatiga ta'sir qiladi

Severity va priority farqi — QA 8.

</details>

Mashq 6: Code review

Hamkasbingiz quyidagi testni PR'ga yubordi. Kamida 5 ta muammoni toping.

test("test1", async ({ page }) => {
  await page.goto("https://www.saucedemo.com/");
  await page.locator("#user-name").fill("standard_user");
  await page.locator("#password").fill("secret_sauce");
  await page.locator("#login-button").click();
  await page.waitForTimeout(2000);
  page.locator(".inventory_item button").nth(3).click();
  if (await page.locator(".shopping_cart_badge").isVisible()) {
    expect(await page.locator(".shopping_cart_badge").textContent()).toBe("1");
  }
});

<details> <summary>💡 Javobni ko'rish</summary>

  1. Nom ma'nosiz — test1 hech narsa aytmaydi; "mahsulot qo'shilganda badge 1 bo'ladi" kabi bo'lsin.
  2. To'liq URL — baseURL bor, page.goto('/') yetarli; aks holda staging'da ishga tushirib bo'lmaydi.
  3. UI login har testda — storageState bilan setup project ishlatilishi kerak (4-bo'lim).
  4. **waitForTimeout(2000)** — qattiq kutish: sekin muhitda yetmaydi, tez muhitda vaqtni yeydi.
  5. **await yo'q** — click() oldida; test yiqilmasdan "o'tib ketishi" mumkin.
  6. Mo'rt locator — .nth(3) tartib o'zgarsa boshqa mahsulotni bosadi; nom bo'yicha filter({ hasText }) kerak.
  7. Shartli assertion — badge ko'rinmasa, test hech narsani tekshirmay o'tadi (yolg'on yashil). To'g'risi: await expect(header.cartBadge).toHaveText('1').
  8. POM yo'q — locator'lar testda; LoginPage/InventoryPage ishlatilishi kerak.

</details>


O'zingizni tekshiring {#checklist}

  • [ ] Har sahifa uchun locator va harakatlarni page object'ga ajrata olaman
  • [ ] test.extend() bilan test, worker va auto fixture yoza olaman
  • [ ] Setup project + storageState bilan login'ni bir marta qila olaman
  • [ ] page.route() bilan javobni mock, modify va abort qila olaman
  • [ ] toHaveScreenshot() etalonlari qayerda saqlanishini va qanday yangilanishini bilaman
  • [ ] Reporter'larni (html, junit, allure) sozlay olaman

Xulosa {#xulosa}

  • POM locator va harakatlarni bitta class'ga yig'adi; UI o'zgarsa bitta joy tuzatiladi. Takrorlanuvchi qismlar (Header) — composition orqali komponent.
  • Fixture'lar (test.extend) testni "tayyorlash — ishlatish — tozalash" siklidan ozod qiladi; test, worker va auto doiralari bor.
  • storageState + setup project — bir marta login, yuzlab test tayyor sessiya bilan; login formasining o'zi esa sessiyasiz test qilinadi.
  • Data-driven testlarda nomlar unikal, test ichida if/else yo'q; sozlamalar .envda, USERNAME kabi tizim nomlaridan qoching.
  • **page.route()** chekka holatlarni (500, bo'sh ro'yxat, 0 narx) soniyalarda yaratadi; biz shu usul bilan Toolshop'da xato holati ko'rsatilmasligini topdik. HAR — backend'siz qayta o'ynatiladigan trafik.
  • **toHaveScreenshot()** funksional testlar ko'rmaydigan buglarni ushlaydi; etalonlar platformaga bog'liq, shuning uchun CI bilan bir xil muhitda yaratiladi.
  • ESLint (eslint-plugin-playwright) va tsc — testlardan oldingi arzon himoya qatlami.
  • Keyingi dars: QA 20: API avtomatlashtirish — Playwright request, Zod schema va API+UI gibrid testlar

Manbalar {#manbalar}

  1. Playwright — Page object models

Markdown manba

iOS Lessons