Шаги в тестах (test.step)
Урок: Шаги в тестах (test.step)
Введение
Представь, что ты объясняешь кому-то, как пройти регистрацию на сайте. Ты не говоришь всё сразу одним потоком, а разбиваешь на шаги:
- Открой страницу
- Введи email
- Введи пароль
- Нажми кнопку
Так проще понять, где произошла ошибка, если что-то пошло не так.
В тестах происходит то же самое. Когда тест падает, ты хочешь быстро понять:
- на каком этапе это произошло;
- что именно выполнялось в этот момент;
- какая логика привела к ошибке.
Если тест — это просто длинная последовательность действий, разобраться сложно.
Именно здесь помогают шаги — test.step.
Они позволяют структурировать тест и сделать его понятным как для человека, так и для отчётов.
Официальная справка: test.step, test.step.skip, объект шага TestStepInfo.
Что такое test.step
test.step — это способ разбить тест на логические шаги. Имя шага показывается в HTML-отчёте, трейсе и списке шагов в UI — так проще понять, на каком этапе упал сценарий.
Третьим аргументом — объект опций. Полезные поля:
box: true— «зaboxить» шаг: при ошибке внутри шага стек/указатель в отчёте ведут на вызов шага, а не на строку глубоко внутри вспомогательной функции (удобно для Page Object и хелперов);location— явная позиция в отчёте/трейсе;timeout— лимит времени на выполнение шага в миллисекундах (0= без отдельного лимита); при превышении будетTimeoutError.
Пример:
await test.step('Открыть страницу логина', async () => {
await page.goto('/login');
});
Здесь:
'Открыть страницу логина'— название шага;- внутри — действия, которые относятся к этому шагу.
Важно: шаг — это не новая логика, а обёртка вокруг существующего кода.
Как выглядит тест без шагов
await page.goto('/login');
await page.getByLabel('Email').fill('test@mail.com');
await page.getByLabel('Пароль').fill('1234');
await page.getByRole('button', { name: 'Войти' }).click();
await expect(page.getByText('Добро пожаловать')).toBeVisible();
Такой тест работает, но:
- сложно быстро понять структуру;
- при падении неясно, где именно проблема;
- в отчётах всё выглядит как единый блок.
Тот же тест с шагами
await test.step('Открыть страницу', async () => {
await page.goto('/login');
});
await test.step('Ввести данные', async () => {
await page.getByLabel('Email').fill('test@mail.com');
await page.getByLabel('Пароль').fill('1234');
});
await test.step('Нажать кнопку входа', async () => {
await page.getByRole('button', { name: 'Войти' }).click();
});
await test.step('Проверить результат', async () => {
await expect(page.getByText('Добро пожаловать')).toBeVisible();
});
Теперь:
- тест читается как сценарий;
- каждый шаг логически отделён;
- легче понять, что происходит.
Как работает test.step
Когда ты используешь test.step, Playwright:
- фиксирует название шага;
- выполняет код внутри;
- записывает результат в отчёт.
Если тест упадёт, ты увидишь:
- на каком шаге это произошло;
- какой шаг был последним выполненным.
Это особенно полезно в CI и отчётах.
Аргумент step и TestStepInfo
Телу шага можно передать колбэк с параметром step (тип TestStepInfo):
Условный пропуск части шага — step.skip()
Рекомендуемый способ временно не выполнять остаток шага (в документации его предпочитают статичному test.step.skip):
test('пример', async ({ page, isMobile }) => {
await test.step('Проверки десктопа', async (step) => {
step.skip(isMobile, 'На мобильном другой макет');
await expect(page.getByRole('complementary')).toBeVisible();
});
});
После step.skip(...) код ниже в этом шаге не выполняется.
Вложения к шагу — step.attach()
Вложения привязываются к шагу (а не только к тесту целиком), что удобно в отчётах:
await test.step('Проверка отрисовки', async (step) => {
const png = await page.screenshot();
await step.attach('скриншот', { body: png, contentType: 'image/png' });
});
Также доступно свойство titlePath — полный путь заголовков, см. TestStepInfo.
Опция box: куда указывает ошибка
Если шаг обёртывает вызов хелпера или метода Page Object, без box ошибка в отчёте часто указывает внутрь реализации. С { box: true } стек ведёт на строку вызова шага — проще найти место в тесте. Подробности и пример с login(page) — в блоке Boxing в документации test.step.
Пример:
async function login(page) {
await test.step(
'Вход',
async () => {
await page.getByLabel('Email').fill('user@example.com');
await page.getByRole('button', { name: 'Войти' }).click();
},
{ box: true },
);
}
test.step.skip и отдельный таймаут шага
Статически пометить шаг как пропускаемый можно через test.step.skip('название', async () => { ... }) — Playwright не запустит тело. В документации рекомендуют по возможности использовать step.skip() внутри обычного test.step — так гибче (условие, одна ветка сценария).
Отдельный таймаут шага задаётся опцией timeout (мс); по умолчанию 0 — без ограничения сверх общего таймаута теста.
Вложенные шаги
Шаги можно вкладывать друг в друга.
await test.step('Авторизация пользователя', async () => {
await test.step('Открыть страницу', async () => {
await page.goto('/login');
});
await test.step('Ввести данные', async () => {
await page.getByLabel('Email').fill('test@mail.com');
await page.getByLabel('Пароль').fill('1234');
});
});
Это позволяет:
- создавать более детальную структуру;
- группировать действия.
Но важно не переусложнять — шаги должны оставаться понятными.
Возврат значений из шага
test.step может возвращать результат:
const username = await test.step('Получить имя пользователя', async () => {
return await page.getByTestId('username').textContent();
});
Здесь:
- шаг выполняет действие;
- возвращает значение;
- мы используем его дальше.
Это полезно, когда шаг не только выполняет действия, но и даёт результат.
Декораторы TypeScript (шаг = метод)
В документации показан приём: декоратор метода класса оборачивает тело в test.step с { box: true }, чтобы каждый вызов метода (например, LoginPage.login) отображался в отчёте как отдельный шаг. Это уже синтаксис TypeScript и настройка компилятора (experimentalDecorators); для JavaScript тот же эффект достигается явными вызовами test.step вокруг функций.
Когда использовать шаги
Шаги особенно полезны:
- в длинных тестах;
- в сложных сценариях;
- в e2e тестах;
- при работе в команде.
Например:
- авторизация;
- оформление заказа;
- работа с несколькими страницами.
Когда шаги не нужны
Если тест очень простой:
await page.click('button');
await expect(page.getByText('OK')).toBeVisible();
Добавление шагов может быть лишним.
Шаги нужны там, где есть смысловая структура.
Связь с отчётами
Одна из главных причин использовать test.step — это отчёты.
В отчёте Playwright:
- каждый шаг отображается отдельно;
- видно, что именно выполнялось;
- легко найти место ошибки.
Без шагов:
- отчёт менее информативный;
- сложнее дебажить.
Пример из реального сценария
test('покупка товара', async ({ page }) => {
await test.step('Открыть страницу товара', async () => {
await page.goto('/product');
});
await test.step('Добавить товар в корзину', async () => {
await page.getByRole('button', { name: 'В корзину' }).click();
});
await test.step('Перейти в корзину', async () => {
await page.goto('/cart');
});
await test.step('Проверить наличие товара', async () => {
await expect(page.getByText('Товар')).toBeVisible();
});
});
Этот тест:
- читается как инструкция;
- легко анализируется;
- удобно дебажится.
Итоговое понимание
test.step превращает тест из набора команд в структурированный сценарий, видимый в отчёте и трейсе.
Из документации Playwright полезно помнить:
- вложенные шаги и возврат значения из колбэка;
- опции
box,location,timeout; - объект
TestStepInfo:step.skip,step.attach,titlePath; - при необходимости —
test.step.skip(чаще заменяют на условныйstep.skipвнутри шага); - для Page Object — декораторы / обёртки с
box, чтобы ошибки указывали на вызов сценария.
Хороший e2e-тест читается как история; test.step делает эту историю явной для людей и инструментов.