Skip to main content
Cypress应用

自定义查询

从Cypress 12开始,Cypress提供了创建自定义查询的API。内置的Cypress查询使用的正是下文介绍的相同API。

查询是一种命令类型,用于查询应用程序的状态。它们与其他命令的不同之处在于遵循三个重要规则:

  1. 查询是同步的,不会返回或等待Promise。
  2. 查询是可重试的。一旦返回内部函数,Cypress将接管控制权,自动处理重试。
  3. 查询是幂等的。一旦返回内部函数,Cypress会多次调用它。多次调用内部函数不得改变应用程序状态。

遵循这些规则,查询编写简单且功能强大。它们是构建Cypress API的基础模块。要了解更多关于命令和查询的区别,请参阅我们的重试能力指南

info

如果想将现有的Cypress命令链式调用作为快捷方式,可能需要编写自定义命令而非查询。

如果方法需要异步执行或只能调用一次,也应编写命令而非查询。

info

建议在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的函数;内部函数可能被多次调用。

info

查询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不验证其类型——可能是任何值,包括nullundefined

.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查询的行为,扩展内置命令功能。

caution

Cypress.Commands.overwriteQuery只能覆盖查询,不能覆盖其他命令。如需修改非查询命令的行为,需使用Cypress.Commands.overwrite

info

记住查询函数依赖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):接受包含optionalelementdocumentwindow的数组。命令中的prevSubject验证即由此实现。
  • cy.ensureElement(subject, queryName):确保传入的subject是一个或多个DOM元素。
  • cy.ensureWindow(subject):确保subjectwindow
  • cy.ensureDocument(subject):确保subjectdocument
  • 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

另请参阅