[In one Paper]: Playwright
source icon
Playwright. Фреймворк для быстрого E2E-тестирования — Разработка на vc.ru
Тестирование с помощью Playwright
Playwright - фреймворк для E2E-тестирования приложений, он хорошо интегрируется с любыми приложениями и сайтами, предлагает большой ассортимент для кастомизации.
Установка
Установка Playwright достаточно проста. Можно сразу же инициализировать конфигурацию и установить необходимые пакеты:
# pnpm
pnpm dlx create-playwright

# npm
npm init playwright@latest
Если же мы не хотим устанавливать Playwright c дефолтной конфигурацией, то нам нужно:
  1. Установить пакет Playwright
  2. Самим добавить конфигурацию
pnpm i -D @playwright/test
touch playwright.config.js
Конфигурация
Вся конфигурация для Playwright пишется в одном файле - playwright.config.js.
Вот как выглядит базовая версия конфигурации:
import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  // Директория для тестов
  testDir: 'tests',

  // Нужно ли запускать все тесты параллельно
  fullyParallel: true,

  // Данная опция положит все тесты на CI, если хоть в одном тесте установлен test.only
  forbidOnly: !!process.env.CI,

  // Сколько будет попыток повторить тест, если он упадет
  retries: process.env.CI ? 2 : 0,

  // Настройка количества параллельных воркеров для тестов
  workers: process.env.CI ? 1 : undefined,

  // Репортер (в каком формате будет представлена информация о прошедших/не прошедших тестах)
  reporter: 'html',

    // Используемые в тестах данные
  use: {
    // BaseURL в тестах. Если перейдем на page.goto('/'), то окажемся на localhost:3000
    baseURL: 'http://127.0.0.1:3000',

    // Собирать данные при падении, даже если тест повторяется
    trace: 'on-first-retry',
  },

  // Конфигурируем браузеры для проекта
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
  ],

  // Запускаем devServer, перед тем как запустить тесты
  webServer: {
    command: 'npm run start',
    url: 'http://127.0.0.1:3000',
    reuseExistingServer: !process.env.CI,
  },
});
Информация
К слову, process.env.CI - равен true, только тогда, когда мы запускаем наши тесты внутри CI/CD (Gitlab CI/CD, Github Actions, Jenkins, и так далее).
Продвинутая конфигурация
Вверху был представлен базовый файлик для конфигурации Playwright. Он хорош тем, что является достаточно минималистичным, однако, если нам нужно что-то по сложнее, то нам могут потребоваться дополнительные опции. Внизу предоставлены некоторые из них:
export default defineConfig({
  // Директория, в которую будут идти все отчеты, артефакты, видео, скриншоты
  outputDir: 'test-results',

  // Файл, который выполнится до начала тестов. Это так называемый бутстрап
  globalSetup: require.resolve('./global-setup'),

  // То же самое что и вверху, однако данный файл выполнится после окончания тестов
  globalTeardown: require.resolve('./global-teardown'),

  // Таймаут для тестов 💆‍♂️
  timeout: 30000,
});
Настройки вверху позволяют нам изменить входные и выходные данные для тестов, а также обозначить время, в пределах которого тест будет выполняться.
Не факт, что наши тесты будут лежать в директории test, поэтому мы явно можем указать какие тесты мы хотим обрабатывать, а какие нет, с помощью двух следующих опций:
export default defineConfig({
  // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут игнорироваться
  testIgnore: '*test-assets',

  // Паттерн или регулярное выражение, по которому будет определяться какие тесты будут выполняться
  testMatch: '*todo-tests/*.spec.ts',
});
Также, нам может потребоваться изменить эмуляционные данные:
  • Время;
  • Геолокацию;
  • Разрешения;
  • ViewPort;
  • Тему;
Все это также можно сделать внутри конфигурации. Поле, которое мы уже ранее обсуждали (use) - отвечает за то, какие внутри браузера будут мета-данные:
export default defineConfig({
  use: {
    // Эмулирует @media (prefers-colors-scheme)
    colorScheme: 'dark',

    // Геолокация
    geolocation: { longitude: 12.492507, latitude: 41.889938 },

    // Локализация
    locale: 'en-GB',

    // Разрешения
    permissions: ['geolocation'],

    // Часовой пояс
    timezoneId: 'Europe/Paris',

    // Viewport
    viewport: { width: 1280, height: 720 },
  },
});
Мы также можем изменять параметры сети:
  • Давать разрешение на загрузку;
  • Добавлять кастомные заголовки к HTTP-запросам;
  • Использовать HTTP-Auth;
  • Использовать приложение оффлайн;
  • Использовать прокси;
export default defineConfig({
  use: {
      // Контроллировать разрешение на загрузку файлов
    acceptDownloads: false,

    // Добавить кастомные HTTP-заголовки к каждому реквесту
    extraHTTPHeaders: {
      'X-My-Header': 'value',
    },

    // HTTP-Auth
    httpCredentials: {
      username: 'user',
      password: 'pass',
    },

    // Игнорировать ошибки HTTPS во время навигации
    ignoreHTTPSErrors: true,

    // Оффлайн
    offline: true,

    // Прокси
    proxy: {
      server: 'http://myproxy.com:3128',
      bypass: 'localhost',
    },
  },
});
Также полезно оставлять артефакты, если с тестом что-то не так:
export default defineConfig({
  use: {
    // Делать скриншот, если тест упадет
    screenshot: 'only-on-failure'

    // Записывать tracelist после первой неудачной попытки
    trace: 'on-first-retry',

    // Записывать видео после первой неудачной попытки
    video: 'on-first-retry'
  },
});
Установка необходимых браузеров
Для того чтобы начать пользоваться Playwright необходимо еще установить нужные нам браузеры. По умолчанию Playwright поддерживает Google Chrome на Desktop, однако можно добавить в конфигурацию достаточно много устройств и браузеров.
Мы можем тестировать на:
  • Safari;
  • Edge;
  • Google Chrome;
  • Firefox;
Для того чтобы установить нужные нам браузеры мы должны обратиться к конфигурациии прописать все нужные нам устройства, как это сделано здесь:
  // Конфигурируем браузеры для проекта
  projects: [
    {
      name: 'chromium',
      use: { ...devices['Desktop Chrome'] },
    },
    {
      name: 'firefox',
      use: { ...devices['Desktop Firefox'] },
    },
    {
      name: 'webkit',
      use: { ...devices['Desktop Safari'] },
    },
    {
      name: 'Mobile Chrome',
      use: { ...devices['Pixel 5'] },
    },
    {
      name: 'Mobile Safari',
      use: { ...devices['iPhone 12'] },
    },
  ],
Далее нам достаточно просто ввести npx playwright install, playwright сам найдет и скачает нужные нам браузеры.
Тестирование
Само тестирование заключается в том, чтобы провернуть в браузере сценарий, который наиболее вероятно выполнит пользователь.
Playwright предлагает нам управлять браузером программно. Вот как выглядит простой тест на Playwright:
import { test, expect } from '@playwright/test';

test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Проверяем, чтобы страница имела в заголовке Playwright
  await expect(page).toHaveTitle(/Playwright/);
});

test('get started link', async ({ page }) => {
  await page.goto('https://playwright.dev/');

  // Кликаем на ссылку
  await page.getByRole('link', { name: 'Get started' }).click();

  // Проверяем что в URL было слово intro
  await expect(page).toHaveURL(/.*intro/);
});
Нахождение элементов
Для того чтобы производить базовые действия в браузере программно - нам понадобится Locator API .
Сам объект Locator может только находить элементы, все действия (такие как ховер, клик и так далее) находятся в объекте элемента.
Найти элемент с помощью Locator достаточно легко, для этого нам нужно обратиться к Pages API, которое предоставляет нам доступ к странице. Мы можем сами создать новую страницу:
const page = await context.newPage();
И с помощью этой страницы уже перейти на нужную нам страницу (вспоминаем await page.goto(‘url’), или же воспользоваться объектом page, который Playwright автоматически передает в каждый тест-кейс:
// В аргументах к коллбэку мы забираем объект page
test('has title', async ({ page }) => {
  await page.goto('https://playwright.dev/');
  await expect(page).toHaveTitle(/Playwright/);
});
С помощью page мы можем найти нужный нам элемент. Внизу перечислены несколько способов как это можно сделать:
// Находим по роли
page.getByRole('textbox');

// Находим по лэйблу
page.getByLabel('Birth date');

// Находим по тексту внутри элемента
page.getByText('Item')

// Находим по селектору
page.locator('#area')
Действия с элементами
Как только мы нашли нужный нам элемент - мы можем проделать с ним какие-либо действия.
Информация
Следует упомянуть, что нахождение элемента в Playwright является синхронной операцией. А выполнение действия с элементом - асинхронной.
Внизу предоставлен листинг кода, в котором показано что мы можем сделать с элементом:
// Находим элемент
const example = page.locator('#example');

// Кликаем по нему
await example.click();

// Активируем чекбокс
await example.check();

// Дезактивируем чекбокс
await example.uncheck();

// Наводим мышь на элемент
await example.hover();

// Заполняем форму (быстро)
await example.fill();

// Заполняем форму (медленно, эмулируя реальный ввод)
await example.type();

// Фокусируем элемент
await example.focus();

// Нажимаем клавишу
await example.press();

// Отправляем файлы (Drag n' drop)
await example.setInputFiles();

// Выбираем опции из выпадающего списка
await example.selectOption();
Информация
Если вам нужна полная документация по действиям, то ее можно найти вот тут.
Сравнения
Теперь пришло время для сравнений. Следует сразу же рассказать о том что во всех E2E тестах есть два типа сравнений: промежуточное и финальное.
Промежуточное сравнение помогает понять тест-кейсу правильно ли он выполняет действия и должен ли он дожидаться определенной реакции браузера, прежде чем делать следующее действие.
Например, мы отправили форму и должны увидеть тостер (уведомление) вверху экрана. Прежде чем он появится мы точно знаем, что кнопка отправки должна исчезнуть (или стать снова активной, тут в зависимости от программного решения).
Для того чтобы тест-кейс не бежал сразу проверять есть ли у нас тостер мы можем проверить предварительные условия (кнопка не должна быть видна) или дождаться покуда она видна не будет (что будет означать, что реквест ушел на сервер):
// Данная строка будет дожидаться покуда кнопка исчезнет
await page.locator('#button').not.isVisible();
Такие промежуточные сравнения работают без expect, и как уже было сказано ранее - не обрывают тест, если условие ложно, они ждут покуда условие будет правиво в течение определенного времени.
Промежуточные сравнения находятся в Locator API, вот некоторые из них:
// Видим ли элемент
await page.locator('#button').isVisible();

// Скрыт ли элемент
await page.locator('#button').isHidden();

// Активен ли элемент (нет ли disabled среди атрибутов)
await page.locator('#button').isEnabled();

// Неактивен ли элемент (есть ли disabled среди атрибутов)
await page.locator('#button').isDisabled();

// Можно ли элемент редактировать
await page.locator('#button').isEditable();

// Активен ли чекбокс
await page.locator('#button').isChecked();
Финальное сравнение используется тогда, когда нам нужно проверить состояние страницы или элемента на наличие какого-то атрибута, видимости элемента.
Финальное сравнение отличается от промежуточного тем, что заключается в expect. Хорошей практикой является делать одно финальное сравнение в одном тест-кейсе:
expect(await page.locator('#button').isVisible()).toBe(true);
Хорошие практики
Для того чтобы писать хорошие E2E-тесты нужно следовать рекомендациям, иначе можно оказаться в ситуации, что тесты
  • Работают слишком медленно;
  • Влияют на друг-друга;
  • Работают неправильно;
Хорошие практики: Атомарность
Первым и пожалуй одним из самых важных советов является атомарность.
Информация
Атомарная ( греч. άτομος — неделимое) операция — операция, которая либо выполняется целиком, либо не выполняется вовсе; операция, которая не может быть частично выполнена и частично не выполнена.
Атомарная операция — Википедия
Тест называется атомарным, когда он из одного состояния приложения переводит приложение в другое состояние, что-то проверяет и возвращает все на свое место.
Представим кейс, где мы переходим по ссылке и проверяем на другой странице какой-то текст. После того как мы проверили текст - мы завершаем тест-кейс. Затем начинается следующий тест-кейс. Он сразу же падает, почему? Потому что следующий тест-кейс ожидает того, что мы начнем тестирование с той же страницы, с которой начинали и предыдущий. Мы же в свою очередь забыли в прошлом тест-кейсе «убрать за собой мусор», то есть оставили приложение висеть на странице, на которую мы перешли.
Для того чтобы такого не допустить, в Playwright, как и во всех популярных фреймворках для тестирования существуют хуки:
  • beforeEach - действие перед каждым тестом;
  • afterEach - действие после каждого теста;
  • beforeAll - действие перед всеми тестами;
  • afterAll - действие после всех тестов;
Если переход со страницы на страницу занимает немного времени, то мы могли зафиксить нашу проблему следюущим образом:
test.beforeEach(async ({page}) => {
    await page.goto('/');
  // или
  await page.reload();
});
Хорошие практики: Селекторы
В Playwright рекомендуют использовать селекторы, которые вам даст codegen. Codegen по умолчанию не будет цепляться к вашим CSS-селекторам, он будет пытаться цепляться за текст на странице, так как текст меняется реже, нежели селекторы (по крайней мере так думают разработчики Playwright).
Информация
Сам codegen можно запустить с помощью команды
npx playwright codegen
На моей практике оба методы (цепляться за текст и цепляться на CSS-селекторы) показывали себя крайне ненадежно. Можно конечно цепляться за id, однако тогда у элементов будут куча ID’шников в продакшене.
Часто я привязываю специальные селекторы к атрибуту data-test, сами селекторы я заношу в отдельный файл selectors.js и с помощью них уже цепляюсь в тестах. Внизу приведен пример на Vue:
<template>
    <UniqueElement
      :data-test="UNIQUE_SELECTOR"
      class="unique-element"
    />
</template>
В тестах же, зацепиться за данные селекторы достаточно просто, главное чтобы такие селекторы были уникальными:
await page.locator(`[data-test="`${UNIQUE_SELECTOR}`"]`).click();
Для того чтобы эти селекторы не попадали в продакшн, мы можем просто вырезать их на этапе сборки.
Хорошие практики: Минимальные проверки
Суть минимальных проверок состоит в том, чтобы проверять только те элементы (на видимость, наличие текста и так далее), которые отражают состояние приложения. Допустим у нас отправляется форма и мы ждем покуда лоадер исчезнет. Мы проверим только элемент лоадера и не будем проверять никакие другие элементы на факт их исчезновения.
Если вам сложновато с данной практикой используйте npx playwright codegen и просто выполните действие которое вам нужно. Таким образом вы просто напишите минимальный набор команд, который нужен для того чтобы проверить как приложение отреагировало на пользовательский ввод.