A Quick React Component Test

Confirm the onClick handler is executed when clicking on the button component.

I laid My Vision for Component Tests in Cypress in Cypress a long time ago. The first public presentation about End-to-End + Component test combination was made in the presentation The Shape Of Testing Pyramid at AssertJS in February of 2018, ughh 4 years ago. Where did the time go? Where is Cypress v10 (aka Cypress X) with full production component testing? I don't know.

But here is a user asking about it on Cypress Discord channel

How to confirm the button was really clicked?

Let me answer this question in the strongest way - but creating a working example.

🎁 You can find my source code in the repository bahmutov/click-returns-true.

I have created a new project using the following commands:

1
2
3
4
5
6
7
8
9
10
11
# make a new folder
$ mkdir click-returns-true
$ cd click-returns-true
# create a new Git repository
$ git init
# initialize a new NPM project
$ npm init --yes
# add Cypress and Prettier
$ npm i -D cypress prettier
+ [email protected]
+ [email protected]

I added Cypress and Prettier as dev dependencies. I love using Prettier. Note: the coming Cypress v10 release might change how the application looks and runs, this blog post uses v9. I hope the post shows the main principles that still apply.

Ok, we need to execute a React component. So I will add React, Read DOM, and react-scripts dependencies.

1
2
3
4
$ npm i -S react@17 react-dom@17 react-scripts
+ [email protected]
+ [email protected]
+ [email protected]

I add browserslist field to the package.json file

package.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
{
"name": "click-returns-true",
"dependencies": {
"react": "^17.0.2",
"react-dom": "^17.0.2",
"react-scripts": "^5.0.1"
},
"devDependencies": {
"@cypress/react": "^5.12.4",
"@cypress/webpack-dev-server": "^1.8.4",
"cypress": "^9.6.0",
"prettier": "^2.6.2"
},
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 11"
]
}

Question: why did I not use Yarn? In this example it does not matter if I use npm or yarn to install NPM dependencies.

Now I need to follow the Cypress component testing guide to install specific dependencies for mounting React components inside Cypress. I suggest following the examples in cypress-io/cypress-component-testing-examples. In particular, I installed two dev dependencies:

1
2
3
$ npm i -D @cypress/react @cypress/webpack-dev-server
+ @cypress/[email protected]
+ @cypress/[email protected]

Super, and here is my modified plugin file that uses the React dev server provided by the react-scripts

cypress/plugins/index.js
1
2
3
4
5
6
const injectDevServer = require('@cypress/react/plugins/react-scripts')

module.exports = (on, config) => {
injectDevServer(on, config)
return config
}

Now let's test our React component. I write the following component spec file

cypress/component/click.spec.ct.js
1
2
3
4
5
6
7
8
import React from 'react'
import { mount } from '@cypress/react'

const Button = ({ onClick }) => <button onClick={onClick}>Click me</button>

it('renders an active base button', () => {
mount(<Button onClick={cy.stub().as('click')}>Click me</Button>)
})

Let's open the Cypress in component testing mode

1
$ npx cypress open-ct

Click on the "click.spec.ct.js" name shown in the browser file explorer. You see the button component running as a mini React web application.

The mounted Button component inside a browser

A cool thing about React component testing in Cypress - the React DevTools are mounted automatically to make inspecting the complex components a breeze.

The Button component inspected using included React DevTools

The stub

Wait, did we just pass a stub reference from the spec code to the component? Yes we did

1
<Button onClick={cy.stub().as('click')}>Click me</Button>

This is the most powerful feature in the Cypress test runner. In the E2E and component tests, you can access the actual application and spy / stub its methods, or browser APIs, because the test spec is running inside the browser itself, and not just sending browser automation commands. Read my blog post Cypress vs Other Test Runners for more information.

Ok, let's click on the button. There is cy.click command, and all Cypress commands used normally during E2E tests work in the component tests (except the cy.visit, since you don't really visit an URL in a component test)

cypress/component/click.spec.ct.js
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from 'react'
import { mount } from '@cypress/react'

const Button = ({ onClick }) => <button onClick={onClick}>Click me</button>

it('renders an active base button', () => {
mount(<Button onClick={cy.stub().as('click')}>Click me</Button>)

cy.get('button').click()
cy.get('@click').should('have.been.calledOnce').invoke('resetHistory')
cy.get('button').click().click()
cy.get('@click').should('have.been.calledTwice')
})

Confirming the Button component calls the passed prop on click

Yup, this React component really calls the prop onClick when the user clicks the HTML element button.

Disabled button

What happens if the HTML button is disabled? Does the onClick event handler fire? Let's try it out. By default cy.click requires the button to be actionable - being visible, enabled, and that sort of thing. Thus we need to use .click({ force: true }) to skip those checks.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React from 'react'
import { mount } from '@cypress/react'

const Button = ({ onClick, disabled }) => (
<button disabled={disabled} onClick={onClick}>
Click me
</button>
)

it('does not call the handler on the disabled button', () => {
mount(
<Button disabled={true} onClick={cy.stub().as('click')}>
Click me
</Button>,
)

cy.get('button').click({ force: true })
cy.get('@click').should('not.have.been.called')
})

Confirming the click event is ignored on a disabled button

See also