21. Fixtures и Page Objects

Fixtures в Playwright
Заголовок раздела «Fixtures в Playwright»Fixtures — переиспользуемые настройки и состояния для тестов (аналог beforeEach, но мощнее).
import { test as base, expect, Page } from '@playwright/test';import { LoginPage } from './pages/LoginPage';import { DashboardPage } from './pages/DashboardPage';
// Расширяем базовый testtype MyFixtures = { loginPage: LoginPage; dashboardPage: DashboardPage; authenticatedPage: Page;};
export const test = base.extend<MyFixtures>({ // Fixture: страница логина loginPage: async ({ page }, use) => { const loginPage = new LoginPage(page); await use(loginPage); },
// Fixture: уже залогиненный пользователь authenticatedPage: async ({ browser }, use) => { const context = await browser.newContext({ storageState: './auth.json', // сохранённая сессия }); const page = await context.newPage(); await use(page); await context.close(); },});
export { expect };import { test, expect } from './fixtures';
test('login and view dashboard', async ({ loginPage, page }) => { await loginPage.goto(); await expect(page).toHaveURL('/dashboard');});
// Используем authenticatedPage — логин уже сделанtest('view profile (authenticated)', async ({ authenticatedPage: page }) => { await page.goto('/profile'); await expect(page.getByRole('heading', { name: 'Profile' })).toBeVisible();});Сохранение состояния авторизации
Заголовок раздела «Сохранение состояния авторизации»// auth.setup.ts — выполняется один разimport { test as setup } from '@playwright/test';
setup('authenticate', async ({ page }) => { await page.goto('/login'); await page.fill('[name="password"]', 'password123'); await page.click('[type="submit"]'); await page.waitForURL('/dashboard');
// Сохранить cookies + localStorage await page.context().storageState({ path: './auth.json' });});export default defineConfig({ projects: [ { name: 'setup', testMatch: /.*\.setup\.ts/, }, { name: 'authenticated tests', dependencies: ['setup'], use: { storageState: './auth.json', // использовать сохранённую сессию }, }, ],});Page Object Model (полный пример)
Заголовок раздела «Page Object Model (полный пример)»export class BasePage { constructor(protected page: Page) {}
async waitForPageLoad() { await this.page.waitForLoadState('networkidle'); }
async takeScreenshot(name: string) { await this.page.screenshot({ path: `screenshots/${name}.png` }); }}
// pages/ProductsPage.tsimport { Page, Locator, expect } from '@playwright/test';import { BasePage } from './BasePage';
export class ProductsPage extends BasePage { readonly searchInput: Locator; readonly productCards: Locator; readonly sortSelect: Locator; readonly cartButton: Locator;
constructor(page: Page) { super(page); this.searchInput = page.getByPlaceholder('Search products...'); this.productCards = page.locator('[data-testid="product-card"]'); this.sortSelect = page.getByRole('combobox', { name: /sort/i }); this.cartButton = page.getByRole('link', { name: /cart/i }); }
async goto() { await this.page.goto('/products'); await this.waitForPageLoad(); }
async search(query: string) { await this.searchInput.fill(query); await this.searchInput.press('Enter'); await this.waitForPageLoad(); }
async sortBy(option: string) { await this.sortSelect.selectOption(option); await this.waitForPageLoad(); }
async addToCart(productName: string) { const product = this.productCards.filter({ hasText: productName }); await product.getByRole('button', { name: /add to cart/i }).click(); }
async getProductNames(): Promise<string[]> { return this.productCards.locator('.product-name').allTextContents(); }
async expectProductCount(count: number) { await expect(this.productCards).toHaveCount(count); }}
// tests/e2e/products.spec.tsimport { test, expect } from '../fixtures';import { ProductsPage } from '../pages/ProductsPage';
test('product search works', async ({ page }) => { const productsPage = new ProductsPage(page); await productsPage.goto();
await productsPage.search('iPhone'); const names = await productsPage.getProductNames();
expect(names.every(n => n.toLowerCase().includes('iphone'))).toBe(true);});
test('cart gets updated', async ({ page }) => { const productsPage = new ProductsPage(page); await productsPage.goto();
await productsPage.addToCart('iPhone 15');
await expect(page.getByTestId('cart-count')).toHaveText('1');});Параллельное выполнение
Заголовок раздела «Параллельное выполнение»// Тесты в файле выполняются параллельноtest.describe.parallel('product tests', () => { test('first test', async ({ page }) => { ... }); test('second test', async ({ page }) => { ... });});
// Серийное выполнение (когда тесты зависят друг от друга)test.describe.serial('checkout flow', () => { test('step 1: add to cart', async ({ page }) => { ... }); test('step 2: checkout', async ({ page }) => { ... }); test('step 3: confirm', async ({ page }) => { ... });});Практические задания
Заголовок раздела «Практические задания»- Создай Page Object для корзины (CartPage) со всеми методами
- Настрой fixture для аутентифицированного пользователя
- Напиши полный checkout flow через Page Objects
- Fixtures — переиспользуемые setup/teardown
- Сохранённая авторизация экономит время (один login на все тесты)
- Page Object Model = читаемость + переиспользование
- test.extend() позволяет добавить любые кастомные fixtures