概要
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
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
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
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
※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
自動化するために以下をインストールし、テストランナーファイルを作成する。
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
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
CLI実行についても確認する。
npm run run-cypress
GitHub ActionsでのUIテストの実行
GitHub ActionsでCIを構築する。
Chromaticのプロジェクトトークンをsecretsに登録する。
Name: CHROMATIC_PROJECT_TOKEN
Secret: project-token
- NPMなどのライブラリキャッシュの作成
- テスト実行(以下並行)
- インタラクションテストとアクセシビリティテストの実行
- Chromaticビジュアルテストの実行
- 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
プッシュして実行を確認する。
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
カバレッジ
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
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
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