自定义查询
从Cypress 12开始,Cypress提供了创建自定义查询的API。内置的Cypress查询使用的正是下文介绍的相同API。
查询是一种命令类型,用于查询应用程序的状态。它们与其他命令的不同之处在于遵循三个重要规则:
- 查询是同步的,不会返回或等待Promise。
- 查询是可重试的。一旦返回内部函数,Cypress将接管控制权,自动处理重试。
- 查询是幂等的。一旦返回内部函数,Cypress会多次调用它。多次调用内部函数不得改变应用程序状态。
遵循这些规则,查询编写简单且功能强大。它们是构建Cypress API的基础模块。要了解更多关于命令和查询的区别,请参阅我们的重试能力指南。
如果想将现有的Cypress命令链式调用作为快捷方式,可能需要编写自定义命令而非查询。
如果方法需要异步执行或只能调用一次,也应编写命令而非查询。
建议在cypress/support/commands.js
文件中定义查询,因为该文件会在任何测试文件执行前通过supportFile中的import语句加载。
语法
Cypress.Commands.addQuery(name, callbackFn)
Cypress.Commands.overwriteQuery(name, callbackFn)
用法
正确用法
Cypress.Commands.addQuery('getById', function (id) {
return (subject) => newSubject
})
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
return originalFn.apply(this, args)
})
参数
name (String)
要添加的查询名称。
callbackFn (Function)
传入一个接收查询参数的回调函数。
外部函数只调用一次,应返回一个接收subject并返回新subject的函数;内部函数可能被多次调用。
查询API依赖this
设置超时,因此callbackFn
应始终使用function () {}
而非箭头函数(() => {}
)。
示例
.focused()
回调函数可分为两部分:只调用一次的外部函数(用于设置和状态管理)和可能被多次调用的查询函数。
以下是一个实际Cypress代码示例——.focused()
的内部实现,稍作调整以在支持文件中工作。为简化省略了TypeScript定义。
Cypress.Commands.addQuery('focused2', function focused2(options = {}) {
const log = options.log !== false && Cypress.log({ timeout: options.timeout })
this.set('timeout', options.timeout)
return () => {
let $el = cy.getFocused()
log &&
cy.state('current') === this &&
log.set({
$el,
consoleProps: () => {
return {
Yielded: $el?.length ? $el[0] : '--nothing--',
Elements: $el != null ? $el.length : 0,
}
},
})
if (!$el) {
$el = cy.$$(null)
$el.selector = 'focused'
}
return $el
}
})
外部函数
外部函数在测试每次使用查询时调用一次,执行设置和状态管理:
function focused2(options = {}) {
const log = options.log !== false && Cypress.log({ timeout: options.timeout })
this.set('timeout', options.timeout)
return () => { ... } // 内部函数
}
逐部分解析:
function focused2(options = {}) { ... }
Cypress将用户调用时传入的参数原样传递给外部函数,不进行任何处理或验证。本例中,.focused2()
接受一个可选参数options
。
如需验证参数,可添加:
if (options === null || !_.isPlainObject(options)) {
const err = `cy.root() requires an \`options\` object. You passed in: \`{options}\``
throw new TypeError(err)
}
这是通用模式:出错时查询直接抛出错误,Cypress负责在命令日志中显示错误。
const log = options.log !== false && Cypress.log({ timeout: options.timeout })
如果用户未设置{ log: false }
,则创建新的Cypress.log()
实例。详见Cypress.log()
。
此行是设置代码,因此放在外部函数中——只需运行一次,在Cypress开始执行查询时创建日志消息。保留Log
实例引用,稍后会在内部函数执行时更新详细信息。
this.set('timeout', options.timeout)
定义focused2()
时,注意使用function
而非箭头函数,以便访问this
设置超时。如果不调用this.set('timeout')
或传入null
/undefined
,查询将使用默认超时。
return () => { ... }
内部函数
外部函数的返回值是内部函数。
内部函数会被调用多次。首先在超时前重复调用直至通过;之后可能在获取别名或确定后续命令subject时再次调用。
内部函数接收一个参数:前一个subject。Cypress不验证其类型——可能是任何值,包括null
或undefined
。
.focused2()
忽略前一个subject,但许多查询不会——例如.contains()
只接受特定类型的subject。可以使用Cypress内置的ensures
函数,如.contains()
所做:cy.ensureSubjectByType(subject, ['optional', 'element', 'window', 'document'], this)
或自行验证并抛出错误:if (!_.isString(subject)) { throw new Error('MyCustomCommand only accepts strings as a subject!') }
如果内部函数抛出错误,Cypress会在短暂延迟后重试,直到通过或查询超时。这是Cypress重试能力的核心,保证测试像用户一样与页面交互。
回到.focused2()
示例:
return () => {
let $el = cy.getFocused()
log &&
cy.state('current') === this &&
log.set({
$el,
consoleProps: () => {
return {
Yielded: $el?.length ? $el[0] : '--nothing--',
Elements: $el != null ? $el.length : 0,
}
},
})
if (!$el) {
$el = cy.$$(null)
$el.selector = 'focused'
}
return $el
}
逐部分解析:
let $el = cy.getFocused()
这是.focused2()
的"业务端"——查找页面上当前获得焦点的元素。
log && cy.state('current') === this && log.set({...})
如果log
已定义(即用户未传入{ log: false }
)且当前命令是此查询,则更新日志消息,如$el
(此查询即将返回的subject)和consoleProps
(返回用户控制台输出的函数)。
if (!$el) {
$el = cy.$$(null)
$el.selector = 'focused'
}
如果页面上没有焦点元素,创建一个空的jquery对象。
return $el
内部函数的返回值成为下一个命令的新subject。
Cypress用此返回值验证后续断言,如用户的.should()
命令,或默认隐式断言subject存在。
覆盖现有查询
也可以修改现有Cypress查询的行为,扩展内置命令功能。
Cypress.Commands.overwriteQuery
只能覆盖查询,不能覆盖其他命令。如需修改非查询命令的行为, 需使用Cypress.Commands.overwrite
。
记住查询函数依赖this
——调用originalFn
时务必使用.call
或.apply
。
Cypress.Commands.overwriteQuery('get', function (originalFn, ...args) {
console.log('get called with args:', args)
const innerFn = originalFn.apply(this, args)
return (subject) => {
console.log('get inner function called with subject:', subject)
return innerFn(subject)
}
})
originalFn
是最初传给Cypress.Commands.addQuery
的函数——它是一个返回函数的函数。这让你既能访问外部参数(调用originalFn
前),也能访问内部函数(originalFn
的返回值),从而高度控制查询执行方式。
为.contains()
添加别名支持
此示例扩展cy.contains()
以支持查询别名subject,如cy.contains('@foo')
。
Cypress.Commands.overwriteQuery(
'contains',
function (originalFn, filter, text, userOptions) {
if (_.isString(filter) && filter[0] === '@') {
let alias = cy.state('aliases')[filter.slice(1)]
let subject = cy.getSubjectFromChain(alias?.subjectChain)
filter = subject
}
if (_.isString(text) && text[0] === '@') {
let alias = cy.state('aliases')[text.slice(1)]
let subject = cy.getSubjectFromChain(alias?.subjectChain)
text = subject
}
return originalFn.call(this, filter, text, userOptions)
}
)
cy.wrap('li').as('element')
cy.wrap('asdf 1').as('content')
cy.contains('@element', '@content')
验证
如前所述,Cypress对查询的验证很少——每个实现需自行确保参数和subject类型正确。
Cypress有几个内置的"ensure"辅助函数:
cy.ensureSubjectByType(subject, types, this)
:接受包含optional
、element
、document
或window
的数组。命令中的prevSubject
验证即由此实现。cy.ensureElement(subject, queryName)
:确保传入的subject
是一个或多个DOM元素。cy.ensureWindow(subject)
:确保subject
是window
。cy.ensureDocument(subject)
:确保subject
是document
。cy.ensureAttached(subject, queryName)
:确保DOM元素已附加到页面。cy.ensureNotDisabled(subject)
:确保表单元素未禁用。cy.ensureVisibility(subject)
:确保DOM元素在页面上可见。
这些函数并无特殊之处——只是验证参数并在失败时抛出错误。可以在查询中随时抛出任何类型的错误——Cypress会捕获并适当处理。
注意事项
最佳实践
1. 不要将所有内容都变成自定义查询
当需要在所有测试中描述行为时,自定义查询效果最佳。例如cy.findBreadcrumbs()
或cy.getLoginForm()
,这些特定于应用程序且可全局使用。
但此模式可能被滥用。别忘了——编写Cypress测试就是编写JavaScript,通常编写函数处理可重复行为比实现自定义查询更高效。
2. 不要过度复杂化
每个自定义查询通常是定位页面元素的抽象。这意味着你和团队成员需要更多精力理解其作用。
当内置查询已非常强大时,无需增加这种复杂性。
避免以下做法:
-
cy.getButton()
-
.getFirstTableRow()
这些都只是包装cy.get(selector)
,完全没必要。直接调用.get('button')
或.get('tr:first')
即可。
Cypress测试重在可读性和简单性。无需太多编程即可完成大量工作。也不必过度追求代码DRY原则。测试代码与应用程序代码目的不同,应优先考虑可理解性和可调试性。
尽量避免过度复杂化和创建过多抽象。
历史
版本 | 变更 |
---|---|
12.6.0 | 新增overrideQuery API |
12.0.0 | 新增addQuery API |
另请参阅
- 了解如何为自定义命令添加TypeScript支持
-
使用自定义命令的插件
- Cypress.log()