Параллельные сценарии
До этого момента конфигурация использовала одного worker. Это упрощало знакомство с последовательностью действий, но не должно быть условием правильности самостоятельных сценариев. Теперь разрешим несколько проверок одновременно и разберём, какие ресурсы можно делить, а какие должны принадлежать конкретному тесту.
Пример остаётся маленьким: один сценарий выбирает фронтенд, другой — публикацию. Оба получают собственную catalogPage и ожидают два курса с разными названиями. Файлы lesson-13/expected архива содержат полный сценарий и конфигурацию. Параллельный запуск не выполнялся, поэтому ниже нет измерения ускорения или утверждения о фактическом порядке работы.
Worker и отдельный браузерный контекст
Worker — процесс runner, который выполняет назначенные ему проверки. Это не пользовательская сессия и не DOM-элемент. Браузерные контексты создаются для тестов внутри работы runner. Несколько проверок могут пользоваться одним процессом браузера, оставаясь изолированными по cookie и хранилищам.
В конфигурации ожидаемого состояния изменены два значения:
fullyParallel: true,
workers: 2,
retries: 0
Число workers ограничивает доступный параллелизм. Оно не обещает, что любые два теста обязательно начнутся одновременно, и не задаёт их порядок. fullyParallel:true позволяет независимым тестам одного файла выполняться параллельно. Повторные попытки остаются выключенными, чтобы случайная удача второй попытки не маскировала недостаток данных или ожиданий.
Модель runner описана в Playwright Parallelism. Для нашей задачи важно практическое следствие: порядок объявлений не может использоваться как подготовка следующего теста. Если второй сценарий ожидает результат первого, он уже не является самостоятельной проверкой и требует другого оформления.
Общие данные и собственное состояние
Массив курсов неизменяем на сервере: c001 и c002 относятся к frontend, c003 и c004 — к publishing. Его можно читать из нескольких сценариев. Выбор раздела, текст запроса и тема страницы принадлежат контексту каждого теста. Один сценарий не должен выбирать значение в форме другого.
Собственная фикстура устанавливает маршрут на page, открывает страницу и ждёт исходные четыре курса. Перехват не переписывает базу и не меняет серверный ответ для соседних страниц. Два теста получают одинаковый исходный массив, но затем самостоятельно фильтруют его. Такая подготовка подходит параллельной работе, потому что действия локальны.
Полный файл объявляет варианты через цикл:
for (const topic of ['frontend', 'publishing']) {
test(`собственный фильтр ${topic}`, async ({ catalogPage: page }, testInfo) => {
testInfo.annotations.push({
type: 'worker', description: String(testInfo.parallelIndex)
});
await page.getByLabel('Раздел', { exact: true }).selectOption(topic);
await page.getByRole('button', { name: 'Найти', exact: true }).click();
await expect(page.getByTestId('results-status'))
.toHaveText('Найдено курсов: 2');
});
}
Это фрагмент: в архиве каждый вариант также сравнивает точные названия двух ссылок. Сценарий frontend ожидает HTML и JavaScript, publishing — HTTP и Markdown. Совпадение количества не должно скрывать перепутанный раздел. Оба результата рассматриваются независимо, даже если runner выполнит один из них раньше другого.
Аннотация записывает индекс worker для будущей диагностики, а не подтверждает, что код уже выполнялся. Этот индекс не является постоянным идентификатором пользователя. Для данной лаборатории он вообще не участвует в формировании данных. Сессии из предыдущего урока получили собственные случайные ID, поэтому не требуют угадывать, какой worker запустит роль.
Опасный общий сброс
Представим другой сервер с изменяемой таблицей курсов. Каждый тест в подготовке вызывает «удалить все записи», затем добавляет свои. Когда второй тест начинает подготовку, он удаляет данные, которые первый уже проверяет. Отдельные cookie не спасут: источник конфликта находится в общей базе, а не в браузерном хранилище.
Для такой задачи создают выделенные области данных: отдельный проект, tenant, уникальный префикс записи или изолированную базу. Очистка касается только принадлежащей сценарию области. Выбор зависит от модели приложения; универсальный глобальный reset подходит лишь строго одиночной лаборатории с явной договорённостью, а не произвольному параллельному набору.
Наш пример специально избегает этой проблемы. Каталог не меняется, API сообщения не сохраняет обращения, вход создаёт новую сессию без удаления других, а фильтр работает на странице. Поэтому изменение числа workers не требует добавлять сброс. Если в курс позже будет введено редактирование каталога, границы данных придётся определить заново до обещания параллельности.
Один сервер для одного набора
webServer поднимает один процесс на выбранном порту для текущего запуска runner. Workers этого набора обращаются к нему одновременно. Для неизменяемого каталога это допустимо. Однако два независимых запуска набора на одном компьютере не могут одновременно занять 4407. Такую ситуацию решают выделенным портом или отдельным окружением для каждого запуска, согласованным во всех файлах.
Разрешение reuseExistingServer не исправляет конфликт областей данных. Оно лишь допускает использование существующего процесса и может смешать два набора условий. В нашей конфигурации значение остаётся false, чтобы неизвестный сервер не выдавался за собственный учебный ресурс. Старт сервера и старт тестов сохраняют ясную границу.
Скорость не является единственной целью параллельности. Даже если два коротких теста почти не ускорятся, независимая подготовка делает их проще запускать выборочно и разбирать отдельно. Нам важнее, что сценарий не требует результатов соседей. Увеличение workers не должно менять смысл ожидаемого состояния.
Неудача и порядок выполнения
Если при будущем запуске неверные карточки появляются только с двумя workers, сначала ищите общий изменяемый ресурс или глобальную переменную. В нашем исходнике фильтр локален, поэтому перенос состояния между контекстами был бы подозрительным. Но сам факт параллельной неудачи ещё не доказывает серверный конфликт: возможно, изменились условия загрузки и обнаружилось неправильное ожидание.
Не стоит автоматически переводить весь файл в последовательный режим, не объяснив причину. Это может временно скрыть столкновение, но оставит сценарии зависимыми от порядка. Если действия действительно образуют один пользовательский путь, лучше оформить их одним тестом с понятными шагами. Если это разные задачи, подготовка должна стать независимой.
Теперь два фильтра имеют одинаковый вход и отдельный контекст, а общий сервер предоставляет только безопасные для данного примера ресурсы. Мы подготовили параллельный набор без обещания конкретного ускорения. Следующий урок проверит более тонкую независимость: сохранённая внутри одного контекста тема не должна становиться начальным состоянием другого.
Даже при общей неизменяемой коллекции рабочие файлы могут стать причиной гонки. Если оба сценария пишут один result.json без согласованного протокола, последний записавший заменит результат первого. Для диагностических материалов полезно использовать пути, связанные с конкретным тестом, а не вручную выбранное общее имя.
Наши сценарии не пишут таких файлов. Аннотация worker принадлежит метаданным текущего теста, а состояние формы — его странице. Это ещё одна причина, по которой увеличение параллелизма не требует передавать индекс worker в фильтр или менять идентификаторы курсов. Неизменяемые данные остаются общими, рабочие результаты — отдельными.