Мое знакомство с Playwright на примере организации E2E-тестирования проекта в среде Docker Compose
Начну с того, что некоторое время назад я делал полифил для добавления поддержки псевдокласса :has в DOM API старых браузеров, ну и разумеется этот полифил нужно было хоть как-то протестировать, для чего изначально было задействовано модульное тестирование с использованием Jest и JSDOM.
Очевидно что такое тестирование не гарантировало реальную работоспособность полифила в существующих браузерах, поэтому через какое-то время пришлось делать еще и E2E-тестирование, с использованием уже реальных браузерных движков. В общем-то про это все дальше и пойдет речь в статье.
Немного о самом E2E-тестировании
Если упрощенно говорить, то E2E (End-to-End) тестирование представляет собой проверку работы всей системы в целом с максимально возможной эмуляцией рабочего окружения и имитацией реального пользовательского сценария использования.
Глубоко про общую теорию и нюансы E2E-тестирования я тут расписывать не хочу, этого и так в интернете и учебниках полно, просто отмечу, что такое тестирование ни в коем случае не заменяет другие типы тестов, а только дополняет их. Но поскольку E2E-тестирование зачастую довольно затратно и по ресурсам и времени выполнения, то оно обычно запускается уже после успешного выполнения других видов тестов.
В моем случае, в дополнение к модульным тестам (с использованием Jest), мне нужно было отдельно организовать тестирование в различных браузерных движках, благо уже во всю стал среди веб-разработчиков набирать популярность фреймворк Playwright, с которым я и начал знакомиться.
Знакомство с Playwright
Ранее для пользовательского тестирования (и не только) я уже имел опыт использования связки Jest + Puppeteer, но т.к. Puppeteer рассчитан в основном на работу с Chromium, то в моем случае он не подходит и поэтому я решил попробовать быстро набирающий популярность Playwright, который изначально рассчитан на работу с различными браузерными движками. Разрабатывает этот фреймворк команда разработчиков из Microsoft, поэтому неудивительно, что он так быстро развивается и имеет хорошую поддержку.
На первый взгляд Playwright оказался достаточно стабильным, хорошо документированным и что важно, достаточно гибким. Фактически Playwright представляет собой готовое комплексное решение "всё-в-одном" для E2E-тестирования, где есть и тест-раннер (вместо Jest там используется свой) и взаимодействие с несколькими браузерными движками и полная поддержка TypeScript и всё это вместе работает без особой головной боли как единое целое. После Puppeteer и Jest многие вещи в API Playwright мне казались уже знакомыми (даже смешивание кода из разных JavaScript-окружений в одно, хотя в Playwright с этим полегче), что логично, ибо разработчики Playwright явно опирались на опыт других фреймворков.
Тем не менее, в своей задаче я столкнулся с некоторыми нюансами Playwright, которые приходилось учитывать:
- Версии Playwright достаточно сильно зависимы от версий браузеров и соответственно для тестирования в старых браузерах подойдут только старые версии Playwright, что совсем не подходит (об этом подробнее ниже).
- Полная установка Playwright тянет за собой кучу зависимостей. Сам Playwright можно устанавливать как с браузерными движками, так и без. Если делать полную установку со всеми движками, тогда он будет
засорятьдоустанавливать в систему все необходимые ему зависимости, что не всегда желательно. Да и для установки системных зависимостей ему понадобятся привилигированные права. Поэтому на мой взгляд тесты, даже во время разработки, лучше запускать на выделенной для этого машине или еще лучше в контейнере. - Опять же полная установка Playwright внутри Docker контейнера может вызвать некоторые трудности. К сожалению на Alpine Linux это чудо завести не удалось (хотя теоретически можно), а при использовании обычного Debian в качестве основы для контейнера, он разворачивался очень долго (ибо нужно качать кучу зависимостей) и относительно неплохо так раздувается в размере. Это решаемо, но повозиться надо будет.
Поскольку я веду разработку в Docker контейнерах, то все эти проблемы нивелируются за счет того, что Playwright может работать в клиент-серверном варианте и всю серверную часть можно вынести в отдельный контейнер используя официальный готовый базовый образ контейнера Playwright из Docker Hub (или собрав свой), который можно объединить вместе с проверяемым проектом в единый Docker Compose. При этом можно написать конфиги Playwright так, чтобы его можно было использовать и при клиент-серверном и при обычном варианте установки.
Пример организации E2E-тестирования с Playwright и Docker Compose
Обратите внимание, что в примерах идет речь о версии Playwright 1.52 и на момент прочтения статьи, актуальной может быть уже другая версия.
Со всеми описанными примерами можно ознакомиться в репозитории проекта, хотя допускается, что со временем там может что-то уже поменяться. В общем будьте внимательны.
Итак, у нас на руках в Docker Compose есть два контейнера, между которыми надо организовать правильное взаимодействие для успешного E2E-тестирования:
- Контейнер polyfill-pseudoclass-has — на котором сам проект и клиент Playwright.
- Контейнер playwright — представляет собой сервер Playwright со всеми возможными зависимостями. Для этого взят готовый Docker образ mcr.microsoft.com/playwright:v1.52.0-noble .
В клиент-серверном варианте Playwright сам обеспечивает всю передачу тестов с клиента на сервер и их выполнение, но вот для передачи тестируемых данных нам уже нужен отдельный веб-сервер, к которому будет обращаться сервер Playwright (из одноименного контейнера playwright).
Итого docker-compose.yml выглядит примерно так:
services:
nodejs:
container_name: polyfill-pseudoclass-has
image: node:22-alpine
working_dir: /home/node/app
user: '1000:1000'
restart: 'no'
environment:
- PW_TEST_CONNECT_WS_ENDPOINT=ws://playwright:3000/
- SAMPLE_DEV_SERVER_PROTOCOL=http
- SAMPLE_DEV_SERVER_DOMAIN=polyfill-pseudoclass-has
- SAMPLE_DEV_SERVER_PORT=3000
- SAMPLE_DEV_SERVER_PATH=/
volumes:
- ./:/home/node/app
stdin_open: true
tty: true
playwright:
container_name: playwright
ports:
- '3000:3000'
init: true
stdin_open: true
tty: true
working_dir: /home/pwuser
user: pwuser
image: mcr.microsoft.com/playwright:v1.52.0-noble
command: /bin/sh -c "npx -y playwright@1.52.0 run-server --port 3000 --host 0.0.0.0"
Обратите внимание на переменные окружения, которые используются в основном контейнере:
PW_TEST_CONNECT_WS_ENDPOINT— с помощью этой переменной окружения мы передаем Playwright адрес сервера, а также согласно документации явно задаем использование Playwright именно в таком клиент-серверном варианте. Т.к. мы все это запускаем через Docker Compose, то по умолчанию, мы можем в качестве адреса использовать имя контейнера "playwright" и из первого контейнера подключиться ко второму просто по адресуws://playwright:3000/SAMPLE_DEV_SERVER_PROTOCOL,SAMPLE_DEV_SERVER_DOMAIN,SAMPLE_DEV_SERVER_PORT,SAMPLE_DEV_SERVER_PATH— формируют адрес веб-сервера, по которому должен обращаться второй контейнер (playwright) к первому (polyfill-pseudoclass-has). Обратите внимание, что в качестве домена (SAMPLE_DEV_SERVER_DOMAIN) в нашем примере указано имя контейнера polyfill-pseudoclass-has .
Если очень упрощенно, то мы внутри Docker Compose можем обращаться к серверам, используя в качестве домена имена контейнеров. Сформированные по этому принципу адреса мы явно задаем в переменные окружения в docker-compose.yml, которые в свою очередь будут читаться в тестах и конфигах Playwright.
В основном проекте нужно установить сам клиент Playwright через npm install -D @playwright/test@1.52.0 , но без системных зависимостей в виде браузеров.
Для того чтобы удаленный сервер Playwright из одноименного контейнера playwright, смог получить тестовую страничку с полифилом, необходимо создать веб-сервер с этой самой тестовой страничкой. Поскольку я не хотел плодить лишние сущности и конфиги в проекте, я использовал в качестве веб-сервера возможности уже имеющегося в проекте Vite, а тестовую страничку разместил в в ./tests/samples/index.html относительно корня проекта.
Запуск веб-сервера задан в виде npm-скрипта в package.json:
{
"scripts": {
… ,
"serve": "vite preview --host --port=3000 --outDir=./tests/samples"
}
}
Далее нам надо как-то автоматически запускать веб-сервер во время тестов. Для этого в конфиге Playwright есть опция "webserver", которая отвечает за управление служебным веб-сервером. Эту опцию, а также несколько дополнительных мы в итоге добавим в defineConfig в playwright.config.js :
import {defineConfig, devices} from '@playwright/test';
const sampleWebServer = {
protocol: process.env.SAMPLE_DEV_SERVER_PROTOCOL || 'http',
domain: process.env.SAMPLE_DEV_SERVER_DOMAIN || 'localhost',
port: process.env.SAMPLE_DEV_SERVER_PORT || '3000',
path: process.env.SAMPLE_DEV_SERVER_PATH || '',
};
const sampleWebServerURI = new URL(
`${sampleWebServer.protocol}://${sampleWebServer.domain}:${sampleWebServer.port}${sampleWebServer.path}`
);
export default defineConfig({
… ,
testDir: './tests/e2e',
use: {
… ,
baseURL: sampleWebServerURI.toString(),
},
webServer: {
… ,
command: 'npm run serve',
url: sampleWebServerURI.toString(),
ignoreHTTPSErrors: true,
},
});
Скажу пару слов касательно этих опций в playwright.config.js:
webServer:command— тут мы прописываем команду запуска нашего вебсервера. В нашем случае, для этого мы уже создали npm-скрипт выше, поэтому просто запускаем его командойnpm run serveurl— адрес, по которому Playwright будет пытаться обратиться, чтобы понять, что веб-сервер уже запущен.ignoreHTTPSErrors— внутри мы делаем простой HTTP запрос и ошибки HTTPS будут мешать.
use:baseURL— через этот параметр мы можем глобально задать основной URL адрес тестовых данных, чтобы убрать необходимость определения адреса уже из самих тестов.
testDir— все E2E-тесты вынесены в отдельную директорию, чтобы не конфликтовать с другими видами тестов (например unit-тестами).
Для удобства запуска E2E-тестов в package.json можно добавить npm-скрипт:
{
"scripts": {
… ,
"test:e2e": "npx playwright test",
}
}
В итоге получилось универсально:
- При разработке мы спокойно используем удаленный контейнер со всеми зависимостями и не замусориваем свое девелоперское окружение.
- Для отдельного тестирования (например для удобства при CI/CD) мы можем развернуть и проект и Playwright в одном контейнере и спокойно протестировать.
Поскольку репозиторий проекта размещен на Github, то я решил воспользоваться его возможностями и добавить через Github Actions автоматическое E2E-тестирование при внесении изменений в репозиторий.
Как ни странно, но с этим никаких проблем не оказалось, поскольку еще при установке Playwright предлагает сгенерировать необходимые настройки Github Actions в виде стандартного конфига .github/workflows/playwright.yml , который в моем случае выглядел так:
name: Playwright Tests
on:
push:
branches: [ main, master ]
pull_request:
branches: [ main, master ]
jobs:
test:
timeout-minutes: 60
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: lts/*
- name: Install dependencies
run: npm ci
- name: Install Playwright Browsers
run: npx playwright install --with-deps
- name: Build library
run: npm run build
- name: Run Playwright tests
run: npx playwright test
- uses: actions/upload-artifact@v4
if: ${{ !cancelled() }}
with:
name: playwright-report
path: playwright-report/
retention-days: 30
С учетом всех ранее описанных настроек, в этом конфиге ничего особо менять не пришлось и он сам по себе прекрасно работает.
Особенность адаптации полифила для возможности тестирования в современных версиях Playwright
Как я уже говорил, свежие версии Playwright официально совместимы только с относительно свежими версиями браузерных движков, а старый Playwright использовать совсем не вариант. В связи с этим встала проблема, проверки работоспособности полифила в браузерах, которые не поддерживают :has, имея в фреймворке только новые движки которые уже поддерживают этот самый :has.
Для такого случая в полифил пришлось добавить возможность переопределять ключевой идентификатор селектора :has, например на :_has (который точно нативно не поддерживается). Затем в тестах, выполнялись два одинаковых CSS-селектора с :has и :_has и общий результат в итоге проверялся на идентичность и в интегрированном в браузерное API варианте и в случае отдельно используемого API. По итогу получилось еще и исключить возможное влияние нативного :has.
Конечно в каждом тесте делать такое переопределение неудобно, поэтому это было сделано в качестве глобальной фикстуры, а в самих тестах просто учитывается, что тестируется уже альтернативный селектор :_has.
Заключение
Вообще, кстати, в какой-то момент у меня появилось чувство, что в команде разработчиков Microsoft несколько лет назад от той существовавшей веб-разработки настолько устали, что аж выделили ресурсы на создание VSCode, TypeScript ну и в догонку ко всему этому еще и Playwright. Их понять можно.
Что касается тестирования полифила, то я прекрасно понимаю, что в примерах есть много спорных моментов и решений и что многие вещи стоило бы переработать. Допускаю, что опытные тестировщики многое сильно раскритикуют, причем обоснованно. Тем не менее это вполне работает и решает проблему даже в таком виде.
Что касается самого Playwright, то меня этот фреймворк реально порадовал, поскольку в нем есть всё для полноценного E2E-тестирования веб-приложений и ничего не надо дополнительно костылить (что редкость в современной веб-разработке!). Со временем конечно все может измениться, но пока что Playwright уверенно занял свое место в моей личной копилке используемых программных инструментов.