Как применять тестовые шаги Playwright с декораторами TypeScript

Перевод статьи «How to apply Playwright test steps with TypeScript decorators».

Для написания сквозных тестов в Playwright можно использовать как JavaScript, так и TypeScript. Но что же выбрать?

Если вы только начинаете писать автоматизированные тесты, то наверняка выберете JavaScript, чтобы избежать сложностей с типами. Так можно начать писать тесты без лишних промедлений.

Однако сегодня JavaScript используется реже, и на то есть две причины.

Во-первых, если вас пугают работа с типами, дженерики TypeScript и бесконечные красные подчёркивания в редакторе, когда вы просто хотите протестировать функции продукта, то есть хорошая новость: Playwright не проверяет типы в вашем коде. Он понимает TypeScript и компилирует его в JavaScript, а вы можете как использовать привычный JavaScript, так и применять any.

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

К тому же, TypeScript имеет полезную функцию, которая упрощает работу с тестовыми шагами. Она позволяет структурировать сложные сквозные тесты и писать меньше кода. Звучит интересно, не так ли?

Давайте посмотрим, как можно заменить повторяющиеся вызовы test.step одним декоратором @step.

Содержание

Первая проблема: отчеты Playwright могут быть трудны для восприятия

По умолчанию в UI-режиме или в отчётах все ваши действия и проверки в тестах отображаются длинным списком команд. Если в тесте всего двадцать шагов, это не проблема. Но при тестировании сложного интерфейса количество тестовых команд может достигать сотни, и тогда отчёт становится неудобным для анализа.

Длинный список команд

Чтобы решить эту проблему, можно группировать действия в тестовых шагах Playwright.

Вот пример из документации Playwright.

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

test('test', async ({ page }) => {
  await test.step('Log in', async () => {
    // ...
  });

  await test.step('Outer step', async () => {
    // ...
    // You can nest steps inside each other.
    await test.step('Inner step', async () => {
    // ...
    });
  });
});

Вы определяете шаг, даёте ему название и оборачиваете ваш код Playwright в асинхронную функцию обратного вызова. Такой подход делает сложные тест-кейсы более читаемыми.

test('Customer is able to create account', async ({
  page
}) => {
  await test.step('Customer can create account', async () => {
    // create account instructions
  });

  await test.step('Customer can log out', async () => {
    // log out instructions
  });

  await test.step('Customer can log in', async () => {
    // log in instruction
  });
});

Посмотрите на этот красивый и хорошо структурированный HTML-отчёт о тестировании!

Вид отчета до и после (без тестовых шагов и с ними).

Теперь может возникнуть вопрос: «А работает ли это, если используется POM (Page Object Model)?».

Да, работает, но оборачивать каждый открытый метод класса в тестовый шаг не очень удобно. И это приводит нас к следующей проблеме.

Вторая проблема: оборачивать все методы в тестовые шаги утомительно

Рассмотрим пример PlaywrightPage POM для тестирования поиска по официальной документации Playwright.

export class PlaywrightPage {
  readonly page: Page
  readonly searchBtn: Locator
  readonly searchInput: Locator

  constructor(page: Page) {
    this.name = name
    this.page = page
    this.searchBtn = page.getByLabel("Search")
    this.searchInput = page.getByPlaceholder("Search docs")
  }

  async goto() {
    await this.page.goto("https://playwright.dev")
  }

  async search() {
    // add a test step 👇
    await test.step("Search", async () => {
      await this.searchBtn.click()
      await this.searchInput.fill("getting started")
      await this.page.getByRole("link", { name: "Writing tests" }).click()
      await this.page.getByRole("heading", { name: "Writing tests" }).click()
    })
  }
}

Обернуть один метод в тестовый шаг (в данном случае search) несложно. Но если оборачивать каждый открытый метод POM, это быстро надоест.

export class YourPageObject {
  async methodOne() {
    await test.step("methodOne", async () => { /* ... */ })
  }

  async methodTwo() {
    await test.step("methodTwo", async () => { /* ... */ })
  }

  async methodThree() {
    await test.step("methodThree", async () => { /* ... */ })
  }

  // There has to be a better way. 👆
}

Есть ли лучший способ быстрого добавления тестовых шагов?

Решение: автоматически оборачивать методы POM с помощью декораторов TypeScript

Playwright не предоставляет готового решения, которое избавило бы нас от повторяющихся действий по оборачиванию методов, но можно использовать возможности TypeScript. Одним из преимуществ TypeScript является то, что это компилятор, который преобразует .ts-файлы в JavaScript.

Это означает, что вы можете использовать современные функции JavaScript в TypeScript и преобразовывать их в JavaScript-код, поддерживаемый более старыми браузерами или средами для выполнения кода.

Одна из таких современных функций JavaScript — декораторы. Они ни в одном браузере не поддерживаются, но работают, если вы используете TypeScript.

Декораторы JavaScript — предложение, над которым работают уже давно

Предложение декораторов в JavaScript было представлено восемь лет назад и дошло до третьей стадии в процессе стандартизации ECMAScript. Предложения на третьей стадии считаются «готовыми к внедрению».

@defineElement("my-class")
class C extends HTMLElement {
  @reactive accessor clicked = false;
}

К сожалению, ни один браузер ещё не внедрил эту новую функцию языка. Однако это не мешает команде TypeScript ее использовать.

Но что это такое? Если взглянуть на предложение, то можно понять, что:

Декораторы – это функции, которые вызываются при определении классов, их элементов или других синтаксических конструкций JavaScript.

Это формулировка может показаться немного запутанной. Суть декораторов можно объяснить проще.

Декораторы позволяют изменять, заменять или оборачивать методы классов (например, методы в POM-классах) с помощью других функций с простым и удобным для разработчиков синтаксисом.

Оборачивание методов класса — это то, что нужно, чтобы избавиться от всех инструкций test.step. Давайте разберёмся, как определить декоратор!

Как заменить инструкции test.step декораторами TypeScript? 

Вернемся к классу POM:

class PlaywrightPage {
  constructor(page: Page) { /* ... */ }

  async search() {
    // we want to remove the `test.step`...
    await test.step("Search", async () => {
      // and "somehow" wrap these Playwright instructions...
      await this.searchBtn.click()
      await this.searchInput.fill("getting started")
      await this.page.getByRole("link", { name: "Writing tests" }).click()
      await this.page.getByRole("heading", { name: "Writing tests" }).click()
    })
  }
}

В методе search нужно убрать test.step из тела функции и как-то обернуть команды Playwright в тестовый шаг. Для этого идеально подойдут декораторы.

Сначала добавим новый декоратор.

export class PlaywrightPage {
  constructor(page: Page) { /* ... */ }

  // Use `@` to define a decorator
  @step()
  async search() {
    await this.searchBtn.click()
    await this.searchInput.fill("getting started")
    await this.page.getByRole("link", { name: "Writing tests" }).click()
    await this.page.getByRole("heading", { name: "Writing tests" }).click()
  }
}

Чтобы декорировать метод класса, нужно вставить строку @step перед его определением. Но TypeScript сообщит об ошибке, потому что декоратор ещё не определён.

В редакторе кода выводится ошибка: "Cannot find name step".

Декораторы — это обычные функции JavaScript. Давайте определим новую функцию step.

// 1. Define the `step` decorator
function step(target: Function, context: ClassMethodDecoratorContext) {
  // 2. Return a method that will replace the original class method
  return function replacementMethod(...args: any) {
    // 3. Call the original method with the same arguments
    return await target.call(this, ...args)
  }
}

Разберёмся, как работает эта функция.

Когда TypeScript обнаруживает синтаксис @step, он вызывает функцию step. Эта функция может быть доступна в текущей области видимости или импортирована из другого места в коде.

Когда функция декоратора step будет найдена, она будет вызвана со ссылкой на наш исходный метод (target) и должна будет вернуть другую функцию (replacementMethod). Эта возвращенная функция заменит декорированный метод. С помощью низкоуровневого JavaScript можно затем вызвать исходный метод (target) с переданными аргументами (args).

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

function step(target: Function, context: ClassMethodDecoratorContext) {
  return function replacementMethod(...args: any) {
    // 2. Use the surrounding context to define the step name
    const name = this.constructor.name + "." + (context.name as string)
    // 1. Wrap the target method with `test.step`
    return test.step(name, async () => {
      return await target.call(this, ...args)
    })
  }
}

Сперва оборачиваем target.call в test.step. Затем нужно придумать способ, как задать название шага. Будет идеально, если оно будет связано с оригинальным методом.

Функция-декоратор будет вызываться не только со ссылкой на функцию, но и с контекстом. Замещённые методы также будут выполняться в том же контексте, что и оригинальный метод. Это позволит нам объединить название класса POM (this.constructor.name) и название метода(context.name),  чтобы определить соотносимое название шага.

Когда мы повторно запустим тест, декоратор @step автоматически обернёт методы POM в шаг Playwright с отображением названия класса POM и метода.

В Playwright видно, что метод класса красиво обернут.

Теперь вы можете заменить все эти вызовы test.step на декораторы @step!

export class YourPageObject {
  @step
  async methodOne() { /* ... */ }


  @step
  async methodTwo() { /* ... */ }
  
  @step
  async methodThree() { /* ... */ }
}

А что, если вы захотите дать своим тестовым шагам понятные названия?

Как передать пользовательские названия шагов декоратору

Чтобы передать декоратору пользовательское название шага, нужно изменить подход к декорированию методов класса. Вместо простого использования @step, декораторы можно вызывать с помощью @step().

Это позволяет передавать аргументы, например название шага.

export class YourPage {
  @step("A great method")
  async methodOne() { /* ... */ }
}

Но чтобы методы декоратора работали, нужно добавить ещё один уровень функции.

// 1. Make `@step` executable to enable function arguments
function step(stepName?: string) {
  // 2. Return the original decorator
  return function decorator(
    target: Function,
    context: ClassMethodDecoratorContext
  ) {
    return function replacementMethod(...args: any) {
      // 3. Use `stepName` when it's defined or
      // fall back to class name / method name
      const name = stepName || `${this.constructor.name + "." + (context.name as string)}`
      return test.step(name, async () => {
        return await target.call(this, ...args)
      })
    }
  }
}

Переименуйте свой оригинальный декоратор и оберните его в другую функцию, которая будет его возвращать. Затем используйте область видимости функции и повторно используйте аргумент названия шага (stepName) для определения нового названия в вашем заменяющем методе. Если stepName не будет определён, код откатится к комбинации имен класса и метода.

Теперь вы можете задавать названия шагов одной строкой!

К сожалению, у этого подхода есть один недостаток. Как только вы сделаете функцию-декоратор step вызываемой, вы должны будете вызывать её везде. То есть вместо @step нужно будет использовать @step().

export class PlaywrightPage {
  constructor(page: Page) { /* ... */ }

  @step('Perform a simple search and check heading') // "Performan a simple ..."
  async search() { /* ... */ }

  @step() // "PlaywrightPage.login"
  async login() { /* ... */ }
}

Но добавление круглых скобок – это вполне разумная плата за возможность задавать собственные названия шагов.

Заключение

Как думаете, стоит ли внедрение декораторов для тестовых шагов в Playwright добавляемой сложности? Ответ однозначный: «Конечно, да!»

Хотя код для декораторов может показаться сложным, на практике с ним работают не часто. Это случай из серии «настроил один раз и забыл». Зато вы сможете быстро создавать тестовые шаги одной строкой.

А если хочется увидеть полную реализацию этого декоратора, то пример можно найти на GitHub.

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *