# 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 18](/ios-lessons/qa-playwright-basics)dagi "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](#muammo)
2. [Page Object Model (POM)](#pom)
3. [Fixtures: test.extend() bilan toza testlar](#fixtures)
4. [Autentifikatsiya: storageState va setup project](#auth-state)
5. [Data-driven testlar va muhit sozlamalari](#data-driven)
6. [Tarmoqni boshqarish: route, waitForResponse, HAR](#network)
7. [Vizual testlar: toHaveScreenshot()](#visual)
8. [Projects, retries, serial rejim va reporter'lar](#projects-reporters)
9. [Best practices va ESLint](#best-practices)
10. [Amaliy mashqlar](#mashqlar)
11. [Xulosa](#xulosa)
12. [Manbalar](#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:

```typescript
// 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:

| Muammo              | Real oqibat                                                                      |
| ------------------- | -------------------------------------------------------------------------------- |
| **Takrorlanish**    | Dasturchi `data-test="login-button"`ni o'zgartirsa, 300 ta faylni tuzatish kerak |
| **O'qilishi qiyin** | Test biznes ssenariyni emas, "qaysi tugma bosildi"ni hikoya qiladi               |
| **Sekin**           | Har 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:

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

```text
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](/lessons/images/qa/qa_19_pom.svg)

### 2.1. LoginPage

```typescript
// 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:

```typescript
// 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:

```typescript
// 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

```typescript
// 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();
  }
}
```

```typescript
// 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:

| Qayerda         | Nima                                                                               | Nega                                                         |
| --------------- | ---------------------------------------------------------------------------------- | ------------------------------------------------------------ |
| **Page object** | locator'lar, harakatlar, kerak bo'lsa "sahifa ochildi" kabi navigatsiya kutishlari | qayta ishlatiladigan "qanday qilinadi"                       |
| **Test**        | biznes 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 `await`ni 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:

| Fixture       | Nima beradi                                                                                   | Doirasi (scope) |
| ------------- | --------------------------------------------------------------------------------------------- | --------------- |
| `page`        | har test uchun yangi, izolyatsiyalangan sahifa                                                | test            |
| `context`     | `page` tegishli bo'lgan BrowserContext (cookie, storage)                                      | test            |
| `browser`     | ishga tushgan brauzer                                                                         | worker          |
| `browserName` | `'chromium'`, `'firefox'` yoki `'webkit'`                                                     | worker          |
| `request`     | brauzersiz HTTP so'rovlar uchun `APIRequestContext` ([QA 20](/ios-lessons/qa-api-automation)) | test            |

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

### 3.1. O'z fixture'larimiz

```typescript
// 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:

```text
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](/ios-lessons/qa-bug-reports)).

### 3.2. Refactor qilingan test

Endi testlar `@playwright/test`dan emas, **o'zimizning** `fixtures/test.ts`dan import qiladi:

```typescript
// 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

| Tur                                | Qachon yaratiladi                           | Misol                                      |
| ---------------------------------- | ------------------------------------------- | ------------------------------------------ |
| **test** (default)                 | har bir test uchun yangidan                 | page object'lar, test ma'lumoti            |
| **worker** (`{ scope: 'worker' }`) | har bir worker jarayoni uchun **bir marta** | API token, test bazasiga ulanish           |
| **auto** (`{ auto: true }`)        | hech kim so'ramasa ham har testda           | konsol xatolarini yig'ish, log biriktirish |

Worker fixture'ning to'liq real misolini (restful-booker uchun API token) [QA 20](/ios-lessons/qa-api-automation)da yozamiz. Qisqa ko'rinishi:

```typescript
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 `page`ni, `newBooking` esa `bookingClient`ni 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)

```typescript
// 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

```typescript
// 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:

```text
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 `setup`ni 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:

```typescript
// 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

```typescript
// 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 test`da 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 `storageState`ni 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:

```typescript
// 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

```json
[
  { "firstName": "Aziz", "lastName": "Karimov", "postalCode": "100000" },
  { "firstName": "Madina", "lastName": "Yusupova", "postalCode": "140100" },
  { "firstName": "Jasur", "lastName": "To'xtayev", "postalCode": "200100" }
]
```

```typescript
// 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.json`da `"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 qiymatlar**dan foydalaning ([QA 5](/ios-lessons/qa-test-design-1)). 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:

```bash
# .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.example`dan nusxa olib `.env` yaratasiz (u `.gitignore`da). 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:

```bash
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.

| Metod                                                 | Nima qiladi                                     | Qachon                          |
| ----------------------------------------------------- | ----------------------------------------------- | ------------------------------- |
| `route.fulfill({...})`                                | serverga bormasdan javobni o'zimiz beramiz      | to'liq mock: 500, bo'sh ro'yxat |
| `route.fetch()` + `route.fulfill({ response, json })` | haqiqiy javobni olib, bir qismini o'zgartiramiz | bitta maydonni almashtirish     |
| `route.abort()`                                       | so'rovni bekor qilamiz                          | rasm/analitika/CDN ishlamasa    |
| `route.continue({ headers })`                         | so'rovni o'zgartirib serverga yuboramiz         | header qo'shish                 |

### 6.1. Javobni o'zgartirish (modify)

```typescript
// 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

```typescript
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](/ios-lessons/qa-bug-reports)), kutilgan natija sifatida esa "xato holati uchun tushunarli xabar va qayta urinish tugmasi"ni ko'rsatasiz.

### 6.3. So'rovni bekor qilish (abort)

```typescript
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

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

```typescript
  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();
  });
});
```

```bash
# 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 [Proxyman](/ios-lessons/qa-proxyman)ning 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.

```typescript
// 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:

```text
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.png`ni qidiradi.

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

```bash
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

| Muammo                                  | Yechim                                                                                    |
| --------------------------------------- | ----------------------------------------------------------------------------------------- |
| Animatsiya, kursor miltillashi          | `animations: 'disabled'` (CSS animatsiya va transition'lar to'xtatiladi)                  |
| Sana, vaqt, reklama, tasodifiy ma'lumot | `mask: [locator]` bilan yopish yoki ma'lumotni mock qilish (6-bo'lim)                     |
| Shrift va anti-aliasing farqlari        | etalonni testlar ishlaydigan muhitda yaratish (Docker/CI, [QA 21](/ios-lessons/qa-ci-cd)) |
| Kichik piksel shovqini                  | `maxDiffPixels` yoki `maxDiffPixelRatio` bilan kichik chegara                             |
| Butun sahifa juda katta                 | muhim 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 25](/ios-lessons/qa-a11y-l10n-visual)da.

---

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

```typescript
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](/ios-lessons/qa-mobile-testing)).

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

### 8.2. Retries va flaky testlar

```typescript
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 21](/ios-lessons/qa-ci-cd)da.

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

```typescript
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

| Reporter      | Natija                                                  | Qachon                                      |
| ------------- | ------------------------------------------------------- | ------------------------------------------- |
| `list`        | har test bir qatorda                                    | lokal ishlash                               |
| `line`, `dot` | ixcham chiqish                                          | ko'p testli CI loglari                      |
| `html`        | interaktiv sayt: trace, skrinshot, video, biriktirmalar | asosiy hisobot                              |
| `json`        | mashina o'qiydigan natija                               | o'z dashboard'ingiz uchun                   |
| `junit`       | XML                                                     | Jenkins, GitLab, Azure DevOps test tab'lari |
| `blob`        | sharding uchun "xom" hisobot                            | keyin `merge-reports` bilan birlashtiriladi |
| `github`      | GitHub Actions'da xatolarga annotatsiya                 | GitHub CI                                   |

Bir vaqtning o'zida bir nechtasi:

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

```bash
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**:

| Annotatsiya    | Ma'nosi                                             | Qachon                                       |
| -------------- | --------------------------------------------------- | -------------------------------------------- |
| `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 barobar                                 | og'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:

```bash
npm i -D eslint typescript-eslint eslint-plugin-playwright
```

```javascript
// 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:

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

```text
  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>

```typescript
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>

```typescript
// 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}`);
  }
}
```

```typescript
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: `ProductDetailsPage`ni ham `fixtures/test.ts`ga 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>

```typescript
// 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 };
```

```typescript
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 `./test`ni 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>

```typescript
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](/ios-lessons/qa-a11y-l10n-visual)). 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>

| Farq                              | Taklif etilgan severity | Izoh                                                                             |
| --------------------------------- | ----------------------- | -------------------------------------------------------------------------------- |
| Narxlar o'zgargan                 | Critical                | Foydalanuvchi bir narxni ko'rib, boshqasini to'lashi — moliyaviy va huquqiy xavf |
| Mahsulot nomlari o'zgargan        | Major                   | Noto'g'ri mahsulot buyurtma qilinishi mumkin                                     |
| Birinchi rasm boshqa mahsulotniki | Major                   | Xaridorni chalg'itadi                                                            |
| "Add to cart" tugmasi siljigan    | Minor                   | Ishlaydi, lekin dizayn buzilgan; kichik ekranda bosish qiyinlashishi mumkin      |
| Savatcha ikonkasi siljigan        | Minor                   | Kosmetik, lekin brend sifatiga ta'sir qiladi                                     |

Severity va priority farqi — [QA 8](/ios-lessons/qa-bug-reports).

</details>

### Mashq 6: Code review

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

```typescript
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 `.env`da, `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](/ios-lessons/qa-api-automation)

## Manbalar {#manbalar}

1. [Playwright — Page object models](https://playwright.dev/docs/pom)
2. [Playwright — Fixtures](https://playwright.dev/docs/test-fixtures)
3. [Playwright — Authentication](https://playwright.dev/docs/auth)
4. [Playwright — Parameterize tests](https://playwright.dev/docs/test-parameterize)
5. [Playwright — Mock APIs](https://playwright.dev/docs/mock)
6. [Playwright — Network](https://playwright.dev/docs/network)
7. [Playwright — Visual comparisons](https://playwright.dev/docs/test-snapshots)
8. [Playwright — Emulation (devices)](https://playwright.dev/docs/emulation)
9. [Playwright — Retries](https://playwright.dev/docs/test-retries)
10. [Playwright — Parallelism](https://playwright.dev/docs/test-parallel)
11. [Playwright — Reporters](https://playwright.dev/docs/test-reporters)
12. [Playwright — Annotations (fail, fixme, skip, slow)](https://playwright.dev/docs/test-annotations)
13. [eslint-plugin-playwright (GitHub)](https://github.com/mskelton/eslint-plugin-playwright)
14. [Allure Report — Playwright integratsiyasi](https://allurereport.org/docs/playwright/)
15. [Sauce Demo (Swag Labs)](https://www.saucedemo.com/)
16. [Practice Software Testing — Toolshop](https://practicesoftwaretesting.com/)
