- Проблема со стандартными assertions
- Создаем первый матчер
- Применяем
- Матчеры объектов страницы
- Мощь кастомных матчеров
- Советы
Будучи автоматизатором, я провел бесчисленное количество времени за написанием и поддержкой сквозных тестов. Один из инструментов, который изменил мою работу — Playwright. Он быстрый, кроссбраузерный и эргономичный. Но несмотря на все плюсы, я часто обнаруживал, что в тестах нужны более конкретные, специализированные утверждения-assertions. Вот тут-то и должны помочь кастомные матчеры.
В этой статье я проведу вас по пути создания и применения кастомных матчеров Playwright (такие матчеры еще называются пользовательскими или настраиваемыми). Начнем с простого, затем перейдем к более сложным сценариям, включая работу с объектами страницы. Уверен, к концу этой статьи вам захочется делать свои собственные матчеры 🙂.
Проблема со стандартными assertions
Начнем с обычного сценария. Вы тестируете веб-приложение и должны проверить, что элемент содержит нужный текст. Используя встроенные в Playwright утверждения, вы напишете что-то вроде этого:
await expect(page.locator('.user-greeting')).toContainText('Welcome back, John!');
const text = await page.locator('.user-greeting').textContent(); expect(text?.toLowerCase()).toContain('welcome back');
Это работает, но не очень красиво. А если нужно выполнить подобную проверку в нескольких местах, то в итоге получите повторяющийся и трудно поддерживаемый код. Здесь-то и нужны кастомные матчеры.
Создаем свой первый матчер
Напишем матчер, который решит эту проблему. Назовем его toHaveTextContent. Вот как это выглядит:
import { expect, Locator } from '@playwright/test'; const customMatchers = { async toHaveTextContent(locator: Locator, expectedText: string, options = { caseSensitive: true }) { const actualText = await locator.textContent(); let pass: boolean; if (options.caseSensitive) { pass = actualText?.includes(expectedText) ?? false; } else { pass = actualText?.toLowerCase().includes(expectedText.toLowerCase()) ?? false; } return { pass, message: () => pass ? `Expected element not to have text content "${expectedText}"` : `Expected element to have text content "${expectedText}", but found "${actualText}"`, }; }, }; expect.extend(customMatchers); declare global { namespace PlaywrightTest { interface Matchers<R> { toHaveTextContent(expectedText: string, options?: { caseSensitive?: boolean }): Promise<R>; } } }
Сначала это может показаться сложным, но давайте разберемся по пунктам:
- Определяем нашу матчер-функцию, которая принимает Locator, ожидаемый текст и объект options.
- Получаем фактический текстовый контент элемента.
- Выполняем проверку, учитывая регистр.
- Возвращаем объект с результатом и соответствующими сообщениями об ошибках.
Части expect.extend и declare global — это магия TypeScript, благодаря которой наш кастомный матчер отлично работает с существующей системой ассертов Playwright.
Применение
С новым матчером перепишем наш тест следующим образом:
await expect(page.locator('.user-greeting')).toHaveTextContent('Welcome back', { caseSensitive: false });
Такой код выглядит чище. И что самое приятное, мы можем повторно использовать этот матчер во всем тестовом наборе.
Кастомные матчеры с объектами страниц
По мере роста тестового набора мы часто обращаемся к объектной модели страницы, чтобы упорядочить структуру набора. Посмотрим, как создать более сложный кастомный матчер, работающий с объектами страниц.
Сначала определим простой объект страницы для условного пользовательского дашборда:
class UserDashboard { constructor(private page: Page) {} async getUserGreeting() { return this.page.locator('.user-greeting'); } async getLatestNotification() { return this.page.locator('.notification').first(); } }
Теперь создадим кастомный матчер, который проверяет, приходит ли пользователю новое уведомление:
const customMatchers = { // ... our previous matcher ... async toHaveNewNotification(dashboard: UserDashboard) { const notification = await dashboard.getLatestNotification(); const isVisible = await notification.isVisible(); const text = await notification.textContent(); return { pass: isVisible && text?.includes('New'), message: () => isVisible && text?.includes('New') ? `Expected user not to have a new notification, but found: "${text}"` : `Expected user to have a new notification, but found none`, }; }, }; expect.extend(customMatchers); declare global { namespace PlaywrightTest { interface Matchers<R> { // ... our previous matcher type ... toHaveNewNotification(): Promise<R>; } } }
Теперь мы можем использовать это в тестах следующим образом:
test('user sees new notification', async ({ page }) => { const dashboard = new UserDashboard(page); await page.goto('/dashboard'); await expect(dashboard).toHaveNewNotification(); });
Мощь кастомных матчеров
К этому моменту вы, вероятно, уже начали понимать потенциал кастомных матчеров. Они позволяют нам:
- Инкапсулировать сложную логику ассертов
- Сделать тесты более читаемыми и понятными
- Сократить дублирование кода
- Создать доменный язык для своих тестов
По моему опыту, затраты времени на создание хороших кастомных матчеров окупаются в огромной степени по мере роста набора тестов. Они особенно полезны, если вы работаете в команде, поскольку позволяют создать общий словарь для ваших тестов.
Лучшие практики и советы
Прежде чем мы закончим, вот несколько советов:
- Будьте проще: Начните с простых матчеров и усложняйте их по мере необходимости.
- Будьте описательны: Используйте понятные, описательные имена для своих матчеров. toHaveNewNotification гораздо лучше, чем toHaveNot.
- Сообщения об ошибках имеют значение: Потратьте время на написание понятных сообщений об ошибках. Ваши коллеги по команде скажут вам спасибо при отладке упавших тестов.
- Используйте TypeScript: Безопасность типов и автодополнение в TS неоценимы при работе с кастомными матчерами.
- Не переусердствуйте: Не нужно для всего использовать кастомные матчеры. Используйте их для стандартных, сложных или узкоспецифических ассертов.
Резюме
Кастомные матчеры матчеры Playwright стали неотъемлемой частью моего набора инструментов. Они позволяют мне писать чистые и понятные тесты и значительно сократили время, которое я трачу на обслуживание тестовых наборов. Рассмотренные здесь примеры — лишь верхушка айсберга. Настоящая мощь кастомных матчеров проявляется, когда вы создаете их с учетом особенностей вашего приложения и домена.