Test A Slow-Loading jQuery Plugin

How to both simulate a slow-loading plugin and to wait for it to load from a Cypress test.

Imagine a page that uses jQuery plugins. For example, the page might look something like this:

index.html
1
2
3
4
5
6
7
8
9
10
11
12
13
<html>
<head>
<title>The jQuery Example</title>
<script src="src/jquery.min.js"></script>
<script src="src/app.js"></script>
</head>

<body>
<p>This is paragraph</p>
<div>This is division</div>
<button id="warn">Warn</button>
</body>
</html>

We are loading jQuery and the application code. The application code loads the jQuery plugin:

src/app.js
1
2
3
4
5
6
7
8
9
10
// load the jQuery plugin dynamically
$.getScript('src/jquery.warning.js')

// use the jQuery.warning plugin on button click
$(function () {
$('button#warn').on('click', () => {
$('div').warning()
$('p').warning()
})
})

🎁 You can find the source code for this blog post in the repo bahmutov/cypress-jquery-example.

The jquery.warning.js plugin extends jQuery with a .warning() method that alerts the user

src/jquery.warning.js
1
2
3
4
5
jQuery.fn.warning = function () {
return this.each(function () {
alert('Tag Name:"' + $(this).prop('tagName') + '".')
})
}

A "normal" end-to-end test could confirm the page calls the alert twice.

cypress/e2e/spec.cy.js
1
2
3
4
5
6
7
8
9
it('checks out the jQuery plugin', () => {
cy.visit('index.html', {
onBeforeLoad(win) {
cy.stub(win, 'alert').as('alert')
},
})
cy.contains('button', 'Warn').click()
cy.get('@alert').should('have.been.calledTwice')
})

The test works nicely

The passing test

Warning: the test does pass locally, but I have noticed this test fail sometimes on CI. Remember: the timings on CI can be slow and unpredictable, thus a hidden race condition between the JS loading and the test clicking might show up.

Slowing down the plugin request

But what happens if the plugin jquery.warning.js is slow to load? Will our site still work? Probably not! Let's test it. We can slow the network request to load the plugin using the cy.intercept command.

1
2
3
4
5
6
7
8
9
10
11
it('delays the jQuery plugin load', () => {
cy.intercept(
{
method: 'GET',
pathname: '/src/jquery.warning.js',
},
() => Cypress.Promise.delay(100),
).as('plugin')
cy.visit('index.html')
cy.contains('button', 'Warn').click()
})

Cypress automatically fails the test if the application throws an error, or does not handle a rejected promise.

Testing the slow-loading jQuery plugin

So delaying the plugin source code by 100ms breaks the application, since we click the button immediately.

Tip: do you like the way I delayed loading the request using () => Cypress.Promise.delay(100)? If yes, you will find my course Cypress Network Testing Exercises pretty useful.

Waiting for the network call

Our test clicks on the button too fast - the application hasn't finished loading its source code yet. The most straightforward way for the test to "know" when to proceed is to wait for that network request to finish.

1
2
3
4
5
6
7
8
9
10
11
12
it('waits for the delayed plugin load', () => {
cy.intercept(
{
method: 'GET',
pathname: '/src/jquery.warning.js',
},
() => Cypress.Promise.delay(1000),
).as('plugin')
cy.visit('index.html')
cy.wait('@plugin')
cy.contains('button', 'Warn').click()
})

The test waits for the network call to finish

Great, we can wait for the network request. Once it finishes, we expect the application to work.

Waiting for the object property

There is another to know when the application is ready to work. Cypress tests run in the browser, thus they have direct visibility into the objects created and modified by the application. How do we know when the application has finished registering the jquery.warning.js? When the code inside the plugin executes jQuery.fn.warning = function () { statement. The jQuery object is a global set on the application's window object, and a Cypress test can get that object using the cy.window command. Then we can automatically wait for that object to get the property jQuery and even to get a nested path jQuery.fn.warning using the cy.its command.

1
2
3
4
5
6
7
8
9
10
11
it('waits for the jQuery plugin to register itself', () => {
cy.intercept(
{
method: 'GET',
pathname: '/src/jquery.warning.js',
},
() => Cypress.Promise.delay(1000),
).as('plugin')
cy.visit('index.html').its('jQuery.fn.warning')
cy.contains('button', 'Warn').click()
})

The test waits for the plugin to register itself

The key is the chain cy.visit('index.html').its('jQuery.fn.warning'). The cy.visit command yields the application's window object, and .its('jQuery.fn.warning') retries until the plugin loads and sets the warning property on the window.jQuery.fn object. If you want to be more explicit, you could write a different chain of commands:

1
2
3
4
cy.visit('index.html')
cy.window()
.its('jQuery.fn.warning')
.should('be.a', 'function')

Tip: see the cy.its examples at glebbahmutov.com/cypress-examples.

Tip 2: I would rethink how the application works in this case. A slow network plus a fast-clicking user will get an error. You might want to disable the button until the plugin loads and the button is ready to work without crashing.