Skip to main content

origin

在单个测试中访问多个不同的网站。

通常情况下,单个Cypress测试只能在单一源中运行命令,这是由浏览器的标准Web安全特性决定的限制。cy.origin()命令允许你的测试绕过这一限制。

caution

阻碍性第三方代码

默认情况下,Cypress会从服务器返回的第一方.html.js文件响应流中搜索并替换常见的框架破坏代码模式。使用cy.origin()命令时,第三方代码可能也需要针对框架破坏技术进行修改。可以通过在Cypress配置中将experimentalModifyObstructiveThirdPartyCode标志设置为true来启用此功能。关于这个实验性标志的更多信息可以在我们的跨源测试文档中找到。

info
Cypress v14.0.0中的变更

Cypress默认不再注入document.domain,这意味着现在必须在同一测试中使用cy.origin()来导航任意两个源,即使这两个源属于同一个超级域名。 可以通过将injectDocumentDomain配置选项设置为true来禁用此行为,以便平滑过渡测试到新行为。 此配置选项将在未来的Cypress版本中移除。

语法

cy.origin(url, callbackFn)
cy.origin(url, options, callbackFn)

用法

正确用法

const hits = getHits() // 在其他地方定义
// 在次要源中运行命令,传入可序列化的值
cy.origin('https://example.cypress.io', { args: { hits } }, ({ hits }) => {
// 回调内部baseUrl为https://example.cypress.io
cy.visit('/history/founder')
// 命令在次要源中执行
cy.get('h1').contains('About our Founder')
// 通过回调参数访问传入的值
cy.get('#hitcounter').contains(hits)
})
// 即使我们在次要源块外部,
// 仍然在cypress.io上,所以返回baseUrl
cy.visit('/')
// 继续在主源上运行命令
cy.get('h1').contains('My cool site under test')

错误用法

const hits = getHits()
cy.visit('https://example.cypress.io/history/founder')
// 要与跨源内容交互,将此移到cy.origin()回调内部
cy.get('h1').contains('Kitchen Sink')
// 源必须精确匹配,包括协议、子域名和端口,例如https://www.cypress.io
cy.origin('https://www.cypress.io', () => {
cy.visit('/about-us')
cy.get('h1').contains('About us')
// 失败,因为downloads未通过args传入
cy.contains(downloads)
})
// 不会工作,因为仍在www.cypress.io上
cy.get('h1').contains('Kitchen Sink')

参数

origin (String)

指定回调执行源的字符串。至少应包含主机名。也可以包含协议和端口号。可以包含路径,但不是必需的。主机名必须精确匹配次要源,包括所有子域名。不支持查询参数。如果未提供协议,默认为https

此参数将用于两种方式:

  1. 它唯一标识一个次要源,回调中的命令将在其中执行。Cypress将自身注入此源,然后向其发送代码在该源中评估,而不会违反浏览器的同源策略。

  2. 它在回调内部覆盖全局配置中的baseUrl。因此cy.visit()cy.request()将使用此URL作为前缀,而不是配置的baseUrl

options (Object)

传入一个选项对象以控制cy.origin()的行为。

选项描述
args纯JavaScript对象,将被序列化并从主源发送到次要源,在那里反序列化并作为第一个也是唯一的参数传递给回调函数。
caution

args对象是唯一可以将数据注入回调的机制,回调不是闭包,不保留对其声明时JavaScript上下文的访问权限。传入args的值必须可序列化。

callbackFn (Function)

包含要在次要源中执行的命令的函数。

此函数将被字符串化,发送到次要源中的Cypress实例并评估。如果指定了args选项,反序列化的args对象将作为第一个也是唯一的参数传递给函数。

回调内运行的命令有许多限制,请参阅回调限制部分获取完整列表。

生成结果 了解主题管理

  • cy.origin()返回回调函数中最后一个Cypress命令产生的值。
  • 如果回调不包含任何Cypress命令,cy.origin()返回函数的返回值。
  • 在上述两种情况下,如果值不可序列化,尝试访问返回的值将抛出错误。

示例

在次要源中使用动态数据

回调在一个完全独立的Cypress实例中执行,因此参数必须通过结构化克隆算法传输到另一个实例。此机制的接口是args选项。

const sentArgs = { username: 'username', password: 'P@55w0rd!' }
cy.origin(
'supersecurelogons.com',
// 在这里发送参数...
{ args: sentArgs },
// ...在这里接收它们!
({ username, password }) => {
cy.visit('/login')
cy.get('input#username').type(username)
cy.get('input#password').type(password)
cy.contains('button', 'Login').click()
}
)

返回值

从回调函数返回或产生的值必须可序列化,否则它们不会返回到主源。例如,以下代码不会工作:

错误用法

cy.origin('https://example.cypress.io', () => {
cy.visit('/')
cy.get('h1') // 产生一个元素,无法序列化...
}).contains('Kitchen Sink') // ...所以这会失败

相反,你必须显式产生一个可序列化的值:

正确用法

cy.origin('https://example.cypress.io', () => {
cy.visit('/')
cy.get('h1').invoke('text') // 产生一个字符串...
}).should('equal', 'Kitchen Sink') // 👍

使用cy.visit导航到次要源

当使用cy.visit()导航到次要源时,你可以在cy.origin块之前或之后导航。跨源导航不再抛出错误,但当命令与跨源页面交互时会抛出错误。

// 在主源中执行操作...

cy.origin('example.cypress.io', () => {
// 访问https://example.cypress.io/history/founder
cy.visit('/history/founder')
cy.get('h1').contains('About our Founder')
})

这里cy.origin()回调内部的baseUrl设置为example.cypress.io,协议默认为https。当调用cy.visit()并传入路径/history/founder时,三者拼接为https://example.cypress.io/history/founder

替代导航方式

// 在主源中执行操作...

cy.visit('https://example.cypress.io/history/founder')

// 需要cy.origin块来与跨源页面交互。
cy.origin('example.cypress.io', () => {
cy.get('h1').contains('About our Founder')
})

这里跨源页面在cy.origin块之前访问,但与窗口的任何交互都在块内执行,该块可以与跨源页面通信。

错误用法

// 在主源中执行操作...

cy.visit('https://www.cypress.io/history/founder')

// 此命令将失败,它在localhost上执行但应用程序在cypress.io上
cy.get('h1').contains('About our Founder, Marvin Acme')

这里cy.get('h1')失败,因为我们试图在cy.origin块外部与跨源页面交互,由于"同源"限制,"localhost"的JavaScript上下文无法与"cypress.io"通信。

通过UI导航到次要源

支持通过点击主源中的链接或按钮导航到次要源。

// 主源中的按钮跳转到https://example.cypress.io
cy.contains('button', 'Go').click()

cy.origin('example.cypress.io', () => {
// 不需要cy.visit,因为按钮已带我们到这里
cy.get('h1').contains('CYPRESS')
})

连续导航到多个次要源

回调本身不能包含cy.origin()调用,因此当访问多个源时,在测试的顶层进行。

cy.origin('example.cypress.com', () => {
cy.visit('/')
cy.url().should('contain', 'example.cypress.com')
})

cy.origin('cypress-dx.com', () => {
cy.visit('/')
cy.url().should('contain', 'cypress-dx.com')
})

SSO登录自定义命令

一个非常常见的需求是在运行测试之前登录到网站。如果登录本身不是测试的特定重点,最好将此功能封装在login自定义命令中,这样就不必在每个测试中重复此登录代码。这是一个理想化的示例,展示如何使用cy.origin()实现这一点。

低效用法

Cypress.Commands.add('login', (username, password) => {
// 记得通过`args`传入参数
const args = { username, password }
cy.origin('cypress.io', { args }, ({ username, password }) => {
// 访问https://example.cypress.com/login
cy.visit('/login')
cy.contains('Username').find('input').type(username)
cy.contains('Password').find('input').type(password)
cy.get('button').contains('Login').click()
})
// 确认我们回到主源后再继续
cy.url().should('contain', '/home')
})

在每个测试之前都要经历整个登录流程并不是很高效。到目前为止,你可以通过将登录代码放在文件的第一个测试中,然后在后续测试中重用相同的会话来避免这个问题。

然而,这不再可能,因为现在所有会话状态在测试之间都会被清除。因此,为了避免这种开销,我们建议你利用cy.session()命令,它允许你轻松缓存会话信息并在测试之间重用。现在让我们用cy.session()增强我们的自定义登录命令,实现一个完整的联合登录流程,包括会话缓存和验证。无需模拟,无需变通,无需第三方插件!

Cypress.Commands.add('login', (username, password) => {
const args = { username, password }
cy.session(
// 用户名和密码也可以用作缓存键
args,
() => {
cy.origin('cypress.io', { args }, ({ username, password }) => {
cy.visit('/login')
cy.contains('Username').find('input').type(username)
cy.contains('Password').find('input').type(password)
cy.get('button').contains('Login').click()
})
cy.url().should('contain', '/home')
},
{
validate() {
cy.request('/api/user').its('status').should('eq', 200)
},
}
)
})

了解更多

如何测试多个源

在这个视频中,我们逐步介绍如何在单个测试中测试多个源。我们还看看如何使用cy.session()命令缓存会话信息并在测试之间重用。 视频中提到的配置选项experimentalSessionAndOriginCypress 12.0.0起不再使用,相关功能默认启用。

注意事项

序列化

当进入cy.origin()块时,Cypress在运行时将所有配置设置注入到请求的源中,并设置与该实例的双向通信。这种协调模型要求从一个实例发送到另一个实例的任何数据都必须序列化以进行传输。非常重要的是要理解回调内部的变量不与回调外部的作用域共享。例如,以下代码不会工作:

错误用法

const foo = 1
cy.origin('cypress.io', () => {
cy.visit('/')
// 此行将抛出ReferenceError,因为
// `foo`未在回调作用域中定义
cy.get('input').type(foo)
})

相反,必须使用args选项将变量显式传递到回调中:

正确用法

const foo = 1
cy.origin('cypress.io', { args: { foo } }, ({ foo }) => {
cy.visit('/')
// 现在会通过
cy.get('input').type(foo)
})

Cypress使用结构化克隆算法args选项传输到次要源。这引入了对可以传递到回调中的数据的许多限制

依赖项/共享代码

cy.origin()回调内部,可以使用Cypress.require()来包含npm包和其他文件。它在功能上与在浏览器目标代码中使用CommonJS require()相同。

注意,在回调内部无法使用CommonJS require()ES模块import()

caution

在回调内部使用Cypress.require()需要在Cypress配置中启用experimentalOriginDependencies选项。

更多信息请参阅Cypress.require()文档本身。

示例

cy.origin('cypress.io', () => {
const _ = Cypress.require('lodash')
const utils = Cypress.require('../support/utils')

// ... 使用lodash和utils ...
})

自定义命令

这使得可以在主源和次要源运行的测试之间共享自定义命令。我们建议使用以下模式设置你的支持文件并设置在cy.origin()回调内运行的自定义命令:

cypress/support/commands.js:

Cypress.Commands.add('clickLink', (label) => {
cy.get('a').contains(label).click()
})

cypress/support/e2e.js:

// 使自定义命令可用于此规范中的所有Cypress测试,
// 在cy.origin()回调外部
import './commands'

// 我们只想每个测试运行的代码,所以它不应该作为
// cy.origin()执行的一部分运行
beforeEach(() => {
// ... 每个测试前运行的代码 ...
})

cypress/e2e/spec.cy.js:

before(() => {
// 使自定义命令可用于此规范中所有后续的cy.origin('cypress.io')调用。
// 将其放在支持文件中可使它们对所有规范可用
cy.origin('cypress.io', () => {
Cypress.require('../support/commands')
})
})

it('tests cypress.io', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})
})

共享执行上下文

JavaScript执行上下文在共享相同源的cy.origin()回调之间保持持久。这可以用于在连续的cy.origin()调用之间共享代码。

before(() => {
cy.origin('cypress.io', () => {
// 使此文件中定义的命令对所有回调可用
// 对于cypress.io
Cypress.require('../support/commands')
})
})

it('uses cy.origin() + custom command', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})
})

it('also uses cy.origin() + custom command', () => {
cy.origin('cypress.io', () => {
cy.visit('/page')
cy.clickLink('Click Me')
})

cy.origin('cypress-dx.com', () => {
// 警告:cy.clickLink()将不可用,因为它是
// 不同的源
})
})

回调限制

由于回调传输和执行的方式,回调内可以运行的代码有一些限制。特别是,以下Cypress命令如果在回调中使用会抛出错误:

其他限制

还有其他测试场景目前不被cy.origin()覆盖:

历史

版本变更
14.0.0cy.origin()现在需要在同一测试中导航不同源时使用,而不是超级域名。
12.6.0添加了对Cypress.require()的支持,并移除了对CommonJS require()和ES模块import()的支持
10.11.0添加了对CommonJS require()和ES模块import()的支持,并移除了对Cypress.require()的支持
10.7.0添加了对Cypress.require()的支持

另请参阅