Разделы портала

Онлайн-тренинги

.
Начало работы с TypeScript в Cypress
29.03.2022 00:00

Автор: Филип Рик (Filip Hric)
Оригинал статьи
Перевод: Ольга Алифанова

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

Использование TypeScript в Cypress

Начнем с простого примера. Как всегда, я буду использовать свой клон Trello, который можно скачать с GitHub. В коде ниже очень простой набор шагов. Я открываю приложение и создаю новую доску:

/// <reference types="cypress" />
 
it('creating a board', () => {
 
     cy
         .visit('/')
 
     cy
        .get('[data-cy="create-board"]')
         .click();
 
    cy
         .get('[data-cy=new-board-input]')
         .type('new board{enter}');
 
})

Так как создание новой доски – распространенное действие в моих тестах, я хочу выделить строки 8-14 в отдельную функцию. Эта функция способна принимать аргумент, и мы сможем задавать имя доски при использовании функции.

/// <reference types="cypress" />
 
const addBoard = (input) => {
     cy
          .get('[data-cy="create-board"]')
          .click();
 
     cy
          .get('[data-cy=new-board-input]')
          .type(`${input}{enter}`);
}
 
it('creating a board', () => {
 
      cy
          .visit('/')
 
      addBoard()
 
})

Возможно, смотря на код, вы заметили, что я оставил функцию .addBoard() пустой. Так как я не вводил текст, тест упадет. Мое приложение не позволяет создавать доски с пустым именем. Давайте просто сменим расширение с .js на .ts и посмотрим, что произойдет в текстовом редакторе.

 

Как можно видеть, теперь функция показывает ошибку в редакторе. Я сделал скриншот, наведя мышь на подчеркнутую функцию. VS Code дает объяснение – функция ожидает как минимум одного аргумента, но не был задан ни один.

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

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

const addBoard = (input: string) => {
     cy
          .get('[data-cy="create-board"]')
          .click();
 
     cy
           .get('[data-cy=new-board-input]')
           .type(`${input}{enter}`);
}
 
it('creating a board', () => {
 
      cy
           .visit('/')
 
      addBoard('new board')
 
})

Теперь, определив тип нашего аргумента, мы будем получать ошибку при любой попытке передать что-либо другое, вроде Boolean или числа. Для этой функции это немного, но представьте функцию, которая вызывает конечную точку API с разными данными вроде массивов, объектов, строк и чисел. Для этого тоже можно задать типы.

Поиграем с типами

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

const addBoard = (input: 'new board' | 'my board') => {
     cy
            .get('[data-cy="create-board"]')
            .click();
 
      cy
           .get('[data-cy=new-board-input]')
            .type(`${input}{enter}`);
}

Добавив это, я могу уточнять, какие типы ввода принимает моя функция. Что еще приятнее, VS Code автодополняет ввод:

 

Могу себе представить классные варианты использования. Не уверен, почему, но мне немедленно пришли в голову селекторы атрибута data-cy. У меня есть команда getDataCy – простая обертка вокруг команды Cypress .get(). Она выбирает элементы на основании атрибута data-cy, и мне не приходится каждый раз печатать текст [data-cy=selector]. Что, если я смогу автоматически дополнять свои селекторы? Если я разберусь, подойдет ли такой способ, я дам вам знать в своем блоге.

Предположим теперь, что я хочу добавить несколько досок при помощи этой функции. Внутри функции будет цикл с массивом строк, добавляющий все нужные доски. Вместо одной строки можно будет ввести массив строк.

const addTodo = (titles: string[]) => {
 
      titles.forEach(title => {
 
          cy
                .get('[data-cy="create-board"]')
                 .click();
 
           cy
                 .get('[data-cy=new-board-input]')
                 .type(`${title}{enter}`);
 
      })
 
}

Если я добавлю в функцию addTodo что-то, кроме массива, я немедленно получу ошибку. Еще до запуска моего теста я получаю обратную связь о невалидном использовании функции. Это чудесно помогает при опечатках, неверных типах или забытых запятых.

Если вы уже используете TypeScript в вашем проекте, то с шансами большая часть вашего проекта уже содержит типы. Вы сможете повторно использовать типы вашего приложения внутри своих тестов. По моим представлением строгая проверка типов приведет к тому, что изменение в приложении немедленно спровоцирует ошибку в тестах.

Использование JSDoc

У TypeScript есть еще одна классная функция, которую можно использовать даже с чистым JavaScript. При помощи JSDoc можно добавлять к функциям документацию. В VS Code это можно сделать, введя /** */. Это создаст специальный комментарий, всплывающий при наведении на функцию.

 

Тут множество различных флагов - @param для объяснения параметров, @example для примера использования команды, @deprecated для пометки команды как потерявшей актуальность. JSDoc с Cypress-командами тоже используется и даже может давать ссылки на документацию. Представьте, как это можно применить к кастомным командам или вашим Page Object, немедленно давая им контекст.

Настройка TypeScript в проекте

Вернемся чуть назад. Пока что мы просто меняли расширение файла с .js на .ts. Однако прежде, чем мы сможем запустить эти тесты в Cypress, нужно сделать еще две вещи. В документации Cypress есть отличная статья на эту тему. Резюмируя, вам нужно установить TypeScript через npm или yarn, а затем создать tsconfig.json.

{
     "compilerOptions": {
          "target": "es5",
           "lib": ["es5", "dom"],
           "types": ["cypress"]
     },
     "include": [
           "**/*.ts"
      ]
}

Просто копирование и вставка этого в проект уже даст возможность начать работу, однако этот файл можно разнообразно настраивать. Заметьте, что в строке 5 мы определяем типы. Это в точности то же самое, что и добавление /// <reference types="cypress" /> в начале вашего файла для работы автодополнения. При помощи TypeScript это можно не делать – это будет доступно глобально.

Создание кастомной команды

Теперь вынесем функцию .addBoard() из нашего файла и создадим кастомную команду Cypress. Затем ее можно будет использовать в проекте.

support/commands/addBoard.ts
Cypress.Commands.add('addBoard', (input: string) => {
      cy
            .get('[data-cy="create-board"]')
             .click();
 
      cy
            .get('[data-cy=new-board-input]')
            .type(`${input}{enter}`);
})

Все это здорово, но просто добавление этой команды не включит автодополнение при попытке использования в тестах. Чтобы это сделать, нужно расширить объект cy.

Это можно сделать двумя способами. Один из них описан в вышеупомянутой документации. Вам нужно создать файл определений с расширением .d.ts и объявить команду там:

support/commands.d.ts
declare namespace Cypress {
     interface Chainable {
          addBoard(value: string): void
      }
}

Тут есть о чем поговорить, но я не хочу чересчур вдаваться в детали. Простыми словами, мы добавляем нашу Cypress-команду addBoard в интерфейс Cypress. Void в конце функции означает, что наша функция не будет ничего передавать. При желании она может возвращать выбранный элемент или тело ответа на вызов API, в зависимости от того, что мы собираемся делать с этой функцией.

Второй способ добавления кастомной команды – добавление определения прямо внутри кастомной команды. Файл будет выглядеть так:

support/commands/addBoard.ts
declare global {
     namespace Cypress {
            interface Chainable {
                  addBoard: typeof addBoard;
           }
     }
}
 
export const addBoard = (input: string) => {
      cy
            .get('[data-cy="create-board"]')
            .click();
 
      cy
          .get('[data-cy=new-board-input]')
           .type(`${input}{enter}`);
}

Так как тут я не пользуюсь Cypress.Commands.add api и вместо этого экспортирую функцию, мне нужно добавить это в мой файл my support/index.ts или прямо в тест. Обычно это выглядит как-то так:

support/index.ts
import { addBoard } from './commands/addBoard';
 
Cypress.Commands.add('addBoard', addBoard);

Я нашел этот подход на GitHub, и он мне очень нравится. Он позволяет хранить все в одном и том же файле, что лучше мне подходит. Возможно, это вопрос личных предпочтений. Может также случиться, что я не вижу очевидных проблем (я все еще учусь) – в этом случае сообщите мне об этом через Twitter, LinkedIn или Discord. Буду рад узнать об этом.

Обсудить в форуме