Пользовательские команды
Cypress поставляется со своим собственным API для создания пользовательских команд и перезаписи существующих команд. Встроенные команды Cypress используют тот же самый API, который определен ниже.
Отличное место для определения или перезаписи команд — это ваш файл
cypress/support/commands.js, так как он загружается до оценки любых файлов тестов через оператор импорта в вашем файле поддержки (cypress/support/index.jsпо умолчанию).
Синтаксис
Cypress.Commands.add(name, callbackFn)
Cypress.Commands.add(name, options, callbackFn)
Cypress.Commands.overwrite(name, callbackFn)
Использование
Правильное использование
Cypress.Commands.add('login', (email, pw) => {})
Cypress.Commands.overwrite('visit', (orig, url, options) => {})
Аргументы
name (Строка)
Имя команды, которую вы добавляете или перезаписываете.
callbackFn (Функция)
Передайте функцию, которая получает аргументы, переданные команде.
options (Объект)
Передайте объект опций для определения неявного поведения пользовательской команды.
optionsподдерживается только для использования вCypress.Commands.add()и не поддерживается для использования вCypress.Commands.overwrite()
| Опция | Принимает | По умолчанию | Описание |
|---|---|---|---|
prevSubject |
Boolean, String или Array
|
false |
как обработать ранее сгенерированный объект. |
prevSubject принимает следующие значения:
-
false: игнорировать любые предыдущие объекты: (родительская команда) -
true: получает предыдущий объект: (дочерняя команда) -
optional: может начать цепочку или использовать существующую цепочку: (двойная команда)
В дополнение к управлению неявным поведением команды вы также можете добавить декларативные проверки объекта, такие как:
-
element: требует, чтобы предыдущий объект был элементом DOM -
document: требует, чтобы предыдущий объект был документом -
window: требует, чтобы предыдущий объект был окном
Примеры
Родительские команды
Родительские команды всегда начинают новую цепочку команд. Даже если вы привязали ее к предыдущей команде, родительские команды всегда будут начинать новую цепочку и игнорировать ранее сгенерированные объекты.
Примеры родительских команд:
Нажать ссылку, содержащую текст
Cypress.Commands.add('clickLink', (label) => {
cy.get('a').contains(label).click()
})
cy.clickLink('Buy Now')
Проверить токен
Cypress.Commands.add('checkToken', (token) => {
cy.window().its('localStorage.token').should('eq', token)
})
cy.checkToken('abc123')
Скачать файл
Изначально использовалась в cypress-downloadfile, эта команда вызывает другие команды Cypress.
Cypress.Commands.add('downloadFile', (url, directory, fileName) => {
return cy.getCookies().then((cookies) => {
return cy.task('downloadFile', {
url,
directory,
cookies,
fileName,
})
})
})
cy.downloadFile('https://path_to_file.pdf', 'mydownloads', 'demo.pdf')
Команды для работы с sessionStorage
Cypress.Commands.add('getSessionStorage', (key) => {
cy.window().then((window) => window.sessionStorage.getItem(key))
})
Cypress.Commands.add('setSessionStorage', (key, value) => {
cy.window().then((window) => {
window.sessionStorage.setItem(key, value)
})
})
cy.setSessionStorage('token', 'abc123')
cy.getSessionStorage('token').should('eq', 'abc123')
Команда входа с помощью графического интерфейса
Cypress.Commands.add('typeLogin', (user) => {
cy.get('input[name=email]').type(user.email)
cy.get('input[name=password]').type(user.password)
})
cy.typeLogin({ email: 'fake@email.com', password: 'Secret1' })
Команда входа с помощью запроса
Cypress.Commands.add('login', (userType, options = {}) => {
// this is an example of skipping your UI and logging in programmatically
// setup some basic types
// and user properties
const types = {
admin: {
name: 'Jane Lane',
admin: true,
},
user: {
name: 'Jim Bob',
admin: false,
},
}
// grab the user
const user = types[userType]
// create the user first in the DB
cy.request({
url: '/seed/users', // assuming you've exposed a seeds route
method: 'POST',
body: user,
})
.its('body')
.then((body) => {
// assuming the server sends back the user details
// including a randomly generated password
//
// we can now login as this newly created user
cy.request({
url: '/login',
method: 'POST',
body: {
email: body.email,
password: body.password,
},
})
})
})
// can start a chain off of cy
cy.login('admin')
// can be chained but will not receive the previous subject
cy.get('button').login('user')
Команда выхода с помощью графического интерфейса
Cypress.Commands.add('logout', () => {
cy.contains('Login').should('not.exist')
cy.get('.avatar').click()
cy.contains('Logout').click()
})
Команда выхода с помощью localStorage
Cypress.Commands.add('logout', () => {
cy.window().its('localStorage').invoke('removeItem', 'session')
cy.visit('/login')
})
cy.logout()
Создать пользователя
Cypress.Commands.add('createUser', (user) => {
cy.request({
method: 'POST',
url: 'https://www.example.com/tokens',
body: {
email: 'admin_username',
password: 'admin_password',
},
}).then((resp) => {
cy.request({
method: 'POST',
url: 'https://www.example.com/users',
headers: { Authorization: 'Bearer ' + resp.body.token },
body: user,
})
})
})
cy.createUser({
id: 123,
name: 'Jane Lane',
})
Журнал команд
Знаете ли вы, что вы можете управлять отображением пользовательских команд в журнале команд? Узнайте больше о Ведении журнала команд.
Дочерние команды
Дочерние команды всегда привязаны к родительской команде или другой дочерней команде.
Предыдущий объект автоматически передается в функцию обратного вызова.
Примеры дочерних команд:
Пользовательская команда console
// not a super useful custom command
// but demonstrates how subject is passed
// and how the arguments are shifted
Cypress.Commands.add(
'console',
{
prevSubject: true,
},
(subject, method) => {
// the previous subject is automatically received
// and the commands arguments are shifted
// allow us to change the console method used
method = method || 'log'
// log the subject to the console
console[method]('The subject is', subject)
// whatever we return becomes the new subject
//
// we don't want to change the subject so
// we return whatever was passed in
return subject
}
)
cy.get('button')
.console('info')
.then(($button) => {
// subject is still $button
})
Установив { prevSubject: true }, наша новая команда .console() потребует объекта.
Вызов ее вот так приведет к ошибке:
cy.console() // error about how you can't call console without a subject
Всякий раз, когда вы используете дочернюю команду, вам, вероятно, нужно использовать cy.wrap() для объекта. Это позволит вам сразу использовать дополнительные команды Cypress с этим объектом.
Двойные команды
Двойная команда может либо начать цепочку команд, либо быть привязана к существующей. По сути, это гибрид родительской и дочерней команды. Вы, скорее всего, редко будете использовать это, и только несколько наших внутренних команд используют этот тип.
Тем не менее, это полезно, если ваша команда может работать несколькими способами — либо с существующим объектом, либо без него.
Примеры двойных команд:
Пользовательская двойная команда
Cypress.Commands.add('dismiss', {
prevSubject: 'optional'
}, (subject, arg1, arg2) => {
// subject may be defined or undefined
// so you likely want to branch the logic
// based off of that
if (subject) {
// wrap the existing subject
// and do something with it
cy.wrap(subject)
...
} else {
...
}
})
cy.dismiss() // no subject
cy.get('#dialog').dismiss() // with subject
Перезапись существующих команд
Вы также можете изменить поведение существующих команд Cypress. Это полезно для установки по умолчанию, чтобы избежать создания другой команды, которая в конечном итоге использует исходную.
Перезапись команды visit
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
const domain = Cypress.env('BASE_DOMAIN')
if (domain === '...') {
url = '...'
}
if (options.something === 'else') {
url = '...'
}
// originalFn is the existing `visit` command that you need to call
// and it will receive whatever you pass in here.
//
// make sure to add a return here!
return originalFn(url, options)
})
Мы видим, что многие из наших пользователей создают собственную команду
visitApp. Мы часто видим, что всё, что вы делаете, это меняете базовые URL для средdevelopmentиproduction.Это обычно не нужно, потому что Cypress уже настроен для переключения
baseUrl, который используют как cy.visit(), так и cy.request(). Установите свойство конфигурацииbaseUrlв вашем файле конфигурации конфигурации (cypress.jsonпо умолчанию) и переопределите его переменной средыCYPRESS_BASE_URL.Для более сложных случаев, вы можете свободно перезаписывать существующие команды.
Перезапись команды type
Если вы вводите данные в поле пароля, ввод пароля автоматически маскируется в вашем приложении. Но .type() автоматически записывает введённый текст в журнал команд Test Runner.
cy.get('#username').type('username@email.com')
cy.get('#password').type('superSecret123')
Вы можете замаскировать некоторые значения, передаваемые команде .type(), чтобы конфиденциальные данные не отображались на скриншотах или видео ваших тестов. Этот пример перезаписывает команду .type(), чтобы позволить вам маскировать конфиденциальные данные в журнале команд Test Runner.
Cypress.Commands.overwrite('type', (originalFn, element, text, options) => {
if (options && options.sensitive) {
// turn off original log
options.log = false
// create our own log with masked message
Cypress.log({
$el: element,
name: 'type',
message: '*'.repeat(text.length),
})
}
return originalFn(element, text, options)
})
cy.get('#username').type('username@email.com')
cy.get('#password').type('superSecret123', { sensitive: true })
Теперь наш конфиденциальный пароль не отображается в журнале команд Test Runner, когда sensitive: true передаётся как опция команде .type().
Блог "Сохраняем пароли в секрете"
Посмотрите этот блог, чтобы узнать другой способ сохранения паролей в секрете в ваших тестах.
Перезапись команды screenshot
В этом примере перезаписывается cy.screenshot() для того, чтобы всегда дожидаться видимости определенного элемента.
Cypress.Commands.overwrite(
'screenshot',
(originalFn, subject, name, options) => {
// call another command, no need to return as it is managed
cy.get('.app')
.should('be.visible')
// overwrite the default timeout, because screenshot does that internally
// otherwise the `then` is limited to the default command timeout
.then({ timeout: Cypress.config('responseTimeout') }, () => {
// return the original function so that cypress waits for it
return originalFn(subject, name, options)
})
}
)
Перезапись команды contains
В этом примере перезаписывается .contains() для того, чтобы опция matchCase всегда была установлена в значение false.
Cypress.Commands.overwrite(
'contains',
(originalFn, subject, filter, text, options = {}) => {
// determine if a filter argument was passed
if (typeof text === 'object') {
options = text
text = filter
filter = undefined
}
options.matchCase = false
return originalFn(subject, filter, text, options)
}
)
Проверки
Как указано в Аргументах выше, вы также можете установить prevSubject в одно из следующих значений:
elementdocumentwindow
В этом случае Cypress автоматически проверит ваш объект, чтобы убедиться, что он соответствует одному из этих типов.
Добавление проверок необязательно. Передача
{ prevSubject: true }потребует объекта, но не проверит его тип.
Требование элемента
Требуется, чтобы объект был типа: element.
// this is how .click() is implemented
Cypress.Commands.add(
'click',
{
prevSubject: 'element',
},
(subject, options) => {
// receives the previous subject and it's
// guaranteed to be an element
}
)
Допустимое использование
cy.get('button').click() // has subject, and is `element`
Недопустимое использование
cy.click() // no subject, will error
cy.wrap([]).click() // has subject, but not `element`, will error
Разрешение нескольких типов
.trigger()
Требуется, чтобы тема была одним из следующих типов: element, document или window
// this is how .trigger() is implemented
Cypress.Commands.add(
'trigger',
{
prevSubject: ['element', 'document', 'window'],
},
(subject, eventName, options) => {
// receives the previous subject and it's
// guaranteed to be an element, document, or window
}
)
Допустимое использование
cy.get('button').trigger() // has subject, and is `element`
cy.document().trigger() // has subject, and is `document`
cy.window().trigger() // has subject, and is `window`
Недопустимое использование
cy.trigger() // no subject, will error
cy.wrap(true).trigger() // has subject, but not `element`, will error
Валидация всегда работает как «или», а не как «и».
Необязательные с типами
Вы также можете смешивать необязательные команды с валидациями.
// this is how .contains() is implemented
Cypress.Commands.add(
'contains',
{
prevSubject: ['optional', 'window', 'document', 'element'],
},
(subject, options) => {
// subject could be undefined
// since it's optional.
//
// if it's present
// then it's window, document, or element.
// - when window or document we'll query the entire DOM.
// - when element we'll query only inside of its children.
if (subject) {
// ...
} else {
// ...
}
}
)
Допустимое использование
cy.contains() // no subject, but valid because it's optional
cy.get('#main').contains() // has subject, and is `element`
cy.window().contains() // has subject, and is `window`
cy.document().contains() // has subject, and is `document`
cy.visit().contains() // has subject, and since visit yields `window` it's ok
Недопустимое использование
cy.wrap(null).contains() // has subject, but not `element`, will error
Примечания
Ведение журнала команд
При создании собственной пользовательской команды вы можете контролировать, как она отображается и ведет себя в журнале команд.
Воспользуйтесь API Cypress.log(). При выполнении большого количества внутренних команд Cypress, рассмотрите возможность передачи { log: false } этим командам и программно управлять своей пользовательской командой. Это очистит журнал команд и будет намного более наглядным и понятным.
Рекомендации по практическому применению
1. Не делайте все пользовательскими командами
Пользовательские команды хорошо работают, когда вам нужно описать поведение, которое желательно во всех ваших тестах. Примеры включают cy.setup() или cy.login() или расширение поведения вашего приложения, как cy.get('.dropdown').dropdown('Apples'). Они специфичны для вашего приложения и могут использоваться везде.
Однако этот подход может быть использован и злоупотреблен. Не будем забывать – написание тестов Cypress – это JavaScript, и часто более эффективно написать функцию для повторяемого поведения, специфичного только для одного файла спецификаций.
Если вы работаете с файлом search_spec.js и хотите объединить несколько повторяемых действий, вы должны сначала спросить себя:
Можно ли это написать как функцию?
Ответ обычно да. Вот пример:
// There's no reason to create something like a cy.search() custom
// command because this behavior is only applicable to a single spec file
//
// Use a regular ol' javascript function folks!
const search = (term, options = {}) => {
// example massaging to defaults
_.defaults(options, {
headers: {},
})
const { fixture, headers } = options
// return cy chain here so we can
// chain off this function below
return cy
.log(`Searching for: ${term} `)
.route({
url: '/search/**',
response: `fixture:${fixture}`,
headers: headers,
})
.as('getSearchResults')
.get('#search')
.type(term)
.wait('@getSearchResults')
}
it('displays a list of search results', () => {
cy.visit('/page')
.then(() => {
search('cypress.io', {
fixture: 'list',
}).then((reqRes) => {
// do something with the '@getSearchResults'
// request such as make assertions on the
// request body or url params
// {
// url: 'http://app.com/search?cypress.io'
// method: 'GET',
// duration: 123,
// request: {...},
// response: {...},
// }
})
})
.get('#results li')
.should('have.length', 5)
.get('#pagination')
.should('not.exist')
})
it('displays no search results', () => {
cy.visit('/page')
.then(() => {
search('cypress.io', {
fixture: 'zero',
})
})
.get('#results')
.should('contain', 'No results found')
})
it('paginates many search results', () => {
cy.visit('/page')
.then(() => {
search('cypress.io', {
fixture: 'list',
headers: {
// trick our app into thinking
// there's a bunch of pages
'x-pagination-total': 3,
},
})
})
.get('#pagination')
.should(($pagination) => {
// should offer to goto next page
expect($pagination).to.contain('Next')
// should have provided 3 page links
expect($pagination.find('li.page')).to.have.length(3)
})
})
2. Не усложняйте вещи
Каждая созданная вами пользовательская команда, как правило, является абстракцией целого ряда внутренних команд. Это означает, что вы и ваши коллеги потратите больше умственных усилий, чтобы понять, что делает ваша пользовательская команда.
Нет причин добавлять этот уровень сложности, когда вы просто оборачиваете несколько команд.
Не делайте таких вещей:
-
cy.clickButton(selector) -
.shouldBeVisible()
Эта первая пользовательская команда оборачивает cy.get(selector).click(). Такой подход приведет к созданию десятков или даже сотен пользовательских команд для охвата всех возможных комбинаций взаимодействий с элементами. Это совершенно не нужно.
Пользовательская команда .shouldBeVisible() не стоит усилий и абстракции, когда вы уже можете использовать: .should('be.visible')
Тестирование в Cypress сосредоточено на читаемости и простоте. Вам не нужно много программировать, чтобы добиться многого. Вам также не нужно беспокоиться о том, чтобы ваш код был как можно более сухим. Код тестов служит другой цели, чем код приложения. Понятность и отладка должны быть приоритетными перед всем остальным.
Старайтесь не усложнять вещи и не создавать слишком много абстракций. В случае сомнений используйте обычную функцию для отдельных файлов спецификаций.
3. Не делайте слишком много в одной команде
Делайте ваши пользовательские команды композируемыми и максимально необязательными. Наполнение их слишком многим делает их негибкими и требует всё больше и больше опций для управления их поведением.
Старайтесь добавлять как можно меньше или вообще не добавлять утверждений в вашу пользовательскую команду. Они, как правило, превращают команду в более жёсткую структуру. Иногда это неизбежно, но лучшая практика – позволить вызывающему коду выбирать, когда и как использовать утверждения.
4. По возможности обходите интерфейс пользователя
Пользовательские команды – отличный способ абстрагировать начальную настройку (специфичную для вашего приложения). При выполнении таких задач старайтесь как можно больше обойти интерфейс пользователя. Используйте cy.request(), чтобы войти в систему, установить куки или localStorage напрямую, подменять и имитировать функции ваших приложений и/или программно вызывать события.
Использование пользовательских команд для многократного повторения одних и тех же действий в интерфейсе пользователя медленное и не нужно. Старайтесь использовать как можно больше сокращений.
5. Напишите определения TypeScript
Вы можете описать сигнатуру метода для вашей пользовательской команды, что позволит IntelliSense отобразить полезную документацию. Посмотрите репозиторий cypress-example-todomvc для рабочего примера.
История
| Версия | Изменения |
|---|---|
| 0.20.0 |
Cypress.Commands API добавлен |
См. также
- Узнайте, как добавить поддержку TypeScript для пользовательских команд
- Записи в блоге Написание пользовательской команды Cypress и Как опубликовать пользовательскую команду Cypress в NPM.
- Плагины с использованием пользовательских команд
-
cypress-xpathдобавляет командуcy.xpath()и демонстрирует лучшие практики написания пользовательских команд: повторные попытки, ведение журнала и определение TypeScript. - Cypress.log()
- Рецепт: Вход в систему
© 2017 Cypress.io
Licensed under the MIT License.
https://docs.cypress.io/api/cypress-api/custom-commands