“Контекст. Сейчас я работаю над проектом, 500 UI-тестов в Playwright, на TypeScript. В рамках инициативы по постоянному совершенствованию мы начали разрабатывать пайплайн для запуска всех тестов после мержа каждого PR в Staging.
Разработка была довольно простой благодаря GitHub Actions, так как большинство процессов были реюзабельными. Однако реальная проблема возникла при параллелизации: многие тесты стали падать из-за конфликтов, потому что они пытались одновременно взаимодействовать с одними и теми же компонентами, даже если до этого выполнялись параллельно.
Мне удалось найти несколько обходных путей, которые позволили решить проблемы без сериализации тестов.
Следующие советы перечислены от наиболее до наименее затратных с точки зрения времени выполнения в конвейере.
Ретраи
В Playwright есть опция повторных попыток, которая определяет, сколько раз тест должен быть повторно выполнен в случае неудачи. Это самый затратный вариант с точки зрения времени, поскольку он предполагает перезапуск выполнения теста с нуля.
test_suite: - --grep “@tag” --workers=2 --retries=3
В итоговом репорте повторные тесты будут помечены как flaky, с указанием ошибок, возникших при каждой попытке. Рекомендуется минимизировать количество тестов в этом статусе, так как они могут значительно увеличить время выполнения конвейера.
Пример отчета:

Сериализация с помощью describe.serial
В Playwright можно последовательно запускать нужный тестовый набор с помощью describe.serial
. Это гарантирует, что тесты в наборе не будут выполняться параллельно, что позволит избежать возможных конфликтов. Пример:
import test, { expect } from '@playwright/test'; test.describe.serial('Serialized tests', async () => { test('Test 1', async () => { expect(true).toBe(true); }); test('Test 2', async () => { expect(true).toBe(true); }); test('Test 3', async () => { expect(true).toBe(true); }); });
Эта опция менее затратная по времени, чем повторные попытки, поскольку она просто обеспечивает последовательное выполнение определенных тестов, а не перезапуск с нуля. Ее можно применить, определив тесты, вызывающие конфликты, и сгруппировав их в describe.serial
, обеспечив контролируемое выполнение без конфликтов.
Expect.polling
С помощью expect.polling
можно повторять утверждение до тех пор, пока не будет выполнено ожидаемое условие. Это полезно в ситуациях, когда для обновления значения может потребоваться время, например, при ожидании, пока HTTP-запрос вернет статус 200
или когда таблица отобразит определенное количество результатов.
await test.step('Expect row count should be 10', async () => { await expect .poll( async () => { return await page.locator('GetRowAriaRowIndex'); }, { timeout: 10000, intervals: [500], }, ) .toContain('10'); });
Timeout
задает максимальное время ожидания утверждения, а интервал — время между каждым повтором.
Кроме того, Playwright предлагает альтернативу expect.toPass
, которая позволяет повторять целые блоки кода, а не только отдельные утверждения. Это полезно, когда необходимо проверить несколько условий за одну попытку.
WaitFor
Аргументы в waitFor
очень полезны, когда нужно дождаться определенных локаторов. Одной из областей, где они помогли мне добиться стабильности в тестах, была анимация загрузки. Это элементы, которые через определенное время должны перестать присутствовать в DOM. Чтобы справиться с этим, мы можем использовать опцию detached
, которая ждет, пока элемент не будет удален из DOM.
Пример кода:
await test.step('Set pagination to 25', async () => { await page.locator('paginator-ddl').click(); await page.locator('paginator-opt-25').click(); await page.locator('load-icon').waitFor({ state: 'detached' }); });