DEV Community

Smilepk
Smilepk

Posted on

Control React Applications From Cypress Tests

How to access and change the internal component state from end-to-end tests using cypress-react-app-actions.
In the previous blog post Access React Components From Cypress E2E Tests I have shown how the test code could get to the React component's internals, similar to what the React DevTools browser extension does. In this blog post, I will show how to use this approach to drastically speed up end-to-end tests. The idea is to control the application by setting its internal state rather than using the page UI in every test. We will split a single long test into individual tests, each starting the app where it needs it to be in an instant, rather than going through already tested UI commands. It is similar to what I have shown a long time ago in the blog post Split a very long Cypress test into shorter ones using App Actions. But the approach described in this blog post does not need any modifications to the application code, which is a big deal.

A single long test #

Imagine our application contains several forms to fill. The test has to fill the first page before the second page appears. Once the second page is filled, the third page is shown. After filling the third page, the form is submitted and the test is done


cypress/integration/single-test.js

/// <reference types="cypress" />
const typeOptions = { delay: 35 }

it('books hotel (all pages)', () => {
  cy.visit('/')

  cy.log('**First page**')
  cy.contains('h1', 'Book Hotel 1')

  cy.get('#first').type('Joe', typeOptions)
  cy.get('#last').type('Smith', typeOptions)
  cy.get('#email').type('my-email@foo.bar', typeOptions)

  cy.get('#field1a').type('Field 1a text value', typeOptions)
  cy.get('#field1b').type('Field 1b text value', typeOptions)
  cy.get('#field1c').type('Field 1c text value', typeOptions)
  cy.get('#field1d').type('Field 1d text value', typeOptions)
  cy.get('#field1e').type('Field 1e text value', typeOptions)

  cy.contains('Next').click()

  cy.log('**Second page**')
  cy.contains('h1', 'Book Hotel 2')
  // we are on the second page

  cy.get('#username').type('JoeSmith', typeOptions)
  cy.get('#field2a').type('Field 2a text value', typeOptions)
  cy.get('#field2b').type('Field 2b text value', typeOptions)
  cy.get('#field2c').type('Field 2c text value', typeOptions)
  cy.get('#field2d').type('Field 2d text value', typeOptions)
  cy.get('#field2e').type('Field 2e text value', typeOptions)
  cy.get('#field2f').type('Field 2f text value', typeOptions)
  cy.get('#field2g').type('Field 2g text value', typeOptions)
  cy.contains('Next').click()

  cy.log('**Third page**')
  cy.contains('h1', 'Book Hotel 3')

  cy.get('#field3a').type('Field 3a text value', typeOptions)
  cy.get('#field3b').type('Field 3b text value', typeOptions)
  cy.get('#field3c').type('Field 3c text value', typeOptions)
  cy.get('#field3d').type('Field 3d text value', typeOptions)
  cy.get('#field3e').type('Field 3e text value', typeOptions)
  cy.get('#field3f').type('Field 3f text value', typeOptions)
  cy.get('#field3g').type('Field 3g text value', typeOptions)
  cy.contains('button', 'Sign up').click()

  cy.contains('button', 'Thank you')
})

Enter fullscreen mode Exit fullscreen mode

The above test takes almost 19 seconds to finish. Of course, it is the slowest end-to-end test in the world, but you have to sit and wait for it, even if you are only interested in changing how it tests the form submission for example.

Image description

The app state after the first page #

All the fields we fill on the first page go into the internal state of the application. The application creates a form for each page and passes the change handler function as a prop.


index.js
import Step1 from './Step1.jsx'

handleChange = (event) => {
  const { name, value } = event.target
  this.setState({
    [name]: value,
  })
}

handleSubmit = (event) => {
  event.preventDefault()

  console.log('submitting state', this.state)

  const { email, username } = this.state

  this.setState({
    submitted: true,
  })

  alert(`Your registration detail: \n
          Email: ${email} \n
          Username: ${username}`)
}

<Step1
  currentStep={this.state.currentStep}
  handleChange={this.handleChange}
  email={this.state.email}
/>
<Step2
  currentStep={this.state.currentStep}
  handleChange={this.handleChange}
  username={this.state.username}
/>
<Step3
  currentStep={this.state.currentStep}
  handleChange={this.handleChange}
  password={this.state.password}
  submitted={this.state.submitted}
/>

Enter fullscreen mode Exit fullscreen mode

Thus we can validate that the Step1 component is working correctly by checking the state after we fill the form through the page.


cypress/integration/actions.js

beforeEach(() => {
  cy.visit('/')
})

it('first page', () => {
  cy.log('**First page**')
  cy.contains('h1', 'Book Hotel 1')

  cy.get('#first').type('Joe', typeOptions)
  cy.get('#last').type('Smith', typeOptions)
  cy.get('#email').type('my-email@foo.bar', typeOptions)

  cy.get('#field1a').type('Field 1a text value', typeOptions)
  cy.get('#field1b').type('Field 1b text value', typeOptions)
  cy.get('#field1c').type('Field 1c text value', typeOptions)
  cy.get('#field1d').type('Field 1d text value', typeOptions)
  cy.get('#field1e').type('Field 1e text value', typeOptions)

  cy.contains('Next').click()

  cy.log('Second page')
  cy.contains('h1', 'Book Hotel 2')
})

Enter fullscreen mode Exit fullscreen mode

We are testing the page just like a human user would - by going to each input field and typing text. Once the fields are filled, we click the button "Next" and check if we end up on the second page. But how do we check if the values we typed really were stored correctly by the application?

By getting access to the application state through React internals. I wrote cypress-react-app-actions plugin that gets to the React component from a DOM element, similar to how React DevTools browser extension works.


$ npm i -D cypress-react-app-actions
+ cypress-react-app-actions@1.0.2

Enter fullscreen mode Exit fullscreen mode

We should import the plugin from our spec or from the support file


/ https://github.com/bahmutov/cypress-react-app-actions
import 'cypress-react-app-actions'
// now we can use the child command .getComponent()

Enter fullscreen mode Exit fullscreen mode

Let's see what fields the component has at the end of the test above.

cy.log('Second page')
cy.contains('h1', 'Book Hotel 2')
cy.get('form')
  .getComponent()
  .its('state')
  .then(console.log)

``

The application state object after finishing step one

Tip: you can see all component fields and methods by printing it to the console with cy.get('form').getComponent().then(console.log) command.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lsj0svk123alg2y2a1wb.png)

The component's state should always include the field values we have typed, so let's verify this. We could use "deep.equal" or "deep.include" assertion, or even cy-spok here.

Enter fullscreen mode Exit fullscreen mode

cypress/integration/actions.js
Image description
const startOfSecondPageState = {
currentStep: 2,
email: 'my-email@foo.bar',
field1a: 'Field 1a text value',
field1b: 'Field 1b text value',
field1c: 'Field 1c text value',
field1d: 'Field 1d text value',
field1e: 'Field 1e text value',
first: 'Joe',
last: 'Smith',
username: '',
}

beforeEach(() => {
cy.visit('/')
})

it('first page', () => {
...
cy.contains('Next').click()

cy.log('Second page')
cy.contains('h1', 'Book Hotel 2')
cy.get('form')
.getComponent()
.its('state')
.should('deep.equal', startOfSecondPageState)
})


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/m0ytmdw6r0qdm8gygock.png)

Enter fullscreen mode Exit fullscreen mode

// the end of the first test
cy.get('form')
.getComponent()
.its('state')
.should('deep.equal', startOfSecondPageSt


Thus we can set the app's state to the object startOfSecondPageState and the application will behave as if we went through the form, filling it by typing. It is the same application behaviour.

Enter fullscreen mode Exit fullscreen mode

beforeEach(() => {
cy.visit('/')
})

it('second page', () => {
cy.get('form').getComponent().invoke('setState', startOfSecondPageState)

cy.log('Second page')
cy.contains('h1', 'Book Hotel 2')
// start filling input fields on page 2
cy.get('#username').type('JoeSmith', typeOptions)
cy.get('#field2a').type('Field 2a text value', typeOptions)
cy.get('#field2b').type('Field 2b text value', typeOptions)
cy.get('#field2c').type('Field 2c text value', typeOptions)
cy.get('#field2d').type('Field 2d text value', typeOptions)
cy.get('#field2e').type('Field 2e text value', typeOptions)
cy.get('#field2f').type('Field 2f text value', typeOptions)
cy.get('#field2g').type('Field 2g text value', typeOptions)
cy.contains('Next').click()

cy.log('Third page')
cy.contains('h1', 'Book Hotel 3')
})


[](https://glebbahmutov.com/blog/images/react-app-actions/second.gif)

Beautiful. How does the application finish? Again - it has a certain internal state we can verify.

Enter fullscreen mode Exit fullscreen mode

const startOfThirdPageState = {
...startOfSecondPageState,
currentStep: 3,
username: 'JoeSmith',
field2a: 'Field 2a text value',
field2b: 'Field 2b text value',
field2c: 'Field 2c text value',
field2d: 'Field 2d text value',
field2e: 'Field 2e text value',
field2f: 'Field 2f text value',
field2g: 'Field 2g text value',
}
...
cy.log('Third page')
cy.contains('h1', 'Book Hotel 3')
cy.get('form')
.getComponent()
.its('state')
.should('deep.equal', startOfThirdPageState)


The third page #

We similarly start the third test to verify we can fill the form on the third page. We set the state to the same state object the second test has finished with. Even better - we know the user will submit the form, so we can spy on the component's method handleSubmit.

Enter fullscreen mode Exit fullscreen mode

it('third page', () => {
cy.get('form')
.getComponent()
.then((comp) => {
cy.spy(comp, 'handleSubmit').as('handleSubmit')
})
.invoke('setState', startOfThirdPageState)

cy.log('Third page')
cy.contains('h1', 'Book Hotel 3')
...
cy.contains('button', 'Sign up').click()
cy.contains('button', 'Thank you')

cy.get('form').parent().getComponent().its('state').should('deep.include', {
submitted: true,
username: 'JoeSmith',
})

// the spy is called once
cy.get('@handleSubmit').should('be.calledOnce')
})



[](https://glebbahmutov.com/blog/images/react-app-actions/second.gif)


![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/38ql60juorhbzn2vz9i6.png)

The third test verifies the form was submitted

It is up to the developer to decide which application internal properties to verify.

Invoking app actions #

We can verify the internal application state and we can call the component's methods. For example, we can call the form's submit method ourselves.











Enter fullscreen mode Exit fullscreen mode

Top comments (1)

Collapse
 
kitty_garrison profile image
Kitty Garrison

Looks like you are missing a backtick ` in the code block after "Let's see what fields the component has..." that is causing opposite formatting to the rest of your article. I look forward to reading it after the format fix.