Imagine you have a parent window that calls window.open
. If the parent and child windows are from the same domain, the parent window can check the child window object and "see" when the user closes the child window or tab. A common code is using setInterval
to poll the childWindow.closed
property.
1 | // parent window |
The parent window might show an overlay, asking the user to work in the child window before closing it.
How would we test this scenario in Cypress?
🎁 You can find the full source code used in this blog post in the repo bahmutov/child-window-closed.
The first test
First, let's stub the window.open
to prevent Cypress from "losing" the window under test
1 | it('opens the child window', () => { |
What about closing the window? When the user closes the opened child window, the browser sets the closed: true
property. Thus we can set the same property ourselves from the test. First, let me show incorrect way of doing so.
1 | // 🚨 INCORRECT CODE, JUST FOR DEMO |
The test is green, but notice how the overlay is blinking and NOT staying open for 1 second
There is a race condition between checking if the .overlay
is visible and setting the mockWindow.closed = true
statement. A clear error is shown if we check if the overlay is visible after 1 second delay.
1 | // 🚨 INCORRECT CODE, JUST FOR DEMO |
Let's fix the test. We want to set the mockWindow.closed = true
after the second has passed. Thus we use cy.then
command.
1 | // ✅ CORRECT TEST |
Now the test shows the correct behavior. The mock window is "opened", the overlay becomes visible and stays for one second, then hides.
Using cy.invoke
In the previous example, we have created a race condition by mixing asynchronous queue of Cypress commands and immediate JavaScript statement mockWindow.closed = true
. We solved it by moving the assignment into a Cypress cy.then(callback)
command. Let me show you another way of making such assignment using Cypress command cy.invoke
to avoid thinking about the order of commands.
1 | it('opens the child window (using cy.invoke)', () => { |
The above test works correctly too.
The key is the statement line cy.wrap(mockWindowController).invoke('close')
which makes the assignment from inside the method mockWindowController.close
called during Cypress command execution, preserving the order. Unfortunately, we had to create a dummy object "mockWindowController" just to be able to "invoke" mockWindow.closed = true
method. Fortunately, there is a shortcut.
Using cy.invoke and Reflect
There is a special JavaScript API Reflect that can be used to interact with objects dynamically. The global Reflect
has set(object, method, ...args)
method we can use to set the closed
property on the mockWindow
object right from Cypress cy.invoke
.
1 | it('opens the child window (using Reflect.set)', () => { |
The key is the statement cy.wrap(Reflect).invoke('set', mockWindow, 'closed', true)
which makes it all work.
Interesting. We cy.wrap(Reflect)
to be able to invoke its method .set(...)
after all previous commands have finished (checking the overlay, waiting one second, checking if the overlay is still visible). After setting the mockWindow.closed
to true
, we check the overlay, and yes, the parent window has hidden it.