5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

Next.js & Tailwind CSS & Storybookのフロントエンド環境の構築メモ

Last updated at Posted at 2023-03-26

概要

Next.js & Tailwind CSS & Storybookのフロントエンド環境の構築メモ。

前提

  • GitHubを使用
  • Chromaticを使用
  • 各種インストールはバージョンを指定せずに実行 (2023/03/27〜28辺りで実行)
  • Prettierを使用

構成

  • Next.js v13.2.4
  • React v18.2.0
  • Tailwind CSS v3.2.7
  • Storybook(react) v6.5.16
  • TypeScript v5.0.2

手順

Next.JS

create-next-appを実行して構築する。

npx create-next-app@latest sample-project --typescript --eslint

✔ Would you like to use `src/` directory with this project? … Yes
✔ Would you like to use experimental `app/` directory with this project? … No
✔ What import alias would you like configured? … @/*

作成されたディレクトリへ移動する。

cd sample-project

npmで定義されている各コマンドを実行し、正常に動作することを確認する。

npm run dev
npm run build
npm run start
npm run lint

Tailwind CSS

Tailwind CSSなどをインストールし、初期化をする。

npm install --save-dev tailwindcss postcss autoprefixer
npx tailwindcss init -p

Tailwind CSSの設定を修正する。

// tailwind.config.js
module.exports = {
  content: ['./src/**/*.{js,ts,jsx,tsx}'],
  theme: {
    extend: {},
  },
  plugins: [],
}

CSSを修正する。

/* src/styles/globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

ページを修正する。

// src/pages/index.tsx
export default function Home() {
  return <h1 className="text-3xl font-bold underline">Hello world!</h1>
}

Next.jsを起動して読み込まれているか確認する。

npm run dev

image.png

Prettier

Prettierやプラグインをインストールする。

npm install --save-dev --save-exact prettier
echo {}> .prettierrc.json
echo .next> .prettierignore

npm install --save-dev eslint-config-prettier
npm install --save-dev prettier-plugin-tailwindcss

.eslintrc.jsonを修正する。

{
  "extends": [
    "next/core-web-vitals",
    "prettier"
  ]
}

Storybook

Storybookのインストールと初期化する。

npx storybook init

Storybookのコマンドを実行して動作を確認する。

npm run storybook
npm run build-storybook

上記でエラーが発生する場合は、以下の記事の通りの現象の可能性ありなので対応する。
https://qiita.com/hiroaki-suzuki/items/8cdd0b2d1a032b8f9eb1

src/storiesは削除して代わりに、componentsディレクトリを作成する。
※これは好みだが、以下からは上記を前提に記述。

Tailwind CSSを利用したコンポーネントとストーリーをsrc/componentsに作成する。

例)

// components/button/Button.tsx
import React from 'react'

export type ButtonProps = {
  children?: React.ReactNode
  className?: string
  onClick?: (event: React.MouseEvent<HTMLButtonElement>) => void
}

export const Button: React.FC<ButtonProps> = ({ children, className = '', onClick = () => {} }) => {
  return (
    <button className={`${className} rounded`.trim()} onClick={onClick}>
      {children}
    </button>
  )
}
// components/button/Button.stories.tsx
import React from 'react'
import { ComponentMeta, ComponentStory } from '@storybook/react'
import { Button, ButtonProps } from './Button'

export default {
  title: 'Button',
  component: Button,
  argTypes: { onClick: { action: 'clicked' } },
} as ComponentMeta<typeof Button>

const Template: ComponentStory<typeof Button> = (args: ButtonProps) => <Button {...args} />

export const Primary = Template.bind({})
Primary.args = {
  children: 'Primary Button',
  className: 'bg-blue-600 text-white p-2',
}

export const Secondary = Template.bind({})
Secondary.args = {
  children: 'Secondary Button',
  className: 'bg-gray-300 text-black p-2',
}

export const Disabled = Template.bind({})
Disabled.args = {
  children: 'Disabled Button',
  className: 'bg-gray-200 text-gray-600 p-2 cursor-not-allowed',
}

StorybookにTailwind CSSを読み込ませるための記述と@エイリアスを有効にするための設定も追加する。

// .storybook/main.js
const path = require('path')

module.exports = {
  typescript: {
    reactDocgen: 'react-docgen-typescript-plugin',
  },
  stories: [
    '../src/components/**/*.stories.mdx',
    '../src/components/**/*.stories.@(js|jsx|ts|tsx)'
  ],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions'
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  webpackFinal: async (config) => {
    // パスの@エイリアスの解決
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, '../src'),
    }

    // Tailwindの読み込み解決
    config.module.rules.push({
      test: /\.css$/,
      use: [
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              plugins: { tailwindcss: {}, autoprefixer: {} },
            },
          },
        },
      ],
      include: path.resolve(__dirname, '../'),
    })

    return config
  },
}
// .storybook/preview.js
import '../src/styles/globals.css'

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

Storybookを起動する。

npm run storybook

image.png

Storybook関連のテスト

ビジュアルテスト

Chromaticにアクセスし登録などを行う。
https://www.chromatic.com/
https://www.chromatic.com/docs/setup?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook

Chromaticをインストールする。

npm install --save-dev chromatic

.envを作成する。.envファイルは、.gitignoreへ追加しGit管理外にする。

CHROMATIC_PROJECT_TOKEN=Chromaticのproject-tokenを設定

コマンドを実行して確認する。

npx chromatic

スクリーンショット 2023-03-26 23.36.13.png

image.png

Mock Service Worker

https://storybook.js.org/tutorials/ui-testing-handbook/react/en/composition-testing/
https://storybook.js.org/addons/msw-storybook-addon

APIリクエストをモック化してくれるライブラリをインストールする。

npm install --save-dev msw msw-storybook-addon
npx msw init public/
// .storybook/preview.js
import '../src/styles/globals.css'
import { initialize, mswDecorator } from 'msw-storybook-addon'

initialize()

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

.storybook/main.jsに静的ディレクトリとして、publicディレクトリを追加

  staticDirs: ['../public'],

インタラクションテスト

ライブラリをインストールする。

npm install --save-dev @storybook/jest @storybook/test-runner

featuresのブロックを追加する。

// .storybook/main.js
const path = require('path')

module.exports = {
  typescript: {
    reactDocgen: 'react-docgen-typescript-plugin',
  },
  stories: ['../src/components/**/*.stories.mdx', '../src/components/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: ['@storybook/addon-links', '@storybook/addon-essentials', '@storybook/addon-interactions'],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  features: {
    interactionsDebugger: true,
  },
  webpackFinal: async (config) => {
    // パスの@エイリアスの解決
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, '../src'),
    }

    // Tailwindの読み込み解決
    config.module.rules.push({
      test: /\.css$/,
      use: [
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              plugins: { tailwindcss: {}, autoprefixer: {} },
            },
          },
        },
      ],
      include: path.resolve(__dirname, '../'),
    })

    return config
  },
}

package.jsonにテストコマンドを追加する。

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

コマンドを実行して動作しているか確認する。

npm run storybook
npm run test-storybook

image.png

※msw関連で、失敗することがある?要調査

アクセシビリティテスト

アクセシビリティテストのアドオンをインストールする。

npm install --save-dev @storybook/addon-a11y

アクセシビリティテストのアドオンを追加する。

// .storybook/main.js
const path = require('path')

module.exports = {
  typescript: {
    reactDocgen: 'react-docgen-typescript-plugin',
  },
  stories: ['../src/components/**/*.stories.mdx', '../src/components/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
  ],
  framework: '@storybook/react',
  core: {
    builder: '@storybook/builder-webpack5',
  },
  features: {
    interactionsDebugger: true,
  },
  webpackFinal: async (config) => {
    // パスの@エイリアスの解決
    config.resolve.alias = {
      ...config.resolve.alias,
      '@': path.resolve(__dirname, '../src'),
    }

    // Tailwindの読み込み解決
    config.module.rules.push({
      test: /\.css$/,
      use: [
        {
          loader: 'postcss-loader',
          options: {
            postcssOptions: {
              plugins: { tailwindcss: {}, autoprefixer: {} },
            },
          },
        },
      ],
      include: path.resolve(__dirname, '../'),
    })

    return config
  },
}

Storybookで確認する。

npm run storybook

image.png

自動化するために以下をインストールし、テストランナーファイルを作成する。

npm install --save-dev axe-playwright
// .storybook/test-runner.js
const { injectAxe, checkA11y } = require('axe-playwright')

module.exports = {
  async preRender(page, context) {
    await injectAxe(page)
  },
  async postRender(page, context) {
    await checkA11y(page, '#root', {
      detailedReport: true,
      detailedReportOptions: {
        html: true,
      },
    })
  },
}

コマンドを実行し確認する。

npm run storybook
npm run test-storybook

image.png

E2Eテスト

Cypressをインストールする。

npm install --save-dev cypress

package.jsonに実行コマンドを追加する。

  "scripts": {
    "open-cypress": "cypress open",
    "run-cypress": "cypress run"
  }

cypress.config.tsを作成する。

// cypress.config.ts
import { defineConfig } from 'cypress'

export default defineConfig({
  e2e: {
    baseUrl: 'http://localhost:3000/',
    supportFile: false,
  },
})

テストファイルを作成する。

// cypress/e2e/home.cy.ts
describe('Home Page', () => {
  it('Hello world', () => {
    cy.visit('/')
    cy.get('h1').should('have.text', 'Hello world!')
  })
})

コマンドを実行して確認する。

npm run dev
npm run open-cypress

image.png
image.png
image.png
image.png

CLI実行についても確認する。

npm run run-cypress

image.png

GitHub ActionsでのUIテストの実行

GitHub ActionsでCIを構築する。

Chromaticのプロジェクトトークンをsecretsに登録する。
image.png

Name: CHROMATIC_PROJECT_TOKEN
Secret: project-token
image.png

  1. NPMなどのライブラリキャッシュの作成
  2. テスト実行(以下並行)
    1. インタラクションテストとアクセシビリティテストの実行
    2. Chromaticビジュアルテストの実行
    3. Cypressでのワークフローテストの実行
# .github/workflows/ui-tests.yml
name: 'UI Tests'

on: push

jobs:
  # ライブラリのインストールとキャッシュ
  install-cache:
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
      - name: Cache npm dependencies and cypress
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-npm-ci-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-ci
      - name: Install dependencies if cache invalid
        if: steps.npm-cache.outputs.cache-hit != 'true'
        run: npm ci
  # インタラクションテストとアクセシビリティテストの実行
  interaction-and-accessibility:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - uses: actions/setup-node@v3
        with:
          node-version: '18.15.0'
      - name: Restore npm dependencies
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-npm-ci-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-ci
      - name: Install Playwright
        run: npx playwright install --with-deps
      - name: Build Storybook
        run: npm run build-storybook --quiet
      - name: Serve Storybook and run tests
        run: |
          npx concurrently -k -s first -n "SB,TEST" -c "magenta,blue" \
          "npx http-server storybook-static --port 6006 --silent" \
          "npx wait-on tcp:127.0.0.1:6006 && npm run test-storybook"
  # Chromaticビジュアルテストの実行
  visual-and-composition:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - name: Checkout
        uses: actions/checkout@v3
        with:
          fetch-depth: 0
      - name: Restore npm dependencies
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-npm-ci-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-ci
      - name: Publish to Chromatic
        uses: chromaui/action@v1
        with:
          projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }}
  # Cypressでのワークフローテストの実行
  user-flow:
    runs-on: ubuntu-latest
    needs: install-cache
    steps:
      - name: Checkout
        uses: actions/checkout@v3
      - name: Restore npm dependencies
        uses: actions/cache@v3
        id: npm-cache
        with:
          path: |
            ~/.cache/Cypress
            node_modules
          key: ${{ runner.os }}-npm-ci-${{ hashFiles('**/package-lock.json') }}
          restore-keys: |
            ${{ runner.os }}-npm-ci
      - name: Next.js build
        run: npm run build
      - name: Cypress run
        uses: cypress-io/github-action@v5
        with:
          start: npm start

プッシュして実行を確認する。

image.png
image.png

Jest

コンポーネント以外の単体テストのためJestをインストール

npm install --save-dev jest @types/jest ts-jest

jest.config.jsを作成する

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  coverageDirectory: '<rootDir>/coverage/jest',
}

package.jsonに実行コマンドを追加する。

  "scripts": {
    "test": "jest"
  }

コマンドを実行して動作を確認する

npm run test

image.png

カバレッジ

JestとStorybookのカバレッジを収集する

Jestのカバレッジ

jest.config.jsにcoverageDirectoryを追加する

module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  coverageDirectory: '<rootDir>/coverage/jest',
}

package.jsonに実行コマンドを追加する。

  "scripts": {
    "test-coverage": "jest --coverage"
  }

コマンドを実行して動作を確認する

npm run test-coverage

カバレッジファイルを確認する
coverage/jest/lcov-report/index.html
image.png

Storybookのカバレッジ

https://storybook.js.org/docs/react/writing-tests/test-coverage
https://storybook.js.org/addons/@storybook/addon-coverage

npm install --save-dev @storybook/addon-coverage

main.jsのアドオンにカバレッジのアドオンを追加する

// .storybook/main.js
const path = require('path')

module.exports = {
  typescript: {
    reactDocgen: 'react-docgen-typescript-plugin',
  },
  stories: ['../src/components/**/*.stories.mdx', '../src/components/**/*.stories.@(js|jsx|ts|tsx)'],
  addons: [
    '@storybook/addon-links',
    '@storybook/addon-essentials',
    '@storybook/addon-interactions',
    '@storybook/addon-a11y',
    '@storybook/addon-coverage',
  ],
  // ....

package.jsonに実行コマンドを追加する。

  "scripts": {
    "test-storybook-coverage": "test-storybook --coverage && npx nyc report --reporter=lcov -t coverage/storybook --report-dir coverage/storybook",
  }

コマンドを実行して動作を確認する

npm run storybook
npm run test-storybook-coverage

カバレッジファイルを確認する
coverage/storybook/lcov-report/index.html
image.png

JestとStorybookのカバレッジをマージ

package.jsonに実行コマンドを追加する。

  "scripts": {
    "test-merge-coverage": "npx istanbul-merge --out coverage/merged/coverage.json ./coverage/jest/coverage-final.json ./coverage/storybook/coverage-storybook.json",
    "coverage-report": "npx nyc report --reporter=lcov -t coverage/merged --report-dir coverage/merged"
  }

コマンドを実行して動作を確認する

npm run test-merge-coverage
npm run coverage-report

カバレッジファイルを確認する
coverage/merged/lcov-report/index.html
image.png

5
3
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
3

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?