Writing E2E and component tests for react-number-format component.
React library react-number-format is a powerful component for showing formatted input fields, like phone numbers and money amounts. I have created a small Vite.js example in the repo bahmutov/react-number-format-example to show how these components can be tested using Cypres.io test runner.
Simple, isn't it? The entered text is formatted according to the props. The formatted values are set as the current value attribute of the input elements.
Let's confirm the application is working as expected and indeed formats the phone number and the dollar amount.
End-to-end test
First, I will write an end-to-end test. I install Cypress and cypress-real-events because nothing is real without it.
it('enters phone number and dollar amount', () => { cy.visit('/') cy.get('[data-cy=phone]') .type('2345678900') // cy.realPress command comes from cypress-real-events .realPress('Tab') // confirm the "Tab" press moves the focus to the price input field cy.focused().should('have.attr', 'data-cy', 'price').clear().type('18.99') cy.log('**confirm formatted values**') cy.get('[data-cy=phone]').should('have.value', '+1 (234) 567 8900') cy.get('[data-cy=price]').should('have.value', '$18.99') })
The test enters raw text and verifies the react-number-format logic formats the values correctly.
Unhappy tests
The react-number-format components have a lot of features. We might need to investigate how a component behaves if the user tries to enter invalid information. For example, the price input field should ignore multiple ., not allow entering negative numbers, and strip the leading zeroes. Let's write a test to confirm it.
Notice a curious behavior: the price input ignores the duplicate . and all digits after the two decimals. But it does not strip the leading zero until the element loses its focus.
Let's describe this behavior in our test to avoid accidentally changing it in the future. We can use cy.blur command to remove focus from an element. Since we are working with the same DOM element <input data-cy=price ... /> we can simply chain our commands and assertions into a single chain.
Another unhappy test could verify the phone number input formatter removes all characters but 10 digits.
1 2 3 4 5 6 7
it('ignores invalid characters in phone input', () => { cy.visit('/') cy.get('[data-cy=phone]') .type('-phone 123/456-7890.1234') // all characters but 10 digit phone are removed .should('have.value', '+1 (123) 456 7890') })
Beautiful.
Component tests
End-to-End tests are powerful. They check the entire user flow. But what about learning how a component works? In our app we are using the `` prop that should notify the parent component about the formatted user values. Does it work? Is it safe to upgrade the react-number-format version?
Here is where the component tests come into play. Let's configure Cypress component testing; there is nothing to install, since it uses Vite.js and React already used by the application itself.
Step 1: change from E2E to Component testing type
Step 2: confirm the detected framework and bundler options
Step 3: create a new component spec
Here is my first test. It simply mounts the component to play with.
describe('onValueChange.cy.jsx', () => { it('playground', () => { cy.mount( <NumericFormat value={1234} prefix="$" allowNegative={false} decimalScale={2} inputMode="decimal" onValueChange={cy.stub().as('onValueChange')} data-cy="price" />, ) // work with the component as if it were a web app cy.get('[data-cy=price]').clear().type('50.99') }) })
We can see the stub function called 6 times. The first call is when we cleared the input element using cy.clear command. Then we types 5 characters: '5', '0', '.', '9', and '9'. For each character, the component called our onValueChange prop. Let's confirm each call.
describe('onValueChange.cy.jsx', () => { it('calls onValueChange', () => { cy.mount( <NumericFormat value={1234} prefix="$" allowNegative={false} decimalScale={2} inputMode="decimal" onValueChange={cy.stub().as('onValueChange')} data-cy="price" />, ) // work with the component as if it were a web app cy.get('[data-cy=price]').clear() cy.get('@onValueChange') .should('have.been.calledOnceWith', { formattedValue: '', value: '', floatValue: undefined, }) .invoke('resetHistory') cy.get('[data-cy=price]').type('50.99') // check each call cy.get('@onValueChange').should('have.property', 'callCount', 5) cy.get('@onValueChange') .invoke('getCalls') .map('args.0') .should('deep.equal', [ { formattedValue: '$5', value: '5', floatValue: 5 }, { formattedValue: '$50', value: '50', floatValue: 50, }, { formattedValue: '$50.', value: '50.', floatValue: 50, }, { formattedValue: '$50.9', value: '50.9', floatValue: 50.9, }, { formattedValue: '$50.99', value: '50.99', floatValue: 50.99, }, ]) }) })
It is up to you to decide which test variant is more readable.
Bonus 1: use cy-spok to check multiple items
We can simplify the above test. By using cy-spok assertion we can check properties from multiple objects in an array of calls at once. Here is the relevant assertion: