Cypress + Storybook. Stockage du scénario de test, des données et du rendu des composants en un seul endroit

Au dĂ©but, Cypress Ă©tait perçu comme un outil de test e2e. Il Ă©tait intĂ©ressant d'observer l'intĂ©rĂȘt croissant des ingĂ©nieurs front-end pour un sujet dans lequel le sĂ©lĂ©nium a rĂ©gnĂ© toute sa vie. À cette Ă©poque, une vidĂ©o ou un article typique dĂ©montrant les capacitĂ©s de Cypress se limitait Ă  se promener sur un site sĂ©lectionnĂ© au hasard et Ă  des critiques flatteuses bien mĂ©ritĂ©es sur l'API pour la saisie de donnĂ©es.


Beaucoup d'entre nous ont deviné utiliser Cypress pour tester les composants isolés fournis par des environnements tels que Storybook / Styleguidist / Docz. Un bon exemple est l'article de Stefano Magni "Tester un composant de liste virtuelle avec Cypress et Storybook" . Il propose de créer un Storybook Story, d'y placer un composant et de mettre dans une variable globale des données qui seront utiles pour le test. Cette approche est bonne, mais le test est partagé entre le livre de contes et Cypress. Si nous avons de nombreux composants, ces tests seront difficiles à lire et à maintenir.


Dans cet article, je vais essayer de montrer comment aller un peu plus loin et tirer le maximum de la possibilité d'exécuter JavaScript dans Cypress. Pour voir comment cela fonctionne, veuillez télécharger le code source à l'adresse et exécuter les commandes de test npm i et npm run .


tl; dr:


  • Vous pouvez placer un lien dans une fenĂȘtre vers un composant du Storybook Story pour le tester dans son intĂ©gralitĂ© par Cypress (sans casser la logique de test en plusieurs parties).
  • Cypress semblait si puissant Ă  notre Ă©quipe que nous avons complĂštement abandonnĂ© les outils qui utilisent js-dom sous le capot pour tester les composants de l'interface utilisateur.

Formulation du problĂšme


, 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, ..


Mais maintenant, cet outil a un certain nombre de problÚmes qui ne permettent pas de l'utiliser comme un environnement à part entiÚre pour exécuter des tests.


J'espÚre que Gleb Bahmutov et l'équipe Cypress feront face à ces difficultés.


PS: Mon avis et l'avis de mes collÚgues conviennent que l'approche proposée nous permet de revoir le monopole des outils utilisant js-dom. Qu'est ce que tu penses de ça?


All Articles