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
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 = newFormData(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 = newXMLHttpRequest() 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) } } } })
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')
// 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 }) })
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.
afterEach(() => { // clean up Cypress.track property deleteCypress.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.
// 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) }) })
Tip: use cy-spok to confirm properties of any object, thank me later.