У Cypress есть… трудности с работой с фреймами. В основном потому, что все встроенные cy
команды обхода DOM делают жесткую остановку в тот момент, когда они достигают #document
узла внутри iframe.
Если ваше веб-приложение использует фреймы iframe, то для работы с элементами в этих фреймах потребуется собственный код. В этом сообщении блога я покажу, как взаимодействовать с элементами DOM внутри iframe (даже если iframe обслуживается из другого домена), как отслеживать window.fetch
запросы, которые делает iframe, и даже как заглушать запросы XHR от iframe.
Примечание: вы можете найти исходный код для этой записи в блоге в рецепте Работа с фреймами, расположенном в репозитории 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
Давайте попробуем написать тестовые команды, чтобы найти кнопку «Попробовать», а затем нажать на нее. Эта кнопка находится внутри элемента 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
.
Наша проблема в том, что наш тест работает под доменом localhost
(его можно увидеть в адресе браузера), а кнопка и сам iframe происходят из домена jsonplaceholder.cypress.io
. Браузеры не позволяют JavaScript из одного домена получать доступ к элементам в другом домене — это было бы огромной дырой в безопасности. Таким образом, нам нужно сообщить нашему браузеру, выполняющему тесты, разрешить такой доступ — в конце концов, это наш тест, мы контролируем приложение и знаем, что сторонний iframe, который он внедряет, безопасен для использования.
Чтобы разрешить междоменный доступ к iframe, я установлю для свойства chromeWebSecurity
значение false в файле cypress.json
и перезапущу тест.
{
"chromeWebSecurity": false
}
Тест проходит!
Медленно загружаемые фреймы
Прежде чем мы продолжим, я хотел бы подтвердить, что наш код работает, даже если сторонний iframe загружается медленно. Я переключу Cypress, который по умолчанию использует браузер Electron для запуска тестов в браузере Chrome.
Как только Chrome запускает тест (под тестовым профилем пользователя, созданным Cypress), я открываю Магазин расширений Chrome и устанавливаю расширение URL Throttler. Я включаю это расширение и добавляю URL-адрес https://jsonplaceholder.cypress.io/
для замедления на 2 секунды.
Обратите внимание, что тест теперь занимает больше 2 секунд — потому что iframe задерживается расширением.
Совет. вы можете включить расширение Chrome в свой репозиторий и установить его автоматически. Подробнее читайте в нашем блоге Как загрузить расширение React DevTools в Cypress.
Наш тест автоматически ожидает загрузки кадра с помощью встроенных повторных попыток.
// 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')
, что может привести к сбою тестов. Например, веб-приложение может включать заполнитель iframe, который позже изменяет свое body
, но наш код не увидит изменения, поскольку оно уже имеет свойство contentDocument
и только пытается получить body
. (Я видел, как это происходит при использовании виджета кредитной карты Stripe, который имеет собственный элемент iframe).
Таким образом, чтобы сделать тестовый код более надежным и повторить все, мы должны объединить все 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 внутри файла 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 нажимает кнопку «Попробовать», веб-приложение отправляет запрос на выборку в конечную точку REST API.
Мы можем проверить ответ, возвращенный сервером, нажав на запрос.
В данном случае это объект JSON, представляющий ресурс todo с определенными ключами и значениями. Подтвердим, что метод 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, затем настраиваем метод-шпион, используя cy.spy(win, 'fetch')
, и даем ему псевдоним as('fetch')
для извлечения вызовов, которые проходят через этот метод позже. Мы видим шпионов, и когда они были вызваны в журнале команд, я отметил их зелеными стрелками на скриншоте ниже.
Совет. мы можем переместить служебную функцию getIframeWindow
в пользовательскую команду, аналогично тому, как мы создали команду cy.getIframeBody()
.
Ajax-вызовы из iframe
Шпионить за вызовами методов вроде window.fetch
весело, но давайте сделаем еще один шаг вперед. Cypress может напрямую отслеживать и блокировать сетевые запросы приложения, но только если веб-приложение использует объект XMLHttpRequest
, а не window.fetch
(мы исправим это в #95). Итак, если мы хотим напрямую отслеживать или блокировать сетевые вызовы приложения, которые делает iframe, нам необходимо:
- Замените
window.fetch
внутри iframe наXMLHttpRequest
из окна приложения, потому что этот объект имеет шпионские и заглушки, добавленные Cypress Test Runner. - Вызовите cy.server, а затем используйте cy.route для наблюдения за сетевыми вызовами.
Скопировать объект XMLHttpRequest
Я следую рецепту Stubbing window.fetch из cypress-example-recipes, чтобы заменить 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
spy выше.
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,
})
Совет: прочтите запись в блоге Подтверждение сетевых вызовов из тестов Cypress для получения дополнительных примеров утверждений против сетевых вызовов.
Перехват сетевого вызова
Опора на сторонние 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 показывает текст «написать тесты».
Бонус: плагин 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"') }) })
Вывод
iframe раздражают — хотелось бы, чтобы у нашей команды Cypress было достаточно времени, чтобы разобраться с ними раз и навсегда. Тем не менее, они не останавливают шоу — вам просто нужно следовать этому сообщению в блоге в качестве руководства и посмотреть код в рецепте Работа с iframes в репозитории cypress-example-recipes, чтобы обойти препятствие.
Первоначально опубликовано на https://cypress.io 12 февраля 2020 г.