Stub window.open

How to stub the window.open method for every window

Stub window.open

Motivation

If an application calls window.open during Cypress test it might lead to two problems:

  • the new URL might point at a different domain, rendering Cypress "blind" and unable to continue the test
  • the new URL might open in the second tab, invisible to Cypress

Stubbing an object's method

In order to stub (replace) an object's method we need three things:

  • a reference to the object
  • method's name
  • we also have to register the stub before the application calls the method we are replacing

🖥 I explain how the commands cy.spy and cy.stub work at the start of the presentation How cy.intercept works.

Luckily for us stubbing the window.open satisfies all the criteria easily

  • the command cy.window gets the reference to the application's window object
  • the method's name is open
  • we can use onBeforeLoad callback in the cy.visit to stub the window.open method. This callback runs when the window object is ready, but before any application code runs
1
2
3
4
5
6
7
8
9
10
it('opens a new window', () => {
cy.visit('/', {
onBeforeLoad (win) {
cy.stub(win, 'open').as('open')
}
})
// triggers the application to call window.open
cy.click('Open new window')
cy.get('@open').should('have.been.calledOnce')
})

When window changes

If the application navigates to a new page or even reloads, the old window object is destroyed and the new window object is created. Thus our window.open stub can "disappear": the application calls window.open, but the stub does not intercept the new calls.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 🚨 SHOWING THE PROBLEM
it('opens a new window', () => {
cy.visit('/', {
onBeforeLoad (win) {
cy.stub(win, 'open').as('open')
}
})
// triggers the application to call window.open
cy.click('Open new window')
cy.get('@open').should('have.been.calledOnce')

// cause the window to be recreated
cy.reload()
cy.click('Open new window')
// THE PROBLEM: the stub does not "see"
// second the window.open call
cy.get('@open').should('have.been.calledOnce')
})

We need to register window.open stub for every window object created during the test. We can use cy.on('window:before:load') event:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// ✅ CORRECT SOLUTION
it('opens a new window', () => {
// create a single stub we will use
const stub = cy.stub().as('open')
cy.on('window:before:load', (win) => {
cy.stub(win, 'open').callsFake(stub)
})

cy.visit('/')
// triggers the application to call window.open
cy.click('Open new window')
cy.get('@open').should('have.been.calledOnce')

// cause the window to be recreated
cy.reload()
cy.click('Open new window')
// all window.open calls are correctly forwarded to our stub
cy.get('@open').should('have.been.calledTwice')
})

The above test correctly prevents window.open from causing problems when the application reloads or navigates to another page.

cy.on vs Cypress.on

We have used cy.on('window:before:load') to register our stub. We could have used Cypress.on('window:before:load'), but then we could not use the cy.stub command inside the callback - Cypress.on runs outside a test context and thus cannot use any cy. commands. It is often used to perform general application actions. For example we could remove window.fetch method from every window object

1
2
3
4
5
Cypress.on('window:before:load', (win) => {
// test how the application handles
// older browsers without fetch support
delete window.fetch
})

In our window.open case we can bypass the problem. We can return the same stub for every window, we just have to return the variable prepared by the test via closure scope:

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
26
27
28
29
30
31
32
33
34
35
36
// variable that will hold cy.stub created in the test
let stub

Cypress.on('window:before:load', (win) => {
// if the test has prepared a stub
if (stub) {
// the stub function is ready
// always returns it when the application
// is trying to use "window.open"
Object.defineProperty(win, 'open', {
get() {
return stub
}
})
}
})

beforeEach(() => {
// let the test create the stub if it needs it
stub = null
})

it('works', () => {
// note: cy.stub returns a function
stub = cy.stub().as('open')
cy.visit('/')
// triggers the application to call window.open
cy.click('Open new window')
cy.get('@open').should('have.been.calledOnce')

// cause the window to be recreated
cy.reload()
cy.click('Open new window')
// all window.open calls are correctly forwarded to our stub
cy.get('@open').should('have.been.calledTwice')
})

Tip: you can reset a spy or a stub by invoking its reset() method

1
2
3
4
5
6
7
8
9
10
// triggers the application to call window.open
cy.click('Open new window')
cy.get('@open')
.should('have.been.calledOnce')
.invoke('reset')

// the stub was reset and started from zero
cy.reload()
cy.click('Open new window')
cy.get('@open').should('have.been.calledOnce')

See also

I have described in detail how to deal with anchor links and window.open in two sections in the blog post Cypress Tips and Tricks:

There is also more documentation and examples available at:

You can also read about and practice with cy.stub in the section of the Cypress Testing Workshop

See the blog post Stub window.track