自定义命令
Cypress提供了创建自定义命令和覆盖现有命令的API。内置的Cypress命令使用的正是下文定义的相同API。
有两种API可用于添加自定义命令:
Cypress.Commands.add()
- 用于添加自定义命令以便在编写测试时使用Cypress.Commands.overwrite()
- 用于覆盖现有的内置Cypress命令或保留的内部函数。**注意:**这会同时覆盖Cypress的行为,可能影响Cypress的运行方式。
我们建议在cypress/support/commands.js
文件中定义查询,因为该文件通过supportFile中的import语句在任何测试文件执行前加载。
语法
Cypress.Commands.add(name, callbackFn)
Cypress.Commands.add(name, options, callbackFn)
Cypress.Commands.addAll(callbackObj)
Cypress.Commands.addAll(options, callbackObj)
Cypress.Commands.overwrite(name, callbackFn)
用法
正确用法
Cypress.Commands.add('login', (email, pw) => {})
Cypress.Commands.addAll({
login(email, pw) {},
visit(orig, url, options) {},
})
Cypress.Commands.overwrite('visit', (orig, url, options) => {})
参数
name (String)
要添加或覆盖的命令名称。
callbackFn (Function)
传入一个接收命令参数的函数。
callbackObj (Object)
包含callbackFn
作为属性的对象。
options (Object)
传入一个选项对象以定义自定义命令的隐式行为。
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('立即购买')
检查令牌
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')
通过UI登录的命令
Cypress.Commands.add('loginViaUi', (user) => {
cy.session(
user,
() => {
cy.visit('/login')
cy.get('input[name=email]').type(user.email)
cy.get('input[name=password]').type(user.password)
cy.get('button#login').click()
cy.get('h1').contains(`欢迎回来 ${user.name}!`)
},
{
validate: () => {
cy.getCookie('auth_key').should('exist')
},
}
)
})
cy.loginViaUi({ email: 'fake@email.com', password: '$ecret1', name: 'johndoe' })
通过API登录的命令
Cypress.Commands.add('loginViaApi', (userType, options = {}) => {
// 这是一个跳过UI直接通过编程方式登录的示例
// 设置一些基本类型
// 和用户属性
const types = {
admin: {
name: '简·莱恩',
admin: true,
},
user: {
name: '吉姆·鲍勃',
admin: false,
},
}
// 获取用 户
const user = types[userType]
// 首先在数据库中创建用户
cy.request({
url: '/seed/users', // 假设你暴露了一个种子路由
method: 'POST',
body: user,
})
.its('body')
.then((body) => {
// 假设服务器返回用户详情
// 包括随机生成的密码
//
// 我们现在可以以这个新创建的用户登录
cy.request({
url: '/login',
method: 'POST',
body: {
email: body.email,
password: body.password,
},
})
})
})
// 可以从cy开始一个链
cy.loginViaApi('admin')
// 可以链式调用但不会接收先前主题
cy.get('button').loginViaApi('user')
通过UI注销的命令
Cypress.Commands.add('logout', () => {
cy.contains('登录').should('not.exist')
cy.get('.avatar').click()
cy.contains('注销').click()
cy.get('h1').contains('登录')
cy.getCookie('auth_key').should('not.exist')
})
使用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: '简·莱恩',
})
你知道你可以控制自定义命令在命令日志中的显示方式吗?阅读更多关于命令日志的内容。
子命令
子命令总是从父命令或另一个子命令链式调用。
先前主题会自动传递给回调函数。
子命令的示例:
自定义console
命令
// 不是一个非常有用的自定义命令
// 但演示了如何传递主题
// 以及参数如何移位
Cypress.Commands.add(
'console',
{
prevSubject: true,
},
(subject, method) => {
// 自动接收先前主题
// 命令参数移位
// 允许我们更改使用的控制台方法
method = method || 'log'
// 将主题记录到控制台
console[method]('主题是', subject)
// 我们返回的任何内容都会成为新主题
//
// 我们不想更改主题,所以
// 返回传入的任何内容
return subject
}
)
cy.get('button')
.console('info')
.then(($button) => {
// 主题仍然是$button
})
通过设置{ prevSubject: true }
,我们的新.console()
命令将需要一个主题。
这样调用会出错:
cy.console() // 错误:没有主题不能调用console
当你使用子命令时,你可能希望在主题上使用cy.wrap()。包装它使你能够立即在该主题上使用更多Cypress命令。
双重命令
双重命令可以开始一个命令链或链式调用现有命令。它基本上是父命令和子命令的混合体。你可能很少使用这个,只有少数内部命令使用这个。
尽管如此,如果你的命令可以以多种方式工作——有或没有现有主题,它会很有用。
双重命令的示例:
自定义双重命令
Cypress.Commands.add('dismiss', {
prevSubject: 'optional'
}, (subject, arg1, arg2) => {
// 主题可能已定义或未定义
// 因此你可能希望基于此分支逻辑
if (subject) {
// 包装现有主题
// 并对其进行操作
cy.wrap(subject)
...
} else {
...
}
})
cy.dismiss() // 无主题
cy.get('#dialog').dismiss() // 有主题
覆盖现有命令
你也可以修改现有Cypress命令的行为。这对于总是设置一些默认值以避免创建另一个最终使用原始命令的命令很有用。
Cypress.Commands.overwrite
只能覆盖命令,不能覆盖查询。如果你想修改查询的行为,需要使用Cypress.Commands.overwriteQuery
代替。
覆盖visit
命令
Cypress.Commands.overwrite('visit', (originalFn, url, options) => {
const domain = Cypress.env('BASE_DOMAIN')
if (domain === '...') {
url = '...'
}
if (options.something === 'else') {
url = '...'
}
// originalFn是现有的`visit`命令,你需要调用它
// 它会接收你在这里传入的任何内容。
//
// 确保在这里添加一个返回!
return originalFn(url, options)
})
我们看到许多用户创建自己的visitApp
命令。我们常见的是你只是为development
和production
环境交换基本URL。
这通常是不必要的,因为Cypress已经配置为交换cy.visit()和cy.request()都使用的baseUrl
。在你的Cypress配置中设置baseUrl
配置属性,并使用CYPRESS_BASE_URL
环境变量覆盖它。
对于更复杂的用例,可以随意覆盖现有命令。
覆盖type
命令
如果你在密码字段中输入内容,密码输入在你的应用程序中会自动被屏蔽。但.type()会自动将任何输入的内容记录到Cypress命令日志中。
cy.get('#username').type('username@email.com')
cy.get('#password').type('superSecret123')

你可能希望屏蔽传递给.type()命令的一些值,以便敏感数据不会显示在测试运行的截图或视频中。这个示例覆盖了.type()命令,允许你在Cypress命令日志中屏蔽敏感数据。
Cypress.Commands.overwrite('type', (originalFn, element, text, options) => {
if (options && options.sensitive) {
// 关闭原始日志
options.log = false
// 创建我们自己的带有屏蔽消息的日志
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 })
现在,当传递sensitive: true
作为.type()的选项时,我们的敏感密码不会打印到Cypress命令日志中。

覆盖screenshot
命令
这个示例覆盖了cy.screenshot(),以始终等待直到某个元素可见。
Cypress.Commands.overwrite(
'screenshot',
(originalFn, subject, fileName, options) => {
// 调用另一个命令,无需返回,因为它被管理
cy.get('.app')
.should('be.visible')
// 覆盖默认超时,因为screenshot内部会这样做
// 否则`then`受限于默认命令超时
.then({ timeout: Cypress.config('responseTimeout') }, () => {
// 返回原始函数以便Cypress等待它
return originalFn(subject, fileName, options)
})
}
)
覆盖click
命令
这个示例覆盖了.click(),以始终将waitForAnimations
选项设置为false
。
Cypress.Commands.overwrite(
'click',
(originalFn, subject, positionOrX, y, options = {}) => {
options.waitForAnimations = false
return originalFn(subject, positionOrX, y, options)
}
)
验证
如参数中所述,你也可以将prevSubject
设置为以下之一:
element
document
window
这样做时,Cypress会自动验证你的主题以确保它符合其中一种类型。
添加验证是可选的。传递{ prevSubject: true }
将需要一个主题,但不会验证其类型。
要求元素
要求主题类型为:element
。
// 这是.click()的实现方式
Cypress.Commands.add(
'click',
{
prevSubject: 'element',
},
(subject, options) => {
// 接收先前主题,并且它
// 保证是一个元素
}
)
正确用法
cy.get('button').click() // 有主题,且是`element`
错误用法
cy.click() // 无主题,会出错
cy.wrap([]).click() // 有主题,但不是`element`,会出错
允许多种类型
.trigger()
要求主题是以下类型之一:element
, document
或 window
// 这是.trigger()的实现方式
Cypress.Commands.add(
'trigger',
{
prevSubject: ['element', 'document', 'window'],
},
(subject, eventName, options) => {
// 接收先前主题,并且它
// 保证是一个元素、文档或窗口
}
)
正确用法
cy.get('button').trigger() // 有主题,且是`element`
cy.document().trigger() // 有主题,且是`document`
cy.window().trigger() // 有主题,且是`window`
错误用法
cy.trigger() // 无主题,会出错
cy.wrap(true).trigger() // 有主题,但不是`element`,会出错
验证始终作为“或”而不是“与”工作。
可选类型
你也可以将可选命令与验证混合使用。
// 这是.scrollTo()的实现方式
Cypress.Commands.add(
'scrollTo',
{
prevSubject: ['optional', 'element', 'window'],
},
(subject, ...args) => {
// 主题可能是undefined
// 因为它是可选的。
//
// 如果存在,则它是一个元素或窗口。
// - 当是窗口时,我们将滚动到页面上的位置。
// - 当是元素时,我们将滚动到与元素相关的位置。
if (subject) {
// ...
} else {
// ...
}
}
)
正确用法
cy.scrollTo() // 无主题,但有效,因为它是可选的
cy.get('#main').scrollTo() // 有主题,且是`element`
cy.visit().scrollTo() // 有主题,因为visit产生`window`,所以没问题
错误用法
cy.document().scrollTo() // 有主题,但它是`document`,会出错
cy.wrap(null).scrollTo() // 有主题,但它是`null`,会出错
注意事项
命令日志
创建自定义命令时,你可以控制它在命令日志中的显示和行为。
利用Cypress.log()
API。当你发出许多内部Cypress命令时,考虑传递{ log: false }
给这些命令,并以编程方式控制你的自定义命令。这将清理命令日志,使其更加视觉上吸引人和易于理解。
cy.hover()
和 cy.mount()
Cypress默认不包含cy.hover()
或cy.mount()
命令。查看如何制作你自己的cy.hover()
和cy.mount()
自定义命令。