Хороший E2E-тест читается как рассказ о том, что делает пользователь и что он получает в ответ. Это не формальность: тест, написанный как история, документирует поведение системы и остаётся понятным через год, когда автор уже забыл детали. Первый же сценарий задаёт тон всему набору - поэтому его пишут не как техническую последовательность кликов, а как утверждение о конкретном пользовательском обещании.
Каркас теста минимален. Из @playwright/test импортируют test и expect, а сам тест - это функция с осмысленным именем и асинхронным телом, получающая page через деструктуризацию аргумента. page - это страница в изолированном контексте, отдельном для каждого теста. Асинхронность здесь не формальность: каждое действие в браузере - это await, и именно поэтому сценарий читается сверху вниз как последовательность шагов.
Имя теста несёт смысл наравне с кодом. "Пользователь входит и видит личный кабинет" сразу говорит и условие, и ожидаемый исход - по одному имени в отчёте понятно, что именно сломалось, ещё до чтения тела. Плохое имя вроде "тест логина" этого не даёт: при падении придётся лезть в код, чтобы понять, какое поведение больше не выполняется. Имя - первая строка диагностики.
Сами шаги описывают действия пользователя на языке интерфейса. page.goto('/login') открывает страницу, getByLabel('Email').fill(...) вводит данные в поле по его подписи, getByRole('button', { name: 'Войти' }).click() нажимает кнопку по её роли и видимому имени. Тест не знает и не хочет знать, какой внутри React-компонент или обработчик - он делает ровно то, что сделал бы человек перед экраном.
Проверки в конце наблюдают наблюдаемое. await expect(page).toHaveURL('/dashboard') убеждается, что пользователь оказался там, где обещано, а expect(заголовок).toBeVisible() - что личный кабинет действительно показан. Это web-first assertions: они повторяют проверку до появления нужного состояния, поэтому надёжны на асинхронном интерфейсе, где данные и переходы приходят не мгновенно.
Полезно один раз увидеть такой сценарий целиком, чтобы почувствовать его форму: короткое осмысленное имя, несколько действий на языке пользователя, несколько проверок на публичных границах. Ниже - тот самый первый тест входа; к этой форме возвращаются как к образцу, когда пишут новый сценарий и хотят удержать его читаемым.
Принципиально то, чего в тесте нет. Он не заглядывает во внутреннее состояние React, не проверяет, какие функции вызвались, не читает переменные компонентов. Он наблюдает только то, что видит пользователь: адрес страницы и интерфейс. Это и делает его E2E - и одновременно устойчивым к рефакторингу: пока обещание "вошёл и увидел кабинет" выполняется, тест зелёный, как бы ни менялся код внутри.
Типичные провалы первого теста задают плохой тон всему набору. Имя-заглушка вроде "test1" или "проверка формы", по которому при падении непонятно, что сломалось. Проверка внутренней реализации вместо URL и интерфейса, из-за которой тест краснеет на безобидном рефакторинге. И локаторы по случайному CSS вместо роли и подписи, привязывающие сценарий к вёрстке. Пишите тест как пользовательскую историю - и он будет и понятным, и живучим.
import { test, expect } from '@playwright/test'
test('пользователь входит и видит личный кабинет', async ({ page }) => {
await page.goto('/login')
await page.getByLabel('Email').fill('anna@example.com')
await page.getByLabel('Пароль').fill('correct-password')
await page.getByRole('button', { name: 'Войти' }).click()
await expect(page).toHaveURL('/dashboard')
await expect(
page.getByRole('heading', { name: 'Личный кабинет' }),
).toBeVisible()
})