Cypress + Storybook. Storage of test case, data and component rendering in one place

At first, Cypress was perceived as an e2e testing tool. It was interesting to observe the growing interest of front-end engineers in a topic in which Selenium ruled all its life. At that time, a typical video or article demonstrating the capabilities of Cypress was limited to wandering around a randomly selected site and well-deserved flattering reviews about the API for data entry.


Many of us have guessed to use Cypress to test components in isolation provided by such environments as Storybook / Styleguidist / Docz. A good example is Stefano Magni's article "Testing a Virtual List component with Cypress and Storybook" . It proposes to create a Storybook Story, place a component in it and put in a global variable data that will be useful for the test. This approach is good, but in it the test is torn between the Storybook and Cypress. If we have many components, such tests will be difficult to read and maintain.


In this article I will try to show how to go a little further and take the maximum from the ability to execute JavaScript in Cypress. In order to see how it works, please download the source code at the address and run the npm i and npm run test commands .


tl; dr:


  • You can place a link in a window to a component from the Storybook Story to test it in its entirety by Cypress (without breaking the test logic into several parts).
  • Cypress seemed so powerful to our team that we completely abandoned the tools that use js-dom under the hood to test UI components.

Formulation of the problem


, Datepicker . , .


Storybook


Storybook , β€” Story . , Story DOM-. β€” , Cypress .


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>
    )
};

Storybook. Cypress.


Cypress


-. , :


/// <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.', () => { });
})

. . Storybook. Story, "Open canvas in new tab" sidebar. URL Cypress:


const rootToMountSelector = '#component-test-mount-point';

before(() => {
    cy.visit('http://localhost:12345/iframe.html?id=datepicker--empty-story');
    cy.get(rootToMountSelector);
});

, div id=component-test-mount-point. , . :


afterEach(() => {
    cy.document()
        .then((doc) => {
            ReactDOM.unmountComponentAtNode(doc.querySelector(rootToMountSelector));
        });
});

. , :


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');
});

? props. . . β€” Cypress!


,


, props.


<Popup /> c props "showed". "showed" true, <Popup /> . "showed" c true false, <Popup /> .
?


, React - .


state. state boolean, "showed" props.


let setPopupTestWrapperState = null;
const PopupTestWrapper = ({ showed, win }) => {
    const [isShown, setState] = React.useState(showed);
    setPopupTestWrapperState = setState;
    return <win.Popup showed={isShown} />
}

, :


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');
});

: hook setState , class.


, , . , - -.


Cypress . ref . , ref state .


<Popup /> , ( ). :


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:


  • Storybook Stories React .
  • .
  • Story window ( Cypress).
  • Story , ( ).
  • .

: Storybook . Stories .

Cypress:


  • JavaScript .
  • Stories, .
  • (, ).
  • .
  • UI .


, . , , .


js-dom . ?


  • Js-dom , . DOM .
  • js-dom . .
  • -, CSS z-index? Cypress, .
  • - . ?

?


β€” !
β€” .
"" - react-lifecycle β€” … . . , ? , ?


cypress-react-unit-test? Storybook?


β€” . Storybook, Cypress, ..


But now this tool has a number of problems that do not allow using it as a full-fledged environment for running tests.


I hope that Gleb Bahmutov and the Cypress team will deal with these difficulties.


PS: My opinion and the opinion of my colleagues agree that the proposed approach allows us to review the monopoly of tools using js-dom. What do you think about that?


All Articles