QA 19: Playwright (2) — Page Object Model, fixtures, auth state, network mocking va visual testlar
🧪 QA Engineer — Zero to Hero
QA 19: Playwright (2) — Page Object Model, fixtures, auth state, network mocking va visual testlar
Holat: 2026-yil oktabr. Dars Playwright 1.64 (
@playwright/test), Node.js 24 LTS va TypeScript asosida yozilgan. Barcha kodlar www.saucedemo.com va practicesoftwaretesting.com (Toolshop) saytlarida Chromium'da ishga tushirib tekshirilgan.
Bu darsdan keyin siz: QA 18dagi "ishlaydigan" testlarni katta jamoada yillab yashay oladigan framework'ga aylantirasiz.
- Page Object Model (POM) bilan locator va harakatlarni bitta joyga yig'asiz;
test.extend()bilan o'z fixture'laringizni yozasiz (test, worker va auto fixture);- login'ni bir marta qilib,
storageStatebilan 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
- Muammo: nusxa-ko'chirilgan testlar
- Page Object Model (POM)
- Fixtures: test.extend() bilan toza testlar
- Autentifikatsiya: storageState va setup project
- Data-driven testlar va muhit sozlamalari
- Tarmoqni boshqarish: route, waitForResponse, HAR
- Vizual testlar: toHaveScreenshot()
- Projects, retries, serial rejim va reporter'lar
- Best practices va ESLint
- Amaliy mashqlar
- Xulosa
- Manbalar
1. Muammo: nusxa-ko'chirilgan testlar {#muammo}
QA 18'da biz saucedemo uchun to'g'ridan-to'g'ri locator'lar bilan testlar yozdik. 3–5 ta test bo'lganda bu juda qulay. Endi tasavvur qiling: loyihada 300 ta UI test bor va ularning har biri shunday boshlanadi:
// tests/ui/cart.spec.ts — "oldingi" uslub
test("savatchaga mahsulot qo'shish", async ({ page }) => {
await page.goto("/");
await page.getByTestId("username").fill("standard_user");
await page.getByTestId("password").fill("secret_sauce");
await page.getByTestId("login-button").click();
await page.getByTestId("add-to-cart-sauce-labs-backpack").click();
await expect(page.getByTestId("shopping-cart-badge")).toHaveText("1");
});
// tests/ui/checkout.spec.ts — yana o'sha 4 qator login
test("buyurtma berish", async ({ page }) => {
await page.goto("/");
await page.getByTestId("username").fill("standard_user");
await page.getByTestId("password").fill("secret_sauce");
await page.getByTestId("login-button").click();
// ...
});
Bu yondashuvning muammolari:
| 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:
qa-demo-tests/
├── package.json
├── playwright.config.ts # projects, baseURL, testIdAttribute: 'data-test'
├── .env.example # BASE_URL, SAUCE_USERNAME, SAUCE_PASSWORD ...
├── pages/ # POM: LoginPage, InventoryPage, CartPage, CheckoutPage, Header
├── fixtures/test.ts # test.extend — page object'lar fixture sifatida
├── data/checkout-customers.json
├── tests/
│ ├── auth.setup.ts # bir marta login -> playwright/.auth/*.json
│ └── ui/
│ ├── login.spec.ts
│ ├── cart.spec.ts
│ ├── checkout.spec.ts
│ ├── network.spec.ts # Toolshop: route, HAR
│ └── visual.spec.ts
└── playwright/.auth/ # .gitignore'da! (sessiya ma'lumoti)
.gitignore fayliga albatta qo'shing:
node_modules/
test-results/
playwright-report/
blob-report/
playwright/.auth/
.env
⚠️ Diqqat:
playwright/.auth/*.jsonichida 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:
- Locator'lar — sahifadagi elementlar (
readonlyproperty sifatida, constructor'da bir marta e'lon qilinadi); - Harakatlar (methods) — foydalanuvchi tilida:
login(),addToCart(),checkout().
Natijada test "nima qilinyapti"ni aytadi, page object esa "qanday qilinadi"ni yashiradi. UI o'zgarsa — faqat bitta class tuzatiladi.
Page Object Model: testlar, fixture'lar, page object'lar va locator'lar qatlami
2.1. LoginPage
// pages/LoginPage.ts
import type { Locator, Page } from "@playwright/test";
export class LoginPage {
readonly page: Page;
readonly usernameInput: Locator;
readonly passwordInput: Locator;
readonly loginButton: Locator;
readonly errorMessage: Locator;
constructor(page: Page) {
this.page = page;
this.usernameInput = page.getByTestId("username");
this.passwordInput = page.getByTestId("password");
this.loginButton = page.getByTestId("login-button");
this.errorMessage = page.getByTestId("error");
}
async goto() {
await this.page.goto("/");
}
async login(username: string, password: string) {
await this.usernameInput.fill(username);
await this.passwordInput.fill(password);
await this.loginButton.click();
}
}
🧠 Esda tuting: Locator — bu "qidiruv retsepti", element emas.
page.getByTestId('username')constructor'da yaratilganda hali hech narsa qidirilmaydi. Qidiruv faqatfill(),click()yokiexpect()paytida bo'ladi. Shuning uchun locator'larni constructor'da e'lon qilish xavfsiz.
2.2. Header — takrorlanuvchi qism uchun "component object"
Savatcha ikonkasi va menyu har bir sahifada bor. Uni har bir page class'ga ko'chirish o'rniga alohida komponent qilamiz va composition orqali ishlatamiz:
// pages/Header.ts
import type { Locator, Page } from "@playwright/test";
// Har sahifada takrorlanadigan qism — alohida "component object"
export class Header {
readonly cartLink: Locator;
readonly cartBadge: Locator;
readonly menuButton: Locator;
readonly logoutLink: Locator;
constructor(page: Page) {
this.cartLink = page.getByTestId("shopping-cart-link");
this.cartBadge = page.getByTestId("shopping-cart-badge");
this.menuButton = page.getByRole("button", { name: "Open Menu" });
this.logoutLink = page.getByTestId("logout-sidebar-link");
}
async openCart() {
await this.cartLink.click();
}
async logout() {
await this.menuButton.click();
await this.logoutLink.click();
}
}
2.3. InventoryPage — filter va chaining bilan
Mahsulot tugmalarining data-test qiymatlari nomdan yasaladi (add-to-cart-sauce-labs-backpack). Bunga bog'lanib qolish o'rniga, kartani nomi bo'yicha topib, uning ichidagi tugmani bosamiz — bu ancha barqaror:
// pages/InventoryPage.ts
import type { Locator, Page } from "@playwright/test";
import { Header } from "./Header";
export type SortOption = "az" | "za" | "lohi" | "hilo";
export class InventoryPage {
readonly page: Page;
readonly header: Header;
readonly title: Locator;
readonly items: Locator;
readonly itemNames: Locator;
readonly itemPrices: Locator;
readonly sortSelect: Locator;
constructor(page: Page) {
this.page = page;
this.header = new Header(page);
this.title = page.getByTestId("title");
this.items = page.getByTestId("inventory-item");
this.itemNames = page.getByTestId("inventory-item-name");
this.itemPrices = page.getByTestId("inventory-item-price");
this.sortSelect = page.getByTestId("product-sort-container");
}
async goto() {
await this.page.goto("/inventory.html");
}
// Mahsulot kartasini nomi bo'yicha topamiz
productCard(name: string): Locator {
return this.items.filter({ hasText: name });
}
async addToCart(name: string) {
await this.productCard(name)
.getByRole("button", { name: "Add to cart" })
.click();
}
async removeFromCart(name: string) {
await this.productCard(name)
.getByRole("button", { name: "Remove" })
.click();
}
async sortBy(option: SortOption) {
await this.sortSelect.selectOption(option);
}
// "$29.99" -> 29.99
async prices(): Promise<number[]> {
const texts = await this.itemPrices.allTextContents();
return texts.map((text) => Number(text.replace("$", "")));
}
}
SortOption turi saucedemo'dagi <select> qiymatlariga mos: az, za, lohi, hilo. TypeScript endi sortBy('lowhigh') kabi xatoni test ishga tushmasdan oldin ushlaydi.
2.4. CartPage va CheckoutPage
// pages/CartPage.ts
import type { Locator, Page } from "@playwright/test";
export class CartPage {
readonly page: Page;
readonly title: Locator;
readonly itemNames: Locator;
readonly checkoutButton: Locator;
readonly continueShoppingButton: Locator;
constructor(page: Page) {
this.page = page;
this.title = page.getByTestId("title");
this.itemNames = page.getByTestId("inventory-item-name");
this.checkoutButton = page.getByTestId("checkout");
this.continueShoppingButton = page.getByTestId("continue-shopping");
}
async goto() {
await this.page.goto("/cart.html");
}
async checkout() {
await this.checkoutButton.click();
}
}
// pages/CheckoutPage.ts
import type { Locator, Page } from "@playwright/test";
export interface CustomerInfo {
firstName: string;
lastName: string;
postalCode: string;
}
export class CheckoutPage {
readonly page: Page;
readonly firstNameInput: Locator;
readonly lastNameInput: Locator;
readonly postalCodeInput: Locator;
readonly continueButton: Locator;
readonly finishButton: Locator;
readonly errorMessage: Locator;
readonly subtotalLabel: Locator;
readonly taxLabel: Locator;
readonly totalLabel: Locator;
readonly completeHeader: Locator;
constructor(page: Page) {
this.page = page;
this.firstNameInput = page.getByTestId("firstName");
this.lastNameInput = page.getByTestId("lastName");
this.postalCodeInput = page.getByTestId("postalCode");
this.continueButton = page.getByTestId("continue");
this.finishButton = page.getByTestId("finish");
this.errorMessage = page.getByTestId("error");
this.subtotalLabel = page.getByTestId("subtotal-label");
this.taxLabel = page.getByTestId("tax-label");
this.totalLabel = page.getByTestId("total-label");
this.completeHeader = page.getByTestId("complete-header");
}
async fillInformation(info: CustomerInfo) {
await this.firstNameInput.fill(info.firstName);
await this.lastNameInput.fill(info.lastName);
await this.postalCodeInput.fill(info.postalCode);
}
async continue() {
await this.continueButton.click();
}
async finish() {
await this.finishButton.click();
}
}
2.5. Assertion'lar qayerda yashaydi?
Bu bahsli mavzu. Playwright hujjatidagi POM misolida ham page object ichida expect bor. Bizning qoidamiz:
| 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 (Headerkabi kichik komponentlar) yaxshiroq. - **Metod ichida
awaitni unutish** —this.loginButton.click()oldidaawaitbo'lmasa, test keyingi qatorga o'tib ketadi va tasodifiy yiqiladi. - **Locator o'rniga
ElementHandlesaqlash** (page.$('...')) — eskirgan va DOM yangilanganda "stale" bo'ladi. FaqatLocatorishlating. - 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) | test |
Fixture'lar dangasa (lazy): test so'ramagan fixture umuman yaratilmaydi.
3.1. O'z fixture'larimiz
// fixtures/test.ts
import { test as base, expect } from "@playwright/test";
import { LoginPage } from "../pages/LoginPage";
import { InventoryPage } from "../pages/InventoryPage";
import { CartPage } from "../pages/CartPage";
import { CheckoutPage } from "../pages/CheckoutPage";
type Pages = {
loginPage: LoginPage;
inventoryPage: InventoryPage;
cartPage: CartPage;
checkoutPage: CheckoutPage;
consoleErrors: string[];
};
export const test = base.extend<Pages>({
loginPage: async ({ page }, use) => {
await use(new LoginPage(page));
},
inventoryPage: async ({ page }, use) => {
await use(new InventoryPage(page));
},
cartPage: async ({ page }, use) => {
await use(new CartPage(page));
},
checkoutPage: async ({ page }, use) => {
await use(new CheckoutPage(page));
},
// auto: true — har bir testda o'zi ishga tushadi, testda so'rash shart emas
consoleErrors: [
async ({ page }, use, testInfo) => {
const errors: string[] = [];
page.on("console", (message) => {
if (message.type() === "error") errors.push(message.text());
});
await use(errors);
if (errors.length > 0) {
await testInfo.attach("console-errors.txt", {
body: errors.join("\n"),
contentType: "text/plain",
});
}
},
{ auto: true },
],
});
export { expect };
Fixture tanasini uch qismga bo'lib o'qing:
async ({ page }, use, testInfo) => {
... SETUP — testdan oldin (obyekt yaratish, ma'lumot tayyorlash)
await use(x) — test shu yerda ishlaydi va x'ni oladi
... TEARDOWN — testdan keyin (tozalash, hisobotga fayl biriktirish)
}
consoleErrors — auto fixture: hech bir test uni so'ramaydi, lekin u har bir testda ishlaydi va brauzer konsolidagi xatolarni HTML hisobotga console-errors.txt qilib biriktiradi. Bug report uchun tayyor dalil (QA 8).
3.2. Refactor qilingan test
Endi testlar @playwright/testdan emas, o'zimizning fixtures/test.tsdan import qiladi:
// tests/ui/cart.spec.ts
import { test, expect } from "../../fixtures/test";
test.describe("Savatcha", () => {
test.beforeEach(async ({ inventoryPage }) => {
// login yo'q — storageState tufayli darhol ichkaridamiz (4-bo'lim)
await inventoryPage.goto();
});
test(
"2 ta mahsulot qo'shish va bittasini olib tashlash",
{ tag: ["@smoke", "@mobile"] },
async ({ inventoryPage, cartPage }) => {
await inventoryPage.addToCart("Sauce Labs Backpack");
await inventoryPage.addToCart("Sauce Labs Bike Light");
await expect(inventoryPage.header.cartBadge).toHaveText("2");
await inventoryPage.removeFromCart("Sauce Labs Backpack");
await expect(inventoryPage.header.cartBadge).toHaveText("1");
await inventoryPage.header.openCart();
await expect(cartPage.title).toHaveText("Your Cart");
await expect(cartPage.itemNames).toHaveText(["Sauce Labs Bike Light"]);
},
);
test("narx bo'yicha saralash (arzondan qimmatga)", async ({
inventoryPage,
}) => {
await inventoryPage.sortBy("lohi");
const prices = await inventoryPage.prices();
const sorted = [...prices].sort((a, b) => a - b);
expect(prices).toEqual(sorted);
});
});
Solishtiring: 1-bo'limdagi testda locator'lar va login qatorlari bor edi, bu yerda esa faqat biznes qadamlari qoldi.
3.3. Fixture doiralari: test, worker, auto
| 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 20da yozamiz. Qisqa ko'rinishi:
export const test = base.extend<{}, { authToken: string }>({
authToken: [
async ({ playwright }, use) => {
const token = "..."; // worker boshida bir marta olinadi
await use(token);
},
{ scope: "worker" },
],
});
💡 Maslahat: Fixture'lar bir-biriga bog'lanishi mumkin:
cartPagefixture'ipageni,newBookingesabookingClientni so'raydi. Playwright tartibni o'zi hal qiladi — xuddi DI (dependency injection) konteyneri kabi.
🎯 Intervyuda: "
beforeEachva fixture farqi nima?" —beforeEachdescribe ichidagi barcha testlar uchun ishlaydi va natijani global o'zgaruvchiga yozishga majbur qiladi. Fixture esa faqat so'ragan testda yaratiladi, tipizatsiyalangan, qayta ishlatiladi va o'z teardown'iga ega.
4. Autentifikatsiya: storageState va setup project {#auth-state}
Login — eng ko'p takrorlanadigan qadam. Har testda UI orqali login qilish ham sekin, ham login sahifasidagi har bir muammo 300 ta testni qizartiradi. Yechim: bir marta login qilib, sessiyani faylga saqlash va barcha testlarni shu holatdan boshlash.
storageState brauzer kontekstining cookie va localStorage'ini JSON faylga yozadi (Playwright 1.51+ da indexedDB: true opsiyasi bilan IndexedDB'ni ham). Saucedemo login holatini session-username cookie'sida saqlaydi, shuning uchun bu usul unga to'liq mos keladi.
4.1. Setup fayli (ikki rol bilan)
// tests/auth.setup.ts
import { test as setup, expect } from "@playwright/test";
const users = ["standard_user", "problem_user"] as const;
for (const username of users) {
setup(`${username} sifatida login`, async ({ page }) => {
await page.goto("/");
await page.getByTestId("username").fill(username);
await page
.getByTestId("password")
.fill(process.env.SAUCE_PASSWORD ?? "secret_sauce");
await page.getByTestId("login-button").click();
await expect(page).toHaveURL(/inventory\.html/);
// cookie va localStorage'ni faylga yozamiz
await page
.context()
.storageState({ path: `playwright/.auth/${username}.json` });
});
}
4.2. Config: setup project va dependencies
// playwright.config.ts
import { defineConfig, devices } from "@playwright/test";
import dotenv from "dotenv";
import path from "node:path";
// .env faylidan BASE_URL, login va parollarni o'qiymiz
dotenv.config({ path: path.resolve(__dirname, ".env"), quiet: true });
const AUTH_FILE = "playwright/.auth/standard_user.json";
export default defineConfig({
testDir: "./tests",
fullyParallel: true,
forbidOnly: !!process.env.CI,
retries: process.env.CI ? 2 : 0,
workers: process.env.CI ? 1 : undefined,
reporter: [["list"], ["html", { open: "never" }]],
use: {
baseURL: process.env.BASE_URL ?? "https://www.saucedemo.com",
testIdAttribute: "data-test",
trace: "on-first-retry",
screenshot: "only-on-failure",
},
projects: [
// 1) avval login qilib, holatni faylga saqlaydi
{ name: "setup", testMatch: /.*\.setup\.ts/ },
// 2) UI testlar tayyor login holati bilan boshlanadi
{
name: "chromium",
testDir: "./tests/ui",
use: { ...devices["Desktop Chrome"], storageState: AUTH_FILE },
dependencies: ["setup"],
},
// 3) mobil emulyatsiya: faqat @mobile tegli testlar
{
name: "mobile-chrome",
testDir: "./tests/ui",
grep: /@mobile/,
use: { ...devices["Pixel 7"], storageState: AUTH_FILE },
dependencies: ["setup"],
},
// 4) API testlar — brauzer kerak emas (QA 20)
{
name: "api",
testDir: "./tests/api",
use: {
baseURL:
process.env.API_BASE_URL ?? "https://restful-booker.herokuapp.com",
extraHTTPHeaders: { Accept: "application/json" },
},
},
],
});
Ishga tushirish tartibi:
npx playwright test
│
├─ [setup] auth.setup.ts → playwright/.auth/standard_user.json, problem_user.json
│
├─ [chromium] ui/*.spec.ts (storageState: standard_user.json) ← dependencies: setup
├─ [mobile-chrome] ui/*.spec.ts @mobile ← dependencies: setup
└─ [api] api/*.spec.ts (brauzersiz, setup'ga bog'liq emas)
--project=chromium deb ishga tushirsangiz ham Playwright avval setupni bajaradi — chunki u dependency. Buni o'chirish uchun --no-deps flag'i bor.
4.3. Login'ni tekshiradigan test sessiyasiz bo'lishi shart
login.spec.ts login formasini tekshiradi, unga tayyor sessiya xalaqit beradi. Fayl boshida bo'sh holat beramiz:
// tests/ui/login.spec.ts (boshi)
import { test, expect } from "../../fixtures/test";
// Bu fayl login formasini tekshiradi — saqlangan sessiya kerak emas
test.use({ storageState: { cookies: [], origins: [] } });
4.4. Boshqa rol bilan test
// tests/ui/problem-user.spec.ts
import { test, expect } from "../../fixtures/test";
// Boshqa rol: problem_user sessiyasi bilan ishlaymiz
test.use({ storageState: "playwright/.auth/problem_user.json" });
test("problem_user: har mahsulotning rasmi har xil bo'lishi kerak", async ({
inventoryPage,
}) => {
test.fail(true, "Ma'lum bug: problem_user uchun barcha rasmlar bir xil");
await inventoryPage.goto();
const sources = await inventoryPage.items
.locator("img")
.evaluateAll((images) => images.map((image) => image.getAttribute("src")));
expect(new Set(sources).size).toBe(sources.length);
});
problem_user — saucedemo'dagi ataylab "buzilgan" foydalanuvchi: unda 6 ta mahsulotning hammasida bir xil rasm chiqadi. Test to'g'ri kutilgan xulq-atvorni tekshiradi (rasmlar unikal bo'lishi kerak) va test.fail() bilan "bu ma'lum bug, hozircha yiqilishi kutiladi" deb belgilanadi. Bug tuzatilgan kuni test kutilmaganda o'tib ketadi va Playwright buni xato sifatida ko'rsatadi — test.fail()ni olib tashlash vaqti keldi degani.
⚠️ Diqqat: Saqlangan sessiyaning ham muddati bor. Bizning tekshiruvimizda saucedemo'ning
session-usernamecookie'si login'dan keyin taxminan 10 daqiqa amal qiladiganexpiresbilan o'rnatildi (buni.auth/*.jsonfaylidagiexpiresmaydonida ko'rasiz). Setup project harnpx playwright testda qayta ishlagani uchun bu muammo emas, lekin eski.authfaylini qo'lda qayta ishlatsangiz, testlar login sahifasiga "qulab" tushadi.
🎯 Intervyuda: "UI testlarda login'ni qanday optimallashtirasiz?" — Setup project'da bir marta login qilib
storageStateni faylga saqlayman, test project'lar uniuse.storageStatevadependenciesorqali qayta ishlatadi. Login formasining o'zi esa alohida, sessiyasiz testlar bilan tekshiriladi. Ilova API'si bo'lsa, login'ni API orqali qilish yanada tezroq (QA 20).
5. Data-driven testlar va muhit sozlamalari {#data-driven}
5.1. Massiv ustida sikl
Bitta ssenariyni turli ma'lumotlar bilan tekshirish uchun testni sikl ichida yaratamiz:
// tests/ui/login.spec.ts (davomi)
const invalidLogins = [
{
username: "locked_out_user",
error: "Epic sadface: Sorry, this user has been locked out.",
},
{
username: "unknown_user",
error:
"Epic sadface: Username and password do not match any user in this service",
},
{ username: "", error: "Epic sadface: Username is required" },
];
test.describe("Login", () => {
test.beforeEach(async ({ loginPage }) => {
await loginPage.goto();
});
test(
"standard_user tizimga kiradi",
{ tag: "@smoke" },
async ({ loginPage, inventoryPage, page }) => {
await loginPage.login("standard_user", "secret_sauce");
await expect(page).toHaveURL(/inventory\.html/);
await expect(inventoryPage.title).toHaveText("Products");
},
);
for (const { username, error } of invalidLogins) {
test(`kira olmaydi: "${username}"`, async ({ loginPage, page }) => {
await loginPage.login(username, "secret_sauce");
await expect(loginPage.errorMessage).toHaveText(error);
await expect(page).toHaveURL("/");
});
}
});
Ikki muhim qoida:
- Test nomlari unikal bo'lsin — shuning uchun nomga
${username}qo'shdik. Bir faylda ikkita bir xil nomli test bo'lsa, Playwright xato beradi. - **Test ichida
if/elseyozmang.** Muvaffaqiyatli va muvaffaqiyatsiz login'ni bitta sikldaif (error)bilan ajratsangiz, test ikki xil narsani tekshiradi va o'qish qiyinlashadi. Biz ularni ikki xil testga ajratdik (9-bo'limda ESLint ham buni talab qilishini ko'rasiz).
5.2. JSON fayldan ma'lumot
[
{ "firstName": "Aziz", "lastName": "Karimov", "postalCode": "100000" },
{ "firstName": "Madina", "lastName": "Yusupova", "postalCode": "140100" },
{ "firstName": "Jasur", "lastName": "To'xtayev", "postalCode": "200100" }
]
// tests/ui/checkout.spec.ts
import { test, expect } from "../../fixtures/test";
import customers from "../../data/checkout-customers.json";
test.describe("Checkout", () => {
test.beforeEach(async ({ inventoryPage }) => {
await inventoryPage.goto();
await inventoryPage.addToCart("Sauce Labs Backpack");
await inventoryPage.addToCart("Sauce Labs Bike Light");
await inventoryPage.header.openCart();
});
for (const customer of customers) {
test(`buyurtma: ${customer.firstName} ${customer.lastName}`, async ({
cartPage,
checkoutPage,
}) => {
await cartPage.checkout();
await checkoutPage.fillInformation(customer);
await checkoutPage.continue();
await expect(checkoutPage.subtotalLabel).toHaveText("Item total: $39.98");
await expect(checkoutPage.taxLabel).toHaveText("Tax: $3.20");
await expect(checkoutPage.totalLabel).toHaveText("Total: $43.18");
await checkoutPage.finish();
await expect(checkoutPage.completeHeader).toHaveText(
"Thank you for your order!",
);
});
}
test("ism kiritilmasa xato chiqadi", async ({ cartPage, checkoutPage }) => {
await cartPage.checkout();
await checkoutPage.continue();
await expect(checkoutPage.errorMessage).toHaveText(
"Error: First Name is required",
);
});
});
JSON'ni import qilish uchun tsconfig.jsonda "resolveJsonModule": true bo'lishi kerak. Tasodifiy, lekin realistik ma'lumot kerak bo'lsa — @faker-js/faker (QA 17'da tanishgansiz): faker.person.firstName(). Uning amaliy ishlatilishini QA 20'dagi buildBooking()da ko'rasiz.
💡 Maslahat: Data-driven testda ekvivalent sinflar va chegaraviy qiymatlardan foydalaning (QA 5). 50 ta tasodifiy ism emas, har bir sinfdan bittadan vakil — testlar kam, qamrov yuqori.
5.3. Muhit sozlamalari: .env va baseURL
Bir xil testlar dev, staging va production-like muhitlarda ishlashi kerak. URL va login'larni kodga yozmaymiz:
# .env.example — git'ga commit qilinadi (namuna)
BASE_URL=https://www.saucedemo.com
API_BASE_URL=https://restful-booker.herokuapp.com
SAUCE_USERNAME=standard_user
SAUCE_PASSWORD=secret_sauce
BOOKER_USERNAME=admin
BOOKER_PASSWORD=password123
.env.exampledan nusxa olib .env yaratasiz (u .gitignoreda). Config dotenv orqali uni o'qiydi (4.2-bo'limdagi dotenv.config(...)). quiet: true — dotenv 17+ versiyalarida har ishga tushganda chiqadigan "injected env" log qatorini o'chiradi. Muhitni bir martalik almashtirish:
BASE_URL=https://staging.demobozor.example npx playwright test --project=chromium
(staging.demobozor.example — kursimizdagi xayoliy DemoBozor misoli.)
⚠️ Diqqat: O'zgaruvchiga
USERNAMEdeb nom bermang. Windows'daUSERNAMEtizim o'zgaruvchisi allaqachon mavjud,dotenvesa sukut bo'yicha mavjud o'zgaruvchini ustidan yozmaydi — testlaringiz Windows foydalanuvchi nomi bilan login qilishga urinadi. Shuning uchunSAUCE_USERNAMEkabi 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)
// tests/ui/network.spec.ts
import { test, expect } from '@playwright/test';
const SHOP = 'https://practicesoftwaretesting.com';
const API = 'https://api.practicesoftwaretesting.com';
test.describe('Tarmoqni boshqarish (Toolshop)', () => {
test('javobni o\'zgartirish: birinchi mahsulot nomi', async ({ page }) => {
await page.route(`${API}/products*`, async (route) => {
const response = await route.fetch(); // haqiqiy serverdan olamiz
const json = await response.json();
json.data[0].name = 'Choynak (mock)'; // bitta maydonni almashtiramiz
await route.fulfill({ response, json });
});
await page.goto(SHOP);
await expect(page.getByTestId('product-name').first()).toHaveText('Choynak (mock)');
});
6.2. Bo'sh holat va server xatosi
test("bo'sh natija holati", async ({ page }) => {
await page.route(`${API}/products*`, async (route) => {
const response = await route.fetch();
const json = await response.json();
await route.fulfill({ response, json: { ...json, data: [], total: 0 } });
});
await page.goto(SHOP);
await expect(page.getByTestId("no-results")).toHaveText(
"There are no products found.",
);
});
test("server 500 qaytarsa UI nima qiladi", async ({ page }) => {
await page.route(`${API}/products*`, (route) =>
route.fulfill({
status: 500,
contentType: "application/json",
body: JSON.stringify({ message: "Server Error" }),
}),
);
await page.goto(SHOP);
await expect(page.getByTestId("sort")).toBeVisible();
// Kuzatuv: mahsulot ham, xato xabari ham ko'rinmaydi -> UX topilma
await expect(page.getByTestId("product-name")).toHaveCount(0);
await expect(page.getByTestId("no-results")).toHaveCount(0);
});
Uchinchi test qiziq natija berdi: 500 xatoda Toolshop foydalanuvchiga hech narsa demaydi — na mahsulot, na "Xatolik yuz berdi, qayta urinib ko'ring" xabari. Bo'sh ro'yxatdagi "There are no products found." ham chiqmaydi. Bu — mock qilish orqali topilgan haqiqiy UX muammo: QA sifatida unga bug report yozasiz (QA 8), kutilgan natija sifatida esa "xato holati uchun tushunarli xabar va qayta urinish tugmasi"ni ko'rsatasiz.
6.3. So'rovni bekor qilish (abort)
test("rasmlarni bloklash (abort)", async ({ page }) => {
await page.route(/\.(png|jpe?g|webp|avif)$/, (route) => route.abort());
await page.goto(SHOP);
await expect(page.getByTestId("product-name").first()).toBeVisible();
});
CDN ishlamay qolsa ham sahifa ishlashi kerak. Bu test aynan shuni tekshiradi (va testlarni biroz tezlashtiradi ham).
6.4. Javobni kutish: waitForResponse
test("saralash so'rovini kutish (waitForResponse)", async ({ page }) => {
await page.goto(SHOP);
await expect(page.getByTestId("product-name").first()).toBeVisible();
const responsePromise = page.waitForResponse(
(response) =>
response.url().startsWith(`${API}/products`) &&
(response.request().postData() ?? "").includes("price,asc"),
);
await page.getByTestId("sort").selectOption("price,asc");
const response = await responsePromise;
expect(response.status()).toBe(200);
const body = await response.json();
await expect(page.getByTestId("product-name").first()).toHaveText(
body.data[0].name,
);
});
E'tibor bering: waitForResponse harakatdan oldin chaqiriladi (promise'ni saqlab qo'yamiz), keyin selectOption, keyin await. Agar avval bosib, keyin kutsangiz — javob allaqachon kelib bo'lgan bo'lishi mumkin va test 30 soniya kutib yiqiladi. Shuningdek, birinchi yuklanishdagi so'rov bilan adashmaslik uchun body'dagi price,asc bo'yicha filtrlaymiz.
6.5. HAR: yozib olish va qayta o'ynatish
HAR (HTTP Archive) — tarmoq trafigining JSON yozuvi. Playwright uni ikki rejimda ishlatadi:
test('HAR: yozib olish va qayta o\'ynatish', async ({ page }) => {
// UPDATE_HAR=1 bo'lsa — haqiqiy javoblar hars/ fayliga yoziladi
await page.routeFromHAR('hars/toolshop-api.har', {
url: `${API}/**`,
update: !!process.env.UPDATE_HAR,
});
await page.goto(SHOP);
await expect(page.getByTestId('product-name').first()).toBeVisible();
});
});
# 1) yozib olish: haqiqiy serverga borib, javoblarni saqlaydi
UPDATE_HAR=1 npx playwright test -g "HAR"
# 2) qayta o'ynatish: API'ga bormaydi, javoblar HAR fayldan
npx playwright test -g "HAR"
Biz yozib olganimizda hars/ papkasida toolshop-api.har va uning yonida javob body'lari alohida .json fayllar bo'lib saqlandi. Eng foydali tomoni: o'sha .json faylda birinchi mahsulot nomini Choynak (HAR) ga o'zgartirib, testni qayta ishga tushirganimizda UI aynan shu nomni ko'rsatdi. Ya'ni HAR — backend'siz, barqaror va qo'lda tahrirlanadigan test ma'lumoti.
💡 Maslahat:
page.route()faqat test ochgan brauzer ichida ishlaydi. Mobil ilova yoki boshqa qurilma trafigini o'zgartirish uchun Proxymanning Map Local va Breakpoint vositalari kerak bo'ladi — g'oya bir xil, faqat "tarmoq darajasida".
🎯 Intervyuda: "Mock qilingan testlar real bug'ni o'tkazib yubormaydimi?" — Yuboradi, shuning uchun mock'lar real integratsiya testlarining o'rnini bosmaydi. Ular backend'da yaratish qiyin bo'lgan chekka holatlarni (500, bo'sh ro'yxat, g'alati qiymatlar) tez va barqaror tekshirish uchun. Asosiy oqimlar real backend bilan alohida tekshiriladi.
7. Vizual testlar: toHaveScreenshot() {#visual}
Funksional test "tugma bosiladimi?" deb so'raydi. Vizual test esa "tugma joyida turibdimi, rangi to'g'rimi, matn ustma-ust tushmadimi?" deb so'raydi. Playwright buni skrinshotlarni piksellab solishtirish orqali qiladi.
// tests/ui/visual.spec.ts
import { test, expect } from "../../fixtures/test";
const screenshotOptions = {
fullPage: true,
animations: "disabled" as const,
maxDiffPixelRatio: 0.01, // 1% gacha farqqa ruxsat
};
test.describe("Vizual regressiya", { tag: "@visual" }, () => {
test("inventory sahifasi", async ({ inventoryPage, page }) => {
await inventoryPage.goto();
await expect(page).toHaveScreenshot("inventory.png", {
...screenshotOptions,
mask: [page.getByTestId("footer-copy")], // o'zgaruvchan matnni yopamiz
});
});
test("bitta mahsulot kartasi", async ({ inventoryPage }) => {
await inventoryPage.goto();
await expect(
inventoryPage.productCard("Sauce Labs Backpack"),
).toHaveScreenshot("backpack-card.png");
});
test.describe("visual_user", () => {
test.use({ storageState: { cookies: [], origins: [] } });
test("xuddi shu sahifa visual_user uchun", async ({ loginPage, page }) => {
test.fail(
true,
"visual_user ataylab vizual buglarga ega — test ularni ushlashi kerak",
);
await loginPage.goto();
await loginPage.login("visual_user", "secret_sauce");
await expect(page).toHaveScreenshot("inventory.png", {
...screenshotOptions,
mask: [page.getByTestId("footer-copy")],
});
});
});
});
7.1. Birinchi ishga tushirish: baseline
Etalon (baseline) rasm hali yo'q bo'lgani uchun birinchi ishga tushirish yiqiladi va rasmni yozib qo'yadi:
Error: A snapshot doesn't exist at .../tests/ui/visual.spec.ts-snapshots/inventory-chromium-darwin.png, writing actual.
Ikkinchi ishga tushirishdan boshlab test har safar yangi skrinshotni shu etalon bilan solishtiradi. Fayl nomiga e'tibor bering: inventory + project (chromium) + platforma (darwin = macOS). Linux CI'da xuddi shu test inventory-chromium-linux.pngni qidiradi.
UI ataylab o'zgargan bo'lsa, etalonlarni yangilaysiz:
npx playwright test --update-snapshots # yoki -u
npx playwright test -u --grep @visual # faqat vizual testlar
7.2. Vizual test bug'ni ushlaydi
Saucedemo'dagi visual_user — ataylab vizual buglarga ega foydalanuvchi. Yuqoridagi uchinchi test u bilan xuddi shu inventory.png etalonini solishtiradi. Natijada test-results/ papkasida uchta fayl paydo bo'ldi:
inventory-expected.png— etalon (standard_user);inventory-actual.png— visual_user'da ko'ringani;inventory-diff.png— farqli piksellar qizil/sariq rangda.
Diff'da biz ko'rgan farqlar: birinchi mahsulot rasmi boshqa, savatcha ikonkasi joyidan siljigan, ba'zi narx va nomlar o'zgargan, oxirgi kartadagi "Add to cart" tugmasi o'ngga surilib ketgan. Funksional testlar bularning hech birini sezmasdi — tugmalar baribir bosiladi.
7.3. Barqaror vizual testlar qoidalari
| 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) |
| 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 25da.
8. Projects, retries, serial rejim va reporter'lar {#projects-reporters}
8.1. Projects: bir xil testlar, turli sharoitlar
Project — testlar to'plami + sozlamalar (brauzer, qurilma, baseURL, storageState). 4.2-bo'limdagi config'da to'rtta project bor: setup, chromium, mobile-chrome, api.
Mobil emulyatsiya devices lug'atidan olinadi. devices['Pixel 7'] — Chromium asosidagi profil: ekran 412×839, isMobile: true, hasTouch: true, Android user-agent. Shu project'da @mobile tegli savatcha testi telefon o'lchamida ham o'tdi.
To'liq cross-browser to'plam esa shunday qo'shiladi:
projects: [
{ name: 'setup', testMatch: /.*\.setup\.ts/ },
{ name: 'chromium', use: { ...devices['Desktop Chrome'] }, dependencies: ['setup'] },
{ name: 'firefox', use: { ...devices['Desktop Firefox'] }, dependencies: ['setup'] },
{ name: 'webkit', use: { ...devices['Desktop Safari'] }, dependencies: ['setup'] },
{ name: 'iphone', use: { ...devices['iPhone 15'] }, dependencies: ['setup'] },
],
⚠️ Diqqat:
devices['iPhone 15']vaDesktop SafariWebKit dvigatelida ishlaydi,Desktop Firefoxesa Firefox'da. Ular uchun brauzerlar o'rnatilgan bo'lishi kerak:npx playwright install(hammasi) yokinpx playwright install webkit firefox. Emulyatsiya haqiqiy iPhone emas — Safari'ning o'ziga xos xatolarini to'liq tekshirish uchun real qurilma yoki device farm kerak (QA 14).
Faqat bitta project'ni ishga tushirish: npx playwright test --project=chromium.
8.2. Retries va flaky testlar
retries: process.env.CI ? 2 : 0,
use: { trace: 'on-first-retry' },
Playwright natijalarni uch toifaga ajratadi: passed (birinchi urinishda o'tdi), flaky (birinchi urinishda yiqildi, qayta urinishda o'tdi), failed (barcha urinishlarda yiqildi). trace: 'on-first-retry' — test qayta urinilganda trace yoziladi, ya'ni beqaror test uchun tayyor "qora quti" bo'ladi. Test ichida qaysi urinish ekanini testInfo.retry bilan bilish mumkin.
🧠 Esda tuting: Retry — kasallikni emas, simptomni yashiradi. Flaky test topilsa, uni tuzatish kerak. Flaky testlar bilan tizimli ishlash — QA 21da.
8.3. Parallel va serial rejim
fullyParallel: true — hatto bitta fayl ichidagi testlar ham parallel ishlaydi. Shuning uchun har bir test mustaqil bo'lishi shart. Ba'zan (masalan, bitta uzun wizard'ni bosqichma-bosqich tekshirganda) testlar ketma-ketligi muhim bo'ladi:
test.describe.configure({ mode: "serial" });
Serial guruhda bitta test yiqilsa, qolganlari o'tkazib yuboriladi va retry'da butun guruh qaytadan ishlaydi. Playwright hujjatlari ham testlarni izolyatsiyalangan qilishni afzal ko'radi — serial rejim istisno bo'lishi kerak, qoida emas.
Testlarni bir nechta mashinaga bo'lish (--shard=1/4) — QA 21 mavzusi.
8.4. Reporter'lar
| 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:
reporter: [
['list'],
['html', { open: 'never' }],
['json', { outputFile: 'test-results/results.json' }],
['junit', { outputFile: 'test-results/junit.xml' }],
['allure-playwright', { resultsDir: 'allure-results' }],
],
Allure — jamoalarda mashhur, chiroyli tarixiy hisobot. allure-playwright (3.x) reporter natijalarni allure-results/ papkasiga yozadi, hisobotni esa Allure 3 CLI (allure npm paketi, Java talab qilmaydi) yasaydi:
npm i -D allure-playwright allure
npx playwright test
npx allure generate ./allure-results # -> allure-report/
npx allure open # hisobotni brauzerda ochish
9. Best practices va ESLint {#best-practices}
9.1. Qisqa qoidalar ro'yxati
- Har test mustaqil: o'z ma'lumotini o'zi yaratadi, boshqa testning natijasiga tayanmaydi.
- Barqaror locator'lar:
getByRole,getByLabel,getByTestId. Dasturchilardandata-testid/data-testatributlarini 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:
npm i -D eslint typescript-eslint eslint-plugin-playwright
// eslint.config.mjs
import { defineConfig } from "eslint/config";
import tseslint from "typescript-eslint";
import playwright from "eslint-plugin-playwright";
export default defineConfig([
{
files: ["**/*.ts"],
extends: [tseslint.configs.base],
languageOptions: { parserOptions: { projectService: true } },
rules: { "@typescript-eslint/no-floating-promises": "error" },
},
{
files: ["tests/**/*.ts"],
extends: [playwright.configs["flat/recommended"]],
},
]);
Mana "yomon odatlar" to'plami:
import { test, expect } from "@playwright/test";
test("yomon odatlar", async ({ page }) => {
await page.goto("/");
page.getByTestId("username").fill("standard_user"); // await yo'q
await page.waitForTimeout(3000); // qattiq kutish
expect(await page.getByTestId("error").isVisible()).toBe(true);
});
npx eslint . haqiqiy natijasi:
5:3 error Promises must be awaited, end with a call to .catch, ... @typescript-eslint/no-floating-promises
6:9 warning Unexpected use of page.waitForTimeout() playwright/no-wait-for-timeout
7:3 error Replace isVisible() with toBeVisible() playwright/prefer-web-first-assertions
Bizning o'z loyihamizda ham lint foydali chiqdi: login testlarining birinchi variantida muvaffaqiyatli va muvaffaqiyatsiz holatlar bitta siklda if (error) {...} else {...} bilan yozilgan edi. ESLint playwright/no-conditional-in-test va playwright/no-conditional-expect ogohlantirishlarini berdi — shundan keyin ularni 5.1-bo'limdagidek ikki xil testga ajratdik.
💡 Maslahat:
npx tsc --noEmitvanpx eslint .ni CI'da testlardan oldin ishga tushiring — ular soniyalarda tugaydi va bir soatlik test yurishidan oldin oddiy xatolarni ushlaydi.
10. Amaliy mashqlar {#mashqlar}
Mashq 1: Qimmatdan arzonga saralash
InventoryPage yordamida narxlar qimmatdan arzonga saralanishini tekshiradigan test yozing.
<details> <summary>💡 Javobni ko'rish</summary>
import { test, expect } from "../../fixtures/test";
test("qimmatdan arzonga saralash", async ({ inventoryPage }) => {
await inventoryPage.goto();
await inventoryPage.sortBy("hilo");
const prices = await inventoryPage.prices();
expect(prices).toEqual([...prices].sort((a, b) => b - a));
});
[...prices] — massiv nusxasi. sort() massivni joyida o'zgartiradi; nusxasiz yozsangiz, prices va "kutilgan" massiv bir xil obyekt bo'lib qoladi va test hech qachon yiqilmaydi.
</details>
Mashq 2: ProductDetailsPage
Saucedemo'da mahsulot sahifasi /inventory-item.html?id=4 (Backpack). Unda data-test qiymatlari: inventory-item-name, inventory-item-price, add-to-cart, back-to-products. Shu sahifa uchun page object yozing va "nomi, narxi to'g'ri; savatchaga qo'shilganda badge 1" testini yozing.
<details> <summary>💡 Javobni ko'rish</summary>
// pages/ProductDetailsPage.ts
import type { Locator, Page } from "@playwright/test";
export class ProductDetailsPage {
readonly page: Page;
readonly name: Locator;
readonly price: Locator;
readonly addToCartButton: Locator;
readonly backButton: Locator;
constructor(page: Page) {
this.page = page;
this.name = page.getByTestId("inventory-item-name");
this.price = page.getByTestId("inventory-item-price");
this.addToCartButton = page.getByTestId("add-to-cart");
this.backButton = page.getByTestId("back-to-products");
}
async goto(id: number) {
await this.page.goto(`/inventory-item.html?id=${id}`);
}
}
import { test, expect } from "../../fixtures/test";
import { ProductDetailsPage } from "../../pages/ProductDetailsPage";
test("mahsulot sahifasi: Backpack", async ({ page, inventoryPage }) => {
const details = new ProductDetailsPage(page);
await details.goto(4);
await expect(details.name).toHaveText("Sauce Labs Backpack");
await expect(details.price).toHaveText("$29.99");
await details.addToCartButton.click();
await expect(inventoryPage.header.cartBadge).toHaveText("1");
});
Header komponenti mahsulot sahifasida ham ishladi — composition'ning foydasi shu. Keyingi qadam: ProductDetailsPageni ham fixtures/test.tsga fixture qilib qo'shish.
</details>
Mashq 3: "Savatda 2 ta mahsulot" fixture'i
Checkout testlari har safar savatchaga 2 ta mahsulot qo'shishdan boshlanadi. Buni cartWithTwoItems fixture'iga aylantiring: fixture mahsulotlarni qo'shsin va ularning nomlari massivini testga bersin.
<details> <summary>💡 Javobni ko'rish</summary>
// fixtures/cart.ts
import { test as base, expect } from "./test";
type CartFixtures = { cartWithTwoItems: string[] };
export const test = base.extend<CartFixtures>({
cartWithTwoItems: async ({ inventoryPage }, use) => {
const products = ["Sauce Labs Backpack", "Sauce Labs Bike Light"];
await inventoryPage.goto();
for (const product of products) {
await inventoryPage.addToCart(product);
}
await use(products);
},
});
export { expect };
import { test, expect } from "../../fixtures/cart";
test("fixture: savatda 2 ta mahsulot tayyor", async ({
cartWithTwoItems,
inventoryPage,
cartPage,
}) => {
await inventoryPage.header.openCart();
await expect(cartPage.itemNames).toHaveText(cartWithTwoItems);
});
E'tibor bering: fixtures/cart.ts bizning ./testni kengaytiryapti — fixture'lar qatlam-qatlam o'sishi mumkin.
</details>
Mashq 4: Chegaraviy narxlar (mock)
Toolshop'da birinchi mahsulot narxini 0, ikkinchisini 1234567.5 qilib mock qiling. UI ularni qanday ko'rsatadi? Natijani testga aylantiring va QA nuqtai nazaridan baholang.
<details> <summary>💡 Javobni ko'rish</summary>
test("Toolshop: narx 0 va katta son qanday ko'rinadi", async ({ page }) => {
await page.route(
"https://api.practicesoftwaretesting.com/products*",
async (route) => {
const response = await route.fetch();
const json = await response.json();
json.data[0].price = 0;
json.data[1].price = 1234567.5;
await route.fulfill({ response, json });
},
);
await page.goto("https://practicesoftwaretesting.com/");
const prices = page.getByTestId("product-price");
await expect(prices.nth(0)).toHaveText("$0.00");
await expect(prices.nth(1)).toHaveText("$1234567.50");
});
Kuzatuvlar:
$0.00— texnik jihatdan to'g'ri, lekin biznes savol tug'iladi: "bepul" mahsulot do'konda ko'rinishi kerakmi? Bu talablar bo'yicha savol.$1234567.50— minglik ajratuvchi yo'q ($1,234,567.50kutilgan bo'lardi). Katta summalarda o'qish qiyin; bu lokalizatsiya/UX topilmasi (QA 25). DemoBozor'da so'm summalari1 234 567 so'mko'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.
</details>
Mashq 6: Code review
Hamkasbingiz quyidagi testni PR'ga yubordi. Kamida 5 ta muammoni toping.
test("test1", async ({ page }) => {
await page.goto("https://www.saucedemo.com/");
await page.locator("#user-name").fill("standard_user");
await page.locator("#password").fill("secret_sauce");
await page.locator("#login-button").click();
await page.waitForTimeout(2000);
page.locator(".inventory_item button").nth(3).click();
if (await page.locator(".shopping_cart_badge").isVisible()) {
expect(await page.locator(".shopping_cart_badge").textContent()).toBe("1");
}
});
<details> <summary>💡 Javobni ko'rish</summary>
- Nom ma'nosiz —
test1hech narsa aytmaydi; "mahsulot qo'shilganda badge 1 bo'ladi" kabi bo'lsin. - To'liq URL —
baseURLbor,page.goto('/')yetarli; aks holda staging'da ishga tushirib bo'lmaydi. - UI login har testda —
storageStatebilan setup project ishlatilishi kerak (4-bo'lim). - **
waitForTimeout(2000)** — qattiq kutish: sekin muhitda yetmaydi, tez muhitda vaqtni yeydi. - **
awaityo'q** —click()oldida; test yiqilmasdan "o'tib ketishi" mumkin. - Mo'rt locator —
.nth(3)tartib o'zgarsa boshqa mahsulotni bosadi; nom bo'yichafilter({ hasText })kerak. - Shartli assertion — badge ko'rinmasa, test hech narsani tekshirmay o'tadi (yolg'on yashil). To'g'risi:
await expect(header.cartBadge).toHaveText('1'). - POM yo'q — locator'lar testda;
LoginPage/InventoryPageishlatilishi 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 +
storageStatebilan 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/elseyo'q; sozlamalar.envda,USERNAMEkabi 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) vatsc— testlardan oldingi arzon himoya qatlami. - Keyingi dars: QA 20: API avtomatlashtirish — Playwright request, Zod schema va API+UI gibrid testlar