В одной из предыдущих статей мы говорили о пользовательских командах и о том, как использовать TypeScript для автозаполнения. Использование TypeScript, безусловно, целесообразно при работе над большим проектом с несколькими сотрудниками. В этой статье мы пойдем дальше и поговорим о том, как настроить пользовательское логирование, которое позволит оптимизировать работу с тест-раннером (test runner).
Если вас заинтересовала эта тема, рекомендуем посмотреть вебинар по паттернам и практикам, который провела команда Cypress DX. В нем много отличных советов, и они также рассказывают о пользовательском логировании.
Все примеры в статье описаны для приложения-клона Trello. Вы можете найти его на этой GitHub-странице и попрактиковаться самостоятельно.
Друзья, поддержите нас вступлением в наш телеграм канал QaRocks. Там много туториалов, задач по автоматизации и книг по QA.
Содержание
- Создание пользовательской ошибки
- Пользовательские сообщения
- Выделение элементов
- Добавление дополнительных логов
- Обработка ошибок
Создание пользовательской ошибки
Давайте создадим простую пользовательскую команду, которая будет взаимодействовать с пользовательским интерфейсом (UI) и создавать новую доску (board). Это будет выглядеть примерно так:
Cypress.Commands.add('addBoardUi', (name: string) => { cy .get('[data-cy="create-board"]') .click(); cy .get('[data-cy="new-board-input"]') .type(`${name}`) .type('{enter}'); });
Затем мы используем её в нашем тесте следующим образом:
it('Creates a new board', () => { cy .visit('/') cy .addBoardUi() // forgot the board name! });
Для использования нашей пользовательской команды нам необходимо указать название доски. В противном случае тест будет запущен, но выдаст ошибку:
Хотя это, безусловно, хорошо написанная ошибка, мы, возможно, захотим предоставить дополнительную информацию о том, что именно пошло не так в данном случае. Для этого мы можем проверить, был ли предоставлен аргумент. Если нет, мы укажем это прямо в нашем тест-раннере:
Cypress.Commands.add('addBoardUi', (name: string) => { if (!name) throw new Error('You need to provide a board name'); cy .get('[data-cy="create-board"]') .click(); cy .get('[data-cy="new-board-input"]') .type(`${name}`) .type('{enter}'); });
Теперь при появлении ошибки мы получаем несколько более информативное сообщение:
Таким образом, при возникновении ошибки мы сразу поймем, в чем проблема и как её исправить.
Пользовательские сообщения
Пока что наша команда представляет собой просто последовательность действий. Однако вы можете оказаться в ситуации, когда вам нужно создать библиотеку команд для использования вашими коллегами, и вы хотите, чтобы ваши пользовательские команды были видны в графическом интерфейсе (GUI).
Для добавления логов можно просто использовать команду cy.log()
, как и любую другую команду в тесте. Но можно сделать еще один шаг вперед, используя API Cypress.log()
. Давайте добавим несколько пользовательских логов в нашу пользовательскую команду:
Cypress.Commands.add('addBoardUi', (name: string) => { Cypress.log({ displayName: 'addBoardUi', message: name, name: 'Add new board' }); cy .get('[data-cy="create-board"]') .click(); cy .get('[data-cy="new-board-input"]') .type(`${name}`) .type('{enter}'); });
Теперь наша пользовательская команда будет отображаться в Cypress runner. Приятным дополнением к этому является параметр name
, который выводится рядом с именем нашей команды:
Чтобы вывести на панель дополнительную информацию, мы можем добавить функцию consoleProps
:
Cypress.log({ consoleProps() { return { 'board name': name } }, displayName: 'addBoardUi', message: name, name: 'Add new board' });
Но что делать, если у нас есть некоторая информация, недоступная в параметрах? Допустим, мы хотим вывести в консоль url нашей доски. Это может быть полезно для отладки после выполнения теста. Для этого нам нужно написать нашу функцию по-другому:
Cypress.Commands.add('addBoardUi', (name: string) => { let boardUrl; const log = Cypress.log({ autoEnd: false, consoleProps() { return { 'board name': name, 'board url': boardUrl } }, displayName: 'addBoardUi', message: name, name: 'Add new board' }); cy .get('[data-cy="create-board"]') .click(); cy .get('[data-cy="new-board-input"]') .type(name) .type('{enter}'); cy .url() .then((url) => { boardUrl = url log.end() }) });
Сначала мы определяем переменную boardUrl
. Она будет использоваться для присвоения нашего url позже, в 30-ой строке.
Другая вещь, которую мы делаем несколько иначе, заключается в том, что мы присваиваем Cypress.log()
переменной. Это позволяет нам непрерывно передавать данные в наш лог. Это означает, что, хотя наш boardUrl
сначала будет undefined
, впоследствии мы сможем заполнить его информацией, которая появится в тест-раннере.
И последнее: атрибут autoEnd
, который будет указывать Cypress не завершать логирование до тех пор, пока мы явно не скажем об этом с помощью функции .end()
в строке 31.
Выделение элементов
Рассмотрим другой пример. В приложении есть несколько атрибутов данных, и нам нужно создать пользовательскую команду для их выбора. Назовем ее take
. Это будет, по сути, сокращение для команды .get()
, чтобы иметь возможность писать .take('create-board')
вместо .get([data-cy='create-board'])
. Основная схема будет выглядеть следующим образом:
Cypress.Commands.add('take', (input: string) => { const log = Cypress.log({ consoleProps() { return { selector: input }; }, displayName: 'take', name: 'Get by [data-cy] attribute' }); cy .get(`[data-cy=${input}]`) });
Но можно заметить, что новая команда не выделяет наш элемент. Давайте исправим это, а также избавимся от команды .get()
, чтобы в тесте не было дублирования:
Cypress.Commands.add('take', (input: string) => { const log = Cypress.log({ autoEnd: false, consoleProps() { return { selector: input, }; }, displayName: 'take', name: 'Get by [data-cy] attribute' }); cy .get(`[data-cy=${input}]`, { log: false }) .then(($el) => { log.set({ $el }); log.snapshot() log.end(); }); });
Теперь в нашем Cypress-раннере есть только одна команда. Более того, с помощью log.set({ $el });
можно выделять элемент, который находит наша команда .get()
.
Аналогично предыдущему примеру, мы используем autoEnd
и функцию .end()
для завершения логирования. Чтобы наше выделение работало, нам необходимо сделать хотя бы один снэпшот в нашей команде с помощью функции .snapshot()
.
Добавление дополнительных логов
По итогу вышло так, что мы потеряли пару консольных логов, которые предоставляла оригинальная команда .get()
. Чтобы исправить это, мы снова создадим переменные-заместители и будем заполнять их информацией по мере выполнения наших действий:
Cypress.Commands.add('take', (input: string) => { let element: JQuery<HTMLElement> | HTMLElement[]; let count: number; const log = Cypress.log({ autoEnd: false, consoleProps() { return { selector: input, 'Yielded': element, 'Elements': count }; }, displayName: 'take', name: 'Get by [data-cy] attribute' }); cy .get(`[data-cy=${input}]`, { log: false }) .then(($el) => { element = Cypress.dom.getElements($el) count = $el.length; log.set({ $el }); log.snapshot().end(); }); });
Наша команда становится довольно аккуратной. Вывод в консоль выглядит точно так же, как и с оригинальной командой .get()
:
Здесь есть один небольшой баг, который может быть заметен не сразу. Поздравляем, если вы его нашли.
Работа с ошибками
Наш атрибут autoEnd
будет ждать, пока не будет вызвана функция.end()
. Но если команда .get()
не найдет элемент, то логирование не завершится. Тест все равно провалится, так что никакого вреда не будет. Просто команда take
застрянет в состоянии загрузки. Чтобы исправить это, можно просто подключиться к событию fail
, завершить логирование и выполнить throw err
:
Cypress.Commands.add('take', (input: string) => { let element: JQuery<HTMLElement> | HTMLElement[]; let count: number; const log = Cypress.log({ autoEnd: false, consoleProps() { return { selector: input, 'Yielded': element, 'Elements': count }; }, displayName: 'take', name: 'Get by [data-cy] attribute' }); cy .get(`[data-cy=${input}]`, { log: false }) .then(($el) => { element = Cypress.dom.getElements($el) count = $el.length; log.set({ $el }); log.snapshot().end(); }); cy .on('fail', (err) => { log.error(err) log.end() throw err }) });
Пользовательские команды можно использовать, например, для логирования информации из API-запросов, где делаются снимки состояния “до” и “после”. Команду, аналогичную нашей пользовательской команде .take()
, можно использовать как двойную команду, то есть получить родительский элемент, а затем выбрать дочерний элемент только в контексте этого родительского элемента. В целом, с помощью этой команды можно сделать очень много интересного.
Перевод статьи «Improve your custom command logs in Cypress».