Listen To The Message

How Cypress test can confirm the message sent by the application via window.postMessage call.

The window.postMessage calls are often used by complex web applications to send messages between different frames. In this blog post, I will show how to confirm the message was sent when using Cypress end-to-end test runner.

The application

🎁 You can find the source code for this blog post in the repository bahmutov/cypress-window-message-example.

The top document loads its JavaScript and includes an inner iframe element.

public/index.html
1
2
3
4
5
6
7
<html>
<body>
<h1>Top window</h1>
<iframe src="inner.html" name="inner" width="200" height="200"></iframe>
<script src="top.js"></script>
</body>
</html>
public/inner.html
1
2
3
4
5
6
<html>
<body>
<h2>Inner</h2>
<script src="inner.js"></script>
</body>
</html>

The top window is listening to the messages sent by the inner frame

public/top.js
1
2
3
4
window.addEventListener('message', (e) => {
console.log('TOP:', 'message', e.data)
})
console.log('listening for messages')
public/inner.js
1
2
3
4
console.log('in the inner script')
setTimeout(() => {
window.top.postMessage('inner frame is ready')
}, 1000)

We see the console messages when visiting the page in the regular browser

The message sent by the inner frame gets to the top window

Let's confirm this communication works.

Make the application work

Let's visit the page from a Cypress test and see if it works

cypress/integration/spec.js
1
2
3
4
5
/// <reference types="cypress" />

it('loads and communicates', () => {
cy.visit('/')
})

Open Cypress with npx cypress open ... and the message from the inner frame never shows up ☚ī¸

The message from the inner frame gets "lost" somehow

The problem happens because the inner frame communicates using window.top.postMessage method call. When Cypress visits the site, it embeds the site in an iframe. Thus the Cypress is the top window, and it receives the message from the inner frame.

The three different windows

Hmm, how do we tell the inner iframe to send the message to the correct application window? Luckily, Cypress can rewrite the window.top references on the fly to make sure the message gets to the original intended top window object. In the cypress.json enable the following flag:

cypress.json
1
2
3
{
"experimentalSourceRewriting": true
}

Now we see the message sent during the test.

The message now gets to the application window

Ok, let's test it.

Spy on the console.log method call

The top window logs the received messages. Let's confirm one of the console.log calls prints the message from the inner frame with text "inner frame is ready". We can spy on the console.log method, see the Stubs, Spies, and Clocks guide.

1
2
3
4
5
6
7
8
9
10
11
12
13
it('spies on console.log call', () => {
cy.visit('/', {
onBeforeLoad(win) {
cy.spy(win.console, 'log').as('log')
},
})
cy.get('@log').should(
'have.been.calledWith',
'TOP:',
Cypress.sinon.match.string,
'inner frame is ready',
)
})

Notice the placeholder Cypress.sinon.match.string used during the assertion - we are not interested in the second argument, we only want to find if the console.log was called with ("TOP:", some string, "inner frame is ready") parameters. For Sinon spying assertion examples see my Spies and stubs examples page. There are several console.log calls made by the application, but we confirm only the one that interests us.

Spying on the console.log test

Listening to the window.postMessage

What if the top application window does not log the message send by the inner frame? What if we are only interested in the message "inner frame is ready" sent by the inner frame to know that the application is ready to be tested? We can listen to that message from the spec file.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
it('receives the window messages', () => {
// https://on.cypress.io/stub
const winMessage = cy.stub().as('message')
cy.visit('/', {
onBeforeLoad(win) {
win.addEventListener('message', (e) => {
// call our cy.stub (which is just a function)
winMessage(e.data)
})
},
})
// confirm the inner frame sent the ready message
cy.get('@message').should(
'have.been.calledOnceWithExactly',
'inner frame is ready',
)
})

The command cy.stub creates a function that we can call ourselves. In the test above, we call the stub function with the message data, which should be just the string "inner frame is ready"

1
2
3
4
win.addEventListener('message', (e) => {
// call our cy.stub (which is just a function)
winMessage(e.data)
})

By giving our stub an alias, we can conveniently load it again and confirm that at some point it gets called with the expected argument

1
2
3
4
5
// confirm the inner frame sent the ready message
cy.get('@message').should(
'have.been.calledOnceWithExactly',
'inner frame is ready',
)

Listening to the window messages and calling our test stub

Pretty sweet, isn't it