Cypress 有一個 ... 使用 iframe 的困難。主要是因為所有內置的cy
DOM 遍歷命令在它們#document
到達 iframe 內的節點時都會硬停止。
iframe 看到 Cypress 命令時(重新制定)
如果您的 Web 應用程序使用 iframe,則處理這些 iframe 中的元素需要您自己的自定義代碼。在這篇博文中,我將展示如何與 iframe 內的 DOM 元素交互(即使 iframe 是從另一個域提供的),如何監視window.fetch
iframe 發出的請求,甚至如何存根來自 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
元素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 })) })
左欄中的命令日志現在看起來好多了。
帶有單個日志和斷言的自定義命令
監視 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.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()
命令的方式。
來自 iframe 的 Ajax 調用
監視像這樣的方法調用window.fetch
很有趣,但讓我們更進一步。Cypress 可以直接監視和存根應用程序的網絡請求,但前提是 Web 應用程序使用該XMLHttpRequest
對象而不是window.fetch
(我們將在#95 中修復此問題)。因此,如果我們想直接觀察或存根 iframe 發出的應用程序網絡調用,我們需要:
- 將
window.fetch
iframe 內部替換為XMLHttpRequest
來自應用程序窗口的內容 - 因為該對象具有 Cypress Test Runner 添加的監視和存根擴展。 - 調用cy.server然后使用cy.route觀察網絡調用。
復制 XMLHttpRequest 對象
我正在按照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 存根響應顯示在結果區域
獎勵: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/