在 Cypress 中使用 iframe


 返回赛普拉斯博客

Cypress 有一个 ... 使用 iframe 的困难。主要是因为所有内置的cyDOM 遍历命令在它们#document到达 iframe 内的节点时都会硬停止

iframe 看到 Cypress 命令时(重新制定)

如果您的 Web 应用程序使用 iframe,则处理这些 iframe 中的元素需要您自己的自定义代码。在这篇博文中,我将展示如何与 iframe 内的 DOM 元素交互(即使 iframe 是从另一个域提供的),如何监视window.fetchiframe 发出的请求,甚至如何存根来自 iframe 的 XHR 请求。

注意:您可以在存储库中的“使用 iframes”配方中找到此博客文章的源代码cypress-example-recipes

使用 iframe 的应用程序

让我们使用一个静态 HTML 页面并嵌入一个 iframe。这是完整的源代码。

<body>
  <style>
    iframe {
      width: 90%;
      height: 100%;
    }
  </style>
  <h1>XHR in iframe</h1>
  <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”按钮时,结果显示在下方

单击 iframe 内的按钮

让我们尝试编写测试命令以找到“尝试”按钮,然后单击它。该按钮位于body元素documentiframe元素内。让我们编写一个辅助函数来获取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 }))
})

左栏中的命令日志现在看起来好多了。

带有单个日志和断言的自定义命令

监视 window.fetch

当用户或 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.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()命令的方式。

来自 iframe 的 Ajax 调用

监视像这样的方法调用window.fetch很有趣,但让我们更进一步。Cypress 可以直接监视和存根应用程序的网络请求,但前提是 Web 应用程序使用该XMLHttpRequest对象而不是window.fetch(我们将在#95 中修复此问题)。因此,如果我们想直接观察或存根 iframe 发出的应用程序网络调用,我们需要:

  1. window.fetchiframe 内部替换XMLHttpRequest来自应用程序窗口的内容 - 因为该对象具有 Cypress Test Runner 添加的监视和存根扩展。
  2. 调用cy.server然后使用cy.route观察网络调用。

复制 XMLHttpRequest 对象

我正在按照cypress-example-recipes 中的配方“Stubbing window.fetch”替换window.fetchunfetch 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 存根响应显示在结果区域

奖励:cypress-iframe 插件

我们的一位用户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"
/// <reference types="cypress-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