Closed43

Storybook7探訪

hajimismhajimism

Storybookを起点にテストを設計できるようになりたい

hajimismhajimism

このスクラップでは概要を掴むにとどめた。
テストは奥が深いのでまた個別にスクラップを立てる。

hajimismhajimism
hajimismhajimism

The above story definition can be further improved to take advantage of Storybook’s “args” concept. Args describes the arguments to Button in a machine-readable way. It unlocks Storybook’s superpower of altering and composing arguments dynamically.

argsを使うことが推奨されている

hajimismhajimism

props無い場合はこれでもいい?

 export default {
  title: 'Button',
  component: Button,
};

export const Primary = {};
hajimismhajimism

The “Docs” page displays auto-generated documentation for components (inferred from the source code). Usage documentation is helpful when sharing reusable components with your team, for example, in an application.

これすごいよなー

hajimismhajimism

Provider系はpreview.jsでdecoratorとして書く

// .storybook/preview.js

import React from 'react';

import { ThemeProvider } from 'styled-components';

export const decorators = [(Story) => <ThemeProvider theme="default">{Story()}</ThemeProvider>];
hajimismhajimism

headでCSSなどを読み込んでいる場合

Alternatively, if you want to inject a CSS link tag to the <head> directly (or some other resource like a webfont link), you can use .storybook/preview-head.html to add arbitrary HTML.

hajimismhajimism
hajimismhajimism

argsを使えというのはこの書き方をさせたいかららしい

// Button.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';

import { Button } from './Button';

const meta: Meta<typeof Button> = {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'Button',
  component: Button,
};

export default meta;
type Story = StoryObj<typeof Button>;

export const Primary: Story = {
  backgroundColor: '#ff0',
  label: 'Button',
};

export const Secondary: Story = {
  args: {
    ...Primary.args,
    label: '😄👍😍💯',
  },
};

export const Tertiary: Story = {
  args: {
    ...Primary.args,
    label: '📚📕📈🤓',
  },
};
hajimismhajimism

List系のComponentのStoryを作るときに、単体のStoryを再利用しろと

// ButtonGroup.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';

import { ButtonGroup } from '../ButtonGroup';

//👇 Imports the Button stories
import * as ButtonStories from './Button.stories';

const meta: Meta<typeof ButtonGroup> = {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'ButtonGroup',
  component: ButtonGroup,
};

export default meta;
type Story = StoryObj<typeof ButtonGroup>;

export const Pair: Story = {
  args: {
    buttons: [{ ...ButtonStories.Primary.args }, { ...ButtonStories.Secondary.args }],
    orientation: 'horizontal',
  },
};
hajimismhajimism

When Button’s signature changes, you only need to change Button’s stories to reflect the new schema, and ButtonGroup’s stories will automatically be updated. This pattern allows you to reuse your data definitions across the component hierarchy, making your stories more maintainable.

たしかに。全体として「再利用」が鍵っぽい。メンテナビリティ意識。

hajimismhajimism

dar/light mode設定はpreview.jsで

// preview.js

// All stories expect a theme arg
export const argTypes = { theme: { control: 'select', options: ['light', 'dark'] } };

// The default value of the theme arg to all stories
export const args = { theme: 'light' };
hajimismhajimism

URLからargsを読み取る

For example, args=obj.key:val;arr[0]:one;arr[1]:two;nil:!null will be interpreted as:

{
  obj: { key: 'val' },
  arr: ['one', 'two'],
  nil: null
}
hajimismhajimism

Parameterの使い方

Parameters are a set of static, named metadata about a story, typically used to control the behavior of Storybook features and addons.

For example, let’s customize the backgrounds addon via a parameter. We’ll use parameters.backgrounds to define which backgrounds appear in the backgrounds toolbar when a story is selected.

hajimismhajimism

Global parameterはやはりpreview.jsで

// .storybook/preview.js

export const parameters = {
  backgrounds: {
    values: [
      { name: 'red', value: '#f00' },
      { name: 'green', value: '#0f0' },
    ],
  },
};
hajimismhajimism

Layoutもdecoratorに書くと良さそう

// YourComponent.stories.js|jsx

import React from 'react';

import { YourComponent } from './YourComponent';

export default {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'YourComponent',
  component: YourComponent,
  decorators: [
    (Story) => (
      <div style={{ margin: '3em' }}>
        <Story />
      </div>
    ),
  ],
};
hajimismhajimism

イメージ:Topページ

const meta: Meta<typeof HomePage> = {
  component: HomePage,
  parameters: {
    layout: 'fullscreen',
  },
  decorators: [
    (Story) => (
      <RootLayout>
        <Story />
      </RootLayout>
    ),
  ],
}

export default meta

type Story = StoryObj<typeof HomePage>

export const Default: Story = {}
hajimismhajimism

いや、/component/以下ではRootLayout欲しくないのでやはりページごとに書くのが良さそう

hajimismhajimism
hajimismhajimism

play関数の再利用

// MyComponent.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';

import { userEvent, within } from '@storybook/testing-library';

import { MyComponent } from './MyComponent';

const meta: Meta<typeof MyComponent> = {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'MyComponent',
  component: MyComponent,
};

export default meta;
type Story = StoryObj<typeof MyComponent>;

/*
 * See https://storybook.js.org/docs/7.0/react/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const FirstStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    userEvent.type(canvas.getByTestId('an-element'), 'example-value');
  },
};

export const SecondStory: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    await userEvent.type(canvas.getByTestId('other-element'), 'another value');
  },
};

export const CombinedStories: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // Runs the FirstStory and Second story play function before running this story's play function
    await FirstStory.play({ canvasElement });
    await SecondStory.play({ canvasElement });
    await userEvent.type(canvas.getByTestId('another-element'), 'random value');
  },
};
hajimismhajimism

fireEventとuseEventはどう違うんだろう。これはtesting-libraryの知識?

// MyComponent.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';

import { fireEvent, userEvent, within } from '@storybook/testing-library';

import { MyComponent } from './MyComponent';

const meta: Meta<typeof MyComponent> = {
  /* 👇 The title prop is optional.
   * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'ClickExamples',
  component: MyComponent,
};

export default meta;
type Story = StoryObj<typeof MyComponent>;

/* See https://storybook.js.org/docs/react/writing-stories/play-function#working-with-the-canvas
 * to learn more about using the canvasElement to query the DOM
 */
export const ClickExample: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // See https://storybook.js.org/docs/7.0/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await userEvent.click(canvas.getByRole('button'));
  },
};

export const FireEventExample: Story = {
  play: async ({ canvasElement }) => {
    const canvas = within(canvasElement);

    // See https://storybook.js.org/docs/7.0/react/essentials/actions#automatically-matching-args to learn how to setup logging in the Actions panel
    await fireEvent.click(canvas.getByTestId('data-testid'));
  },
};
hajimismhajimism

play関数のfactory記述、使い所ありそう

// index.stories.tsx
import type { ComponentStoryObj } from "@storybook/react";
import { screen } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { UserCreateForm } from "./";

type Story = ComponentStoryObj<typeof UserCreateForm>;

export default { component: UserCreateForm };

const type = (step: 0 | 1 | 2 | 3) => {
  if (step === 0) return;
  userEvent.type(screen.getByPlaceholderText("姓"), "田中");
  if (step === 1) return;
  userEvent.type(screen.getByPlaceholderText("名"), "太郎");
  if (step === 2) return;
  userEvent.type(
    screen.getByPlaceholderText("メールアドレス"),
    "example@gmail.com"
  );
};

const playFactory = (step: 0 | 1 | 2 | 3) => async () => {
  type(step);
  userEvent.click(screen.getByRole("button"));
};

export const Invalid1: Story = {
  storyName: "未入力で送信",
  play: playFactory(0),
};

export const Invalid2: Story = {
  storyName: "姓未入力で送信",
  play: playFactory(1),
};

export const Invalid3: Story = {
  storyName: "名未入力で送信",
  play: playFactory(2),
};

export const Valid: Story = {
  storyName: "正常入力で送信",
  args: { handleSubmit: (data) => {} },
  play: playFactory(3),
};

引用元
https://zenn.dev/takepepe/articles/storybook-driven-development

hajimismhajimism
hajimismhajimism

使い方

// TodoItem.stories.js|jsx|ts|tsx

import React from 'react';

import fetch from 'node-fetch';

import { TodoItem } from './TodoItem';

export default {
  /* 👇 The title prop is optional.
  * See https://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
  * to learn how to generate automatic titles
  */
  title: 'Examples/Loader'
  component: TodoItem,
  render: (args, { loaded: { todo } }) => <TodoItem {...args} {...todo} />,
};

export const Primary = {
  loaders: [
    async () => ({
      todo: await (
        await fetch('https://jsonplaceholder.typicode.com/todos/1')
      ).json(),
    }),
  ],
};

いつ使いたくなるだろう

hajimismhajimism

他の機能でも需要は満たせる気がする

// .storybook/preview.js

import fetch from 'node-fetch';

export const loaders = [
  async () => ({
    currentUser: await (await fetch('https://jsonplaceholder.typicode.com/users/1')).json(),
  }),
];
hajimismhajimism

Pageのような大きくて複雑なコンポーネントのStoryを書く際の指針。
以前まではPresentation層書いておけば良かったけど、Suspence活用しようと思ったらデータフェッチは各地で行われる設計になるから純粋にPresentationalなPageを書くのは無理筋になった。こういうときはどうすればいいのだろうか?
https://storybook.js.org/docs/7.0/react/writing-stories/build-pages-with-storybook

hajimismhajimism

Answer:ネットワークリクエストをモックしてください

If you need to render a connected component in Storybook, you can mock the network requests to fetch its data. There are various layers in which you can do that.

hajimismhajimism

モック方法

  • Provider → decorator
  • Web API → MSW addon

ほとんどMSWでやる想定

hajimismhajimism

なんか重要なことが書いてある気がするんだけど例を見てもpainがよくわからんかった

Avoiding mocking dependencies
It's possible to avoid mocking the dependencies of connected "container" components entirely by passing them around via props or React context. However, it requires a strict split of the container and presentational component logic. For example, if you have a component responsible for data fetching logic and rendering DOM, it will need to be mocked as previously described.

It’s common to import and embed container components amongst presentational components. However, as we discovered earlier, we’ll likely have to mock their dependencies or the imports to render them within Storybook.

Not only can this quickly grow to become a tedious task, but it’s also challenging to mock container components that use local states. So, instead of importing containers directly, a solution to this problem is to create a React context that provides the container components. It allows you to freely embed container components as usual, at any level in the component hierarchy without worrying about subsequently mocking their dependencies; since we can swap out the containers themselves with their mocked presentational counterpart.

hajimismhajimism

複数のコンポーネントStoryを記述する際にTemplateを作成する

// List.stories.ts|tsx

import type { Meta, StoryObj } from '@storybook/react';

import { List } from './List';
import { ListItem } from './ListItem';

//👇 Imports a specific story from ListItem stories
import { Unchecked } from './ListItem.stories';

const meta: Meta<typeof List> = {
  /* 👇 The title prop is optional.
   * Seehttps://storybook.js.org/docs/7.0/react/configure/overview#configure-story-loading
   * to learn how to generate automatic titles
   */
  title: 'List',
  component: List,
};

export default meta;
type Story = StoryObj<typeof List>;

//👇 The ListTemplate construct will be spread to the existing stories.
const ListTemplate: Story = {
  render: ({ items, ...args }) => {
    return (
      <List>
        {items.map((item) => (
          <ListItem {...item} />
        ))}
      </List>
    );
  },
};

export const Empty = {
  ...ListTemplate,
  args: {
    items: [],
  },
};

export const OneItem = {
  ...ListTemplate,
  args: {
    items: [{ ...Unchecked.args }],
  },
};

https://storybook.js.org/docs/7.0/react/writing-stories/stories-for-multiple-components

hajimismhajimism

テストについて詳しく見る前に今後の動向について
https://storybook.js.org/blog/future-of-storybook-in-2023/

hajimismhajimism

Our builder ecosystem continues to evolve. One of the biggest new entrants into the space is Webpack’s successor, Turbopack. We’re partnering with the Turbopack team to add support in 2023. In parallel, we’re continuing to invest in Storybook’s Vite support, which improved by leaps and bounds in 2022.

Viteに加えてTurbopackも
Next.jsへのサポート手厚いし順当な流れか?

hajimismhajimism

Accessibility testing
Storybook already supports automated accessibility testing, both as an interactive addon and through automated tests. In 2023, we plan to unify these two approaches to continuously audit component accessibility.

Storybookを追いかけると自ずとa11yの勉強になりそうなのがいいね

hajimismhajimism

Full page testing
Storybook is known as a tool for isolated component development and design systems, but it can also be used to develop connected components and even full-stack applications. We’re investing more in these scenarios from a testing standpoint. This gives devs a powerful way to exercise full application user flows but with the speed & reliability of Storybook’s isolated environment.

気になるところ。

hajimismhajimism

Learning & docs overhaul
With more features than ever, it’s tricky for users to understand how to best use Storybook. We’re investing in more learning content like API docs and code snippets, as well as infrastructure to help automatically keep everything up to date.

がんばってほしい。ぶっちゃけドキュメント心もとなかった

このスクラップは2023/01/11にクローズされました