Access React Components From Cypress E2E Tests

How to access the internal React component state from Cypress end-to-end tests.

Let's take a look at a simple React component with some internal state. You can find this component in src/Example.js file of my repo bahmutov/react-counter.

src/Example.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
32
33
34
35
36
37
import React from 'react'
import './Example.css'

export class Example extends React.Component {
constructor(props) {
super(props)
this.state = {
count: props.initialCount || 0,
}
}

double() {
console.log('doubling the current value', this.state.count)
this.setState({ count: this.state.count * 2 })
}

render() {
return (
<div className="Example">
<p className="full">
You clicked <span data-cy="count">{this.state.count}</span> times
</p>
<button
className="full"
data-cy="add"
onClick={() => this.setState({ count: this.state.count + 1 })}
>
Click me
</button>

<button className="full" data-cy="double" onClick={() => this.double()}>
Double me
</button>
</div>
)
}
}

Using end-to-end tests we can verify the code works by observing the DOM elements the component renders on the page.

cypress/integration/e2e.js
1
2
3
4
5
6
7
8
it('counts', () => {
cy.visit('/')
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.contains('button', 'Double').click()
cy.contains('[data-cy=count]', '4')
})

The test only works with the application through the HTML elements.

E2E test

Access the React component

Can we find the React component instance? Yes, using the plugin cypress-react-selector we can find the Example component, rather than the DOM elements it renders. That is very convenient for checking the internal state of the component. For example, we could verify that clicking the button "Click me" three times changes the state of the component.

cypress/integration/three.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import 'cypress-react-selector'

it('checks the state', () => {
cy.visit('/')
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click().click()
cy.contains('[data-cy=count]', '3')

// find the React component
cy.waitForReact(1000, '#root')

// three equivalent ways of checking component "Example" with "count: 3" state
cy.getReact('Example').getCurrentState().should('have.property', 'count', 3)
cy.getReact('Example').getCurrentState().should('deep.include', {
count: 3,
})
cy.getReact('Example', { state: { count: 3 } })
})

The tests find the component named Example and confirm its internal state.

E2E test that checks the React component state

Tip: for checking a complex object, cy-spok is the best.

When the application creates the Example component, it sets its prop initialCount, which we can use with cy.getReact('Example') to find the right component.

src/index.js
1
<Example initialCount={0} />
cypress/integration/prop.js
1
2
3
4
5
6
7
8
9
10
11
12
import 'cypress-react-selector'

it('uses prop to find the component', () => {
cy.visit('/')
// find the React component
cy.waitForReact(1000, '#root')
cy.react('Example', { props: { initialCount: 0 } })
.should('be.visible')
.contains('button', 'Click me')
.click()
.click()
})

Finding a component by its prop

🎓 The plugin cypress-react-selector provides two high-level commands for finding components. If you need to find the DOM element by React component prop or state, use the cy.react command. If you want to find and access the React component instance, use the cy.getReact command.

In a sense, what we are able to do is what the React DevTools browser extension shows for the component.

The Example component in the React DevTools

Trigger component methods

Our Example component has an instance method double

1
2
3
4
double() {
console.log('doubling the current value', this.state.count)
this.setState({ count: this.state.count * 2 })
}

Can we somehow call that method from Cypress test? From the React component we need to get to the React Fiber, here is my solution following this StackOverflow answer.

cypress/integration/utils.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// note that I am not even using cypress-react-selector here
export const getReactFiber = (el) => {
const key = Object.keys(el).find((key) => {
return (
key.startsWith('__reactFiber$') || // react 17+
key.startsWith('__reactInternalInstance$') // react <17
)
})
if (!key) {
return
}
return el[key]
}

// react 16+
export const getComponent = (fiber) => {
let parentFiber = fiber.return
while (typeof parentFiber.type == 'string') {
parentFiber = parentFiber.return
}
return parentFiber
}

Given a DOM element, we can grab the fiber and the component reference.

cypress/integration/call-method.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { getReactFiber, getComponent } from './utils'

it('calls Example double()', () => {
cy.visit('/')
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.get('.Example').then((el$) => {
const fiber = getReactFiber(el$[0])
console.log(fiber)
const component = getComponent(fiber)
console.log(component.stateNode)
})
})

In the component's prototype, we can discover common methods like setState and double.

Getting to the component reference from the DOM element

Let's call the method double() from the test.

1
2
3
4
5
6
7
8
9
cy.get('.Example').then((el$) => {
const fiber = getReactFiber(el$[0])
console.log(fiber)
const component = getComponent(fiber)
console.log(component.stateNode)
cy.log('calling **double()**')
component.stateNode.double()
})
cy.contains('[data-cy=count]', '4')

Calling the component's method and then checking the updated page

Custom command

Let's take our code to access the React component and make it into a child custom command.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { getReactFiber, getComponent } from './utils'

Cypress.Commands.add('getComponent', { prevSubject: 'element' }, ($el) => {
const fiber = getReactFiber($el[0])
if (!fiber) {
throw new Error('Could not find React Fiber')
}
const component = getComponent(fiber)
if (!component) {
throw new Error('Could not find React Component')
}
if (!component.stateNode) {
throw new Error('Could not find React Component stateNode')
}
return component.stateNode
})

Using this command we can directly access the component's state, overwrite it, and call the component's methods.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
it('checks and modifies state', () => {
cy.visit('/')
cy.contains('[data-cy=count]', '0')
cy.get('[data-cy=add]').click().click()
cy.contains('[data-cy=count]', '2')
cy.get('.Example')
.as('example')
.getComponent()
.its('state')
.should('deep.include', { count: 2 })

cy.get('@example').getComponent().invoke('double')
cy.contains('[data-cy=count]', '4')
cy.get('@example').getComponent().invoke('double')
cy.contains('[data-cy=count]', '8')

cy.log('**call setState**')
// set the application into the state that is normally impossible
// to reach by just using the page interactions
cy.get('@example').getComponent().invoke('setState', { count: -99 })
cy.contains('[data-cy=count]', '-99')
})

Accessing the component using the custom command

A word of caution

Should you call the internal component methods from your end-to-end tests? Only in the extraordinary circumstances, I think. If there is no other way to verify the behavior of the application or trigger an application action, you could. At the same time, remember that you are tying your tests to the implementation, which will make the tests harder to update. But sometimes we need to call these "app actions" to get to the data state we need to test.

Links