Dealing With 3rd Party Scripts In Cypress Tests

How to ensure the external scripts are ready to be used when testing the site, plus how to test a chat widget.

Let's take a look at an application that uses 3rd party script, like a chat widget. Here is my site with the Tidio widget opened:

Site with a 3rd party chat widget

Our first question might be how to ensure the 3rd party JavaScript has loaded before starting to test the site. In this particular application, the button "Open chat 🗣" calls the code tidioChatApi.open() after a click

public/app.js
1
2
3
4
5
6
7
8
9
document.getElementById('open-chat').addEventListener('click', function () {
console.log('opening chat')
// https://docs.tidio.com/docs/other_methods

// safeguard against slow-loading JavaScript code
if (window.tidioChatApi) {
tidioChatApi.open()
}
})

If the tidioChatApi library is slow to load or initialize, the click does nothing, leading to the user frustration. Let's see how to slow down the 3rd party JavaScript to test this.

🎁 You can find the full source code shown in this blog post as well as links to multiple videos explaining the solutions step-by-step in the repo bahmutov/cypress-3rd-party-script-example.

Slow down the JavaScript resource

The application is loading the 3rd party JS library asynchronously

1
2
3
4
5
6
7
8
9
// load 3rd party libraries
window.addEventListener('load', function () {
// add the script tag to the HEAD
const head = document.getElementsByTagName('head')[0]
const script = document.createElement('script')
script.type = 'text/javascript'
script.src = 'https://code.tidio.co/zwjhqkduaeqdmkflwoyfcmqd64fj2a3s.js'
head.appendChild(script)
})

We can intercept the code.tidio.co resource and slow it down using the cy.intercept command

1
2
3
4
5
6
7
8
9
it('by returning a promise', () => {
cy.intercept('https://code.tidio.co/*.js', (req) =>
Cypress.Promise.delay(3000).then(() => req.continue()),
).as('tidio')
cy.visit('public/index.html')
cy.wait('@tidio')
// by now the JS should have loaded
cy.get('#open-chat').click()
})

The test waits for the network resource to finish before clicking the button.

Waiting for the slowed down network request to return

We can shorten the intercept command by returning a delayed promise without anything. This will make Cypress think you are making a spy, thus the request will continue to the server.

1
2
3
4
// slows down the network request by 3 seconds
cy.intercept('https://code.tidio.co/*.js', () =>
Cypress.Promise.delay(3000),
).as('tidio')

Works the same was as req.continue() above.

Wait for 3rd party initialization

Even if the JavaScript is returned, does not mean it is ready to work - it might require additional code to be loaded, DOM elements to be created, etc. The application is using window.tidioChatApi object - let's make our test wait for that object to be ready before clicking the button.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('waits for the chat object to be created', () => {
cy.intercept('https://code.tidio.co/*.js', () =>
Cypress.Promise.delay(3000),
).as('tidio')
cy.visit('public/index.html')
// wait for the chat object to be created
// before clicking on the button
// https://on.cypress.io/its
// we increase the timeout, because the network request
// is slowed down by 3 seconds, leaving very little time
// for the the library to load and start working
cy.window().its('tidioChatApi', { timeout: 6000 })
cy.get('#open-chat').click()
})

The test waits more precisely than the test that simply waited for the network request using cy.wait('@tidio') and you can see it from the recording - it actually opens the browser widget!

Waiting for the window property to exist

Confirm the method was called

Now let's confirm the application calls the method tidioChatApi.open(). We need to create a spy before clicking the button. We get to the method to spy on using the same cy.window().its(...) commands as above.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
it('calls chat open method', () => {
cy.visit('public/index.html')
// get the window.tidioChatApi object
cy.window()
.its('tidioChatApi')
// spy on tidioChatApi.open method call
.then((tidioChatApi) => {
cy.spy(tidioChatApi, 'open').as('open')
})
// click on the button
cy.get('#open-chat').click()
// confirm the spy was called once without arguments
cy.get('@open').should('be.calledOnceWithExactly')
})

The video shows the application does in fact call the method without any arguments.

Spy on the open call the application makes

Call open from the test

We can even call the open() method ourselves from the test if necessary - what I call app action and which is a unique property of the Cypress tests.

1
2
3
4
5
6
7
it('opens the chat from the test code', () => {
cy.visit('public/index.html')
// wait for the chat object to be created
// and then invoke a method on it
// https://on.cypress.io/invoke
cy.window().its('tidioChatApi', { timeout: 10000 }).invoke('open')
})

Call 3rd party method from the test

Subscribe to events

What if we let the application call the 3rd party chat methods, but subscribe from the test runner to the events it delivers? Sure thing:

1
2
3
4
5
it('delivers the ready event', () => {
cy.visit('public/index.html')
cy.window().its('tidioChatApi').invoke('on', 'ready', cy.stub().as('ready'))
cy.get('@ready').should('be.called')
})

The test confirms the chat widget sends the "ready" event

Tip: for more cy.spy and cy.stub examples, see my Spies, Stubs, and Clock page.

See also

I have recorded several videos showing how to deal with 3rd party JavaScript code based on this chat application. Take a look at