返回赛普拉斯博客
Cypress 有一个 ... 使用 iframe 的困难。主要是因为所有内置的cy
DOM 遍历命令在它们#document
到达 iframe 内的节点时都会硬停止。
iframe 看到 Cypress 命令时(重新制定)
如果您的 Web 应用程序使用 iframe,则处理这些 iframe 中的元素需要您自己的自定义代码。在这篇博文中,我将展示如何与 iframe 内的 DOM 元素交互(即使 iframe 是从另一个域提供的),如何监视window.fetch
iframe 发出的请求,甚至如何存根来自 iframe 的 XHR 请求。
注意:您可以在存储库中的“使用 iframes”配方中找到此博客文章的源代码cypress-example-recipes。
让我们使用一个静态 HTML 页面并嵌入一个 iframe。这是完整的源代码。
<body>
<style>
iframe {
width: 90%;
height: 100%;
}
style>
<h1>XHR in iframeh1>
<iframe src="https://jsonplaceholder.cypress.io/"
data-cy="the-frame">iframe>
body>
提示:我们将按照选择元素指南的最佳实践使用data-cy
属性来查找 iframe 。
让我们在cypress/integration/first-spec.js
访问页面的规范文件中编写第一个测试。
it('gets the post', () => {
cy.visit('index.html').contains('XHR in iframe')
cy.get('iframe')
})
测试通过,我们可以看到加载的 iframe。
显示 iframe
如果我们手动单击“尝试”按钮,iframe 确实会获取第一篇文章。
当用户点击“Try It”按钮时,结果显示在下方
让我们尝试编写测试命令以找到“尝试”按钮,然后单击它。该按钮位于body
元素document
的iframe
元素内。让我们编写一个辅助函数来获取body
元素。
const getIframeDocument = () => {
return cy
.get('iframe[data-cy="the-frame"]')
// Cypress yields jQuery element, which has the real
// DOM element under property "0".
// From the real DOM iframe element we can get
// the "document" element, it is stored in "contentDocument" property
// Cypress "its" command can access deep properties using dot notation
// https://on.cypress.io/its
.its('0.contentDocument').should('exist')
}
const getIframeBody = () => {
// get the document
return getIframeDocument()
// automatically retries until body is loaded
.its('body').should('not.be.undefined')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
.then(cy.wrap)
}
it('gets the post', () => {
cy.visit('index.html')
getIframeBody().find('#run-button').should('have.text', 'Try it').click()
getIframeBody().find('#result').should('include.text', '"delectus aut autem"')
})
不幸的是,测试失败了 -contentDocument
元素永远不会从null
.
Cypress 测试无法访问 iframe 的文档
我们的问题是我们的测试在域下运行localhost
(您可以在浏览器的 url 中看到它),而按钮和 iframe 本身来自域jsonplaceholder.cypress.io
。浏览器不允许来自一个域的 Javascript 访问另一个域中的元素——这将是一个巨大的安全漏洞。因此,我们需要告诉运行测试的浏览器允许此类访问——毕竟,这是我们的测试,我们控制应用程序并且知道它嵌入的第 3 方 iframe 可以安全使用。
要启用跨域 iframe 访问,我将chromeWebSecurity
在文件中将该属性设置为 falsecypress.json
并重新运行测试。
{
"chromeWebSecurity": false
}
测试通过!
单击 iframe 内的按钮并断言 UI 更新
在我们继续之前,我想确认即使 3rd 方 iframe 加载缓慢,我们的代码也能正常工作。我将切换默认使用 Electron 浏览器的 Cypress 在 Chrome 浏览器中运行测试。
Chrome 运行测试后(在 Cypress 创建的测试用户配置文件下),我打开Chrome 扩展程序商店并安装URL Throttler扩展程序。我启用此扩展并添加https://jsonplaceholder.cypress.io/
URL 以减慢 2 秒。
URL Throttler 减慢 iframe 的加载速度
请注意测试现在如何花费超过 2 秒的时间 - 因为 iframe 被扩展程序延迟了。
使用 URL Throttler 扩展(黄色蜗牛图标)加载 iframe 会延迟 2 秒
提示:您可以在存储库中包含 Chrome 扩展并自动安装它 - 有关更多详细信息,请阅读我们的“如何在 Cypress 中加载 React DevTools 扩展”博客文章。
我们的测试使用内置命令 retries自动等待帧加载。
// in getIframeDocument()
cy
.get('iframe[data-cy="the-frame"]')
.its('0.contentDocument')
// above "its" command will be retried until
// content document property exists
// in getIframeBody()
getIframeDocument()
// automatically retries until body is loaded
.its('body').should('not.be.undefined')
虽然这有效,但我必须注意,只有最后一个命令会its('body')
被重试,这可能会导致测试失败。例如,Web 应用程序可能包含一个 iframe 占位符,该占位符body
稍后会更改- 但我们的代码不会看到更改,因为它已经具有该contentDocument
属性并且只会重试获取body
. (我在使用具有自己的 iframe 元素的 Stripe 信用卡小部件时看到了这种情况)。
因此,为了使测试代码更健壮并重试所有内容,我们应该将所有its
命令合并为一个命令:
const getIframeBody = () => {
// get the iframe > document > body
// and retry until the body element is not empty
return cy
.get('iframe[data-cy="the-frame"]')
.its('0.contentDocument.body').should('not.be.empty')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
// https://on.cypress.io/wrap
.then(cy.wrap)
}
it('gets the post using single its', () => {
cy.visit('index.html')
getIframeBody().find('#run-button').should('have.text', 'Try it').click()
getIframeBody().find('#result').should('include.text', '"delectus aut autem"')
})
好的。
我们可能会访问iframe的元素在多个测试,因此,让上面的效用函数为赛普拉斯自定义命令里面cypress/support/index.js
的文件。自定义命令将自动在所有规范文件中可用,因为支持文件与每个规范文件连接在一起。
// cypress/support/index.js
Cypress.Commands.add('getIframeBody', () => {
// get the iframe > document > body
// and retry until the body element is not empty
return cy
.get('iframe[data-cy="the-frame"]')
.its('0.contentDocument.body').should('not.be.empty')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
// https://on.cypress.io/wrap
.then(cy.wrap)
})
// cypress/integration/custom-command-spec.js
it('gets the post using custom command', () => {
cy.visit('index.html')
cy.getIframeBody()
.find('#run-button').should('have.text', 'Try it').click()
cy.getIframeBody()
.find('#result').should('include.text', '"delectus aut autem"')
})
我们可以cy.getIframeBody
通过禁用内部命令的日志记录来隐藏代码中每一步的细节。
Cypress.Commands.add('getIframeBody', () => {
// get the iframe > document > body
// and retry until the body element is not empty
cy.log('getIframeBody')
return cy
.get('iframe[data-cy="the-frame"]', { log: false })
.its('0.contentDocument.body', { log: false }).should('not.be.empty')
// wraps "body" DOM element to allow
// chaining more Cypress commands, like ".find(...)"
// https://on.cypress.io/wrap
.then((body) => cy.wrap(body, { log: false }))
})
左栏中的命令日志现在看起来好多了。
带有单个日志和断言的自定义命令
当用户或 Cypress 单击“试用”按钮时,Web 应用程序正在向 REST API 端点发出提取请求。
来自 iframe 的 Ajax 调用
我们可以通过单击请求来检查服务器返回的响应。
在这种情况下,它是一个 JSON 对象,表示具有某些键和值的“待办事项”资源。让我们确认window.fetch
应用程序使用预期参数调用了该方法。我们可以使用命令cy.spy来监视对象的方法。
const getIframeWindow = () => {
return cy
.get('iframe[data-cy="the-frame"]')
.its('0.contentWindow').should('exist')
}
it('spies on window.fetch method call', () => {
cy.visit('index.html')
getIframeWindow().then((win) => {
cy.spy(win, 'fetch').as('fetch')
})
cy.getIframeBody().find('#run-button').should('have.text', 'Try it').click()
cy.getIframeBody().find('#result').should('include.text', '"delectus aut autem"')
// because the UI has already updated, we know the fetch has happened
// so we can use "cy.get" to retrieve it without waiting
// otherwise we would have used "cy.wait('@fetch')"
cy.get('@fetch').should('have.been.calledOnce')
// let's confirm the url argument
.and('have.been.calledWith', 'https://jsonplaceholder.cypress.io/todos/1')
})
我们window
从 iframe获取一个对象,然后设置一个方法 spy usingcy.spy(win, 'fetch')
并给它一个别名,as('fetch')
以便稍后检索通过该方法的调用。我们可以看到间谍,当他们在命令日志中被调用时,我在下面的屏幕截图中用绿色箭头标记了它们。
Cypress 显示间谍和存根
提示:我们可以将实用程序函数移动getIframeWindow
到自定义命令中,类似于我们创建cy.getIframeBody()
命令的方式。
监视像这样的方法调用window.fetch
很有趣,但让我们更进一步。Cypress 可以直接监视和存根应用程序的网络请求,但前提是 Web 应用程序使用该XMLHttpRequest
对象而不是window.fetch
(我们将在#95 中修复此问题)。因此,如果我们想直接观察或存根 iframe 发出的应用程序网络调用,我们需要:
window.fetch
iframe 内部替换为XMLHttpRequest
来自应用程序窗口的内容 - 因为该对象具有 Cypress Test Runner 添加的监视和存根扩展。我正在按照cypress-example-recipes 中的配方“Stubbing window.fetch”替换window.fetch
为unfetch polyfill - 并将XMLHttpRequest
对象复制到 iframe 中。这是我们需要的实用程序代码。
let polyfill
// grab fetch polyfill from remote URL, could be also from a local package
before(() => {
const polyfillUrl = 'https://unpkg.com/unfetch/dist/unfetch.umd.js'
cy.request(polyfillUrl)
.then((response) => {
polyfill = response.body
})
})
const getIframeWindow = () => {
return cy
.get('iframe[data-cy="the-frame"]')
.its('0.contentWindow').should('exist')
}
const replaceIFrameFetchWithXhr = () => {
// see recipe "Stubbing window.fetch" in
// https://github.com/cypress-io/cypress-example-recipes
getIframeWindow().then((iframeWindow) => {
delete iframeWindow.fetch
// since the application code does not ship with a polyfill
// load a polyfilled "fetch" from the test
iframeWindow.eval(polyfill)
iframeWindow.fetch = iframeWindow.unfetch
// BUT to be able to spy on XHR or stub XHR requests
// from the iframe we need to copy OUR window.XMLHttpRequest into the iframe
cy.window().then((appWindow) => {
iframeWindow.XMLHttpRequest = appWindow.XMLHttpRequest
})
})
}
这是第一个测试 - 它window.fetch
监视网络调用,类似于上面的监视测试。
it('spies on XHR request', () => {
cy.visit('index.html')
replaceIFrameFetchWithXhr()
// prepare to spy on XHR before clicking the button
cy.server()
cy.route('/todos/1').as('getTodo')
cy.getIframeBody().find('#run-button')
.should('have.text', 'Try it').click()
// let's wait for XHR request to happen
// for more examples, see recipe "XHR Assertions"
// in repository https://github.com/cypress-io/cypress-example-recipes
cy.wait('@getTodo').its('response.body').should('deep.equal', {
completed: false,
id: 1,
title: 'delectus aut autem',
userId: 1,
})
// and we can confirm the UI has updated correctly
getIframeBody().find('#result')
.should('include.text', '"delectus aut autem"')
})
请注意我们如何等待网络请求发生,并获得对我们可以在断言中使用的请求和响应对象的完全访问权限。
cy.wait('@getTodo').its('response.body').should('deep.equal', {
completed: false,
id: 1,
title: 'delectus aut autem',
userId: 1,
})
提示:阅读博客文章“Asserting Network Calls from Cypress Tests”以获取更多针对网络调用的断言示例。
依赖 3rd 方 API 不太理想。让我们/todos/1
用我们自己的存根响应替换那个调用。该XMLHttpRequest
页面加载后的对象已经被复制和iframe是准备好了,让我们用它来返回一个对象。
it('stubs XHR response', () => {
cy.visit('index.html')
replaceIFrameFetchWithXhr()
// prepare to stub before clicking the button
cy.server()
cy.route('/todos/1', {
completed: true,
id: 1,
title: 'write tests',
userId: 101,
}).as('getTodo')
cy.getIframeBody().find('#run-button')
.should('have.text', 'Try it').click()
// and we can confirm the UI shows our stubbed response
cy.getIframeBody().find('#result')
.should('include.text', '"write tests"')
})
很好,cy.route
用一个对象参数存根匹配的网络请求,我们的断言确认 iframe 显示文本“写测试”。
XHR 存根响应显示在结果区域
我们的一位用户Keving Groat编写了带有自定义命令的cypress-iframe插件,简化了对 iframe 中元素的处理。安装插件,
然后使用自定义命令。npm install -D cypress-iframe
// the next comment line loads the custom commands from the plugin
// so that our editor understands "cy.frameLoaded" and "cy.iframe"
///
import 'cypress-iframe'
describe('Recipe: blogs__iframes', () => {
it('fetches post using iframes plugin', () => {
cy.visit('index.html')
cy.frameLoaded('[data-cy="the-frame"]')
// after the frame has loaded, we can use "cy.iframe()"
// to retrieve it
cy.iframe().find('#run-button').should('have.text', 'Try it').click()
cy.iframe().find('#result').should('include.text', '"delectus aut autem"')
})
})
使用 cypress-iframe 命令的通过测试
iframe 很烦人——我希望我们的 Cypress 团队有足够的时间来一劳永逸地解决它们。然而,它们不是表演者——您只需要按照这篇博文作为指南,并查看存储库中“使用 iframes”配方中的代码cypress-example-recipes绕过障碍。
参考:https://www.cypress.io/blog/2020/02/12/working-with-iframes-in-cypress/
原文链接:https://www.cnblogs.com/aliceyang/p/15322215.html