Return A Fake Window Object

Return a fake window object from `window.open` method stub.

When dealing with Deal with Second Tab in Cypress there might be an application edge case. Instead of window.open(url) the application might create a new window object and navigate by setting its location property. This is a valid syntax, see Dev docs.

1
2
3
4
5
// notice the first argument is an empty string instead of URL
const win = window.open('', '2nd-window', 'popup,width=300,height=300')
if (win) {
win.location = 'https://acme.co'
}

This blog post shows how to test this situation using Cypress test runner. First, you need to stub the window.open method before the application calls it.

1
2
3
4
5
6
// our own object we will return to the application
// instead of opening the real window
const winProxy = {}
cy.window().then((win) => {
cy.stub(win, 'open').as('open').returns(winProxy)
})

🎁 You can find the full source code in my "Stub window.open" recipes at glebbahmutov.com/cypress-examples website.

Now that the stub is set, we can click on the button.

1
cy.contains('button', 'Open window').click()

If we know all expected arguments, we can write a precise assertion

1
2
3
4
5
6
cy.get('@open').should(
'have.been.calledOnceWithExactly',
'',
'2nd-window',
'popup,width=300,height=300',
)

Confirm all window.open arguments

If we know only some expected arguments, we can use Sinon match placeholder. For example, if we only know the third argument to be a string:

1
2
3
4
5
6
cy.get('@open').should(
'have.been.calledOnceWithExactly',
'',
'2nd-window',
Cypress.sinon.match.string,
)

Using a Sinon match placeholder

We can even write a more complex logic to verify individual arguments

1
2
3
4
5
6
7
8
9
10
11
cy.get('@open')
.should('have.been.calledOnce')
.its('firstCall.args')
// destructure the arguments array into named parameters
.then(([url, target, params]) => {
expect(url, 'url').to.equal('')
expect(target, 'target').to.include('window')
expect(params, 'dimensions')
.to.match(/width=\d+/)
.and.to.match(/height=\d+/)
})

Verify individual call arguments

If you want to verify the parsed width and height, capture the numbers using a regular expression. Let's confirm the popup's width is between 250 and 350 pixels.

1
2
3
4
5
6
7
cy.get('@open')
.should('have.been.calledOnce')
.its('firstCall.args.2')
.invoke('match', /width=(?<width>\d+)/)
.its('groups.width')
.then(Number)
.should('be.within', 250, 350)

Parse and verify the popup's width in pixels

Let's confirm the application sets the window's location property. Since we returned the local variable to an empty object, we can confirm it receives a property.

1
2
3
4
5
cy.wrap(winProxy).should(
'have.property',
'location',
'https://acme.co',
)

By the way, the above syntax retries. Let's say the application set the location property after a delay. Imagine the following application:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<button id="open">Open window</button>
<script>
document
.getElementById('open')
.addEventListener('click', () => {
const win = window.open(
'',
'2nd-window',
'popup,width=300,height=300',
)
if (win) {
setTimeout(() => {
win.location = 'https://acme.co'
}, 1500)
}
})
</script>

The following test retries and passes after 1.5 second pause

1
2
3
4
5
6
7
8
9
10
11
12
13
// our own object we will return to the application
// instead of opening the real window
const winProxy = {}
cy.window().then((win) => {
cy.stub(win, 'open').as('open').returns(winProxy)
})
cy.contains('button', 'Open window').click()
// confirm the winProxy object gets the location property
cy.wrap(winProxy).should(
'have.property',
'location',
'https://acme.co',
)

Very nice. For more advice on dealing with 2nd window / tab in Cypress tests, read my Deal with Second Tab in Cypress.