Russian version: Cypress + Storybook. Хранение тестового сценария, данных и рендеринг компонента в одном месте.
Many of us have chosen Cypress as a tool to test components hosted via Storybook/Styleguidist/Docz. @NoriSte’s example suggests creating some Storybook Stories, put components there and expose important data to the global variable in order to have access to the test. The nice approach actually, but the test becomes broken into the pieces between Storybook and Cypress.
Here I’d like to show how to go a little bit further and get the most out of executing JavaScript in Cypress. To see it in action, you may download the source code and execute then npm i
and npm test
in the console.
Imagine that we are writing an adaptor for existing Datepicker component to use it across all company websites. We don’t want to accidentally break anything, so we have to cover it by tests.
All we need from Storybook - an empty Story that saves a reference to the testing component in the global variable. In order not to be so useless this Story renders the single DOM node. This node will be our war zone inside the test.
import React from 'react'
import Datepicker from './Datepicker.jsx'
export default {
component: Datepicker,
title: 'Datepicker',
}
export const emptyStory = () => {
// Reference to retrieve it in Cypress during the test
window.Datepicker = Datepicker
// Just a mount point
return <div id="component-test-mount-point"></div>
}
Okay, we’ve finished with Storybook. Let’s take a look at Cypress.
Personally, I like to get started with test cases enumeration. Seems we have next test structure:
/// <reference types="cypress" />
import React from 'react'
import ReactDOM from 'react-dom'
/**
* <Datepicker />
* * renders text field.
* * renders desired placeholder text.
* * renders chosen date.
* * opens calendar after clicking on text field.
*/
context('<Datepicker />', () => {
it('renders text field.', () => {})
it('renders desired placeholder text.', () => {})
it('renders chosen date.', () => {})
it('opens calendar after clicking on text field.', () => {})
})
Fine. We have to run this test in any environment. Open the Storybook, go directly to the empty Story by clicking at “Open canvas in new tab” button in the sidebar. Copy that URL and make Cypress visit it:
const rootToMountSelector = '#component-test-mount-point'
before(() => {
cy.visit('http://localhost:12345/iframe.html?id=datepicker--empty-story')
cy.get(rootToMountSelector)
})
As you may guess, in order to test we are going to render all components states in the same <div>
with id=component-test-mount-point
. So that the tests do not affect each other, we must unmount any component here before the next test execution. Let’s add some cleanup code:
afterEach(() => {
cy.document().then((doc) => {
ReactDOM.unmountComponentAtNode(doc.querySelector(rootToMountSelector))
})
})
Now we are ready to complete the test. Retrieve the component reference, render the component and make some assertions:
const selectors = {
innerInput: '.react-datepicker__input-container input',
}
it('renders text field.', () => {
cy.window().then((win) => {
ReactDOM.render(<win.Datepicker />, win.document.querySelector(rootToMountSelector))
})
cy.get(selectors.innerInput).should('be.visible')
})
Do you see that? Nothing stops us from passing any props or data to the component directly! It’s all in one place - in Cypress!
Sometimes we’d like to test that component behaves predictably according to changing props.
Examine <Popup />
component with showed
props. When showed
is true
, <Popup />
is visible. After that, changing showed
to false
, <Popup />
should become hidden. How to test that transition?
Those problems are easy to handle in an imperative way, but in case of declarative React we need to come up with something. In our team, we use an additional wrapper component with state to handle it. The state here is boolean, it responses to “showed” props.
let setPopupTestWrapperState = null
const PopupTestWrapper = ({ showed, win }) => {
const [isShown, setState] = React.useState(showed)
setPopupTestWrapperState = setState
return <win.Popup showed={isShown} />
}
Now we are about to finish the test:
it('becomes hidden after being shown when showed=false passed.', () => {
// arrange
cy.window().then((win) => {
// initial state - popup is visible
ReactDOM.render(
<PopupTestWrapper showed={true} win={win} />,
win.document.querySelector(rootToMountSelector)
)
})
// act
cy.then(() => {
setPopupTestWrapperState(false)
})
// assert
cy.get(selectors.popupWindow).should('not.be.visible')
})
Tip: If such hook hasn’t worked or you dislike calling the hook outside the component - rewrite the wrapper via simple class.
Actually, I’ve never written such a test. The idea has come up while writing this article. Probably it may be useful to test a component in a unit test style.
However, you may easily do it in Cypress. Just create a ref to the component before rendering. It is worth mentioning that the ref gives access to state and other elements of the component.
I’ve added the hide
method to <Popup \>
which makes it hidden forcibly (example for the sake of example). The following test looks like this:
it('closes via method call.', () => {
// arrange
let popup = React.createRef()
cy.window().then((win) => {
// initial state - popup is visible
ReactDOM.render(
<win.Popup showed={true} ref={popup} />,
win.document.querySelector(rootToMountSelector)
)
})
// act
cy.then(() => {
popup.current.hide()
})
// assert
cy.get(selectors.popupWindow).should('not.be.visible')
})
Storybook:
Pro-Tip: Please, run another instance of Storybook for your component library or pages.
Cypress:
Here I’d like to express my personal opinion and my colleagues’ position about possible questions that may appear during the reading. Written below doesn’t pretend to be true, may differ from reality and contain nuts.
Probably, it is our future to test components. There will be no need to maintain a separate instance of the Storybook, all tests will be entirely under the responsibility of Cypress, the configuration will be simplified, etc. But now the tool has some problems that make the environment provided incomplete for running tests. Hope that Gleb Bahmutov and the Cypress team will make it worked 🤞
Crossposted by daedalius on Medium and on habr.com (in Russian).