Шаги в тестах (test.step)

Урок: Шаги в тестах (test.step)

Введение

Представь, что ты объясняешь кому-то, как пройти регистрацию на сайте. Ты не говоришь всё сразу одним потоком, а разбиваешь на шаги:

  1. Открой страницу
  2. Введи email
  3. Введи пароль
  4. Нажми кнопку

Так проще понять, где произошла ошибка, если что-то пошло не так.

В тестах происходит то же самое. Когда тест падает, ты хочешь быстро понять:

  • на каком этапе это произошло;
  • что именно выполнялось в этот момент;
  • какая логика привела к ошибке.

Если тест — это просто длинная последовательность действий, разобраться сложно. Именно здесь помогают шаги — 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 делает эту историю явной для людей и инструментов.