在 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/

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM