Back to Intro to Storybook
Chapters
  • Débuter
  • Simple composant
  • Composant complexe
  • Données
  • Écrans
  • Déploiement
  • Test visuel
  • Addons
  • Conclusion
  • Contribuer

Construire un écran

Construire un écran à partir de composants
Cette traduction de la communauté n'a pas encore été mise à jour avec la dernière version de Storybook. Aidez-nous à la mettre à jour en appliquant les modifications du guide français pour cette traduction. Les pull requests sont les bienvenues.

Nous nous sommes concentrés sur la création d'une UI de bas en haut, en commençant simplement et en ajoutant de la complexité. Cela nous a permis de développer chaque composant séparément, de déterminer ses besoins en données et de jouer avec dans Storybook. Tout cela sans avoir besoin de mettre en place un serveur ou de construire des écrans!

Dans ce chapitre, nous continuons à accroître la sophistication en combinant des composants dans un écran et en développant cet écran dans Storybook.

Composants connectés

Comme notre application est très simple, l'écran que nous allons construire est assez trivial, nous allons récupérer la donnée à partir d'une API, utiliser le composant TaskList (qui récupère ses propres données de Redux) puis développer un champ venant de Redux.

Nous allons commencer par mettre à jour notre store Redux (dans src/lib/store.js) pour connecter l'API and gérer les différents états pour notre application (ici error, succeeded):

Copy
src/lib/store.js
/* A simple redux store/actions/reducer implementation.
 * A true app would be more complex and separated into different files.
 */
import {
  configureStore,
  createSlice,
+ createAsyncThunk,
} from '@reduxjs/toolkit';

/*
 * The initial state of our store when the app loads.
 * Usually, you would fetch this from a server. Let's not worry about that now
 */

const TaskBoxData = {
  tasks: [],
  status: "idle",
  error: null,
};

/*
 * Creates an asyncThunk to fetch tasks from a remote endpoint.
 * You can read more about Redux Toolkit's thunks in the docs:
 * https://redux-toolkit.js.org/api/createAsyncThunk
 */
+ export const fetchTasks = createAsyncThunk('todos/fetchTodos', async () => {
+   const response = await fetch(
+     'https://jsonplaceholder.typicode.com/todos?userId=1'
+   );
+   const data = await response.json();
+   const result = data.map((task) => ({
+     id: `${task.id}`,
+     title: task.title,
+     state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX',
+   }));
+   return result;
+ });

/*
 * The store is created here.
 * You can read more about Redux Toolkit's slices in the docs:
 * https://redux-toolkit.js.org/api/createSlice
 */
const TasksSlice = createSlice({
  name: 'taskbox',
  initialState: TaskBoxData,
  reducers: {
    updateTaskState: (state, action) => {
      const { id, newTaskState } = action.payload;
      const task = state.tasks.findIndex((task) => task.id === id);
      if (task >= 0) {
        state.tasks[task].state = newTaskState;
      }
    },
  },
  /*
   * Extends the reducer for the async actions
   * You can read more about it at https://redux-toolkit.js.org/api/createAsyncThunk
   */
+  extraReducers(builder) {
+    builder
+    .addCase(fetchTasks.pending, (state) => {
+      state.status = 'loading';
+      state.error = null;
+      state.tasks = [];
+    })
+    .addCase(fetchTasks.fulfilled, (state, action) => {
+      state.status = 'succeeded';
+      state.error = null;
+      // Add any fetched tasks to the array
+      state.tasks = action.payload;
+     })
+    .addCase(fetchTasks.rejected, (state) => {
+      state.status = 'failed';
+      state.error = "Something went wrong";
+      state.tasks = [];
+    });
+ },
});

// The actions contained in the slice are exported for usage in our components
export const { updateTaskState } = TasksSlice.actions;

/*
 * Our app's store configuration goes here.
 * Read more about Redux's configureStore in the docs:
 * https://redux-toolkit.js.org/api/configureStore
 */
const store = configureStore({
  reducer: {
    taskbox: TasksSlice.reducer,
  },
});

export default store;

Maintenant que nous avons mis à jour notre store pour récupérer les données de l'API et que nous gérons les différents états de notre application, nous pouvons créer InboxScreen.js dans le dossier src/components:

Copy
src/components/InboxScreen.js
import React, { useEffect } from 'react';
import { useDispatch, useSelector } from 'react-redux';
import { fetchTasks } from '../lib/store';
import TaskList from './TaskList';

export default function InboxScreen() {
  const dispatch = useDispatch();
  // We're retrieving the error field from our updated store
  const { error } = useSelector((state) => state.taskbox);
  // The useEffect triggers the data fetching when the component is mounted
  useEffect(() => {
    dispatch(fetchTasks());
  }, []);

  if (error) {
    return (
      <div className="page lists-show">
        <div className="wrapper-message">
          <span className="icon-face-sad" />
          <p className="title-message">Oh no!</p>
          <p className="subtitle-message">Something went wrong</p>
        </div>
      </div>
    );
  }
  return (
    <div className="page lists-show">
      <nav>
        <h1 className="title-page">Taskbox</h1>
      </nav>
      <TaskList />
    </div>
  );
}

Nous avons aussi besoin de changer le composant App pour rendre le InboxScreen (nous utiliserons à la fin un routeur pour choisir le bon écran, mais ne nous inquiétons pas de cela ici):

Copy
src/App.js
- import logo from './logo.svg';
- import './App.css';
+ import './index.css';
+ import store from './lib/store';

+ import { Provider } from 'react-redux';
+ import InboxScreen from './components/InboxScreen';

function App() {
  return (
-   <div className="App">
-     <header className="App-header">
-       <img src={logo} className="App-logo" alt="logo" />
-       <p>
-         Edit <code>src/App.js</code> and save to reload.
-       </p>
-       <a
-         className="App-link"
-         href="https://reactjs.org"
-         target="_blank"
-         rel="noopener noreferrer"
-       >
-         Learn React
-       </a>
-     </header>
-   </div>
+   <Provider store={store}>
+     <InboxScreen />
+   </Provider>
  );
}
export default App;

Cependant, c'est dans le rendu de la story dans Storybook que les choses deviennent intéressantes.

Comme nous l'avons vu précédemment, le composant TaskList est un composant connecté et se fonde sur le store Redux pour rendre ses tâches. Comme InboxScreen est aussi un composant connecté, nous allons faire un travail similaire et fournir un store à la story. Voici comment rendre les stories dans InboxScreen.stories.js:

Copy
src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';
import store from '../lib/store';

import { Provider } from 'react-redux';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
export const Error = Template.bind({});

Nous pouvons constater une erreur dans la story de error. Au lieu d'afficher le bon état, il montre une liste de tâches. Une manière de corriger ce problème est de fournir une version simulée (qu'on appelle un "mock") de chaque étape, comme nous avons fait dans le chapitre précédent. Ici, pour nous aider à corriger cette erreur, nous allons utiliser une librairie de simulation d'API bien connue, grâce à un addon de Storybook.

Boîte de réception non opérationnelle

Simuler les services de l'API

Comme notre application est assez simpliste et ne dépend pas trop des appels à des API, nous allons utiliser Mock Service Worker et Storybook's MSW addon. Mock Service Worker est une librairie de simulation d'API. Il se repose sur les service workers pour capturer les appels faits au réseau et fournir des données simulées en réponse.

Quand nous avons initialisé notre application dans Débuter, ces deux librairies ont été installées. Il reste alors à les configurer et mettre à jour nos stories pour les utiliser.

Dans votre terminal, exécutez les commandes suivants pour générer un service worker générique à l'intérieur du dossier public:

Copy
yarn init-msw

Ensuite, nous devons mettre à jour .storybook/preview.js et les initialiser:

Copy
.storybook/preview.js
import '../src/index.css';

+ // Registers the msw addon
+ import { initialize, mswDecorator } from 'msw-storybook-addon';

+ // Initialize MSW
+ initialize();

+ // Provide the MSW addon decorator globally
+ export const decorators = [mswDecorator];

//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI.
export const parameters = {
  actions: { argTypesRegex: '^on[A-Z].*' },
  controls: {
    matchers: {
      color: /(background|color)$/i,
      date: /Date$/,
    },
  },
};

Enfin, mettez à jour les stories de InboxScreen en incluant un paramètre qui simule les appels à l'API:

Copy
src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';
import store from '../lib/store';
+ import { rest } from 'msw';
+ import { MockedState } from './TaskList.stories';
import { Provider } from 'react-redux';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
+ Default.parameters = {
+   msw: {
+     handlers: [
+       rest.get(
+         'https://jsonplaceholder.typicode.com/todos?userId=1',
+         (req, res, ctx) => {
+           return res(ctx.json(MockedState.tasks));
+         }
+       ),
+     ],
+   },
+ };

export const Error = Template.bind({});
+ Error.parameters = {
+   msw: {
+     handlers: [
+       rest.get(
+         'https://jsonplaceholder.typicode.com/todos?userId=1',
+         (req, res, ctx) => {
+           return res(ctx.status(403));
+         }
+       ),
+     ],
+   },
+ };
💡 En complément, une autre approche valable serait de passer la donnée à travers la hiérarchie des composants, d'autant plus si vous utilisez GraphQL. C'est comment nous avons construit Chromatic à travers plus de 800 stories.

Regardez votre Storybook, et vous verrez que la story error fonctionne dorénavant comme prévue. MSW a intercepté notre appel à l'API et a fourni la réponse adéquate.

Tests d'intéraction

Jusque là, nous avons été capable de construire une application fonctionnelle, en construisant de simples composants jusqu'à un écran, en testant continuellement les changements grâce à nos stories. Mais chaque nouvelle story nécessite aussi une vérification manuelle sur les autres stories, pour s'assurer que l'interface utilisateur n'a pas été changé. Ceci donne beaucoup de travail supplémentaire.

Ne pouvons-nous pas automatiser ce flux et tester nos interactions entre les composants de manière automatique?

Ecrire un test d'interaction en utilisant la fonction play

La fonction de Storybook play et @storybook/addon-interactions nous aide à faire des tests d'interactions. Une fonction play inclue de petits exemples de code qui s'exécutent après que les stories se rendent.

La fonction play nous aide à vérifier les changements d'UI quand les tâches sont mises à jour. Elle utilise les API du DOM, agnostiques du type de librairie utilisé, ce qui signifie que nous pouvons écrire des stories avec cette fonction pour interagir avec l'UI et simuler des actions utilisateurs, quelque soit la librarie front utilisée.

L'addon @storybook/addon-interactions nous aide à visualiser nos tests dans Storybook, en fournissant un flux étape par étape. Il offre aussi un ensemble de contrôles de l'UI, comme pause, recommencer, revenir en arrière, et parcourir chaque intéraction.

Regardons cela! Mettez à jour votre nouvelle story InboxScreen, et créer les interactions avec le composant en ajoutant le code suivant:

Copy
src/components/InboxScreen.stories.js
import React from 'react';

import InboxScreen from './InboxScreen';

import store from '../lib/store';
import { rest } from 'msw';
import { MockedState } from './TaskList.stories';
import { Provider } from 'react-redux';

+ import {
+  fireEvent,
+  within,
+  waitFor,
+  waitForElementToBeRemoved
+ } from '@storybook/testing-library';

export default {
  component: InboxScreen,
  title: 'InboxScreen',
  decorators: [(story) => <Provider store={store}>{story()}</Provider>],
};

const Template = () => <InboxScreen />;

export const Default = Template.bind({});
Default.parameters = {
  msw: {
    handlers: [
      rest.get(
        'https://jsonplaceholder.typicode.com/todos?userId=1',
        (req, res, ctx) => {
          return res(ctx.json(MockedState.tasks));
        }
      ),
    ],
  },
};

+ Default.play = async ({ canvasElement }) => {
+   const canvas = within(canvasElement);
+   // Waits for the component to transition from the loading state
+   await waitForElementToBeRemoved(await canvas.findByTestId('loading'));
+   // Waits for the component to be updated based on the store
+   await waitFor(async () => {
+     // Simulates pinning the first task
+     await fireEvent.click(canvas.getByLabelText('pinTask-1'));
+     // Simulates pinning the third task
+     await fireEvent.click(canvas.getByLabelText('pinTask-3'));
+   });
+ };

export const Error = Template.bind({});
Error.parameters = {
  msw: {
    handlers: [
      rest.get(
        'https://jsonplaceholder.typicode.com/todos?userId=1',
        (req, res, ctx) => {
          return res(ctx.status(403));
        }
       ),
    ],
  },
};

Regardez la story Default . Cliquer sur le section Interactions pour voir la liste des interactions de votre fonction play de votre story.

Automatiser les tests avec le lanceur de tests

Avec la fonction play de Storybook, nous sommes capables de répondre à notre problème, en interagissant avec l'UI et rapidement regarder les impacts sur l'interface utilisateur quand nous mettons à jour les tâches.

Mais si nous regardons de plus près notre Storybook,les tests d'interactions ne se lancent que quand nous regardons la story. Ainsi, nous devons toujours revenir sur chaque story dès que nous faisons un changement. Ne pourrions-nous pas automatiser cela?

La bonne nouvelle est que nous pouvons! Le lanceur de test de Storybook nous permet de faire cela. Il s'agit d'un outil lancé par Playwright — qui lance tous les tests d'interactions et reconnait les stories qui ont été cassées.

Regardons comment cela marche! Lancez la commande suivante et installez la librarie:

Copy
yarn add --dev @storybook/test-runner

Ensuite, mettez à jour package.json, section scripts et ajouter une nouvelle tâche de test:

{
  "scripts": {
    "test-storybook": "test-storybook"
  }
}

Enfin, avec votre Storybook lancé, ouvrez un terminal et lancez la commande suivante:

Copy
yarn test-storybook --watch
💡 Les tests d'interactions avec la fonction play sont une manière fantastique de tester vos composants d'UI. Ils peuvent faire plein plus que ce que nous avons parcouru. Nous vous encourageons à lire la documentation officielle pour apprendre plus à ce sujet.
Pour creuser encore plus les tests, vous pouvez lire Le livre du test. Il agrège les stratégies de tests utilisées par les équipes front reconnues afin de vous faire accélérer votre flux de développement.

Le lanceur de test Storybook a lancé tous les tests

Félicitations! Maintenant nous avons un outil qui nous aide à vérifier si nos stories sont lancées sans erreurs et de manière automatique. De plus, si un test casse, il nous fournira un lien qui ouvre la story cassée dans un navigateur.

Component-Driven Development

Nous avons commencé du bas avec une Task, puis progressé avec TaskList, et maintenant nous avons un écran entier. Notre InboxScreen utilisent des composants connectés avec leur stories associées.

Le Component-Driven Development vous permet d'accroître progressivement la complexité à mesure que vous montez dans la hiérarchie des composants. Parmi les avantages, citons un processus de développement plus ciblé et une couverture accrue de toutes les permutations possibles de l'UI. En bref, le CDD vous aide à construire des interfaces utilisateur de meilleure qualité et plus complexes.

Nous n'avons pas encore terminé - le travail ne s'arrête pas à la construction de l'UI. Nous devons également veiller à ce qu'elle reste durable dans le temps.

💡 N'oubliez pas de commiter vos changements avec git!
Keep your code in sync with this chapter. View 2275632 on GitHub.
Is this free guide helping you? Tweet to give kudos and help other devs find it.
Next Chapter
Déploiement
Découvrez comment déployer Storybook en ligne
✍️ Edit on GitHub – PRs welcome!
Join the community
6,614 developers and counting
WhyWhy StorybookComponent-driven UI
Open source software
Storybook

Maintained by
Chromatic
Special thanks to Netlify and CircleCI