Розділ 1
Що таке фікстура
Документація →Playwright Test побудований навколо ідеї тестових фікстур. Фікстура встановлює середовище для кожного тесту: дає йому все, що потрібно, і нічого зайвого. Фікстури ізольовані між тестами, тож тести можна групувати за змістом, а не за спільним налаштуванням.
Ви вже користувалися фікстурою в першому ж тесті. Аргумент { page } — це не просто деструктуризація: він повідомляє раннеру, що тесту потрібна фікстура page, і раннер налаштує її до запуску тесту, а після завершення прибере.
import { test, expect } from '@playwright/test';
test('basic test', async ({ page }) => {
await page.goto('https://playwright.dev/');
await expect(page).toHaveTitle(/Playwright/);
});За довідником API механіка така: Playwright Test дивиться на оголошення кожного тесту, аналізує, які фікстури йому потрібні, і готує саме їх. Значення, підготовлені фікстурами, зливаються в один об'єкт, який передається першим параметром у тест, хуки, анотації та інші фікстури. Саме тому ви пишете async ({ page, request }) => ... і отримуєте рівно те, що назвали.
Тесту потрібне готове оточення — сторінка, контекст, дані, авторизація — і кожен тест має починати з чистого стану.
Якщо тести ділять між собою змінюваний стан, результат одного починає залежати від іншого, і ізоляція ламається.
Фікстура готує оточення під конкретний тест і прибирає за собою. Раннер створює лише ті фікстури, які тест назвав у своїх аргументах.
Фактично завжди: навіть найпростіший тест із { page } уже працює через фікстуру. Питання лише в тому, чи достатньо вбудованих, чи час писати власні.
Розділ 2
Вбудовані фікстури
Документація →Playwright Test постачається з набором готових фікстур. Ключова відмінність між ними — область життя (scope). Браузер спільний для всіх тестів одного воркера, бо запускати його для кожного тесту дорого. Натомість контекст, сторінка й API-контекст створюються окремо для кожного тесту, тому кожен тест отримує свіже оточення, навіть коли всі вони працюють в одному браузері.
тест
Тип Page. Ізольована сторінка, створена для кожного тесту; сторінки ізольовані між тестами завдяки ізоляції контексту. Це найуживаніша фікстура.
тест
Тип BrowserContext. Ізольований контекст для кожного тесту; фікстура page за замовчуванням належить саме йому. Зручно, коли треба налаштувати щось на рівні контексту, наприклад маршрутизацію запитів.
воркер
Тип Browser. Спільний для всіх тестів у тому самому воркері, що робить тестування ефективним. Кожен тест при цьому все одно працює в ізольованому контексті.
Рядок 'chromium' | 'firefox' | 'webkit', за замовчуванням 'chromium'. Корисний для анотування тестів залежно від браузера.
тест
Тип APIRequestContext. Ізольований екземпляр для кожного тесту — для роботи з API без браузера.
Типові сценарії з довідника API
import { test } from '@playwright/test';
// context: block requests to an external domain for this test only
test('example test', async ({ page, context }) => {
await context.route('*external.com/*', route => route.abort());
// ...
});
// browser: shared between tests in the same worker
test.beforeAll(async ({ browser }) => {
const page = await browser.newPage();
// ...
});
// browserName: annotate tests per browser
test('skip this test in Firefox', async ({ page, browserName }) => {
test.skip(browserName === 'firefox', 'Still working on it');
// ...
});
// request: isolated APIRequestContext per test
test('api test', async ({ request }) => {
await request.post('/signin', {
data: {
username: 'user',
password: 'password',
},
});
// ...
});Вбудовані option-фікстури з TestOptions
Документація →Окрім «робочих» фікстур, Playwright Test надає опції, через які налаштовуються browser, context і page. Це теж фікстури, тільки їхнє значення ви задаєте декларативно: у блоці use конфігу, у конкретному проєкті або локально у файлі через test.use(). Серед них baseURL, storageState, viewport, locale, colorScheme, trace, screenshot, video; у версії 1.63 до них додалися окремі опції reducedMotion, forcedColors і contrast.
import { defineConfig } from '@playwright/test';
export default defineConfig({
use: {
baseURL: 'https://playwright.dev',
storageState: 'state.json',
},
});import { test } from '@playwright/test';
// Override an option fixture for this file only.
test.use({ baseURL: 'https://playwright.dev/docs/intro' });Оскільки це фікстури, від них можуть залежати ваші власні фікстури — наприклад, перевизначена page у розділі 6 бере baseURL зі своїх аргументів.
fixtures.mount NEW in 1.62
Документація →У версії 1.62 компонентне тестування перейшло на модель stories і galleries, і з'явилася фікстура mount. Story — це маленький компонент-обгортка, який вбудовує тестований компонент в один конкретний сценарій: захардкоджені props, мок-дані, провайдери, записані колбеки. Gallery — сторінка, яку ви реалізуєте самі й віддаєте за адресою baseURL; вона надає функції window.mount(params) і window.unmount(), що рендерять story в кореневий елемент.
Кожен виклик mount() переходить на baseURL і викликає window.mount() з ідентифікатором story та props, тож тести повністю ізольовані один від одного. Повертається Locator, що вказує на кореневий елемент, у який відрендерилась story, — і запити варто будувати саме від нього: component.getByRole('button'), а не page.getByRole('button').
import { test, expect } from '@playwright/test';
test('click should expand', async ({ mount }) => {
const component = await mount('components/Expandable/Stateful');
await component.getByRole('button').click();
await expect(component.getByTestId('expanded')).toHaveValue('true');
});Щоб перевіряти типи props, передайте тип story як generic-аргумент:
import { test, expect } from '@playwright/test';
import type { WithTitle } from './Button.story';
test('renders the title', async ({ mount }) => {
const component = await mount<typeof WithTitle>('Button/WithTitle', { title: 'Hello' });
await expect(component).toContainText('Hello');
});Повернутий локатор доповнений двома методами: update(props) перерендерює ту саму story з новими props без перемонтування, зберігаючи стан компонента, а unmount() демонтує story. Аргумент storyId — ідентифікатор, який розпізнає gallery; за конвенцією це шлях до файлу story плюс ім'я експорту, наприклад 'components/Button/Primary'. Props необов'язкові й мають бути простими серіалізованими значеннями. У нотатках до 1.63 додано, що ідентифікатори stories можна типізувати через згенерований реєстр Stories, а старі пакети @playwright/experimental-ct-* для React і Vue більше не оновлюватимуться.
Розділ 3
Без фікстур vs з фікстурами
Документація →Документація порівнює два стилі на прикладі TodoPage — класу за патерном Page Object Model, який працює з todo-списком через внутрішній page. У традиційному стилі екземпляр живе у змінній describe-блоку, створюється в beforeEach і прибирається в afterEach. У стилі з фікстурами той самий життєвий цикл описаний в одному місці.
Код класу TodoPage (todo-page.ts)
import type { Page, Locator } from '@playwright/test';
export class TodoPage {
private readonly inputBox: Locator;
private readonly todoItems: Locator;
constructor(public readonly page: Page) {
this.inputBox = this.page.locator('input.new-todo');
this.todoItems = this.page.getByTestId('todo-item');
}
async goto() {
await this.page.goto('https://demo.playwright.dev/todomvc/');
}
async addToDo(text: string) {
await this.inputBox.fill(text);
await this.inputBox.press('Enter');
}
async remove(text: string) {
const todo = this.todoItems.filter({ hasText: text });
await todo.hover();
await todo.getByLabel('Delete').click();
}
async removeAll() {
while ((await this.todoItems.count()) > 0) {
await this.todoItems.first().hover();
await this.todoItems.getByLabel('Delete').first().click();
}
}
}import { test } from '@playwright/test';
import { TodoPage } from './todo-page';
test.describe('todo tests', () => {
let todoPage: TodoPage;
test.beforeEach(async ({ page }) => {
todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
});
test.afterEach(async () => {
await todoPage.removeAll();
});
test('should add an item', async () => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async () => {
await todoPage.remove('item1');
// ...
});
});Setup і teardown розірвані між двома хуками, стан живе у змінній, спільній для всього блоку, а щоб перевикористати цю підготовку в іншому файлі, її доведеться копіювати.
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Extend basic test by providing a "todoPage" fixture.
const test = base.extend<{ todoPage: TodoPage }>({
todoPage: async ({ page }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
await use(todoPage);
await todoPage.removeAll();
},
});
test('should add an item', async ({ todoPage }) => {
await todoPage.addToDo('my item');
// ...
});
test('should remove an item', async ({ todoPage }) => {
await todoPage.remove('item1');
// ...
});Уся підготовка і прибирання — в одній функції, розділеній викликом await use(todoPage). Тест просто називає todoPage в аргументах і отримує готовий об'єкт, а describe більше не потрібен для спільного setup.
Переваги фікстур за документацією
page.describe заради спільного setup — групуйте їх за змістом.Підготовка оточення розкидана між beforeEach і afterEach, а стан тримається в змінній блоку.
Хуки прив'язані до файлу й describe, тож та сама підготовка дублюється між файлами, а тести групуються за спільним setup, а не за змістом.
Перенести пару «створити — прибрати» у фікстуру і віддавати значення тесту через use().
Коли after-хук прибирає створене before-хуком, або коли допоміжна функція повторюється в кількох тестах.
Розділ 4
Створення кастомної фікстури
Документація →Власна фікстура створюється через test.extend(): метод повертає новий об'єкт test, який уміє все те саме, що й базовий, плюс ваші фікстури. У TypeScript типи фікстур описують окремим типом і передають першим generic-параметром — base.extend<MyFixtures>().
Кожна фікстура — це асинхронна функція виду async ({ залежності }, use, testInfo) => { ... }. Першим аргументом вона деструктуризує інші фікстури, від яких залежить (тут page). Другим отримує функцію use. Третім — testInfo з метаданими поточного тесту; у воркер-фікстур на цьому місці workerInfo. Усе, що написано до await use(value), — фаза setup; значення, передане в use, отримує тест; усе після — фаза teardown, яка виконується, коли фікстура тесту більше не потрібна.
Код класу SettingsPage (settings-page.ts)
import type { Page } from '@playwright/test';
export class SettingsPage {
constructor(public readonly page: Page) {
}
async switchToDarkMode() {
// ...
}
}import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
import { SettingsPage } from './settings-page';
// Declare the types of your fixtures.
type MyFixtures = {
todoPage: TodoPage;
settingsPage: SettingsPage;
};
// Extend base test by providing "todoPage" and "settingsPage".
// This new "test" can be used in multiple test files, and each of them will get the fixtures.
export const test = base.extend<MyFixtures>({
todoPage: async ({ page }, use) => {
// Set up the fixture.
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo('item1');
await todoPage.addToDo('item2');
// Use the fixture value in the test.
await use(todoPage);
// Clean up the fixture.
await todoPage.removeAll();
},
settingsPage: async ({ page }, use) => {
await use(new SettingsPage(page));
},
});
export { expect } from '@playwright/test';Зверніть увагу на останній рядок: файл фікстур реекспортує expect, щоб тести імпортували і test, і expect з одного місця — з ./my-test, а не з @playwright/test.
todoPage і _seed — коректні імена, todo-page чи 2ndPage — ні.Потрібен готовий об'єкт — Page Object, тестові дані, клієнт — з гарантованим прибиранням після тесту.
Без фікстури створення й прибирання доводиться повторювати в кожному файлі або розносити по хуках.
base.extend<MyFixtures>() з функцією, де setup іде до await use(value), а teardown — після; експорт test і expect з окремого файлу.
Коли одна й та сама підготовка потрібна кільком тестам або файлам.
Розділ 5
Використання фікстур у тестах, хуках та інших фікстурах
Документація →Щоб скористатися фікстурою, достатньо згадати її в аргументах тестової функції — про решту подбає раннер. Фікстури доступні так само в хуках і в інших фікстурах: todoPage вище вже залежить від page. Якщо ви пишете на TypeScript, фікстури типобезпечні: редактор підкаже доступні імена й типи значень, а опечатка в назві стане помилкою компіляції.
import { test, expect } from './my-test';
test.beforeEach(async ({ settingsPage }) => {
await settingsPage.switchToDarkMode();
});
test('basic test', async ({ todoPage, page }) => {
await todoPage.addToDo('something nice');
await expect(page.getByTestId('todo-title')).toContainText(['something nice']);
});Тут хук beforeEach отримує settingsPage, а тест — і кастомну todoPage, і вбудовану page. Обидві сторінки-обгортки працюють з тим самим екземпляром page в межах тесту, бо тест-фікстура створюється один раз на тест.
Тесту й хуку потрібні ті самі об'єкти, а фікстурам — інші фікстури.
Ручне передавання об'єктів між хуками та тестами повертає нас до спільних змінних.
Називати фікстуру в першому аргументі — у тесті, хуку чи іншій фікстурі; TypeScript перевіряє імена й типи.
Щоразу, коли тест чи хук потребує підготовленого значення.
Розділ 6
Перевизначення (override) вбудованих фікстур
Документація →Окрім створення нових фікстур, можна перевизначати наявні. Класичний приклад з документації — page, яка автоматично переходить на baseURL. Перевизначена фікстура може залежати від самої себе в базовому варіанті (page в аргументах — це вбудована сторінка) та від інших вбудованих фікстур, як-от option-фікстури baseURL.
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ baseURL, page }, use) => {
// baseURL is typed as string | undefined
if (baseURL)
await page.goto(baseURL);
await use(page);
},
});Тепер baseURL налаштовується в конфігу або локально у файлі:
test.use({ baseURL: 'https://playwright.dev' });Фікстуру можна й повністю замінити на щось інше. Документація показує перевизначення option-фікстури storageState, щоб подати власні дані замість значення з конфігу:
import { test as base } from '@playwright/test';
import { getAuthCookie } from './auth-helpers'; // your own helper
export const test = base.extend({
storageState: async ({}, use) => {
const cookie = await getAuthCookie();
await use({ cookies: [cookie], origins: [] });
},
});У документації передається лише { cookies: [cookie] }; поле origins: [] додано, щоб об'єкт відповідав типу storage state у TypeScript.
Вбудована фікстура майже підходить, але кожен тест робить однакову додаткову дію — перехід на стартову сторінку, логін, підміну стану.
Повторювати ту саму дію на початку кожного тесту — це дублювання, яке легко забути.
Перевизначити фікстуру під тим самим ім'ям у base.extend(): взяти базову версію з аргументів, доповнити й передати в use() — або повністю замінити значення.
Коли всім тестам, що імпортують ваш test, потрібна однаково підготовлена page чи інше вбудоване значення.
Розділ 7
Worker-scoped фікстури
Документація →Playwright Test запускає тестові файли у воркер-процесах. Як тест-фікстури налаштовуються для кожного запуску тесту, так воркер-фікстури налаштовуються для кожного воркер-процесу. Це місце для сервісів, серверів і всього, що дорого створювати. Раннер перевикористовує воркер для стількох тестових файлів, скільки зможе, — за умови, що воркер-фікстури збігаються, тобто оточення ідентичні.
Воркер-фікстура оголошується кортежем: функція плюс об'єкт опцій { scope: 'worker' }. Без цієї опції раннер вважатиме її звичайною тест-фікстурою. Типи воркер-фікстур передаються другим generic-параметром — base.extend<{}, { account: Account }>. Для унікальних даних документація використовує workerInfo.workerIndex, доступний у будь-якому тесті чи фікстурі.
import { test as base, expect } from '@playwright/test';
type Account = {
username: string;
password: string;
};
// Note that we pass worker fixture types as a second template parameter.
export const test = base.extend<{}, { account: Account }>({
account: [async ({ browser }, use, workerInfo) => {
// Unique username.
const username = 'user' + workerInfo.workerIndex;
const password = 'verysecure';
// Create the account with Playwright.
const page = await browser.newPage();
await page.goto('/signup');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign up').click();
// Make sure everything is ok.
await expect(page.getByTestId('result')).toHaveText('Success');
// Do not forget to cleanup.
await page.close();
// Use the account value.
await use({ username, password });
}, { scope: 'worker' }],
page: async ({ page, account }, use) => {
// Sign in with our account.
const { username, password } = account;
await page.goto('/signin');
await page.getByLabel('User Name').fill(username);
await page.getByLabel('Password').fill(password);
await page.getByText('Sign in').click();
await expect(page.getByTestId('userinfo')).toHaveText(username);
// Use signed-in page in the test.
await use(page);
},
});
export { expect };Акаунт створюється один раз на воркер, а перевизначена page логіниться в нього перед кожним тестом. Окрім того, що воркер-фікстури запускаються раз на воркер, вони отримують власний таймаут, рівний таймауту тесту за замовчуванням; змінити його можна опцією timeout (розділ 9).
Візуалізація scope: що живе у воркері, а що в тесті
Натисніть «Відтворити», щоб побачити, як один воркер виконує три тести. Помаранчеві фікстури створюються й знищуються для кожного тесту, темно-сині — один раз на воркер.
Створено: browser — 0, account — 0, page — 0, context — 0, request — 0.
Деякі ресурси дорого створювати для кожного тесту: акаунт, сервер, сервіс.
Тест-фікстура налаштовується для кожного тесту заново — це правильно для ізоляції, але марнотратно для важких і незмінних ресурсів.
Кортеж [fn, { scope: 'worker' }], тип у другому generic-параметрі, workerInfo.workerIndex для унікальності між паралельними воркерами.
Коли ресурс можна безпечно ділити між тестами одного воркера.
Розділ 8
Automatic фікстури
Документація →Автоматичні фікстури налаштовуються для кожного тесту чи воркера, навіть якщо тест не згадує їх у своїх аргументах. Для цього використовується кортежний синтаксис з опцією { auto: true }. Оскільки тест не отримує від такої фікстури значення, її тип зазвичай void, а виклик — await use() без аргументу.
Приклад з документації автоматично прикріплює налагоджувальні логи, коли тест падає. Фікстура використовує testInfo: після await use() порівнює testInfo.status з testInfo.expectedStatus, отримує унікальний шлях до файлу через testInfo.outputPath() і додає вкладення в testInfo.attachments, щоб його було видно в репорті.
import debug from 'debug';
import fs from 'fs';
import { test as base } from '@playwright/test';
export const test = base.extend<{ saveLogs: void }>({
saveLogs: [async ({}, use, testInfo) => {
// Collecting logs during the test.
const logs: string[] = [];
debug.log = (...args) => logs.push(args.map(String).join(''));
debug.enable('myserver');
await use();
// After the test we can check whether the test passed or failed.
if (testInfo.status !== testInfo.expectedStatus) {
// outputPath() API guarantees a unique file name.
const logFile = testInfo.outputPath('logs.txt');
await fs.promises.writeFile(logFile, logs.join('\n'), 'utf8');
testInfo.attachments.push({ name: 'logs', contentType: 'text/plain', path: logFile });
}
}, { auto: true }],
});
export { expect } from '@playwright/test';Приклад використовує npm-пакет debug; для TypeScript також знадобляться його типи.
Деяка логіка має відпрацювати для кожного тесту — збір логів, діагностика — але вимагати від кожного тесту її назвати незручно й ненадійно.
Звичайні фікстури ліниві: якщо тест їх не назвав, вони не запускаються.
Опція { auto: true } у кортежі — фікстура запускається для кожного тесту (або воркера, разом із scope: 'worker').
Для наскрізних речей на кшталт логування, вкладень у репорт, спільної підготовки середовища.
Розділ 9
Таймаут фікстури
Документація →Фікстура вважається частиною тесту, тому час її setup і teardown зараховується в таймаут тесту. Повільна фікстура може спричинити таймаут самого тесту. Щоб не піднімати загальний таймаут, фікстурі можна дати окремий, більший таймаут через опцію timeout у кортежі.
import { test as base, expect } from '@playwright/test';
const test = base.extend<{ slowFixture: string }>({
slowFixture: [async ({}, use) => {
// ... perform a slow operation ...
await use('hello');
}, { timeout: 60000 }],
});
test('example test', async ({ slowFixture }) => {
// ...
});На відміну від звичайних тест-фікстур, кожна воркер-фікстура має власний таймаут, рівний таймауту тесту, і його змінюють так само — опцією timeout.
Тест падає по таймауту, хоча сам він швидкий — повільною є підготовка.
Setup і teardown тест-фікстури входять у бюджет часу тесту.
{ timeout: 60000 } для конкретної фікстури, а загальний таймаут тесту лишається малим.
Для завідомо повільних операцій у фікстурах, коли не хочеться послаблювати таймаут для всіх тестів.
Розділ 10
Option-фікстури
Документація →Playwright Test вміє запускати кілька проєктів з окремими налаштуваннями. Option-фікстури роблять такі налаштування декларативними й типобезпечними. Опція оголошується кортежем [значенняЗаЗамовчуванням, { option: true }], а інші фікстури можуть від неї залежати, як від будь-якої іншої.
import { test as base } from '@playwright/test';
import { TodoPage } from './todo-page';
// Declare your options to type-check your configuration.
export type MyOptions = {
defaultItem: string;
};
type MyFixtures = {
todoPage: TodoPage;
};
// Specify both option and fixture types.
export const test = base.extend<MyOptions & MyFixtures>({
// Define an option and provide a default value.
// We can later override it in the config.
defaultItem: ['Something nice', { option: true }],
// Our "todoPage" fixture depends on the option.
todoPage: async ({ page, defaultItem }, use) => {
const todoPage = new TodoPage(page);
await todoPage.goto();
await todoPage.addToDo(defaultItem);
await use(todoPage);
await todoPage.removeAll();
},
});
export { expect } from '@playwright/test';У конфігу тип опцій передається в defineConfig<MyOptions>, і тоді значення в use перевіряються компілятором. Різні проєкти отримують різні значення:
import { defineConfig } from '@playwright/test';
import type { MyOptions } from './my-test';
export default defineConfig<MyOptions>({
projects: [
{
name: 'shopping',
use: { defaultItem: 'Buy milk' },
},
{
name: 'wellbeing',
use: { defaultItem: 'Exercise!' },
},
],
});Гайд Parameterize tests показує той самий підхід з опцією person: проєкт alice запускає тести зі значенням 'Alice', а bob — з 'Bob'. У нотатках до версії 1.60 зазначено, що раннер видає помилку, якщо конфіг намагається перевизначити фікстуру, яка не є опцією, — тож через use у конфігу задаються саме option-фікстури.
Масив як значення опції
Якщо значення опції — масив, його треба обгорнути ще в один масив і передати scope. Причина в тому, що масив у позиції значення раннер сприймає як кортеж «значення + опції».
import { test as base } from '@playwright/test';
type Person = { name: string };
const test = base.extend<{ persons: Person[] }>({
// Declare the option, default value is an empty array.
persons: [[], { option: true }],
});
// Option value is an array of persons.
const actualPersons = [{ name: 'Alice' }, { name: 'Bob' }];
test.use({
// CORRECT: Wrap the value into an array and pass the scope.
persons: [actualPersons, { scope: 'test' }],
});
test.use({
// WRONG: passing an array value directly will not work.
persons: actualPersons,
});Скидання опції
Щоб повернути опцію до значення з конфігу, присвойте їй undefined. Наприклад, конфіг задає baseURL: 'https://playwright.dev', файл перевизначає його, а окремий блок повертає конфігове значення:
import { test } from '@playwright/test';
// Configure baseURL for this file.
test.use({ baseURL: 'https://playwright.dev/docs/intro' });
test('check intro contents', async ({ page }) => {
// This test will use "https://playwright.dev/docs/intro" base url as defined above.
});
test.describe(() => {
// Reset the value to a config-defined one.
test.use({ baseURL: undefined });
test('can navigate to intro from the home page', async ({ page }) => {
// This test will use "https://playwright.dev" base url as defined in the config.
});
});Якщо ж потрібно повністю зробити значення undefined, а не конфіговим, використовують довгу форму фікстури:
import { test } from '@playwright/test';
// Completely unset baseURL for this file.
test.use({
baseURL: [async ({}, use) => {
await use(undefined);
}, { scope: 'test' }],
});
test('no base url', async ({ page }) => {
// This test will not have a base url.
});Тим самим тестам потрібні різні вхідні параметри в різних проєктах — інші дані, інше оточення.
Хардкод значень у фікстурах змушує дублювати тести або городити умови на змінних середовища.
[default, { option: true }] у фікстурах, defineConfig<MyOptions> і значення в use кожного проєкту.
Для параметризації на рівні проєктів і для значень, які хочеться перевизначати через test.use().
Розділ 11
Порядок виконання
Документація →Кожна фікстура має фазу setup до виклику await use() і фазу teardown після нього. Setup виконується перед тестом або хуком, якому фікстура потрібна, а teardown — коли тест чи хук її більше не використовує. Порядок визначають три правила:
import { test as base } from '@playwright/test';
const test = base.extend<{
testFixture: string,
autoTestFixture: string,
unusedFixture: string,
}, {
workerFixture: string,
autoWorkerFixture: string,
}>({
workerFixture: [async ({ browser }, use) => {
// workerFixture setup...
await use('workerFixture');
// workerFixture teardown...
}, { scope: 'worker' }],
autoWorkerFixture: [async ({ browser }, use) => {
// autoWorkerFixture setup...
await use('autoWorkerFixture');
// autoWorkerFixture teardown...
}, { scope: 'worker', auto: true }],
testFixture: [async ({ page, workerFixture }, use) => {
// testFixture setup...
await use('testFixture');
// testFixture teardown...
}, { scope: 'test' }],
autoTestFixture: [async ({}, use) => {
// autoTestFixture setup...
await use('autoTestFixture');
// autoTestFixture teardown...
}, { scope: 'test', auto: true }],
unusedFixture: [async ({ page }, use) => {
// unusedFixture setup...
await use('unusedFixture');
// unusedFixture teardown...
}, { scope: 'test' }],
});
test.beforeAll(async () => { /* ... */ });
test.beforeEach(async ({ page }) => { /* ... */ });
test('first test', async ({ page }) => { /* ... */ });
test('second test', async ({ testFixture }) => { /* ... */ });
test.afterEach(async () => { /* ... */ });
test.afterAll(async () => { /* ... */ });Коли всі тести проходять без помилок, документація описує такий порядок. На старті воркера налаштовується browser, бо він потрібен autoWorkerFixture, потім сама autoWorkerFixture (автоматичні воркер-фікстури налаштовуються раніше за все) і лише тоді виконується beforeAll. Для першого тесту спочатку налаштовується autoTestFixture — автоматичні тест-фікстури завжди стартують до тесту й beforeEach, потім page, бо її вимагає beforeEach; далі йдуть beforeEach, тест і afterEach, після чого знищуються page і autoTestFixture. Другий тест повторює початок, але після beforeEach лениво налаштовується workerFixture (її потребує testFixture) і сама testFixture; після тесту й afterEach знищуються testFixture, page і autoTestFixture. Наприкінці виконується afterAll, а потім знищуються workerFixture, autoWorkerFixture і browser.
Кілька спостережень з документації: page і autoTestFixture створюються й знищуються для кожного тесту; unusedFixture не налаштовується ніколи, бо її не використовує жоден тест чи хук; testFixture залежить від workerFixture і запускає її setup; workerFixture налаштовується ліниво перед другим тестом, але знищується один раз під час завершення воркера; autoWorkerFixture налаштовується для хука beforeAll, а autoTestFixture — ні.
Візуалізатор порядку виконання
Позначте, які фікстури називають у своїх аргументах beforeEach, «first test» і «second test». Хуки beforeAll, afterEach і afterAll фікстур не використовують, як у прикладі. Залежності такі самі, як у коді вище: testFixture ← page + workerFixture; workerFixture і autoWorkerFixture ← browser.
| Хто використовує | page | testFixture | autoTestFixture | workerFixture | autoWorkerFixture |
|---|
Незрозуміло, коли саме відпрацює код фікстури відносно хуків і тестів, і чому якась фікстура «не запустилася».
Порядок визначають залежності, лінивість і scope, а не порядок оголошення в extend().
Застосовувати три правила: залежність стартує раніше й завершується пізніше; неавтоматичні фікстури ліниві; тест-фікстури живуть до кінця тесту, воркер-фікстури — до кінця воркера.
При налагодженні побічних ефектів у setup/teardown і при проєктуванні ланцюжків залежних фікстур.
Розділ 12
mergeTests: об'єднання фікстур з кількох модулів
Документація →Коли фікстури розкладені по різних файлах чи модулях, mergeTests() об'єднує кілька об'єктів test в один. Тест, що імпортує результат, бачить фікстури з усіх джерел одразу, разом із вбудованими.
import { mergeTests } from '@playwright/test';
import { test as dbTest } from './db-fixtures';
import { test as a11yTest } from './a11y-fixtures';
export const test = mergeTests(dbTest, a11yTest);import { test } from './fixtures';
test('passes', async ({ database, page, a11y }) => {
// use database and a11y fixtures.
});У документації модулі називаються database-test-utils і a11y-test-utils; тут замінено на локальні файли, суть та сама. Для кастомних матчерів з кількох модулів існує парний mergeExpects().
Фікстури для бази, доступності, API живуть в окремих модулях, а тесту потрібні всі одночасно.
Кожен модуль експортує свій test; імпортувати два різні test в один файл не вийде.
mergeTests(a, b) і експорт об'єднаного test з одного файлу.
Коли набір фікстур росте і його розбивають на тематичні модулі.
Розділ 13
Box-фікстури і кастомний title
Документація →Зазвичай кастомні фікстури показуються окремими кроками в UI mode, Trace Viewer і різних репортах, а також з'являються в повідомленнях про помилки раннера. Для часто вживаних фікстур це може давати багато шуму. «Запакована» фікстура — з опцією { box: true } — ховає свої кроки з інтерфейсу. Це зручно для нецікавих допоміжних фікстур, наприклад автоматичної, яка готує спільні дані.
import { test as base } from '@playwright/test';
export const test = base.extend<{ helperFixture: void }>({
helperFixture: [async ({}, use) => {
// ...
await use();
}, { box: true }],
});Значення box: 'self' ховає лише саму фікстуру, але залишає в репорті всі кроки, що виконуються всередині неї.
Кастомний title
Документація →Замість звичайного імені фікстурі можна дати власну назву, яка показуватиметься в тестових репортах і повідомленнях про помилки.
import { test as base } from '@playwright/test';
export const test = base.extend<{ innerFixture: void }>({
innerFixture: [async ({}, use) => {
// ...
await use();
}, { title: 'my fixture' }],
});Репорти й трейси засмічені кроками допоміжних фікстур, а технічні імена фікстур погано читаються в помилках.
За замовчуванням кожна кастомна фікстура стає окремим кроком у UI mode, Trace Viewer і репортах.
box: true ховає фікстуру повністю, box: 'self' — лише її саму, title дає зрозумілу назву.
Для службових і автоматичних фікстур, які не несуть інформації для читача репорту.
Розділ 14
Глобальні хуки через auto-фікстури
Документація →Хуки test.beforeEach() і test.afterEach() працюють лише для тестів у тому самому файлі й тому самому describe-блоці. Щоб отримати глобальний аналог, оголошують автоматичну тест-фікстуру. Код до await use() виконується перед кожним тестом, код після — після кожного.
import { test as base } from '@playwright/test';
export const test = base.extend<{ forEachTest: void }>({
forEachTest: [async ({ page }, use) => {
// This code runs before every test.
await page.goto('http://localhost:8000');
await use();
// This code runs after every test.
console.log('Last URL:', page.url());
}, { auto: true }], // automatically starts for every test.
});import { test } from './fixtures';
import { expect } from '@playwright/test';
test('basic', async ({ page }) => {
await expect(page).toHaveURL('http://localhost:8000');
await page.goto('https://playwright.dev');
});Глобальні beforeAll/afterAll
Документація →test.beforeAll() і test.afterAll() виконуються раз на воркер-процес, але знову ж лише для тестів свого файлу й describe. Глобальний аналог — автоматична воркер-фікстура з { scope: 'worker', auto: true }. Вона так само запускається раз на воркер-процес, просто її не треба оголошувати в кожному файлі.
import { test as base } from '@playwright/test';
export const test = base.extend<{}, { forEachWorker: void }>({
forEachWorker: [async ({}, use, workerInfo) => {
// This code runs before all the tests in the worker process.
console.log(`Starting test worker ${workerInfo.workerIndex}`);
await use();
// This code runs after all the tests in the worker process.
console.log(`Stopping test worker ${workerInfo.workerIndex}`);
}, { scope: 'worker', auto: true }], // automatically starts for every worker.
});Документація отримує індекс через test.info().workerIndex; тут використано третій аргумент workerInfo, який воркер-фікстура отримує напряму, — так TypeScript не скаржиться на звернення до test всередині власного оголошення.
Потрібен хук, що спрацьовує для всіх тестів проєкту, а не одного файлу.
Звичайні хуки обмежені файлом і describe-блоком, де оголошені.
forEachTest з { auto: true } як глобальний beforeEach/afterEach; forEachWorker з { scope: 'worker', auto: true } як глобальний beforeAll/afterAll.
Коли дія має бути наскрізною і не повинна залежати від того, чи не забув автор файлу її додати.
Розділ 15
Шпаргалка опцій кортежу
Документація →Усі опції передаються другим елементом кортежу: name: [fn, { ...опції }] або, для option-фікстур, name: [значення, { option: true }].
| Опція | Що робить |
|---|---|
| scope | 'test' — фікстура налаштовується для кожного тесту і знищується після нього. 'worker' — раз на воркер-процес, знищується при завершенні воркера. |
| auto | true — фікстура налаштовується для кожного тесту чи воркера, навіть якщо тест не згадує її в аргументах. |
| option | true — оголошує опцію: перший елемент кортежу стає значенням за замовчуванням, яке перевизначається в конфігу, проєкті чи test.use(). |
| timeout | Окремий таймаут фікстури в мілісекундах. Setup/teardown тест-фікстури інакше входять у таймаут тесту; воркер-фікстура має власний таймаут, рівний тестовому. |
| box | true ховає кроки фікстури з UI mode, Trace Viewer і репортів; 'self' ховає лише саму фікстуру, залишаючи кроки всередині неї. |
| title | Власна назва фікстури, яку видно в тестових репортах і повідомленнях про помилки. |
Конструктор кортежу
Змінюйте параметри — код фікстури оновлюється одразу.
Документація показує option-фікстури у формі [значення, { option: true }], тому в цьому режимі інші опції вимкнено.
Практика
Знайди помилку
У кожному фрагменті є помилка, яка прямо випливає з документації. Спробуйте знайти її самостійно, а потім відкрийте пояснення.
Перевірка
Фінальний квіз
Десять питань за матеріалом уроку. Відповідь фіксується одразу після вибору.