Cypress using child window

How to test your site in a child window rather than iframe when using Cypress.

Normally, Cypress test runner loads your site inside an iframe. This allows the "top" parent window, controlled by Cypress a direct access to your site. Nice, but many sites work hard to avoid being iframed. Cypress already strips X-frame protection headers, and "fixes" most common frame-busting JavaScript code like if (top !== self).

Imagine the website using the following frame-busting code, and it somehow slipping Cypress JS regex

1
2
3
4
5
6
7
8
<script>
if (top !== self) {
console.log('top !== self !!! frame busted!')
location = 'http://www.cypress.io'
} else {
console.log('all is good, top === self')
}
</script>

The image below shows the frame-busting in action - the site has reference to self window that is different from the top window. Also, the menu shows the different JavaScript contexts - one per window object, which is often a source of confusion.

Different contexts and frame busting

Nothing is foolproof, especially my brain, and having a child iframe for the application under test creates its own confusion. So many times I have opened DevTools, inspecting window or some global object, and wondering - where is the property I have just set? "Ohh, yeah, it was in the APP context, how could I forget!"

What can we do instead of iframing the application under test website? Cypress needs direct access to the window that is going to load the site. Child iframe window is one possibility, another one is a window opened with window.open call. As long as the document.domain values match between the Cypress window and the loaded site, the two windows will be able to communicate. Cypress proxy takes care of setting the document.domain='localhost' for you, you can see that script injected into the HEAD element if you inspect the child iframes.

Document domain set to localhost

So, let's replace cy.visit with window.open. Here is some code I have lifted from the example repo bahmutov/cypress-open-child-window.

Note: I am using Cypress v3.4.1 and Chrome v76 to run this code.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
Cypress.Commands.add('openWindow', (url, features) => {
const w = Cypress.config('viewportWidth')
const h = Cypress.config('viewportHeight')
if (!features) {
features = `width=${w}, height=${h}`
}
console.log('openWindow %s "%s"', url, features)

return new Promise(resolve => {
if (window.top.aut) {
console.log('window exists already')
window.top.aut.close()
}
// https://developer.mozilla.org/en-US/docs/Web/API/Window/open
window.top.aut = window.top.open(url, 'aut', features)

// letting page enough time to load and set "document.domain = localhost"
// so we can access it
setTimeout(() => {
cy.state('document', window.top.aut.document)
cy.state('window', window.top.aut)
resolve()
}, 500)
})
})

Note that after we get window reference, we wait 500ms to let the document to load. After that we hope the document.domain is set to localhost, allowing our Cypress Test Runner to access it without a security exception.

I am cheating here a little bit. I am using the undocumented cy.state function that internally stores document and window references. But this is a privilege of working on Cypress every day 😁

With the child window accessible, the test runs pretty much normally. For example, here is a test that clicks on the button and checks that the counter is displayed correctly.

spec.js
1
2
3
4
5
6
7
8
9
it('counts clicks', () => {
cy.openWindow('/')
cy.contains('Page body')

cy.get('button')
.click()
.click()
cy.get('#clicked').should('have.text', '2')
})
cypress.json
1
2
3
4
5
6
{
"modifyObstructiveCode": false,
"baseUrl": "http://localhost:5001",
"viewportWidth": 400,
"viewportHeight": 200
}

The Chrome windows and Cypress main window are shown below

Tests with separate child window

We can open DevTools in the child window and see that top reference is the same as self reference.

Top is self in the child window

The child window has only the elements from the loaded application under test

Elements in the child window

There is only a single context in the child window, this makes working with JavaScript a little bit simpler.

Single context

Since Cypress takes DOM snapshots for its time-traveling debugger, it still works - and the snapshots are shown inside the iframe.

Child window time traveling debugger

Note: there are probably differences in the way child window is controlled by Cypress. At least if you want to detect from the application code if the site is running inside Cypress, instead of checking window.Cypress you need to test window.opener && window.opener.Cypress.

Try it out, and open any issues please.