Send Data From The Application To The Cypress Test

The application can provide more information to the test runner for more meaningful end-to-end tests.

Imagine an application where the user fills a form. The application takes the form, cleans up the entered data, and sends the form to the backend API. The public/app.js has the following code

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
document
.querySelector('form input[type=submit]')
.addEventListener('click', (e) => {
e.preventDefault()
// grab the form element and form the data to be sent
const form = document.querySelector('form')
const formData = new FormData(form)
const data = {}
formData.forEach((value, key) => {
data[key] = value
})
if (data.phone) {
// clean up the phone number by removing all non-digit characters
data.phone = data.phone.replace(/[^\d]/g, '')
}

// send the Ajax request to the server
const request = new XMLHttpRequest()
request.open('POST', '/api/v1/message')
request.setRequestHeader('Content-Type', 'application/json')
request.send(JSON.stringify(data))
request.onreadystatechange = () => {
if (request.readyState === 4) {
if (request.status === 200) {
// all good
} else {
alert('Error: ' + request.status)
}
}
}
})

🎁 You can find the source code for this blog post in the repository bahmutov/cypress-track-events.

Let's write a test. We need to visit the page, type the test data into the input fields, and confirm the outgoing network call. Because our backend API is not ready yet, we will stub the server's response.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
it('sends a form', () => {
cy.visit('public/index.html')

cy.intercept('POST', '/api/v1/message', {}).as('post')
cy.get('form').within(() => {
cy.get('input[name=name]').type('John Doe')
cy.get('input[name=email]').type('[email protected]')
cy.get('input[name=phone]').type('+1 (555) 555-5555')
cy.get('input[name=message]').type('Hello World')
cy.get('input[type=submit]').click()
})

// confirm the network request was sent correctly
// hmm, how do we verify the request as sent by the application?
cy.wait('@post').its('request.body', { timeout: 0 })
})

The above test enters the data into the form and submits it

We hit a slight problem. The application modifies the data before sending it. Of course, the test should know exactly what the expected data is, but let's pretend we do not know it. How do we check the network call?

When Cypress controls the browser, the application runs in an iframe, and Cypress sets a property Cypress on the application's window object. Thus the application can "know" if it is running inside a Cypress test by checking that property:

public/app.js
1
2
3
4
5
6
7
8
if (data.phone) {
// clean up the phone number by removing all non-digit characters
data.phone = data.phone.replace(/[^\d]/g, '')
}

if (window.Cypress) {
// Hurray, we are being tested!
}

In our case, the application can pass to the test runner the data object it is about to send to the backend. If the application finds the track() method on the Cypress object, then it would call it with the constructed data object.

public/app.js
1
2
3
4
5
6
7
8
9
10
11
if (data.phone) {
// clean up the phone number by removing all non-digit characters
data.phone = data.phone.replace(/[^\d]/g, '')
}

if (window.Cypress) {
// Hurray, we are being tested!
if (window.Cypress.track) {
window.Cypress.track('form', data)
}
}

Using the modern ES6 optional chaining syntax supported by all modern browsers, we can safely pass the data using a one-liner (with lots of comments):

public/app.js
1
2
3
4
5
6
7
8
9
10
11
12
if (data.phone) {
// clean up the phone number by removing all non-digit characters
data.phone = data.phone.replace(/[^\d]/g, '')
}

// if we are running the application inside a Cypress browser test
// send the internal data to be confirmed or used by the test
// We are using optional chaining operator "?." to safely
// access each property if it exists (or do nothing if it doesn't)
// including the last call to the "track()" method if it exists
// See https://github.com/tc39/proposal-optional-chaining
window?.Cypress?.track?.('form', data)

Of course, we need to create the Cypress.track method ourselves. We also need to clean it up to avoid accidentally leaking it from one test to another. Using beforeEach and afterEach hooks works well for this purpose.

cypress/integration/spec.js
1
2
3
4
5
6
7
8
beforeEach(() => {
Cypress.track = cy.stub().as('track')
})

afterEach(() => {
// clean up Cypress.track property
delete Cypress.track
})

Now we can finish our test. We can confirm the stub function Cypress.track was called with the first argument "form". From the first such call, we can grab the second argument, which should be an object. We can confirm some properties of the object, and then also use it to confirm what the application sent to the server.

cypress/integration/spec.js
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
it('sends the expected form', () => {
cy.visit('public/index.html')

cy.intercept('POST', '/api/v1/message', {}).as('post')
cy.get('form').within(() => {
cy.get('input[name=name]').type('John Doe')
cy.get('input[name=email]').type('[email protected]')
cy.get('input[name=phone]').type('+1 (555) 555-5555')
cy.get('input[name=message]').type('Hello World')
cy.get('input[type=submit]').click()
})

// hmm, how do we verify the request as sent by the application?
cy.get('@track')
.should('be.calledWith', 'form')
.its('firstCall.args.1')
// you can confirm some properties of the sent data
// Tip: use https://github.com/bahmutov/cy-spok
// for similar object and array assertions
.should('deep.include', {
name: 'John Doe',
message: 'Hello World',
})
.then((data) => {
cy.log(JSON.stringify(data))
// confirm the network request was sent correctly
cy.wait('@post')
.its('request.body', { timeout: 0 })
.should('deep.equal', data)
})
})

Receive the data from the application and use it to confirm the network call

Tip: use cy-spok to confirm properties of any object, thank me later.