Утро, кофе, открываешь GitLab, а CI красный. Классика. Лезешь в отчёт, а там портянка на 5 экранов и TimeoutError где-то в недрах клика по кнопке. Селектор нормальный, data-testid="checkout-submit". Чтобы понять, что именно сломалось (может отвалилась база или фронт не отрисовал кнопку, возможно, юзер тупит), нужно лезть в код теста и дебажить глазами.
Бесит нереально. Ты или твой коллега тратите время не на фикс багов, а на расследование.
Классический POM: Как мы жили раньше?
Обычно все начинается красиво. Вы пишете чистый Page Object.
import { Page } from '@playwright/test'; export class CartPage { // Добавляем конструктор constructor(private readonly page: Page) {} async clickCheckout() { // Атомарное действие. Никакой логики, только клик. await this.page.getByTestId('checkout-submit').click(); } }
В коде все красиво. Вызовы методов, чистота, порядок. А в отчете - ад.

Как по такому списку понять контекст за 5 секунд? Это нереально. Разработчик лезет в код теста, читает его весь, матерится, восстанавливает контекст в голове. Время уходит.
Почему просто пихать test.step в тесты - это путь в ад?
Сначала вам посоветуют: "Да просто используй test.step в коде, чего паришься?". Не делайте так. На 3-х тестах это прокатит. На 100 вы закопаете проект.
Копипаста убьет все. Цепочка "Авторизация -> Корзина" будет в большинстве файлов. Поменялась логика логина? Поздравляю, нужно править 50 файлов руками.
Ад поддержки. Добавили в чекаут галочку "Согласен с офертой"? Идите и вставляйте
awaitpage.click(...)в сотню мест.Код превратится в кашу. Тест на 10 строк раздуется до 50 из-за постоянных
await test.step(...). Вы просто потеряете суть проверки за этим шумом.
Решение: Слой Flows (Бизнес-сценарии)
Выход один: нужна прослойка. Слой между "тупыми" страницами (POM) и тестами (Flows). Flow - это дирижер. Он вообще не знает про селекторы (.click(), .fill()). Он знает только про бизнес-процесс.
import { test, expect } from '@playwright/test'; import { CartPage, PaymentPage, OrderData } from './types'; export class CheckoutFlow { // Dependency Injection: Flow принимает готовые инстансы страниц constructor(private cartPage: CartPage, private paymentPage: PaymentPage) {} @Step('Пользователь оформляет заказ') async completePurchase(orderData: OrderData) { await test.step('Переход к оплате', async () => { await this.cartPage.clickCheckout(); // Бизнес-проверка: перешли ли мы на нужную страницу? await expect(this.paymentPage.form).toBeVisible(); }); await test.step('Заполнение платежных данных', async () => { // Данные (orderData) приходят извне: из фабрик или фикстур. // Никакого хардкода карт или имен внутри Flow! await this.paymentPage.fillDetails(orderData.card); await this.paymentPage.submit(); }); } }
Что в отчете?
Все. Теперь там действия юзера, а не браузера.

Тест упал? Разраб смотрит отчет. 30 секунд и он понял, на каком шаге бизнес-логики затык.
Как мы это закодили (никакой магии)?
Чтобы не писать тонны бойлерплейта (и не сойти с ума), внедрили две вещи:
Dependency Injection (DI). Используем фикстуры Playwright как DI-контейнер. Flows регистрируем один раз в конфиге. Playwright сам создает страницы, кидает их в конструктор Flow и отдает готовый объект в тест.
new CheckoutFlow(...)в каждом файле? Забудьте.Декоратор @Step. Та самая "магия". Оборачивает метод класса в
test.step, имя берет из декоратора. Код Flows остается чистым.
Как написать такой декоратор и не сломать
thisс типизацией, тема для отдельной статьи. Там хардкор, на пальцах не объяснишь.
Итого
Автотесты пишутся один раз, а читаются постоянно разными людьми. Ассерты отвечают на вопрос «что именно сломалось», а Flows — на вопрос «на каком бизнес‑шаге это произошло».
Хотите, чтобы разрабы и менеджеры не игнорировали ваши отчеты? Сделайте их читаемыми. Разделение на POM и Flows — это не просто «красивая архитектура», это инструмент, который окупается в первый же месяц за счёт того, что вы перестаете тратить часы на расшифровку красных крестиков.
