Open20

テスト入門 〜Storybook〜

ふくえもんふくえもん

Storybook@7

実行環境

  • 言語・フレームワーク
    "typescript": "^5"
    "next": "13.5.4",
  • スタイル
    "sass": "^1.69.0",
    "tailwindcss": "^3",
  • リンター・フォーマッター
    "eslint": "^8",
    "prettier": "^3.0.3",
    "stylelint": "^15.10.3",
ふくえもんふくえもん

インストール

npx storybook@latest init

ESLintが入ってる場合は、plluginの導入も推奨される

• Adding Storybook support to your "Next" app
  ✔ Getting the correct version of 9 packages
? We have detected that you're using ESLint. Storybook provides a plugin that gives the best experience with Storybook and helps follow best practices: https://github.com/storybookjs/eslint-plugin-storybook#readme

Yesを選択

ふくえもんふくえもん

TailwindCSSが適用できるように設定を加える

.storybook/preview.ts
import type { Preview } from '@storybook/react'
+ import '../src/styles/tailwind.css'

const preview: Preview = {
  parameters: {
    actions: { argTypesRegex: '^on[A-Z].*' },
    controls: {
      matchers: {
        color: /(background|color)$/i,
        date: /Date$/
      }
    }
  }
}

export default preview

next.js13はWebpackじゃないので上記の設定だけでいいみたい?
https://storybook.js.org/recipes/tailwindcss#tailwindcss

ふくえもんふくえもん

まずはSCSSで試してみる

TailwindCSSのlinterが警告を出してくるので、offにしておく

module.exports = {
  root: true,
  extends: [
    'plugin:@typescript-eslint/recommended',
    'next/core-web-vitals',
+ // 'plugin:tailwindcss/recommended', (コメントアウト)
    'prettier',
    'plugin:storybook/recommended'
  ],
  plugins: ['unused-imports'],
...
ふくえもんふくえもん

SCSSのButton
labelとprimaryを引数で受け取って、中のテキストを変えたり、真偽値によって、デザインを変えれるようにする
スタイルの拡張ライブラリとして、clsxを使う(shadcnで入ってたため)

import { FC } from 'react'
import { clsx } from 'clsx'
import styles from './style.module.scss'

interface ButtonProps {
  label: string
  primary?: boolean
}

export const Button: FC<ButtonProps> = ({ label, primary = false, ...args }) => {
  return (
    <button className={clsx(styles.button, { [styles.primary]: primary })} {...args}>
      {label}
    </button>
  )
}
ふくえもんふくえもん

stories.tsxちょっと理解した
まずはこれ

import { StoryObj, Meta } from '@storybook/react'
import { Button } from './index'

const meta: Meta<typeof Button> = {
  component: Button
}

export default meta

type Story = StoryObj<typeof Button>

export const Default: Story = {
  render: () => <Button label="でふぉると" />
}

export const Primary: Story = {
  render: () => <Button label="ぷらいまりぃ" primary />
}
ふくえもんふくえもん

CSFで記述される

CSF:ESMを用いてexportするオブジェクトの集合

一つのdefault exportと、1つ以上のnamed exportを用いて構成される

export defaultとexport constの役割

export default=メタデータを定義する

const meta: Meta<typeof Button> = {
  component: Button
}

• 対象コンポーネントのメタデータ及び各ストーリーの共通設定を定義
描画するコンポーネントや、型情報など

export const=コンポーネントを描画するコンポーネント

export const Default: Story = {
  render: () => <Button label="てきすと" />
}

• コンポーネントのストーリーを定義する

ふくえもんふくえもん

書き方メモ

1. 描画したいコンポーネントと必要なモジュールをimport

import { StoryObj, Meta } from '@storybook/react'
import { Button } from './index'

2. 描画するコンポーネントの共通の設定を行うMeta情報の定義

const meta: Meta<typeof Button> = {
  component: Button // 使いたいコンポーネントを定義
}

titleはstorybook上でみられる時の名前を定義できる

3.コンポーネントをstoryに登録するための関数を作成

type Story = StoryObj<typeof Button>

export const Default: Story = {
  render: () => <Button label="でふぉると" />
}

render関数を使って、コンポーネントを描画できる

ふくえもんふくえもん

登録するコンポーネントは何個でもいける

type Story = StoryObj<typeof Button>

export const Default: Story = {
  render: () => <Button label="でふぉると" />
}

export const Primary: Story = {
  render: () => <Button label="ぷらいまりぃ" primary />
}
ふくえもんふくえもん

StoryBookの設定

next.configの方で、styles/commnディレクトリにある共通のSassファイルを自動importできるようにailiasつくる設定をしてたので、storybookのwebpackの設定でailiasを適用できるように設定

import type { StorybookConfig } from '@storybook/nextjs'
import path from 'path'

const config: StorybookConfig = {
  stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|mjs|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-onboarding',
    '@storybook/addon-interactions'
  ],
  framework: {
    name: '@storybook/nextjs',
    options: {}
  },
  docs: {
    autodocs: 'tag'
  },
+ webpackFinal: async (config) => {
+   config.resolve ??= {}
+  config.resolve.alias ??= {}
+    config.resolve.alias['@'] = path.resolve(__dirname, '../src')
+    return config
+ }
}
export default config
ふくえもんふくえもん

title:コンポーネントのタイトル

/で階層構造にできるみたい

@7だとデフォルトで、ワークスペースのディレクトリ階層が反映されてる

arg:デフォルトのプロパティを設定する

https://storybook.js.org/docs/react/writing-stories/args

argTypes:コンポーネントの各プロパティ(引数)をコントロール

https://storybook.js.org/docs/react/api/arg-types

parameters:Storybook上の機能や外観などを設定できる

https://storybook.js.org/docs/react/writing-stories/parameters

ふくえもんふくえもん

Meta情報にプロパティを書くと、コンポーネント全体に反映される

const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    label: {
      control: 'text',
      defaultValue: 'Button'
    },
    primary: {
      control: 'boolean',
      defaultValue: false
    }
  }
}

これでコントローラーで操作できるようになる

ふくえもんふくえもん

描画する方もちょっといじる

export default meta

type Story = StoryObj<typeof Button>

export const Default: Story = {
  render: () => <Button label="でふぉると" />
}

export const Primary: Story = {
  args: {
    label: 'プライマリー',
    primary: true
  }
}

renderがなくてもargsで引数渡せば描画できる

ふくえもんふくえもん

Shadcn UIのコンポーネントを登録してみる

試しにButtonを登録
variant別で定義する

import { StoryObj, Meta } from '@storybook/react'
import { Button } from './button'

const meta: Meta<typeof Button> = {
  component: Button,
  argTypes: {
    variant: {
      control: {
        type: 'select',
        options: ['default', 'destructive', 'outline', 'secondary', 'ghost', 'link']
      }
    },
    size: {
      control: {
        type: 'select',
        options: ['default', 'sm', 'lg', 'icon']
      }
    },
    className: {
      control: 'text'
    }
  }
}

export default meta

type Story = StoryObj<typeof Button>

export const Base: Story = {
  args: {
    children: 'Base'
  }
}

export const Destructive: Story = {
  args: {
    children: 'Destructive',
    variant: 'destructive'
  }
}

export const Outline: Story = {
  args: {
    children: 'Outline',
    variant: 'outline'
  }
}

export const Secondary: Story = {
  args: {
    children: 'Secondary',
    variant: 'secondary'
  }
}

export const Ghost: Story = {
  args: {
    children: 'Ghost',
    variant: 'ghost'
  }
}

export const Link: Story = {
  args: {
    children: 'Link',
    variant: 'link'
  }
}

export const Small: Story = {
  args: {
    children: 'Small',
    size: 'sm'
  }
}

export const Large: Story = {
  args: {
    children: 'Large',
    size: 'lg'
  }
}

export const Icon: Story = {
  args: {
    children: 'Icon',
    size: 'icon'
  }
}

export const Disabled: Story = {
  args: {
    children: 'Disabled',
    disabled: true
  }
}

export const AsChild: Story = {
  args: {
    children: 'AsChild',
    asChild: true
  }
}

export const CustomClass: Story = {
  args: {
    children: 'CustomClass',
    className: 'text-red-500'
  }
}