# QA 18: Playwright (1) — o'rnatish, locator'lar, action'lar, assertion'lar va auto-waiting

> **Holat:** 2026-yil oktabr. Playwright 1.64.0 (`@playwright/test`), Node.js 24 LTS. Darsdagi barcha testlar (jami 23 ta) [saucedemo.com](https://www.saucedemo.com), [TodoMVC demo](https://demo.playwright.dev/todomvc/) va Selenium'ning rasmiy demo sahifalarida Chromium'da ishga tushirib tekshirilgan. Xato xabarlari ham haqiqiy ishga tushirishdan olingan.

> **Bu darsdan keyin siz:** noldan Playwright loyihasi yaratib, haqiqiy saytga barqaror E2E testlar yoza olasiz.

- Playwright'ni o'rnatib, `playwright.config.ts`ni tushunasiz;
- to'g'ri locator tanlashni va strict mode xatosini tuzatishni bilasiz;
- action va web-first assertion'lar bilan login, savatcha va checkout testlarini yozasiz;
- auto-waiting qanday ishlashini va nega `waitForTimeout` yomonligini tushunasiz;
- yiqilgan testni trace viewer, UI mode va Inspector bilan tahlil qilasiz.

## Mundarija

1. [Playwright nima va qanday ishlaydi](#nima)
2. [O'rnatish](#ornatish)
3. [playwright.config.ts asoslari](#config)
4. [Testlarni ishga tushirish va codegen](#ishga-tushirish)
5. [Birinchi test: saucedemo login](#birinchi-test)
6. [Locator'lar](#locators)
7. [Action'lar](#actions)
8. [Assertion'lar](#assertions)
9. [Auto-waiting va nega waitForTimeout yomon](#auto-waiting)
10. [Test tuzilmasi: describe, hook'lar va tag'lar](#tuzilma)
11. [To'liq misol: savatcha va checkout](#savatcha-checkout)
12. [Debugging: trace, UI mode va Inspector](#debugging)
13. [Amaliy mashqlar](#mashqlar)
14. [O'zingizni tekshiring](#checklist)
15. [Xulosa](#xulosa)
16. [Manbalar](#manbalar)

---

## 1. Playwright nima va qanday ishlaydi {#nima}

**Playwright** — Microsoft'ning ochiq kodli (Apache-2.0) brauzer avtomatlashtirish framework'i. `@playwright/test` paketi esa unga test runner, assertion'lar, parallel ishga tushirish, hisobotlar va trace qo'shadi. Nega aynan Playwright tanlanganini [QA 16](/ios-lessons/qa-automation-intro) da muhokama qildik.

Test ishga tushganda nima bo'ladi:

```text
npx playwright test
 └─ Test runner (@playwright/test) — test fayllarni topadi, worker jarayonlarga bo'ladi
     └─ Browser (Chromium / Firefox / WebKit) — worker'da ishga tushadi
         └─ BrowserContext — HAR BIR TEST uchun yangi "inkognito" profil
             └─ Page — brauzer tab'i: page.goto(), click(), fill() ...
```

Eng muhim tushuncha — **test izolyatsiyasi**. Rasmiy hujjat: _"Playwright creates a context for each test, and provides a default Page in that context."_ Ya'ni har bir test o'zining cookie, localStorage va session'iga ega toza brauzer profilida boshlanadi. Natijada:

- bitta test yiqilsa, keyingisiga ta'sir qilmaydi;
- testlarni istalgan tartibda va parallel ishga tushirish mumkin;
- bitta testni alohida, xohlagancha qayta ishga tushirib debug qilasiz.

Testga `async ({ page }) => { ... }` ko'rinishida beriladigan `page` — **fixture**. Playwright har test uchun tayyor obyektlarni beradi: `page`, `context`, `browser`, `request` (API uchun), `browserName`. O'z fixture'larimizni [QA 19](/ios-lessons/qa-playwright-advanced) da yozamiz.

---

## 2. O'rnatish {#ornatish}

### Tizim talablari (rasmiy hujjat)

- Node.js: 22.x, 24.x yoki 26.x (biz 24 LTS ishlatamiz — [QA 17](/ios-lessons/qa-typescript));
- macOS 14 (Sonoma) va yangiroq; Windows 11+ yoki WSL; Debian 12/13, Ubuntu 22.04/24.04/26.04.

### A usul: tayyor shablon — `npm init playwright@latest`

```bash
mkdir qa-demo-tests && cd qa-demo-tests
npm init playwright@latest
```

Buyruq bir nechta savol beradi:

| Savol                                | Standart javob                            | Bizning tanlov                                       |
| ------------------------------------ | ----------------------------------------- | ---------------------------------------------------- |
| TypeScript yoki JavaScript           | TypeScript                                | TypeScript                                           |
| Testlar papkasi nomi                 | `tests` (agar `tests` bor bo'lsa — `e2e`) | `tests`                                              |
| GitHub Actions workflow qo'shilsinmi | —                                         | Ha ([QA 21](/ios-lessons/qa-ci-cd) da kerak bo'ladi) |
| Playwright brauzerlari o'rnatilsinmi | Ha                                        | Ha                                                   |

Natijada shu fayllar paydo bo'ladi:

```text
qa-demo-tests/
├── playwright.config.ts        # test konfiguratsiyasi
├── package.json
├── package-lock.json
├── tests/
│   └── example.spec.ts         # namunaviy test (keyin o'chiramiz)
└── .github/workflows/playwright.yml   # agar GitHub Actions tanlangan bo'lsa
```

### B usul: qo'lda o'rnatish

```bash
mkdir qa-demo-tests && cd qa-demo-tests
npm init -y
npm i -D @playwright/test @types/node@24
npx playwright install            # Chromium, Firefox va WebKit
# yoki faqat bittasi:  npx playwright install chromium
```

Tekshiramiz:

```bash
npx playwright --version   # Version 1.64.0
```

> **⚠️ Diqqat:** Har bir Playwright versiyasi o'ziga mos brauzer build'larini ishlatadi. Shuning uchun yangilashda ikki qadam kerak: avval `npm install -D @playwright/test@latest`, keyin `npx playwright install --with-deps`. Linux CI serverlarda `--with-deps` brauzer uchun kerakli tizim kutubxonalarini ham o'rnatadi.

Kurs loyihamiz tuzilmasi ([QA 16](/ios-lessons/qa-automation-intro#arxitektura) da ko'rgandik): bu darsda `playwright.config.ts` va `tests/ui/` dagi uchta faylni yozamiz — `login.spec.ts`, `cart.spec.ts`, `checkout.spec.ts`.

---

## 3. playwright.config.ts asoslari {#config}

```typescript
import { defineConfig, devices } from "@playwright/test";

export default defineConfig({
  testDir: "./tests",
  fullyParallel: true,
  forbidOnly: !!process.env.CI,
  retries: process.env.CI ? 2 : 0,
  workers: process.env.CI ? 1 : undefined,
  reporter: "html",
  use: {
    baseURL: "https://www.saucedemo.com",
    testIdAttribute: "data-test",
    trace: "on-first-retry",
    screenshot: "only-on-failure",
    video: "retain-on-failure",
  },
  projects: [
    { name: "chromium", use: { ...devices["Desktop Chrome"] } },
    { name: "firefox", use: { ...devices["Desktop Firefox"] } },
    { name: "webkit", use: { ...devices["Desktop Safari"] } },
  ],
});
```

| Sozlama                        | Ma'nosi                                                                                                  |
| ------------------------------ | -------------------------------------------------------------------------------------------------------- |
| `testDir`                      | Testlar qayerdan qidiriladi                                                                              |
| `fullyParallel`                | Bir fayl ichidagi testlar ham parallel ishlaydi                                                          |
| `forbidOnly`                   | CI'da `test.only` qolib ketsa — yiqitadi (hamma testlar o'tkazib yuborilmasin)                           |
| `retries`                      | Yiqilgan testni qayta urinish soni: lokalda 0, CI'da 2                                                   |
| `workers`                      | Parallel jarayonlar soni (`undefined` — standart, CPU yadrolarining 50%)                                 |
| `reporter`                     | Hisobot turi: `list`, `html`, `junit`...                                                                 |
| `use.baseURL`                  | `page.goto('/')` shu manzilga qo'shiladi — muhit almashtirish oson                                       |
| `use.testIdAttribute`          | `getByTestId()` qaysi atributni qidirishi. Standart — `data-testid`, saucedemo esa `data-test` ishlatadi |
| `use.trace`                    | `on-first-retry` — trace faqat birinchi qayta urinishda yoziladi (rasmiy CI tavsiyasi)                   |
| `use.screenshot` / `use.video` | Yiqilgan testning screenshot va videosini saqlaydi                                                       |
| `projects`                     | Bir xil testlar turli brauzer/qurilmalarda                                                               |

> **💡 Maslahat:** `projects`da uchta brauzer bor, lekin lokalda bittasini ishga tushirish mumkin: `npx playwright test --project=chromium`. Brauzerlararo farqlarni CI ko'rsatadi.

---

## 4. Testlarni ishga tushirish va codegen {#ishga-tushirish}

| Buyruq                                         | Nima qiladi                                                  |
| ---------------------------------------------- | ------------------------------------------------------------ |
| `npx playwright test`                          | Barcha testlar, barcha project'lar                           |
| `npx playwright test tests/ui/login.spec.ts`   | Bitta fayl                                                   |
| `npx playwright test tests/ui/login.spec.ts:8` | Faqat 8-qatordagi test                                       |
| `npx playwright test -g "locked"`              | Nomida "locked" bor testlar (katta-kichik harf farqlanmaydi) |
| `npx playwright test --project=chromium`       | Faqat bitta project                                          |
| `npx playwright test --headed`                 | Brauzer oynasini ko'rsatib                                   |
| `npx playwright test --ui`                     | Interaktiv UI mode                                           |
| `npx playwright test --debug`                  | Playwright Inspector bilan qadamma-qadam                     |
| `npx playwright test --workers=1`              | Ketma-ket (parallelsiz)                                      |
| `npx playwright test --repeat-each=10`         | Har testni 10 marta — flaky testni ovlash uchun              |
| `npx playwright test --last-failed`            | Faqat oldingi safar yiqilganlar                              |
| `npx playwright test --list`                   | Testlarni ishga tushirmay, ro'yxatini ko'rsatadi             |
| `npx playwright test -x`                       | Birinchi yiqilishda to'xtaydi                                |
| `npx playwright show-report`                   | HTML hisobotni ochadi                                        |

Bu flag'lar `npx playwright test --help` chiqishidan (1.64.0) tekshirilgan. Tez-tez ishlatiladiganlarini `package.json`ga qo'shing:

```json
{
  "scripts": {
    "test": "playwright test",
    "test:ui": "playwright test --ui",
    "test:smoke": "playwright test --grep @smoke",
    "report": "playwright show-report"
  }
}
```

### Codegen: brauzerda bosib, kod olish

```bash
npx playwright codegen --test-id-attribute=data-test https://www.saucedemo.com
```

Brauzer va Inspector oynasi ochiladi: siz saytda bosgan har bir harakat kodga aylanadi. Rasmiy hujjat bo'yicha codegen eng yaxshi locator'ni o'zi tanlaydi — **role, matn va test id locator'larini ustun qo'yadi**. Toolbar'da uchta tekshiruv tugmasi bor: _assert visibility_, _assert text_, _assert value_. `-o tests/ui/new.spec.ts` bilan natijani faylga saqlash mumkin.

> **⚠️ Diqqat:** Codegen — **boshlang'ich nuqta**, tayyor test emas. U har bir bosishni yozib oladi, lekin test nomini, tekshiruvlarni (assertion), ma'lumotlarni va tuzilmani siz o'ylab topasiz. Generatsiya qilingan kodni doim qayta ko'rib chiqing va tozalang.

---

## 5. Birinchi test: saucedemo login {#birinchi-test}

Saucedemo — Sauce Labs'ning test mashqlari uchun yaratilgan demo do'koni. Barcha foydalanuvchilar uchun parol `secret_sauce`; `standard_user` oddiy, `locked_out_user` bloklangan, `problem_user` va `performance_glitch_user` esa ataylab "buzilgan" foydalanuvchilar.

`tests/ui/login.spec.ts`:

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

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

  test(
    "standard_user muvaffaqiyatli kiradi",
    { tag: "@smoke" },
    async ({ page }) => {
      // Arrange + Act
      await page.getByTestId("username").fill("standard_user");
      await page.getByTestId("password").fill("secret_sauce");
      await page.getByTestId("login-button").click();

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

  test("locked_out_user bloklangani haqida xabar oladi", async ({ page }) => {
    await page.getByTestId("username").fill("locked_out_user");
    await page.getByTestId("password").fill("secret_sauce");
    await page.getByTestId("login-button").click();

    await expect(page.getByTestId("error")).toHaveText(
      "Epic sadface: Sorry, this user has been locked out.",
    );
    await expect(page).toHaveURL("/");
  });

  test("bo'sh forma: username majburiy", async ({ page }) => {
    await page.getByTestId("login-button").click();

    await expect(page.getByTestId("error")).toHaveText(
      "Epic sadface: Username is required",
    );
  });

  test("noto'g'ri parol: aniq xato xabari", async ({ page }) => {
    await page.getByTestId("username").fill("standard_user");
    await page.getByTestId("password").fill("wrong_password");
    await page.getByTestId("login-button").click();

    await expect(page.getByTestId("error")).toHaveText(
      "Epic sadface: Username and password do not match any user in this service",
    );
  });
});
```

Qatorma-qator:

- `import { test, expect }` — test e'lon qilish va tekshirish funksiyalari.
- `test.describe('Login', ...)` — testlarni guruhlash; hisobotda "Login ›" bo'lib ko'rinadi.
- `test.beforeEach` — har bir testdan oldin login sahifasini ochadi.
- `page.goto('/')` — `baseURL` bilan birlashib `https://www.saucedemo.com/` ga boradi.
- `getByTestId('username')` — `data-test="username"` atributli element (config'dagi `testIdAttribute` tufayli).
- `await expect(...).toHaveText(...)` — matn kutilganiga teng bo'lguncha (standart 5 soniyagacha) qayta tekshiradi.
- `toHaveURL('/')` — nisbiy URL ham `baseURL` bo'yicha tekshiriladi.

Ishga tushiramiz:

```bash
npx playwright test tests/ui/login.spec.ts --project=chromium --reporter=list
```

```text
Running 4 tests using 4 workers

  ✓  2 [chromium] › tests/ui/login.spec.ts:36:7 › Login › noto'g'ri parol: aniq xato xabari (1.5s)
  ✓  3 [chromium] › tests/ui/login.spec.ts:30:7 › Login › bo'sh forma: username majburiy (1.5s)
  ✓  4 [chromium] › tests/ui/login.spec.ts:8:7 › Login › standard_user muvaffaqiyatli kiradi @smoke (1.5s)
  ✓  1 [chromium] › tests/ui/login.spec.ts:19:7 › Login › locked_out_user bloklangani haqida xabar oladi (2.1s)

  4 passed (2.6s)
```

To'rtta test 4 ta worker'da parallel ishladi — shuning uchun tartib aralash. Bu normal: testlar mustaqil.

> **🧠 Esda tuting:** Xato matnlarini (`Epic sadface: ...`) **saytdan aynan ko'chiring**. "Taxminan shunday" yozilgan kutilgan natija — yiqiladigan test yoki, undan ham yomoni, noto'g'ri ma'lumotga asoslangan test.

---

## 6. Locator'lar {#locators}

**Locator** — elementni "qanday topish" haqidagi tavsif. U yaratilgan paytda elementni qidirmaydi: action yoki assertion bajarilayotganda qidiradi, kerak bo'lsa kutadi va qayta urinadi. Shuning uchun locator'ni o'zgaruvchiga saqlab, qayta ishlatish xavfsiz.

![Locator tanlash tartibi: getByRole'dan XPath'gacha](/lessons/images/qa/qa_18_locator_priority.svg)

Rasmiy hujjatning asosiy tavsiyasi: **foydalanuvchiga ko'rinadigan atributlar va aniq kontraktlarni (test id) ustun qo'ying, avvalo `getByRole`**. CSS va XPath esa _"DOM tez-tez o'zgargani uchun"_ tavsiya etilmaydi. Yuqoridagi tartib shu tamoyil va Testing Library'ning mashhur ustuvorlik ro'yxatiga asoslangan.

| Locator            | Saucedemo misoli                                     | Qachon                                                  |
| ------------------ | ---------------------------------------------------- | ------------------------------------------------------- |
| `getByRole`        | `page.getByRole('button', { name: 'Login' })`        | Tugma, havola, checkbox, sarlavha — deyarli hamma narsa |
| `getByLabel`       | `page.getByLabel('Text input')` (Selenium web-form)  | Label'ga ega forma maydonlari                           |
| `getByPlaceholder` | `page.getByPlaceholder('Username')`                  | Label yo'q, placeholder bor maydonlar                   |
| `getByText`        | `page.getByText('Sauce Labs Backpack')`              | Interaktiv bo'lmagan matnlar                            |
| `getByAltText`     | `page.getByAltText('Sauce Labs Backpack')`           | Rasmlar                                                 |
| `getByTitle`       | saucedemo'da `title` atributli element yo'q          | `title` atributi bo'yicha (kam ishlatiladi)             |
| `getByTestId`      | `page.getByTestId('shopping-cart-badge')`            | Dasturchilar bilan kelishilgan barqaror kontrakt        |
| CSS                | `page.locator('[data-test="inventory-item"]')`       | Boshqa yo'l qolmaganda                                  |
| XPath              | `page.locator('//input[@data-test="login-button"]')` | Oxirgi chora                                            |

Bu misollarning hammasi testda ishlatib ko'rilgan:

```typescript
test("login sahifasi: role, placeholder va test id", async ({ page }) => {
  await page.goto("/");
  await page.getByPlaceholder("Username").fill("standard_user");
  await page.getByPlaceholder("Password").fill("secret_sauce");
  await page.getByRole("button", { name: "Login" }).click();

  await expect(page.getByTestId("title")).toHaveText("Products");
  await expect(page.getByText("Sauce Labs Backpack")).toBeVisible();
  await expect(page.getByAltText("Sauce Labs Backpack")).toBeVisible();
  await expect(page.locator('[data-test="inventory-item"]')).toHaveCount(6);
});
```

> **💡 Maslahat:** Saucedemo kabi saytlarda dasturchilar har bir muhim elementga `data-test` qo'ygan — bu ular bilan "kontrakt". Shuning uchun kurs testlarida asosan `getByTestId` ishlatamiz. O'z loyihangizda esa birinchi navbatda `getByRole`ni sinab ko'ring: u accessibility'ni ham bilvosita tekshiradi (tugmaning "nomi" bo'lmasa, screen reader foydalanuvchisi ham uni tushunmaydi).

### Filter va zanjir: aniq elementni topish

Inventory sahifasida 6 ta "Add to cart" tugmasi bor. Bizga aynan Backpack kartasidagisi kerak:

```typescript
const backpackCard = page
  .getByTestId("inventory-item")
  .filter({ hasText: "Sauce Labs Backpack" });

await backpackCard.getByRole("button", { name: "Add to cart" }).click();

await expect(
  backpackCard.getByRole("button", { name: "Remove" }),
).toBeVisible();
await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");
```

`filter()` variantlari: `hasText` (matn yoki regex), `hasNotText`, `has` (ichida boshqa locator bor), `hasNot`, `visible`.

### Strict mode — Playwright sizni himoya qiladi

Locator **qat'iy**: agar u bir nechta elementga mos kelsa va siz unga action qilsangiz, Playwright "omadga" birinchisini bosmaydi — xato beradi. Haqiqiy xabar (1.64.0):

```text
Error: locator.click: Error: strict mode violation: getByRole('button', { name: 'Add to cart' }) resolved to 6 elements:
    1) <button id="add-to-cart-sauce-labs-backpack" ...>Add to cart</button> aka getByTestId('add-to-cart-sauce-labs-backpack')
    2) <button id="add-to-cart-sauce-labs-bike-light" ...>Add to cart</button> aka getByTestId('add-to-cart-sauce-labs-bike-light')
    ...
```

E'tibor bering: Playwright har bir element uchun **tavsiya qilingan locator**ni ham ko'rsatadi (`aka getByTestId(...)`). Tuzatish — locator'ni aniqlashtirish (`filter`, test id). `.first()`, `.last()`, `.nth(i)` ham strictness'ni o'chiradi, lekin rasmiy hujjat ularni **tavsiya qilmaydi**: sahifa o'zgarsa, Playwright siz kutmagan elementni bosib yuborishi mumkin.

---

## 7. Action'lar {#actions}

| Action               | Misol                                               | Izoh                                                               |
| -------------------- | --------------------------------------------------- | ------------------------------------------------------------------ |
| `goto`               | `await page.goto('/')`                              | Sahifani ochish                                                    |
| `click` / `dblclick` | `await page.getByTestId('checkout').click()`        | Bosish                                                             |
| `fill`               | `await page.getByTestId('firstName').fill('Aziz')`  | Maydonni tozalab, matn yozadi                                      |
| `pressSequentially`  | `await input.pressSequentially('Aziz')`             | Harfma-harf yozish (har harfga reaksiya qiladigan maydonlar uchun) |
| `press`              | `await input.press('Enter')`                        | Klaviatura tugmasi                                                 |
| `check` / `uncheck`  | `await page.getByLabel('Default checkbox').check()` | Checkbox va radio                                                  |
| `selectOption`       | `await select.selectOption('lohi')`                 | `<select>`dan qiymat yoki label bo'yicha tanlash                   |
| `hover`              | `await item.hover()`                                | Sichqonchani olib borish                                           |
| `setInputFiles`      | `await fileInput.setInputFiles('rasm.png')`         | Fayl yuklash                                                       |
| `dragTo`             | `await card.dragTo(column)`                         | Sudrab tashlash                                                    |

### Misol: narx bo'yicha saralash

Saucedemo'dagi saralash `<select>` qiymatlari: `az`, `za`, `lohi` (Price low to high), `hilo`.

```typescript
test("saralash: narx bo'yicha o'sish tartibida", 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("product-sort-container").selectOption("lohi");
  await expect(page.getByTestId("active-option")).toHaveText(
    "Price (low to high)",
  );

  const priceTexts = await page
    .getByTestId("inventory-item-price")
    .allTextContents();
  const prices = priceTexts.map((text) => Number(text.replace("$", "")));
  const sorted = [...prices].sort((a, b) => a - b);

  expect(prices).toEqual(sorted);
  expect(prices[0]).toBe(7.99);
});
```

E'tibor bering: avval `active-option` matni o'zgarganini **web-first assertion** bilan kutdik, keyin ro'yxatni o'qidik. Agar `allTextContents()`ni darhol chaqirsak, saralash hali qo'llanmagan eski ro'yxatni o'qib qolishimiz mumkin edi. Narxlarni `map` bilan songa aylantirish va nusxa olib saralash — [QA 17](/ios-lessons/qa-typescript#massivlar) dagi massiv metodlari.

---

## 8. Assertion'lar {#assertions}

### Web-first (auto-retrying) assertion'lar

Ular `expect(locator)` yoki `expect(page)` bilan yoziladi va shart bajarilguncha **qayta tekshiradi** (standart 5 soniya):

| Assertion                             | Nima tekshiradi                                                  |
| ------------------------------------- | ---------------------------------------------------------------- |
| `toBeVisible()` / `toBeHidden()`      | Ko'rinadi / ko'rinmaydi (yoki yo'q)                              |
| `toBeEnabled()` / `toBeDisabled()`    | Faol / faol emas                                                 |
| `toBeChecked()`                       | Checkbox yoki radio belgilangan                                  |
| `toHaveText()`                        | Matn to'liq mos (massiv berilsa — ro'yxat)                       |
| `toContainText()`                     | Matn ichida bor                                                  |
| `toHaveValue()`                       | Input qiymati                                                    |
| `toHaveCount()`                       | Elementlar soni                                                  |
| `toHaveAttribute()` / `toHaveClass()` | Atribut / CSS class                                              |
| `toBeFocused()`                       | Fokusda                                                          |
| `toHaveURL()` / `toHaveTitle()`       | Sahifa manzili / sarlavhasi                                      |
| `toHaveScreenshot()`                  | Vizual taqqoslash ([QA 19](/ios-lessons/qa-playwright-advanced)) |

### Qayta urinmaydigan (generic) assertion'lar

`expect(qiymat).toBe(...)`, `toEqual`, `toContain`, `toBeGreaterThan` — qiymat **allaqachon qo'lingizda** bo'lganda (masalan, `allTextContents()` dan keyin). Ular qayta urinmaydi.

```typescript
// ❌ Beqaror: textContent bir marta o'qiladi, badge hali yangilanmagan bo'lishi mumkin
expect(await page.getByTestId("shopping-cart-badge").textContent()).toBe("2");

// ✅ Barqaror: badge "2" bo'lguncha kutadi
await expect(page.getByTestId("shopping-cart-badge")).toHaveText("2");
```

Rasmiy hujjat bu haqda ochiq aytadi: sahifalar ma'lumotni asinxron ko'rsatadi, shuning uchun qayta urinmaydigan assertion'lar flaky testga olib keladi — imkon bo'lsa auto-retrying assertion'ni afzal ko'ring.

### Soft assertion, izohli xabar va inkor

```typescript
await test.step("Summalarni tekshirish", async () => {
  await expect
    .soft(page.getByTestId("subtotal-label"))
    .toHaveText("Item total: $39.98");
  await expect.soft(page.getByTestId("tax-label")).toHaveText("Tax: $3.20");
  await expect
    .soft(page.getByTestId("total-label"), "jami summa noto'g'ri")
    .toHaveText("Total: $43.18");
});
```

- **`expect.soft`** — yiqilsa ham test davom etadi, oxirida test "failed" deb belgilanadi va **barcha** farqlar ro'yxati chiqadi. Bitta sahifadagi bir nechta maydonni tekshirishda qulay.
- **Ikkinchi argument** — hisobotda chiqadigan izoh: `"jami summa noto'g'ri"`.
- **`.not`** — inkor: `await expect(error).not.toBeVisible()`.

### Timeout'lar (rasmiy standart qiymatlar)

| Timeout                  | Standart                                | Qanday o'zgartiriladi                                                             |
| ------------------------ | --------------------------------------- | --------------------------------------------------------------------------------- |
| Test                     | 30 000 ms                               | config: `timeout: 60_000`; testda: `test.setTimeout(120_000)`                     |
| Expect                   | 5 000 ms                                | config: `expect: { timeout: 10_000 }`; joyida: `toBeVisible({ timeout: 10_000 })` |
| Action (`click`, `fill`) | cheklanmagan (test timeout'i doirasida) | config: `use: { actionTimeout: 10_000 }`                                          |
| Navigatsiya (`goto`)     | cheklanmagan                            | config: `use: { navigationTimeout: 30_000 }`                                      |

---

## 9. Auto-waiting va nega waitForTimeout yomon {#auto-waiting}

Playwright action'dan oldin elementning **tayyorligini** o'zi tekshiradi (actionability checks). Rasmiy jadvaldan asosiylari:

| Action                                  | Nimalar kutiladi                          |
| --------------------------------------- | ----------------------------------------- |
| `click`, `check`, `dblclick`, `uncheck` | Visible, Stable, Receives Events, Enabled |
| `fill`, `clear`                         | Visible, Enabled, Editable                |
| `selectOption`                          | Visible, Enabled                          |
| `hover`, `dragTo`                       | Visible, Stable, Receives Events          |

- **Visible** — elementning o'lchami bor va `visibility:hidden` emas;
- **Stable** — kamida ikki animatsiya kadri davomida joyi o'zgarmagan (animatsiya tugagan);
- **Receives Events** — bosiladigan nuqtada aynan shu element turibdi, ustida boshqa oyna (overlay) yo'q;
- **Enabled / Editable** — `disabled` yoki `readonly` emas.

### 1-misol: sekin login (`performance_glitch_user`)

Bu foydalanuvchida "Products" sahifasi ataylab kechikadi. Biz o'lchaganimizda kontent **~5,1 soniyada** chiqdi — standart 5 soniyalik expect timeout'idan biroz ko'p. Sleep yozmaymiz, faqat shu assertion'ga ko'proq vaqt beramiz:

```typescript
test("performance_glitch_user: sekin login, lekin sleep kerak emas", async ({
  page,
}) => {
  await page.goto("/");
  await page.getByTestId("username").fill("performance_glitch_user");
  await page.getByTestId("password").fill("secret_sauce");
  await page.getByTestId("login-button").click();

  // bu foydalanuvchida sahifa ~5 soniyada chiqadi: standart 5s yetmasligi mumkin
  await expect(page.getByTestId("title")).toHaveText("Products", {
    timeout: 15_000,
  });
});
```

Test sahifa chiqqan zahoti davom etadi: tez bo'lsa — tez, sekin bo'lsa — 15 soniyagacha kutadi. `waitForTimeout(15_000)` esa **har doim** 15 soniya yo'qotardi.

> **🎯 Intervyuda:** "Test ba'zan 5 soniyalik timeout bilan yiqiladi. Nima qilasiz?" — Avval sababini aniqlayman: ilova haqiqatan sekinmi (bu performance bug bo'lishi mumkin!) yoki test noto'g'ri narsani kutyaptimi. Agar kechikish kutilgan bo'lsa, faqat o'sha assertion'ga timeout beraman; global timeout'ni ko'tarish yoki `sleep` qo'shish — muammoni yashirish.

### 2-misol: kechikib paydo bo'ladigan element

Selenium'ning rasmiy demo sahifasida "Add a box!" tugmasi bosilgandan **1 soniya** keyin qizil quti qo'shiladi:

```typescript
test("dinamik element: 1 soniyadan keyin paydo bo'ladi", async ({ page }) => {
  await page.goto("https://www.selenium.dev/selenium/web/dynamic.html");
  await page.getByRole("button", { name: "Add a box!" }).click();

  // quti ~1 soniyadan keyin DOM'ga qo'shiladi — Playwright o'zi kutadi
  await expect(page.locator(".redbox")).toHaveCount(1);
});
```

`baseURL` faqat nisbiy manzillarga qo'shiladi — to'liq URL (`https://...`) berilsa, o'sha ochiladi.

### `waitForTimeout` o'rniga nima ishlatiladi

| Vaziyat                         | ❌ Yomon               | ✅ To'g'ri                                                                 |
| ------------------------------- | ---------------------- | -------------------------------------------------------------------------- |
| Element paydo bo'lishini kutish | `waitForTimeout(3000)` | `await expect(locator).toBeVisible()`                                      |
| Sahifa almashishini kutish      | `waitForTimeout(2000)` | `await expect(page).toHaveURL(/inventory/)` yoki `page.waitForURL(...)`    |
| API javobini kutish             | `waitForTimeout(5000)` | `page.waitForResponse(...)` ([QA 19](/ios-lessons/qa-playwright-advanced)) |
| Element holati                  | `waitForTimeout(1000)` | `await locator.waitFor({ state: 'hidden' })`                               |

> **⚠️ Diqqat:** Mashhur mashq sayti `the-internet.herokuapp.com`da ham dinamik yuklanish misollari bor. Lekin biz tekshirgan kuni (2026-yil 9-oktabr) uning statik fayllari HTTP 503 qaytardi va sahifa 30 soniyagacha yuklandi — testlar timeout bilan yiqildi. Shuning uchun mashqlar uchun Selenium'ning rasmiy demo sahifalarini tanladik. Bu ham dars: **tashqi servisga bog'liq test sizning nazoratingizda bo'lmagan sabablar bilan yiqiladi.**

---

## 10. Test tuzilmasi: describe, hook'lar va tag'lar {#tuzilma}

### Hook'lar

| Hook              | Qachon ishlaydi                              | Tipik ishlatish                                                        |
| ----------------- | -------------------------------------------- | ---------------------------------------------------------------------- |
| `test.beforeEach` | Har testdan oldin                            | Sahifani ochish, login                                                 |
| `test.afterEach`  | Har testdan keyin                            | Test ma'lumotini tozalash                                              |
| `test.beforeAll`  | Worker'da bir marta, barcha testlardan oldin | Qimmat tayyorgarlik (ehtiyot bo'ling: holat testlar orasida bo'linadi) |
| `test.afterAll`   | Worker'da bir marta, oxirida                 | Umumiy tozalash                                                        |

### Annotatsiyalar

| Annotatsiya    | Ma'nosi (rasmiy hujjat bo'yicha)                                                             |
| -------------- | -------------------------------------------------------------------------------------------- |
| `test.skip()`  | Test ishga tushirilmaydi                                                                     |
| `test.fail()`  | Test ishga tushiriladi va **yiqilishi kutiladi**; agar o'tib ketsa — Playwright xabar beradi |
| `test.fixme()` | Test yiqiladi deb belgilanadi va ishga tushirilmaydi                                         |
| `test.slow()`  | Test timeout'ini 3 barobar oshiradi                                                          |
| `test.only()`  | Faqat shu test(lar) ishlaydi — CI'da `forbidOnly` bilan taqiqlang                            |

### Tag'lar

```typescript
test(
  "standard_user muvaffaqiyatli kiradi",
  { tag: "@smoke" },
  async ({ page }) => {
    // ...
  },
);

test("to'liq hisobot", { tag: ["@slow", "@regression"] }, async ({ page }) => {
  // ...
});
```

Tag `@` bilan boshlanishi shart. Ishga tushirish:

```bash
npx playwright test --grep @smoke              # faqat @smoke
npx playwright test --grep-invert @slow        # @slow'dan tashqari hammasi
npx playwright test --grep "@smoke|@critical"  # YOKI
```

Bizning loyihada `--grep @smoke --list` natijasi (1.64.0):

```text
Listing tests:
  [chromium] › ui/cart.spec.ts:12:7 › Savatcha › 2 ta mahsulot qo'shilsa badge 2 ni ko'rsatadi
  [chromium] › ui/checkout.spec.ts:12:7 › Checkout › to'liq buyurtma: savatchadan 'Thank you' sahifasigacha
  [chromium] › ui/login.spec.ts:8:7 › Login › standard_user muvaffaqiyatli kiradi
Total: 3 tests in 3 files
```

> **💡 Maslahat:** Har PR'da tez `@smoke` to'plami, kechasi esa to'liq regression — bu strategiyani [QA 21](/ios-lessons/qa-ci-cd) da CI'ga ulaymiz.

### `test.step` — hisobotni o'qiladigan qilish

```typescript
await test.step("Savatchaga 2 ta mahsulot qo'shish", async () => {
  await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
  await page.getByTestId("add-to-cart-sauce-labs-bike-light").click();
});
```

HTML hisobot va trace'da qadamlar shu nomlar bilan ko'rinadi — yiqilganda qaysi bosqichda ekani darhol tushunarli.

---

## 11. To'liq misol: savatcha va checkout {#savatcha-checkout}

`tests/ui/cart.spec.ts`:

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

test.describe("Savatcha", () => {
  test.beforeEach(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 expect(page.getByTestId("title")).toHaveText("Products");
  });

  test(
    "2 ta mahsulot qo'shilsa badge 2 ni ko'rsatadi",
    { tag: "@smoke" },
    async ({ page }) => {
      await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
      await page.getByTestId("add-to-cart-sauce-labs-bike-light").click();

      await expect(page.getByTestId("shopping-cart-badge")).toHaveText("2");
    },
  );

  test("savatcha sahifasida qo'shilgan mahsulotlar ko'rinadi", async ({
    page,
  }) => {
    await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
    await page.getByTestId("add-to-cart-sauce-labs-bike-light").click();
    await page.getByTestId("shopping-cart-link").click();

    await expect(page).toHaveURL(/\/cart\.html$/);
    await expect(page.getByTestId("title")).toHaveText("Your Cart");
    await expect(page.getByTestId("inventory-item-name")).toHaveText([
      "Sauce Labs Backpack",
      "Sauce Labs Bike Light",
    ]);
  });

  test("mahsulot olib tashlansa badge kamayadi va oxirida yo'qoladi", async ({
    page,
  }) => {
    await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
    await page.getByTestId("add-to-cart-sauce-labs-bike-light").click();
    await page.getByTestId("remove-sauce-labs-backpack").click();

    await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");

    await page.getByTestId("remove-sauce-labs-bike-light").click();
    await expect(page.getByTestId("shopping-cart-badge")).toBeHidden();
  });
});
```

`toHaveText([...])` massiv bilan — ro'yxatdagi **barcha** elementlar matnini tartibi bilan tekshiradi. `toBeHidden()` — element yo'q bo'lsa ham o'tadi (bo'sh savatchada badge umuman chizilmaydi).

`tests/ui/checkout.spec.ts`:

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

test.describe("Checkout", () => {
  test.beforeEach(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 expect(page.getByTestId("title")).toHaveText("Products");
  });

  test(
    "to'liq buyurtma: savatchadan 'Thank you' sahifasigacha",
    { tag: "@smoke" },
    async ({ page }) => {
      await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
      await page.getByTestId("add-to-cart-sauce-labs-bike-light").click();
      await page.getByTestId("shopping-cart-link").click();
      await page.getByTestId("checkout").click();

      await page.getByTestId("firstName").fill("Aziz");
      await page.getByTestId("lastName").fill("Karimov");
      await page.getByTestId("postalCode").fill("100000");
      await page.getByTestId("continue").click();

      await expect(page.getByTestId("title")).toHaveText("Checkout: Overview");
      await expect(page.getByTestId("subtotal-label")).toHaveText(
        "Item total: $39.98",
      );
      await expect(page.getByTestId("tax-label")).toHaveText("Tax: $3.20");
      await expect(page.getByTestId("total-label")).toHaveText("Total: $43.18");

      await page.getByTestId("finish").click();

      await expect(page).toHaveURL(/\/checkout-complete\.html$/);
      await expect(page.getByTestId("complete-header")).toHaveText(
        "Thank you for your order!",
      );
    },
  );

  test("bo'sh forma: First Name majburiy", async ({ page }) => {
    await page.getByTestId("shopping-cart-link").click();
    await page.getByTestId("checkout").click();
    await page.getByTestId("continue").click();

    await expect(page.getByTestId("error")).toHaveText(
      "Error: First Name is required",
    );
  });
});
```

Uchala fayl birga (Chromium):

```text
Running 9 tests using 4 workers
  ...
  9 passed (4.8s)
```

### Bu kodda nima yoqmaydi?

Ishlaydi, lekin uchta muammo ko'zga tashlanadi:

1. **Login 3 joyda takrorlangan.** Login sahifasi o'zgarsa — 3 faylni tuzatasiz.
2. **Locator'lar testlar ichida sochilgan.** `getByTestId('shopping-cart-link')` ko'p joyda.
3. **Login ma'lumotlari kod ichida.** `standard_user` / `secret_sauce` testlarga qattiq yozilgan.

Keyingi darsda ([QA 19](/ios-lessons/qa-playwright-advanced)) aynan shu uch faylni **Page Object Model**ga o'tkazamiz: `LoginPage` (`login()`, `errorMessage`), `InventoryPage` (`addToCart(name)`, `sortBy('lohi')`, `prices()`), `Header` (`cartBadge`, `openCart()`), `CartPage` va `CheckoutPage`; login'ni `storageState` bilan bir marta bajaramiz, foydalanuvchi ma'lumotlarini esa `.env`dagi `SAUCE_USERNAME` / `SAUCE_PASSWORD`ga ko'chiramiz.

---

## 12. Debugging: trace, UI mode va Inspector {#debugging}

### Yiqilgan test nima deydi

Kutilgan matnda bitta harf xato bo'lsa (`'Product'` o'rniga `'Products'` kerak edi), Playwright 1.64 shunday yozadi:

```text
Error: expect(locator).toHaveText(expected) failed

Locator:  getByTestId('title')
Expected: "Product"
Received: "Products"
Timeout:  5000ms

Call log:
  - Expect "toHaveText" getByTestId('title') with timeout 5000ms
  - waiting for getByTestId('title')
    13 × locator resolved to <span class="title" data-test="title">Products</span>
       - unexpected value "Products"
```

Xabarni o'qishni o'rganing: **Locator** (nimani qidirdik), **Expected/Received** (nima kutdik va nima keldi), **Call log** (Playwright 5 soniya davomida 13 marta tekshirdi). Bundan tashqari `test-results/` papkasida screenshot, video va `error-context.md` fayli paydo bo'ladi — unda xato va sahifaning ARIA tuzilmasi (snapshot) bor; uni hamkasbingizga yoki AI yordamchiga berish mumkin ([QA 26](/ios-lessons/qa-ai-testing)).

### Asboblar

| Asbob                   | Qanday ochiladi                                                                   | Qachon                                                                       |
| ----------------------- | --------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **UI mode**             | `npx playwright test --ui`                                                        | Kundalik ish: testlarni tanlash, watch rejimi, har qadamning "vaqt sayohati" |
| **Inspector**           | `npx playwright test --debug` yoki kodda `await page.pause()`                     | Qadamma-qadam yurish, locator'ni jonli sinash                                |
| **Trace viewer**        | `npx playwright show-trace path/to/trace.zip` yoki HTML hisobotdagi trace belgisi | CI'da yiqilgan testni tahlil qilish                                          |
| **HTML report**         | `npx playwright show-report`                                                      | Barcha natijalar, screenshot, video, trace                                   |
| **VS Code kengaytmasi** | Testing paneli                                                                    | Testni muharrirdan ishga tushirish, _Record new_, _Pick locator_             |

Trace viewer nimalarni ko'rsatadi (rasmiy hujjat): har bir action va unda ishlatilgan locator, har qadamdagi to'liq DOM snapshot, screencast "film lentasi", tarmoq so'rovlari, brauzer console'i, xatolar va manba kodi. Trace'ni CLI'dan majburan yoqish: `npx playwright test --trace on`.

> **💡 Maslahat:** CI'da video emas, **trace** ishlating — Playwright'ning best practices sahifasi aynan shuni tavsiya qiladi. Trace'ni [trace.playwright.dev](https://trace.playwright.dev) da ham ochish mumkin; rasmiy hujjatga ko'ra u faylni to'liq brauzeringizda yuklaydi va hech qayerga yubormaydi.

---

## 13. Amaliy mashqlar {#mashqlar}

Barcha javoblar Playwright 1.64.0 + Chromium'da ishga tushirib tekshirilgan. Saucedemo mashqlari uchun yuqoridagi config (`baseURL`, `testIdAttribute: 'data-test'`) ishlatiladi.

### Mashq 1: Xato xabarini yopish

`standard_user` + `wrong_password` bilan kirishga urining. Xato matnini tekshiring, keyin xato yonidagi "×" tugmasini (`data-test="error-button"`) bosib, xato yo'qolganini tasdiqlang.

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

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

test("noto'g'ri parol va xatoni yopish", async ({ page }) => {
  await page.goto("/");
  await page.getByTestId("username").fill("standard_user");
  await page.getByTestId("password").fill("wrong_password");
  await page.getByTestId("login-button").click();

  const error = page.getByTestId("error");
  await expect(error).toHaveText(
    "Epic sadface: Username and password do not match any user in this service",
  );

  await page.getByTestId("error-button").click();
  await expect(error).toBeHidden();
});
```

`error` locator'ini bir marta yaratib, ikki marta ishlatdik — locator "dangasa": har safar qaytadan qidiradi.

</details>

### Mashq 2: Saralash

Login qiling va ikkita test yozing: (a) "Name (Z to A)" tanlanganda birinchi mahsulot `Test.allTheThings() T-Shirt (Red)`; (b) "Price (high to low)" tanlanganda birinchi narx `$49.99`, oxirgisi `$7.99`.

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

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

test.describe("Saralash", () => {
  test.beforeEach(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();
  });

  test("Name (Z to A): birinchi mahsulot", async ({ page }) => {
    await page.getByTestId("product-sort-container").selectOption("za");
    await expect(page.getByTestId("inventory-item-name").first()).toHaveText(
      "Test.allTheThings() T-Shirt (Red)",
    );
  });

  test("Price (high to low): eng qimmati birinchi", async ({ page }) => {
    await page.getByTestId("product-sort-container").selectOption("hilo");
    await expect(page.getByTestId("inventory-item-price").first()).toHaveText(
      "$49.99",
    );
    await expect(page.getByTestId("inventory-item-price").last()).toHaveText(
      "$7.99",
    );
  });
});
```

Bu yerda `.first()` va `.last()` o'rinli: biz ataylab "ro'yxatning birinchisi" haqida gapiryapmiz — tartibning o'zi tekshiruv mavzusi.

</details>

### Mashq 3: TodoMVC

[demo.playwright.dev/todomvc](https://demo.playwright.dev/todomvc/) da: 3 ta vazifa qo'shing ("Choy damlash", "Kitob o'qish", "Testlarni yozish"), birinchisini bajarilgan deb belgilang, hisoblagich `2 items left` ekanini, "Active" filtrida 2 ta, "Completed"da 1 ta vazifa qolganini tekshiring. Diqqat: bu sayt `data-test` emas, **`data-testid`** ishlatadi.

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

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

// TodoMVC demo data-testid ishlatadi, shu fayl uchun atributni almashtiramiz
test.use({ testIdAttribute: "data-testid" });

test("TodoMVC — qo'shish, bajarish va filtrlash", async ({ page }) => {
  await page.goto("https://demo.playwright.dev/todomvc/");

  const newTodo = page.getByPlaceholder("What needs to be done?");
  for (const title of ["Choy damlash", "Kitob o'qish", "Testlarni yozish"]) {
    await newTodo.fill(title);
    await newTodo.press("Enter");
  }

  await expect(page.getByTestId("todo-title")).toHaveCount(3);

  await page
    .getByTestId("todo-item")
    .filter({ hasText: "Choy damlash" })
    .getByRole("checkbox", { name: "Toggle Todo" })
    .check();

  await expect(page.getByTestId("todo-count")).toHaveText("2 items left");

  await page.getByRole("link", { name: "Active" }).click();
  await expect(page.getByTestId("todo-title")).toHaveText([
    "Kitob o'qish",
    "Testlarni yozish",
  ]);

  await page.getByRole("link", { name: "Completed" }).click();
  await expect(page.getByTestId("todo-title")).toHaveText(["Choy damlash"]);
});
```

`test.use({ testIdAttribute: 'data-testid' })` — config'dagi sozlamani faqat shu fayl uchun almashtiradi. Qiziq fakt: tekshiruv paytida biz ro'yxatni oddiy (qayta urinmaydigan) usulda o'qiganimizda, filtr bosilgandan keyin hali eski ro'yxat qaytgan edi. `toHaveText([...])` esa ro'yxat yangilanguncha kutdi — web-first assertion'ning qiymati aynan shunda.

</details>

### Mashq 4: Veb-forma (`getByLabel`)

Selenium'ning [rasmiy demo formasi](https://www.selenium.dev/selenium/web/web-form.html)da: "Text input"ga ism, "Password"ga parol yozing; "Dropdown (select)"dan `Two` (qiymati `2`) ni tanlang; "Checked checkbox"ni o'chiring, "Default checkbox"ni yoqing, "Default radio"ni tanlang; "Disabled input" faol emasligini tekshiring. "Submit"dan keyin `Form submitted` sarlavhasi va `Received!` matni chiqishi kerak.

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

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

test("veb-forma — label orqali to'ldirish", async ({ page }) => {
  await page.goto("https://www.selenium.dev/selenium/web/web-form.html");

  await page.getByLabel("Text input").fill("Aziz Karimov");
  await page.getByLabel("Password").fill("Parol123");
  await page.getByLabel("Dropdown (select)").selectOption("2");
  await page.getByLabel("Checked checkbox").uncheck();
  await page.getByLabel("Default checkbox").check();
  await page.getByLabel("Default radio").check();

  await expect(page.getByLabel("Dropdown (select)")).toHaveValue("2");
  await expect(page.getByLabel("Checked checkbox")).not.toBeChecked();
  await expect(page.getByLabel("Default checkbox")).toBeChecked();
  await expect(page.getByLabel("Checked radio")).not.toBeChecked();
  await expect(page.getByLabel("Disabled input")).toBeDisabled();

  await page.getByRole("button", { name: "Submit" }).click();

  await expect(
    page.getByRole("heading", { name: "Form submitted" }),
  ).toBeVisible();
  await expect(page.getByText("Received!")).toBeVisible();
});
```

Label'li formada `getByLabel` eng tabiiy tanlov: foydalanuvchi ham maydonni label'i bo'yicha taniydi. Radio guruhda bittasini tanlash ikkinchisini avtomatik o'chiradi — buni `not.toBeChecked()` bilan tasdiqladik.

</details>

### Mashq 5: Dinamik elementlar — sleep'siz

[dynamic.html](https://www.selenium.dev/selenium/web/dynamic.html) sahifasida: "Add a box!"ni ikki marta bosing va 2 ta qizil quti (`.redbox`) paydo bo'lishini kuting. Keyin "Reveal a new input" bosib, yashirin input (`#revealed`) ko'ringanini tekshiring va unga matn yozing. Hech qayerda `waitForTimeout` ishlatmang.

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

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

test("dinamik elementlar — sleep yozmasdan", async ({ page }) => {
  await page.goto("https://www.selenium.dev/selenium/web/dynamic.html");

  await page.getByRole("button", { name: "Add a box!" }).click();
  await page.getByRole("button", { name: "Add a box!" }).click();
  await expect(page.locator(".redbox")).toHaveCount(2);

  await page.getByRole("button", { name: "Reveal a new input" }).click();
  const revealed = page.locator("#revealed");
  await expect(revealed).toBeVisible();
  await revealed.fill("Paydo bo'ldim!");
  await expect(revealed).toHaveValue("Paydo bo'ldim!");
});
```

Qutilarda na role, na test id bor — shuning uchun CSS (`.redbox`, `#revealed`) ishlatildi. Bu "oxirgi chora" holati: real loyihada dasturchilardan test id qo'shishni so'ragan bo'lardik.

</details>

### Mashq 6: Strict mode xatosini tuzating

Quyidagi kod `strict mode violation ... resolved to 6 elements` xatosini beradi. Uni shunday tuzatingki, aynan **Sauce Labs Bike Light** savatchaga qo'shilsin.

```typescript
await page.getByRole("button", { name: "Add to cart" }).click();
```

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

```typescript
await page
  .getByTestId("inventory-item")
  .filter({ hasText: "Sauce Labs Bike Light" })
  .getByRole("button", { name: "Add to cart" })
  .click();

await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");
```

Yoki qisqaroq — xato xabarining o'zi taklif qilgan locator: `page.getByTestId('add-to-cart-sauce-labs-bike-light')`. `.nth(1)` ham "ishlaydi", lekin mahsulotlar tartibi o'zgarsa, jimgina boshqa mahsulotni qo'shadi — bu yashirin bug.

</details>

### Mashq 7: Bug ovi — `problem_user`

`problem_user` bilan kirganda mahsulot rasmlarida nimadir noto'g'ri. Barcha 6 ta mahsulot rasmining `src` atributini yig'ib, ularning **hammasi har xil** bo'lishi kerakligini tekshiradigan test yozing. Bug ma'lum bo'lgani uchun testni `test.fail()` bilan belgilang.

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

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

test("problem_user rasmlar bug'ini topish", async ({ page }) => {
  test.fail(
    true,
    "problem_user'da barcha mahsulot rasmlari bir xil (ma'lum bug)",
  );

  await page.goto("/");
  await page.getByTestId("username").fill("problem_user");
  await page.getByTestId("password").fill("secret_sauce");
  await page.getByTestId("login-button").click();
  await expect(page.getByTestId("inventory-item")).toHaveCount(6);

  const sources = await page
    .locator(".inventory_item img")
    .evaluateAll((images) => images.map((img) => img.getAttribute("src")));
  const unique = new Set(sources);

  expect(unique.size, "har bir mahsulotning o'z rasmi bo'lishi kerak").toBe(6);
});
```

Biz tekshirganda `problem_user`da 6 ta rasmning hammasi bir xil fayl (`/assets/sl-404-....jpg`) edi, `standard_user`da esa 6 xil. `list` reporter bu testni ✘ belgisi bilan ko'rsatadi, lekin umumiy natija **passed** — test kutilganidek yiqildi. Bug tuzatilgach test "kutilmaganda o'tdi" deb xabar beradi — shunda `test.fail()`ni olib tashlash vaqti keldi.

</details>

### Mashq 8: Tag'lar bilan ishlash

Bizning uchta UI faylda `@smoke` tegi qaysi testlarda bor? Quyidagi buyruqlar nechta testni ishga tushiradi: (a) `--grep @smoke`, (b) `--grep-invert @smoke`, (c) `-g "LOCKED"`? Javobni `--list` bilan tekshiring.

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

- (a) **3 ta**: login (`standard_user muvaffaqiyatli kiradi`), cart (`2 ta mahsulot qo'shilsa badge 2 ni ko'rsatadi`), checkout (`to'liq buyurtma`).
- (b) **6 ta**: qolgan barcha testlar (jami 9 − 3).
- (c) **1 ta**: `locked_out_user bloklangani haqida xabar oladi` — `--grep` katta-kichik harfni farqlamaydi.

```bash
npx playwright test tests/ui --project=chromium --grep @smoke --list
npx playwright test tests/ui --project=chromium --grep-invert @smoke --list
npx playwright test tests/ui --project=chromium -g "LOCKED" --list
```

</details>

---

## 14. O'zingizni tekshiring {#checklist}

- [ ] Playwright'ni ikki usulda o'rnata olaman va brauzerlarni yangilashni bilaman
- [ ] `playwright.config.ts`dagi `baseURL`, `testIdAttribute`, `retries`, `trace`, `projects` nima qilishini tushuntira olaman
- [ ] Locator tanlash tartibini va strict mode xatosini tuzatishni bilaman
- [ ] Web-first va qayta urinmaydigan assertion farqini tushuntira olaman
- [ ] Actionability tekshiruvlarini va `waitForTimeout` o'rniga nima ishlatishni bilaman
- [ ] Hook, annotatsiya va tag'lar bilan testlarni tashkil qila olaman
- [ ] Yiqilgan testni xato xabari, trace va UI mode orqali tahlil qila olaman

---

## Xulosa {#xulosa}

- Playwright har test uchun yangi `BrowserContext` beradi — testlar izolyatsiyalangan va parallel ishlaydi.
- O'rnatish: `npm init playwright@latest` (yoki qo'lda `@playwright/test` + `npx playwright install`); Playwright'ni yangilaganda brauzerlarni ham yangilang.
- `baseURL` va `testIdAttribute` (saucedemo uchun `data-test`) testlarni qisqa va muhitga bog'liq bo'lmagan qiladi.
- Locator tartibi: `getByRole` → label/placeholder/matn → test id → CSS → XPath. Strict mode xatosi — locator'ni aniqlashtirish signali, `.nth()` bilan "o'chirish" emas.
- Web-first assertion'lar (`await expect(locator)...`) kutadi va qayta urinadi; `textContent()` + `toBe()` — flaky test retsepti.
- Auto-waiting actionability'ni o'zi tekshiradi; `waitForTimeout` o'rniga assertion, `waitForURL`, `waitForResponse` ishlating; kutilgan sekinlik uchun faqat o'sha assertion'ga timeout bering.
- Debugging: xato xabarini o'qing (Locator / Expected / Received / Call log), keyin UI mode, Inspector va trace viewer.
- **Keyingi dars:** [QA 19: Playwright (2) — Page Object Model, fixtures, auth state, network mocking va visual testlar](/ios-lessons/qa-playwright-advanced)

## Manbalar {#manbalar}

1. [Playwright — Installation](https://playwright.dev/docs/intro)
2. [Playwright — Writing tests](https://playwright.dev/docs/writing-tests)
3. [Playwright — Running and debugging tests](https://playwright.dev/docs/running-tests)
4. [Playwright — Test configuration](https://playwright.dev/docs/test-configuration)
5. [Playwright — Locators](https://playwright.dev/docs/locators)
6. [Playwright — Actions (input)](https://playwright.dev/docs/input)
7. [Playwright — Assertions](https://playwright.dev/docs/test-assertions)
8. [Playwright — Auto-waiting (actionability)](https://playwright.dev/docs/actionability)
9. [Playwright — Timeouts](https://playwright.dev/docs/test-timeouts)
10. [Playwright — Annotations va tag'lar](https://playwright.dev/docs/test-annotations)
11. [Playwright — Test generator (codegen)](https://playwright.dev/docs/codegen)
12. [Playwright — UI mode](https://playwright.dev/docs/test-ui-mode)
13. [Playwright — Debugging tests](https://playwright.dev/docs/debug)
14. [Playwright — Trace viewer](https://playwright.dev/docs/trace-viewer)
15. [Playwright — Isolation va Browser Contexts](https://playwright.dev/docs/browser-contexts)
16. [Playwright — Getting started with VS Code](https://playwright.dev/docs/getting-started-vscode)
17. [Testing Library — Query priority](https://testing-library.com/docs/queries/about/#priority)
18. [Sauce Labs demo do'koni — saucedemo.com](https://www.saucedemo.com)
19. [Selenium demo sahifalari — web form](https://www.selenium.dev/selenium/web/web-form.html)
