Selenium требует отдельного управления драйверами браузера и явных ожиданий почти на каждое действие; Cypress не работает кросс-браузерно и прячет параллельный запуск за платный план. Playwright делает всё это из коробки: одна команда установки скачивает Chromium, Firefox и WebKit, а авто-ожидание убирает ручные вызовы sleep() перед взаимодействиями. В этом гайде: установка, первый проходящий тест, что делает каждая строка кода и как читать трассировку когда что-то ломается.

Почему Playwright, а не что-то другое

Кратко, до установки: Playwright выбирает большинство новых проектов в 2026 году, и на это есть причины.

Selenium был стандартом много лет. Работает, но медленно настраивается, многословен в написании и требует ручного управления драйверами браузера. Cypress проще, но поддерживает только Chromium-браузеры, а параллельный запуск в CI платный.

Playwright работает с современными веб-приложениями из коробки. Он автоматически ждёт готовности элементов перед взаимодействием с ними, поэтому случайные sleep() в тестах не нужны. Поддерживает Chromium, Firefox и WebKit. Включает встроенный тест-раннер, запись видео при падениях и кодогенератор который пишет локаторы пока ты кликаешь по странице.

Начинаешь с нуля в 2026 году: Playwright правильный выбор.

Установка

Сначала нужен Node.js. Проверяешь наличие:

node --version

Если возвращает версию (v18 и выше подходит), всё готово. Если нет, скачай LTS-версию с nodejs.org.

Создаёшь новую папку для проекта и запускаешь установщик Playwright:

mkdir my-first-tests
cd my-first-tests
npm init playwright@latest

Установщик задаёт несколько вопросов. Для обучения принимаешь дефолтные значения:

  • TypeScript или JavaScript → TypeScript
  • Папка для тестов → tests
  • GitHub Actions workflow → no (добавишь позже)
  • Установить браузеры Playwright → yes

Скачивание браузеров занимает минуту-две. После завершения структура проекта:

my-first-tests/
  tests/
    example.spec.ts
  playwright.config.ts
  package.json

Всё. Никаких конфигурационных файлов для борьбы, никаких менеджеров драйверов, никакого отдельного тест-раннера.

Запускаем пример теста

До написания своего кода запусти то что пришло с установкой:

npx playwright test

Playwright запускает тесты в headless-режиме (без видимого окна браузера) и выводит результаты в терминал. Увидишь что-то вроде:

Running 2 tests using 2 workers
  2 passed (3s)

Чтобы посмотреть что происходило, открываешь HTML-отчёт:

npx playwright show-report

Откроется браузер с детальным отчётом по каждому тесту: какие шаги выполнились, сколько времени заняли, скриншоты в конце.

Запусти npx playwright test --headed чтобы видеть как браузер открывается и выполняет тест в реальном времени. Полезно в начале, когда хочешь понять что происходит.

Пишем первый тест

Удаляешь пример и создаёшь новый файл. Открываешь tests/login.spec.ts и пишешь:

import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  await expect(page.getByText('My Travel Items')).toBeVisible();
});

Запускаешь:

npx playwright test tests/login.spec.ts

Должен увидеть 1 passed. Разберём что делает каждая часть.

Что ты только что написал

test('user can log in', async ({ page }) => {

test: функция определяющая тест-кейс. Первый аргумент: название. Пиши его как предложение описывающее действие пользователя, а не что делает код. async означает что функция асинхронная, это обязательно, потому что каждое действие в браузере занимает время. { page }: фикстура Playwright, которая даёт свежую вкладку браузера для работы.

await page.goto('https://lab.becomeqa.com')

Открывает URL. await говорит JavaScript подождать завершения этого действия перед переходом к следующей строке. await ставится перед каждым действием Playwright.

page.getByRole('button', { name: 'Login' })

Локатор: находит элемент на странице. getByRole ищет элементы по ARIA-роли и доступному имени. Находит кнопку с текстом «Login». Самый надёжный тип локатора, потому что привязан к тому как элемент реально работает, а не к CSS-классу или позиции.

await page.getByLabel('Username').fill('admin@becomeqa.com')

Находит поле ввода связанное с меткой «Username» и вводит email.

await expect(page.getByText('My Travel Items')).toBeVisible()

Проверка: часть которая реально тестирует что-то. Проверяет что текст «My Travel Items» виден на странице после логина. Если логин провалился и текст не появился, тест упадёт.

Локаторы: как находить элементы

Локаторы говорят Playwright с каким элементом взаимодействовать. Несколько типов, у каждого свой сценарий.

getByRole: предпочтительный выбор. Использует ARIA-роли описывающие семантику элементов:

page.getByRole('button', { name: 'Submit' })   // кнопка с подписью Submit
page.getByRole('link', { name: 'Home' })        // ссылка с подписью Home
page.getByRole('textbox', { name: 'Search' })   // текстовое поле с подписью Search

getByLabel подходит для полей формы с видимой меткой:

page.getByLabel('Email address')
page.getByLabel('Password')

getByPlaceholder когда метки нет, но есть placeholder:

page.getByPlaceholder('Enter your email')

getByText находит элементы по видимому тексту:

page.getByText('My Travel Items')
page.getByText('Sign out')

getByTestId использует атрибут data-testid. Пригодится когда разработчики добавляют его специально для тестов:

page.getByTestId('submit-button')

Не используй локаторы по CSS-классам или XPath. Они ломаются когда разработчик переименовывает класс или переструктурирует HTML. Локаторы по роли переживают такие изменения.

Действия: взаимодействие со страницей

Получив локатор, выполняешь действия:

await locator.click()                    // клик по элементу
await locator.fill('some text')          // очистить и ввести текст
await locator.type('some text')          // ввод посимвольно (для полей реагирующих на нажатия)
await locator.check()                    // отметить чекбокс
await locator.selectOption('value')      // выбрать из dropdown
await locator.hover()                    // навести курсор
await locator.press('Enter')             // нажать клавишу

Важно: ждать элементы вручную почти никогда не нужно. Playwright автоматически ждёт пока элемент станет видимым и доступным перед выполнением действия. Это авто-ожидание устраняет большинство флакующих тестов в Playwright.

Проверки: проверяем результат

Проверка: то что делает тест реальным тестом. Без проверок ты просто кликаешь по странице.

// Видимость
await expect(page.getByText('Welcome')).toBeVisible();
await expect(page.getByRole('dialog')).toBeHidden();

// Текстовое содержимое
await expect(page.getByRole('heading')).toHaveText('My Travel Items');
await expect(page.getByRole('heading')).toContainText('Travel');

// Значения полей
await expect(page.getByLabel('Email')).toHaveValue('test@example.com');

// URL
await expect(page).toHaveURL('https://lab.becomeqa.com/dashboard');

// Заголовок страницы
await expect(page).toHaveTitle('BecomeQA Lab');

// Количество элементов
await expect(page.getByRole('row')).toHaveCount(5);

Проверки Playwright тоже авто-ждут. toBeVisible() не проверяет прямо сейчас. Она повторяет попытку до 5 секунд по умолчанию, ожидая появления элемента. Явные ожидания перед проверками тоже не нужны.

Более полный тест

Второй тест, продолжающий после логина:

import { test, expect } from '@playwright/test';

test('user can log in', async ({ page }) => {
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  await expect(page.getByText('My Travel Items')).toBeVisible();
});

test('travel items table has data', async ({ page }) => {
  // сначала логинимся
  await page.goto('https://lab.becomeqa.com');
  await page.getByRole('button', { name: 'Login' }).click();
  await page.getByLabel('Username').fill('admin@becomeqa.com');
  await page.getByLabel('Password').fill('testpass123');
  await page.getByRole('button', { name: 'Submit' }).click();

  // проверяем таблицу
  await expect(page.getByRole('table')).toBeVisible();
  const rows = page.getByRole('row');
  await expect(rows).toHaveCount(6); // заголовок + 5 строк данных
});

Запускаешь оба теста:

npx playwright test tests/login.spec.ts

Шаги логина дублируются между тестами. Пока это нормально. Когда тестов станет больше, повторяющийся сетап выносится в блок beforeEach или в фикстуру, но это уже промежуточная тема.

Каждый тест в Playwright по умолчанию запускается в полностью изолированном контексте браузера. Даже если один тест залогинился, следующий стартует заново. Это сделано намеренно: тесты разделяющие состояние намного сложнее отлаживать.

Файл playwright.config.ts

Открываешь playwright.config.ts. Дефолты работают нормально, но два параметра стоит знать:

export default defineConfig({
  testDir: './tests',
  timeout: 30000,          // максимальное время теста в мс
  retries: 0,              // сколько раз повторять упавший тест
  use: {
    baseURL: 'https://lab.becomeqa.com',   // добавь это
    trace: 'on-first-retry',
  },
  projects: [
    { name: 'chromium', use: { ...devices['Desktop Chrome'] } },
    { name: 'firefox',  use: { ...devices['Desktop Firefox'] } },
    { name: 'webkit',   use: { ...devices['Desktop Safari'] } },
  ],
});

Если задать baseURL, в тестах можно писать page.goto('/') вместо полного URL. Добавь в конфиг и обнови тесты:

await page.goto('/');   // вместо 'https://lab.becomeqa.com'

По умолчанию Playwright запускает тесты во всех трёх браузерах. Пока учишься, удобнее ограничиться Chromium:

npx playwright test --project=chromium

Когда тест падает

Сломаем тест намеренно. Меняешь ожидаемый текст на несуществующий:

await expect(page.getByText('This text does not exist')).toBeVisible();

Запускаешь и видишь:

1 failed
  login.spec.ts:10:3 › user can log in ✗

    Error: expect(locator).toBeVisible()
    Locator: getByText('This text does not exist')
    Expected: visible
    Received: hidden
    Timeout: 5000ms

Ошибка говорит точно что ждала и что это так и не стало видимым. Смотришь трассировку:

npx playwright show-report

Кликаешь на упавший тест. Видишь пошаговую разбивку со скриншотами на каждом действии. Это trace viewer, один из самых полезных инструментов Playwright для отладки.

→ See also: Установка Playwright: пошаговое руководство (2026) | Локаторы Playwright: getByRole, getByLabel, getByText, getByTestId — сравнение | Assertions в Playwright: полное руководство | Playwright Trace Viewer: отлаживайте упавшие тесты как профессионал

Возвращаешь тест к 'My Travel Items' перед тем как двигаться дальше.

FAQ

Нужно ли знать TypeScript перед стартом?

Нет. TypeScript который пишешь для тестов минимален: в основном async/await и базовые типы. Курс Playwright покрывает именно то что нужно по ходу.

Стоит ли использовать JavaScript вместо TypeScript?

TypeScript лучше, даже если он для тебя новый. Ловит ошибки до запуска теста. Дополнительный синтаксис минимален, а преимущества чувствуются сразу.

Как найти правильный локатор для элемента?

Используй codegen: npx playwright codegen https://lab.becomeqa.com. Откроется браузер, будет записывать клики и автоматически генерировать локаторы. Используй для изучения, потом подчищай то что генерируется.

Тест проходит локально, но падает в GitHub Actions. Почему?

Обычно проблема с таймингом или пропущенный await. Запусти локально с --headed чтобы видеть что происходит, затем проверь не пропустил ли где await.