Кастомные матчеры Playwright

Будучи автоматизатором, я провел бесчисленное количество времени за написанием и поддержкой сквозных тестов. Один из инструментов, который изменил мою работу — 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>;
    }
  }
}

Сначала это может показаться сложным, но давайте разберемся по пунктам:

  1. Определяем нашу матчер-функцию, которая принимает Locator, ожидаемый текст и объект options
  2. Получаем фактический текстовый контент элемента. 
  3. Выполняем проверку, учитывая регистр.
  4. Возвращаем объект с результатом и соответствующими сообщениями об ошибках. 

Части 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();
});

Мощь кастомных матчеров

К этому моменту вы, вероятно, уже начали понимать потенциал кастомных матчеров. Они позволяют нам:

  1. Инкапсулировать сложную логику ассертов
  2. Сделать тесты более читаемыми и понятными
  3. Сократить дублирование кода 
  4. Создать доменный язык для своих тестов 

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

Лучшие практики и советы

Прежде чем мы закончим, вот несколько советов: 

  1. Будьте проще: Начните с простых матчеров и усложняйте их по мере необходимости. 
  2. Будьте описательны: Используйте понятные, описательные имена для своих матчеров. toHaveNewNotification гораздо лучше, чем toHaveNot
  3. Сообщения об ошибках имеют значение: Потратьте время на написание понятных сообщений об ошибках. Ваши коллеги по команде скажут вам спасибо при отладке упавших тестов. 
  4. Используйте TypeScript: Безопасность типов и автодополнение в TS неоценимы при работе с кастомными матчерами. 
  5. Не переусердствуйте: Не нужно для всего использовать кастомные матчеры. Используйте их для стандартных, сложных или узкоспецифических ассертов.

Резюме

Кастомные матчеры матчеры Playwright стали неотъемлемой частью моего набора инструментов. Они позволяют мне писать чистые и понятные тесты и значительно сократили время, которое я трачу на обслуживание тестовых наборов. Рассмотренные здесь примеры — лишь верхушка айсберга. Настоящая мощь кастомных матчеров проявляется, когда вы создаете их с учетом особенностей вашего приложения и домена.

Medium


Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

1 КОММЕНТАРИЙ

Подписаться
Уведомить о
guest

1 Комментарий
Старые
Новые Популярные
Межтекстовые Отзывы
Посмотреть все комментарии
Анна
Анна
5 месяцев назад

Спасибо за статью! У меня вопрос — почему в случае pass = true в тернарном операторе для message задается сообщение об ошибке: Expected element not to have text content «${expectedText}» и Expected user not to have a new notification, but found: «${text}» для первого и второго примера соответственно. Ведь если pass = true, то ассерт должен проходить

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас