diff --git a/content/intro-to-storybook/vue/ja/accessibility-testing.md b/content/intro-to-storybook/vue/ja/accessibility-testing.md new file mode 100644 index 000000000..ba0f87d62 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/accessibility-testing.md @@ -0,0 +1,76 @@ +--- +title: 'アクセシビリティテスト' +tocTitle: 'アクセシビリティテスト' +description: 'アクセシビリティテストをワークフローに組み込む方法を学びましょう' +commit: 'c73e174' +--- + +ここまで、機能やビジュアルテストに重点を置きながら、少しずつ複雑さを増しつつ UI コンポーネントを構築してきました。しかし、UI 開発において重要な側面であるアクセシビリティについてはまだ対応していませんでした。 + +## なぜアクセシビリティ (A11y) が重要なのか? + +アクセシビリティとは、視覚、聴覚、運動、認知などの障害を持つユーザーを含め、すべてのユーザーがコンポーネントを効果的に操作できるようにすることです。アクセシビリティは正しいことであるだけでなく、法的要件や業界標準に基づいてますます義務化されています。これらの要件を踏まえ、アクセシビリティの問題を早い段階で頻繁にテストする必要があります。 + +## Storybook でアクセシビリティの問題を検知する + +Storybook には[アクセシビリティアドオン](https://storybook.js.org/addons/@storybook/addon-a11y) (A11y) が用意されており、コンポーネントのアクセシビリティをテストするのに役立ちます。[axe-core](https://github.com/dequelabs/axe-core) をベースに構築されており、[WCAG の問題の最大 57%](https://www.deque.com/blog/automated-testing-study-identifies-57-percent-of-digital-accessibility-issues/) を検知できます。 + +どのように動作するか見てみましょう!以下のコマンドでアドオンをインストールします: + +```shell +yarn exec storybook add @storybook/addon-a11y +``` + +
+ +💡 Storybook の `add` コマンドは、アドオンのインストールと設定を自動化します。利用可能な他のコマンドについては、[公式ドキュメント](https://storybook.js.org/docs/api/cli-options)を参照してください。 + +
+ +Storybook を再起動すると、UI に新しいアドオンが有効になっているのが確認できます。 + +![Storybook で Task のアクセシビリティの問題を表示](/intro-to-storybook/accessibility-issue-task-non-react-9-0.png) + +ストーリーをひととおり確認すると、アドオンがテスト状態の 1 つでアクセシビリティの問題を検知したことがわかります。[**コントラスト**](https://dequeuniversity.com/rules/axe/4.10/color-contrast?application=axeAPI)の違反は、タスクのタイトルと背景の間に十分なコントラストがないことを意味しています。アプリケーションの CSS(`src/index.css`)でテキストの色をより暗いグレーに変更することで、すぐに修正できます。 + +```diff:title=src/index.css +.list-item.TASK_ARCHIVED input[type="text"] { +- color: #a0aec0; ++ color: #4a5568; + text-decoration: line-through; +} +``` + +以上です。UI のアクセシビリティを確保するための最初のステップが完了しました。しかし、まだ作業は終わりではありません。アクセシブルな UI を維持することは継続的なプロセスであり、アプリが進化して UI が複雑になっても、新たなアクセシビリティの問題が導入されてリグレッションが発生しないように監視し続ける必要があります。 + +## Chromatic によるアクセシビリティテスト + +Storybook のアクセシビリティアドオンを使えば、開発中にアクセシビリティの問題をテストして即座にフィードバックを得ることができます。しかし、アクセシビリティの問題を追跡し続けることは難しく、どの問題を優先的に対処すべきかを判断するには専門的な取り組みが必要になることもあります。ここで Chromatic が役立ちます。すでに見てきたように、Chromatic はコンポーネントの[ビジュアルテスト](/intro-to-storybook/vue/ja/test/)を行い、リグレッションを防ぐのに役立ちました。Chromatic の[アクセシビリティテスト機能](https://www.chromatic.com/docs/accessibility)を使って、UI のアクセシビリティを維持し、新しい違反を誤って導入しないようにしましょう。 + +### アクセシビリティテストを有効にする + +Chromatic のプロジェクトに移動し、**Manage** ページを開きます。**Enable** ボタンをクリックして、プロジェクトのアクセシビリティテストを有効にします。 + +![Chromatic のアクセシビリティテストを有効化](/intro-to-storybook/chromatic-a11y-tests-enabled.png) + +### アクセシビリティテストを実行する + +アクセシビリティテストを有効にし、CSS のコントラストの問題を修正したので、変更をプッシュして新しい Chromatic ビルドをトリガーしましょう。 + +```shell:clipboard=false +git add . +git commit -m "Fix color contrast accessibility violation" +git push +``` + +Chromatic が実行されると、[アクセシビリティのベースライン](https://www.chromatic.com/docs/accessibility/#what-is-an-accessibility-baseline)が確立されます。これは、今後のテストが結果を比較するための基準となるものです。これにより、新たなリグレッションを導入することなく、アクセシビリティの問題をより効果的に優先順位付けし、対処し、修正できるようになります。 + + + +![Chromatic のアクセシビリティテスト付きビルド](/intro-to-storybook/chromatic-build-a11y-tests-non-react.png) + +これで、開発の各段階で UI のアクセシビリティを確保するワークフローを構築できました。Storybook は開発中のアクセシビリティの問題を検知し、Chromatic はアクセシビリティのリグレッションを追跡して、時間をかけて段階的に修正しやすくしてくれます。 diff --git a/content/intro-to-storybook/vue/ja/composite-component.md b/content/intro-to-storybook/vue/ja/composite-component.md new file mode 100644 index 000000000..f711d7dc7 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/composite-component.md @@ -0,0 +1,253 @@ +--- +title: '複合的なコンポーネントを組み立てる' +tocTitle: '複合的なコンポーネント' +description: '単純なコンポーネントから複合的なコンポーネントを組み立てましょう' +commit: '554783d' +--- + +前の章では、最初のコンポーネントを作成しました。この章では、学習した内容を基にタスクのリストである `TaskList` を作成します。それではコンポーネントを組み合わせて、複雑になった場合にどうすればよいか見てみましょう。 + +## TaskList (タスクリスト) + +Taskbox はピン留めされたタスクを通常のタスクより上部に表示することで強調します。これにより `TaskList` に、タスクのリストが通常のタスクのみである場合と、ピン留めされたタスクとの組み合わせである場合というストーリーを追加するべき 2 つのバリエーションができます。 + +![デフォルトのタスクとピン留めされたタスク](/intro-to-storybook/tasklist-states-1.png) + +`Task` のデータは非同期に送信されるので、接続がないことを示すため、読み込み中の状態**も併せて**必要となります。さらにタスクがない場合に備え、空の状態も必要です。 + +![空の状態と読み込み中のタスク](/intro-to-storybook/tasklist-states-2.png) + +## セットアップする + +複合的なコンポーネントも基本的なコンポーネントと大きな違いはありません。`TaskList` のコンポーネントとそのストーリーファイル、`src/components/TaskList.vue` と `src/components/TaskList.stories.ts` を作成しましょう。 + +まずは `TaskList` の大まかな実装から始めます。前の章で作成した `Task` コンポーネントをインポートし、属性とアクションを入力として渡します。 + +```html:title=src/components/TaskList.vue + + +``` + +次に `Tasklist` のテスト状態をストーリーファイルに記述します。 + +```ts:title=src/components/TaskList.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import TaskList from './TaskList.vue' + +import * as TaskStories from './Task.stories' + +export const TaskListData = [ + { ...TaskStories.TaskData, id: '1', title: 'Task 1' }, + { ...TaskStories.TaskData, id: '2', title: 'Task 2' }, + { ...TaskStories.TaskData, id: '3', title: 'Task 3' }, + { ...TaskStories.TaskData, id: '4', title: 'Task 4' }, + { ...TaskStories.TaskData, id: '5', title: 'Task 5' }, + { ...TaskStories.TaskData, id: '6', title: 'Task 6' }, +] + +const meta = { + component: TaskList, + title: 'TaskList', + tags: ['autodocs'], + excludeStories: /.*Data$/, + decorators: [() => ({ template: '
' })], + args: { + ...TaskStories.TaskData.events, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: TaskListData, + }, +} + +export const WithPinnedTasks: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: [ + ...Default.args.tasks.slice(0, 5), + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + ], + }, +} + +export const Loading: Story = { + args: { + tasks: [], + loading: true, + }, +} + +export const Empty: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Loading story. + ...Loading.args, + loading: false, + }, +} +``` + +
+ +💡[**デコレーター**](https://storybook.js.org/docs/writing-stories/decorators)は、ストーリーに任意のラッパーを提供する方法です。今回は、デフォルトエクスポートの decorator キーを使って、レンダリングされるコンポーネントの周囲にスタイルを追加しています。後で説明するように、コンポーネントに別のコンテキストを追加することもできます。 + +
+ +`TaskStories` をインポートすることで、ストーリーに必要な引数 (args) を最小限の労力で[組み合わせる](https://storybook.js.org/docs/writing-stories/args#args-composition)ことができます。そうすることで、2 つのコンポーネントが想定するデータとアクション (呼び出しのモック) の一貫性が保たれます。 + +それでは `TaskList` の新しいストーリーを Storybook で確認してみましょう。 + + + +## 状態を作りこむ + +今のコンポーネントはまだ粗削りですが、ストーリーは見えています。単に `.list-items` だけのためにラッパーを作るのは単純すぎると思うかもしれません。実際、その通りです。ほとんどの場合、単なるラッパーのためだけに新しいコンポーネントは作りません。`TaskList` の**本当の複雑さ**は `withPinnedTasks`、`loading`、`empty` といったエッジケースに現れているのです。 + +```html:title=src/components/TaskList.vue + + +``` + +追加したマークアップで UI は以下のようになります。 + + + +リスト内のピン留めされたタスクの位置に注目してください。ピン留めされたタスクはユーザーにとって優先度を高くするため、リストの先頭に描画されます。 + +
+💡 Git へのコミットを忘れずに行ってください! +
diff --git a/content/intro-to-storybook/vue/ja/conclusion.md b/content/intro-to-storybook/vue/ja/conclusion.md new file mode 100644 index 000000000..c2a87f2b6 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/conclusion.md @@ -0,0 +1,35 @@ +--- +title: 'まとめ' +description: '今までの知識をまとめて、Storybook のテクニックをもっと学びましょう' +--- + +Storybook で最初の UI を作成しましたね。お疲れ様でした!ここまでの章で UI コンポーネントを作成し、複合させ、テストし、デプロイする方法を学びました。同じように進めていれば、リポジトリーとデプロイされた Storybook は以下のリンクと同じようになっていることでしょう。 + +[📕 **GitHub のリポジトリ: chromaui/learnstorybook-code**](https://github.com/chromaui/learnstorybook-code/tree/vue) +
+ +[🌎 **デプロイされた Storybook**](https://vue--5ccbe484c994280020b6d128.chromatic.com/) + +Storybook は React、React Native、Vue、Angular、Svelte、その他のフレームワークにとって強力なツールです。開発者コミュニティーも活発でアドオンも充実しています。このチュートリアルで紹介した内容は、Storybook でできることのほんの一部にすぎません。一度 Storybook を導入すれば、強固な UI を効率的に作れることにきっと驚くことでしょう。 + +## さらに学ぶには + +もっと深く掘り下げたい方のために役に立つリソースを紹介します。 + +- [**Storybook の公式ドキュメント**](https://storybook.js.org/docs/get-started/install)には API ドキュメント、コミュニティのリンク、アドオンのギャラリーがあります。 + +- [**UI Testing Playbook**](https://storybook.js.org/blog/ui-testing-playbook/) では Twilio、Adobe、Peloton、Shopify といった効率の良いチームにおけるワークフローのベストプラクティスを紹介しています。 + +- [**視覚的なテストのハンドブック (Visual Testing Handbook)**](https://storybook.js.org/tutorials/visual-testing-handbook/) では、コンポーネントを Storybook で視覚的にテストする方法を掘り下げています。無料の 31 ページある eBook です。 + +- [**Storybook Discord**](https://discord.gg/UUt2PJb) では Storybook のコミュニティに参加できます。他の Storybook ユーザーと協力しましょう。 + +- [**Storybook ブログ**](https://storybook.js.org/blog/)ではリリース情報や、UI 開発のワークフローを合理的にするための機能を紹介します。 + +## 誰が Intro to Storybook チュートリアルを作成しているのでしょうか? + +文書や、コード、製作は [Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) の貢献によるものです。このチュートリアルは Chromatic の [GraphQL + React チュートリアルシリーズ](https://www.chromatic.com/blog/graphql-react-tutorial-part-1-6)を参考にしています。 + +このようなチュートリアルや記事をさらに読みたい場合は、Storybook のメーリングリストに登録することをオススメします。 + + diff --git a/content/intro-to-storybook/vue/ja/contribute.md b/content/intro-to-storybook/vue/ja/contribute.md new file mode 100644 index 000000000..4f1cd6326 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/contribute.md @@ -0,0 +1,12 @@ +--- +title: '貢献する' +description: 'Storybook を世界に広めましょう' +--- + +このチュートリアルの作成にご協力ください!文法や句読点などの些細な問題であれば、プルリクエストを送ってください。もし大規模な問題であれば、 [GitHub の issue を追加](https://github.com/chromaui/learnstorybook.com/issues)して議論しましょう。 + +Storybook のチュートリアルはコミュニティによって作成・運営されていますので、最新の状態を保ち、荒い部分を削るには皆さんのご協力が不可欠です。どのような協力でも歓迎します。 + +## 翻訳 + +Storybook をすべての人が使用できるように、このチュートリアルの他言語への翻訳にお力を貸してください。中国語やスペイン語はとくに歓迎します。ご興味があれば[この issue](https://github.com/chromaui/learnstorybook.com/issues/3) にコメントしてください。 diff --git a/content/intro-to-storybook/vue/ja/data.md b/content/intro-to-storybook/vue/ja/data.md new file mode 100644 index 000000000..c00b5f497 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/data.md @@ -0,0 +1,270 @@ +--- +title: 'データを繋ぐ' +tocTitle: 'データ' +description: 'UI コンポーネントとデータを繋ぐ方法を学びましょう' +commit: 'fafbc81' +--- + +これまでに、Storybook の切り離された環境で、状態を持たないコンポーネントを作成してきました。しかし、究極的には、アプリケーションからコンポーネントにデータを渡さなければ役には立ちません。 + +このチュートリアルは「アプリケーションを作る方法について」ではないので、詳細までは説明しませんが、コンテナコンポーネントを使ってデータを繋ぐ一般的なパターンについて見てみましょう。 + +## 繋がれたコンポーネント + +現在の `TaskList` コンポーネントは「表示用 (presentational)」として書かれており、その実装以外の外部とは何もやりとりをしません。データを中に入れるためには「コンテナ」が必要です。 + +この例では、Vue のデフォルトの状態管理ライブラリである [Pinia](https://pinia.vuejs.org/) を使用して、アプリケーションにシンプルなデータモデルを作り、タスクの状態を管理します。 + +以下のコマンドを実行し、必要な依存関係をプロジェクトに追加しましょう: + +```shell +yarn add pinia +``` + +まず、タスクの状態を変更するアクションに応答するシンプルな Pinia ストアを、`src` ディレクトリ内の `store.ts` というファイルに作成します (あえて簡単にしています): + +```ts:title=src/store.ts +import type { TaskData } from './types' + +/* A simple Pinia store/actions implementation. + * A true app would be more complex and separated into different files. + */ +import { defineStore } from 'pinia' + +interface TaskBoxState { + tasks: TaskData[] + status: 'idle' | 'loading' | 'failed' | 'succeeded' + error: string | null +} + +/* + * 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 defaultTasks: TaskData[] = [ + { id: '1', title: 'Something', state: 'TASK_INBOX' }, + { id: '2', title: 'Something more', state: 'TASK_INBOX' }, + { id: '3', title: 'Something else', state: 'TASK_INBOX' }, + { id: '4', title: 'Something again', state: 'TASK_INBOX' }, +] + +/* + * The store is created here. + * You can read more about Pinia defineStore in the docs: + * https://pinia.vuejs.org/core-concepts/ + */ +export const useTaskStore = defineStore('taskbox', { + state: (): TaskBoxState => ({ + tasks: defaultTasks, + status: 'idle', + error: null, + }), + actions: { + archiveTask(id: string) { + const task = this.tasks.find((task) => task.id === id) + if (task) { + task.state = 'TASK_ARCHIVED' + } + }, + pinTask(id: string) { + const task = this.tasks.find((task) => task.id === id) + if (task) { + task.state = 'TASK_PINNED' + } + }, + }, + getters: { + getFilteredTasks: (state) => { + const filteredTasks = state.tasks.filter( + (t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED', + ) + return filteredTasks + }, + }, +}) +``` + +次に、`TaskList` を更新してストアからデータを読み取るようにします。まず、既存の表示用バージョンを `src/components/PureTaskList.vue` ファイルに移動し (コンポーネント名を `PureTaskList` に変更)、コンテナでラップします。 + +`src/components/PureTaskList.vue`: + +```html:title=src/components/PureTaskList.vue + + + +``` + +`src/components/TaskList.vue`: + +```html:title=src/components/TaskList.vue + + + +``` + +`TaskList` の表示用バージョンを分離しておく理由は、テストや分離が容易になるためです。ストアの存在に依存しないため、テストの観点からはるかに扱いやすくなります。`src/components/TaskList.stories.ts` を `src/components/PureTaskList.stories.ts` にリネームし、ストーリーが表示用バージョンを使うようにしましょう: + +```ts:title=src/components/PureTaskList.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import PureTaskList from './PureTaskList.vue' + +import * as TaskStories from './Task.stories' + +export const TaskListData = [ + { ...TaskStories.TaskData, id: '1', title: 'Task 1' }, + { ...TaskStories.TaskData, id: '2', title: 'Task 2' }, + { ...TaskStories.TaskData, id: '3', title: 'Task 3' }, + { ...TaskStories.TaskData, id: '4', title: 'Task 4' }, + { ...TaskStories.TaskData, id: '5', title: 'Task 5' }, + { ...TaskStories.TaskData, id: '6', title: 'Task 6' }, +] + +const meta = { + component: PureTaskList, + title: 'PureTaskList', + tags: ['autodocs'], + excludeStories: /.*Data$/, + decorators: [() => ({ template: '
' })], + args: { + ...TaskStories.TaskData.events, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: TaskListData, + }, +} + +export const WithPinnedTasks: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Default story. + tasks: [ + ...Default.args.tasks.slice(0, 5), + { id: '6', title: 'Task 6 (pinned)', state: 'TASK_PINNED' }, + ], + }, +} + +export const Loading: Story = { + args: { + tasks: [], + loading: true, + }, +} + +export const Empty: Story = { + args: { + // Shaping the stories through args composition. + // Inherited data coming from the Loading story. + ...Loading.args, + loading: false, + }, +} +``` + + + +
+💡 Git へのコミットを忘れずに行ってください! +
+ +Pinia ストアから取得した実際のデータでコンポーネントが表示されるようになりました。`src/App.vue` に接続してコンポーネントを描画することもできますが、心配しないでください。次の章で対応します。 diff --git a/content/intro-to-storybook/vue/ja/deploy.md b/content/intro-to-storybook/vue/ja/deploy.md new file mode 100644 index 000000000..1e574e04e --- /dev/null +++ b/content/intro-to-storybook/vue/ja/deploy.md @@ -0,0 +1,151 @@ +--- +title: 'Storybook をデプロイする' +tocTitle: 'デプロイ' +description: 'Storybook をインターネット上にデプロイする方法を学びましょう' +--- + +ここまで、ローカルの開発マシンでコンポーネントを作成してきました。しかし、ある時点で、フィードバックを得るためにチームに作業を共有しなければならないこともあるでしょう。チームメートに UI の実装をレビューしてもらうため、Storybook をインターネット上にデプロイしてみましょう。 + +## 静的サイトとしてエクスポートする + +Storybook をデプロイするには、まず静的サイトとしてエクスポートします。この機能はすでに組み込まれて、使える状態となっているので、設定について気にする必要はありません。 + +`yarn build-storybook` を実行すると、`storybook-static` ディレクトリに Storybook が静的サイトとして出力されますので、静的サイトのホスティングサービスにデプロイできます。 + +## Storybook を公開する + +このチュートリアルでは、Storybook のメンテナーが作成した、無料のホスティングサービスである [Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) を使用します。Chromatic を使えば、クラウド上に Storybook を安全にデプロイし、またホストできます。 + +### GitHub にリポジトリを作成する + +デプロイの前に、リモートのバージョン管理サービスへローカルのコードを同期しなければなりません。[はじめの章](/intro-to-storybook/vue/ja/get-started/)でプロジェクトを初期化した際に、ローカルのリポジトリはすでに作成されています。この段階に来れば、リモートリポジトリにプッシュできるコミットがあるはずです。 + +[ここから](https://github.com/new) GitHub にアクセスし、リポジトリを作りましょう。リポジトリの名前はローカルと同じく「taskbox」とします。 + +![GitHub のセットアップ](/intro-to-storybook/github-create-taskbox.png) + +新しいリポジトリを作ったら origin の URL をコピーして、次のコマンドを実行し、ローカルの Git リポジトリを GitHub のリモートリポジトリに追加します。 + +```shell +git remote add origin https://github.com//taskbox.git +``` + +最後にローカルリポジトリを GitHub のリモートリポジトリにプッシュします。 + +```shell +git push -u origin main +``` + +### Chromatic を使う + +パッケージを開発時の依存関係に追加します。 + +```shell +yarn add -D chromatic +``` + +パッケージをインストールしたら、GitHub のアカウントを使用して [Chromatic にログイン](https://www.chromatic.com/start/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook)します。(Chromatic は一部のアクセス許可を要求します。)「taskbox」という名前でプロジェクトを作成し、GitHub のリポジトリと同期させます。 + +ログインしたら `Choose GitHub repo` をクリックし、リポジトリを選択します。 + + + +作成したプロジェクト用に生成された一意の `project-token` をコピーします。次に、Storybook をビルドし、デプロイするため、以下のコマンドを実行します。その際、コマンドの `project-token` の場所にコピーしたトークンを貼り付けてください。 + +```shell +yarn chromatic --project-token= +``` + +![Chromatic を実行する](/intro-to-storybook/chromatic-manual-storybook-console-log.png) + +実行が完了すると、Storybook が発行されて、`https://random-uuid.chromatic.com` のようなリンクができます。このリンクをチームに共有すれば、フィードバックが得られるでしょう。 + + + +![Chromatic パッケージを使用してデプロイされた Storybook](/intro-to-storybook/chromatic-manual-storybook-deploy-non-react.png) + +素晴らしい!Storybook をひとつのコマンドだけで公開できました。しかしながら、UI を実装し、フィードバックを得たいと思ったときに、毎回コマンドを手動で実行するのは非効率です。理想的なのは、コードをプッシュすると自動的に最新のコンポーネントが同期されることです。そのためには、Storybook を継続的にデプロイしていく必要があります。 + +## Chromatic を使用した継続的デプロイメント + +もうプロジェクトは GitHub にホストされているので、Storybook を自動的にデプロイする継続的インテグレーション (CI) が使用できます。[GitHub アクション](https://github.com/features/actions)は GitHub に組み込まれている CI サービスで、自動公開が簡単にできます。 + +### Storybook をデプロイするために GitHub アクションを追加する + +プロジェクトのルートに `.github` というディレクトリを作成し、さらにその中に `workflows` というフォルダーを作成します。 + +`chromatic.yml` を以下の内容で新規に作成します。 + +```yaml:title=.github/workflows/chromatic.yml +# Workflow name +name: 'Chromatic Deployment' + +# Event for the workflow +on: push + +# List of jobs +jobs: + chromatic: + name: 'Run Chromatic' + runs-on: ubuntu-latest + # Job steps + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + - run: yarn + #👇 Adds Chromatic as a step in the workflow + - uses: chromaui/action@latest + # Options required for Chromatic's GitHub Action + with: + #👇 Chromatic projectToken, see https://storybook.js.org/tutorials/intro-to-storybook/svelte/en/deploy/ to obtain it + projectToken: ${{ secrets.CHROMATIC_PROJECT_TOKEN }} + token: ${{ secrets.GITHUB_TOKEN }} +``` + +
+ +💡 簡潔にするため [GitHub シークレット](https://docs.github.com/en/actions/security-guides/encrypted-secrets#creating-encrypted-secrets-for-a-repository)には言及していません。GitHub シークレットは GitHub によって提供されるセキュアな環境変数なので、`project-token` をハードコードする必要はありません。 + +
+ +### アクションをコミットする + +コマンドラインで以下のコマンドを実行し、今までの内容をステージングします。 + +```shell +git add . +``` + +さらに以下のコマンドでコミットします。 + +```shell +git commit -m "GitHub action setup" +``` + +最後にリモートリポジトリにプッシュします。 + +```shell +git push origin main +``` + +一度 GitHub アクションをセットアップすれば、コードをプッシュする度に Chromatic にデプロイされます。Chromatic のプロジェクトのビルド画面で公開されたすべての版の Storybook を確認できます。 + +![Chromatic のユーザーダッシュボード](/intro-to-storybook/chromatic-user-dashboard.png) + +リストの一番上にある最新のビルドをクリックしてください。 + +次に `View Storybook` ボタンをクリックすれば、最新の Storybook を見ることができます。 + +![Chromatic の Storybook リンク](/intro-to-storybook/chromatic-build-storybook-link.png) + +このリンクをチームメンバーに共有しましょう。これは標準的な開発プロセスや、単に作業を公開するのに便利です 💅 diff --git a/content/intro-to-storybook/vue/ja/get-started.md b/content/intro-to-storybook/vue/ja/get-started.md new file mode 100644 index 000000000..4f6174edc --- /dev/null +++ b/content/intro-to-storybook/vue/ja/get-started.md @@ -0,0 +1,80 @@ +--- +title: 'Vue 向け Storybook のチュートリアル' +tocTitle: 'はじめに' +description: '開発環境に Vue Storybook を導入しましょう' +commit: '7bff3f5' +--- + +Storybook は開発時にアプリケーションと並行して動きます。Storybook を使用することで、UI コンポーネントをビジネスロジックやコンテキストから切り離して開発できるようになります。この文書は Vue 向けです。他にも [React](/intro-to-storybook/react/en/get-started/)、[React Native](/intro-to-storybook/react-native/en/get-started/)、[Angular](/intro-to-storybook/angular/en/get-started/)、[Svelte](/intro-to-storybook/svelte/en/get-started/) 向けのものがあります。 + +![Storybook と開発中のアプリの関係](/intro-to-storybook/storybook-relationship.jpg) + +## Vue 向けの Storybook を構築する + +Storybook を開発プロセスに組み込むにあたり、いくつかの手順を踏む必要があります。まずは、[degit](https://github.com/Rich-Harris/degit) を使用してビルド環境をセットアップしましょう。このパッケージを利用することで、テンプレート(アプリケーションの一部をデフォルト設定で構築したもの)をダウンロードし、開発ワークフローの短縮に役立てることができます。 + +それでは、次のコマンドを実行してください。 + +```shell:clipboard=false +# Clone the template +npx degit chromaui/intro-storybook-vue-template taskbox + +cd taskbox + +# Install dependencies +yarn +``` + +
+💡 このテンプレートには本バージョンのチュートリアルに必要なスタイル、アセット、最低限の設定が含まれています。 +
+ +それでは、アプリケーションのさまざまな環境が問題なく動くことを次のコマンドで確認しましょう。 + +```shell:clipboard=false +# Start the component explorer on port 6006: +yarn storybook + +# Run the frontend app proper on port 5173: +yarn dev +``` + +フロントエンド開発の 2 つのモード: コンポーネント開発 (Storybook)、アプリケーション自体 + + + +![2つのモード](/intro-to-storybook/app-main-modalities-vue.png) + +作業をする対象に応じて、このモードのうち 1 つまたは複数を同時に動かしながら作業します。今は単一の UI コンポーネントを作るのに集中するため、Storybook を動かすことにしましょう。 + +## 変更をコミットする + +この段階で、ローカルリポジトリにファイルを追加しても大丈夫です。以下のコマンドを実行して、ローカルリポジトリを初期化し、これまでに行った変更を追加してコミットしてください。 + +```shell +git init +``` + +つづいて: + +```shell +git add . +``` + +さらに: + +```shell +git commit -m "first commit" +``` + +最後に: + +```shell +git branch -M main +``` + +それでは最初のコンポーネントを作り始めましょう! diff --git a/content/intro-to-storybook/vue/ja/screen.md b/content/intro-to-storybook/vue/ja/screen.md new file mode 100644 index 000000000..632fa51b9 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/screen.md @@ -0,0 +1,460 @@ +--- +title: '画面を作る' +tocTitle: '画面' +description: 'コンポーネントをまとめて画面を作りましょう' +commit: 'ce81973' +--- + +今までボトムアップ (小規模な状態から複雑さを追加していく) で UI の作成に集中してきました。ボトムアップで作業することで、Storybook で遊びながら、それぞれのコンポーネントを切り離された環境で、それぞれに必要なデータを考えながら開発することができました。サーバーを立ち上げたり、画面を作ったりする必要はまったくありませんでした! + +この章では Storybook を使用して、コンポーネントを組み合わせて画面を作り、完成度を高めていきます。 + +## 繋がれた画面 + +このアプリケーションはとても単純なので、作る画面は些細なものです。リモート API からデータを取得し、(Pinia から自分でデータを取得する) `TaskList` コンポーネントをレイアウトで囲み、ストアからトップレベルの `error` フィールドを取り出すだけです (サーバーとの接続に問題がある場合にこのフィールドが設定されると仮定しましょう)。 + +まず、リモート API に接続してさまざまな状態 (すなわち、`error`、`succeeded`) をアプリケーションで扱えるようにするために、Pinia ストア (`src/store.ts` 内) をアップデートするところから始めましょう。 + +```ts:title=src/store.ts +import type { TaskData } from './types' + +/* A simple Pinia store/actions implementation. + * A true app would be more complex and separated into different files. + */ +import { defineStore } from 'pinia' + +interface TaskBoxState { + tasks: TaskData[] + status: 'idle' | 'loading' | 'failed' | 'succeeded' + error: string | null +} + +/* + * The store is created here. + * You can read more about Pinia defineStore in the docs: + * https://pinia.vuejs.org/core-concepts/ + */ +export const useTaskStore = defineStore('taskbox', { + state: (): TaskBoxState => ({ + tasks: [], + status: 'idle', + error: null, + }), + actions: { + archiveTask(id: string) { + const task = this.tasks.find((task) => task.id === id) + if (task) { + task.state = 'TASK_ARCHIVED' + } + }, + pinTask(id: string) { + const task = this.tasks.find((task) => task.id === id) + if (task) { + task.state = 'TASK_PINNED' + } + }, + async fetchTasks() { + this.status = 'loading' + try { + const response = await fetch('https://jsonplaceholder.typicode.com/todos?userId=1') + const data = await response.json() + const result = data + .map((task: { id: number; title: string; completed: boolean }) => ({ + id: `${task.id}`, + title: task.title, + state: task.completed ? 'TASK_ARCHIVED' : 'TASK_INBOX', + })) + .filter((task: TaskData) => task.state === 'TASK_INBOX' || task.state === 'TASK_PINNED') + this.tasks = result + this.status = 'succeeded' + } catch (error) { + if (error && typeof error === 'object' && 'message' in error) { + this.error = (error as Error).message + } else { + this.error = String(error) + } + this.status = 'failed' + } + }, + }, + getters: { + getFilteredTasks: (state) => { + const filteredTasks = state.tasks.filter( + (t) => t.state === 'TASK_INBOX' || t.state === 'TASK_PINNED', + ) + return filteredTasks + }, + }, +}) +``` + +リモート API エンドポイントからデータを取得するようにストアを更新し、アプリのさまざまな状態を処理できるように準備したので、`InboxScreen.vue` を `src/components` ディレクトリに作成しましょう。 + +```html:title=src/components/InboxScreen.vue + + +``` + +次に、アプリのエントリーポイント (`src/main.ts`) を更新し、ストアをコンポーネント階層に接続できるようにします。 + +```diff:title=src/main.ts +import { createApp } from 'vue' ++ import { createPinia } from 'pinia' +- import './style.css' + +import App from './App.vue' + + +- createApp(App).mount('#app') ++ createApp(App).use(createPinia()).mount('#app') +``` + +さらに、`App` コンポーネントを `InboxScreen` を描画するように変更します (いずれはルーターにどの画面を表示するか決めてもらいますが、今は気にしないでください)。 + +```html:title=src/App.vue + + + + +``` + +しかし、面白くなるのは Storybook でストーリーをレンダリングするときです。 + +前回見たように、`TaskList` コンポーネントは表示用の `PureTaskList` コンポーネントをレンダリングする**コンテナ**です。定義上、コンテナコンポーネントは切り離された環境ではレンダリングできません。何らかのコンテキストを渡すか、サービスに接続する必要があります。つまり、Storybook でコンテナをレンダリングするには、コンテキストやサービスをモックしなければなりません。 + +`TaskList` を Storybook に表示した際には、コンテナを避けて単に `PureTaskList` をレンダリングすることで、この問題を回避できました。しかし、アプリケーションが成長するにつれて、接続されたコンポーネントを Storybook から除外し、各々に表示用コンポーネントを作成するのはすぐに管理しきれなくなります。`InboxScreen` は接続されたコンポーネントなので、ストアとストアが提供するデータをモックする方法を用意する必要があります。 + +以下のように `InboxScreen.stories.ts` でストーリーを設定します。 + +```ts:title=src/components/InboxScreen.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import InboxScreen from './InboxScreen.vue' + +const meta = { + component: InboxScreen, + title: 'InboxScreen', + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = {} + +export const Error: Story = {} +``` + +私たちはストーリーですぐに問題を発見できます。正しい状態が表示されず、タスクのない空の画面が表示されます。前章で行ったように、`PureInboxScreen` 表示用コンポーネントを作成してタスクとエラー状態を props として受け取る方法もあります。しかし、前述のとおり、接続されたコンポーネントが増えるにつれてこのアプローチは管理しきれなくなります。ストーリーに必要なコンテキストを提供する方法を見てみましょう。 + + + +![壊れた Inbox](/intro-to-storybook/inboxscreen-vue-pinia-tasks-issue.png) + +## ストーリーにコンテキストを提供する + +`InboxScreen` を正しくレンダリングするには、Pinia ストアに適切な状態とアクションを提供し、ストーリー全体で再利用できるようにする必要があります。これを実現するために、`.storybook/preview.ts` を更新し、Storybook の `setup` 関数を使って既存の Pinia ストアを登録します。 + +```diff:title=.storybook/preview.ts +import type { Preview } from '@storybook/vue3-vite' + ++ import { setup } from '@storybook/vue3-vite' + ++ import { createPinia } from 'pinia' + +import '../src/index.css'; + +//👇 Registers a global Pinia instance inside Storybook to be consumed by existing stories ++ setup((app) => { ++ app.use(createPinia()); ++ }); + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +} + +export default preview; +``` + +ストアを登録したので、`InboxScreen` ストーリーがレンダリングされるようになりましたが、`Error` ストーリーにはまだ問題があります。正しい状態が表示されず、タスクのリストが表示されてしまいます。この問題を解決するために、さまざまなアプローチを取ることができますが、代わりに、よく知られた API モッキングライブラリを Storybook アドオンと一緒に使用して解決します。 + +![壊れた Inbox 画面のエラー状態](/intro-to-storybook/broken-inbox-error-state-9-0-non-react.png) + +## API をモックする + +今回のアプリケーションは単純で、リモート API 呼び出しにあまり依存しないので、[Mock Service Worker](https://mswjs.io/) と [Storybook MSW アドオン](https://storybook.js.org/addons/msw-storybook-addon) を使用することにします。Mock Service Worker は、API モックライブラリです。Service Worker に依存してネットワークリクエストを捕捉し、モックデータをレスポンスします。 + +[初めの章](/intro-to-storybook/vue/ja/get-started/) でアプリケーションをセットアップしたときに、これらのパッケージはすでにインストールされています。あとは、それらを設定しストーリーを更新して使用するのみです。 + +ターミナルで以下のコマンドを実行し、`public` フォルダの中にサービスワーカーを生成します。 + +```shell +yarn init-msw +``` + +その後、`.storybook/preview.ts` を更新して初期化します。 + +```diff:title=.storybook/preview.ts +import type { Preview } from '@storybook/vue3-vite' + +import { setup } from '@storybook/vue3-vite' + ++ import { initialize, mswLoader } from 'msw-storybook-addon' + +import { createPinia } from 'pinia' + +import '../src/index.css' + +//👇 Registers a global Pinia instance inside Storybook to be consumed by existing stories +setup((app) => { + app.use(createPinia()) +}) + +// Registers the msw addon ++ initialize() + +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, ++ loaders: [mswLoader], +} + +export default preview +``` + +最後に、`InboxScreen` のストーリーを更新し、リモート API 呼び出しをモックする[パラメーター](https://storybook.js.org/docs/writing-stories/parameters)を組み込みます。 + +```diff:title=src/components/InboxScreen.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + ++ import { http, HttpResponse } from 'msw' + +import InboxScreen from './InboxScreen.vue' + ++ import * as PureTaskListStories from './PureTaskList.stories.ts' + +const meta = { + component: InboxScreen, + title: 'InboxScreen', + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { ++ parameters: { ++ msw: { ++ handlers: [ ++ http.get('https://jsonplaceholder.typicode.com/todos?userId=1', () => { ++ return HttpResponse.json(PureTaskListStories.TaskListData); ++ }), ++ ], ++ }, ++ }, +}; + +export const Error: Story = { ++ parameters: { ++ msw: { ++ handlers: [ ++ http.get('https://jsonplaceholder.typicode.com/todos?userId=1', () => { ++ return new HttpResponse(null, { ++ status: 403, ++ }); ++ }), ++ ], ++ }, ++ }, +}; +``` + +
+ +💡 補足として、データを下の階層に渡していくことは正当な手法です。[GraphQL](http://graphql.org/) を使う場合は特に。[Chromatic](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) を作る際にはこの手法で 800 以上のストーリーを作成しました。 + +
+ +Storybook で `Error` ストーリーが意図したように動作していることが確認できます。MSW がリモート API をインターセプトして、適切なレスポンスを返しました。 + + + +## コンポーネントのテスト + +これまでで、シンプルなコンポーネントから画面まで、完全に機能するアプリケーションを作り上げ、ストーリーを用いてそれぞれの変更を継続的にテストすることができるようになりました。しかし、新しいストーリーを作るたびに、UI が壊れていないかどうか、他のすべてのストーリーを手作業でチェックする必要もあります。これは、とても大変な作業です。 + +この作業を自動化し、コンポーネントの操作を自動的にテストすることはできないのでしょうか? + +### play 関数を使ったコンポーネントのテスト + +Storybook の [`play`](https://storybook.js.org/docs/writing-stories/play-function) 関数が役立ちます。play 関数はストーリーのレンダリング後に実行される小さなコードスニペットを含んでいます。フレームワークに依存しない DOM API を使用しているため、play 関数を使って UI を操作し、人間の行動をシミュレートするストーリーを、フロントエンドのフレームワークに関係なく書くことができます。タスクが更新されたときに UI が期待どおりに動作するかを検証するために使用します。 + +新しく作成した `InboxScreen` ストーリーを更新し、以下のようにコンポーネント操作を追加してみましょう。 + +```diff:title=src/components/InboxScreen.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + ++ import { waitFor, waitForElementToBeRemoved } from 'storybook/test' + +import { http, HttpResponse } from 'msw' + +import InboxScreen from './InboxScreen.vue' + +import * as PureTaskListStories from './PureTaskList.stories.ts' + +const meta = { + component: InboxScreen, + title: 'InboxScreen', + tags: ['autodocs'], +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + parameters: { + msw: { + handlers: [ + http.get('https://jsonplaceholder.typicode.com/todos?userId=1', () => { + return HttpResponse.json(PureTaskListStories.TaskListData); + }), + ], + }, + }, ++ play: async ({ canvas, userEvent }: any) => { ++ await waitForElementToBeRemoved(await canvas.findByTestId('empty')) ++ await waitFor(async () => { ++ await userEvent.click(canvas.getByLabelText('pinTask-1')) ++ await userEvent.click(canvas.getByLabelText('pinTask-3')) ++ }) ++ }, +}; + +export const Error: Story = { + parameters: { + msw: { + handlers: [ + http.get('https://jsonplaceholder.typicode.com/todos?userId=1', () => { + return new HttpResponse(null, { + status: 403, + }); + }), + ], + }, + }, +}; +``` + +
+ +💡 `Interactions` パネルは Storybook 上でテストを可視化するのに役立ちます。ステップごとのフローを提供し、各インタラクションの一時停止、再開、巻き戻し、ステップ実行といった便利な UI 制御機能も備わっています。 + +
+ +`Default` ストーリーを確認します。`Interactions` パネルをクリックすると、ストーリーの play 関数内のインタラクションのリストが表示されます。 + + + +### test runner によるテストの自動化 + +play 関数を利用して、UI を操作し、タスクを更新した場合の反応を素早く確認できます。これによって、余計な手間をかけずに UI の一貫性を保つことができます。しかし、Storybook をよく見ると、インタラクションテストはストーリーを見ているときにしか実行されないことがわかります。つまり、変更を加えたとき、すべてのチェックを実行するためにはすべてのストーリーを手作業で確認しなければなりません。これは自動化できないのでしょうか? + +それは可能です!Storybook の [Vitest アドオン](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon)を使えば、Vitest のパワーを活用して、より自動化された、高速かつ効率的なテスト体験を実現できます。どのように動くのか見てみましょう! + +Storybook を起動した状態で、サイドバーの「Run Tests」をクリックしてください。これにより、ストーリーのレンダリング、動作、play 関数で定義されたインタラクション (先ほど `InboxScreen` ストーリーに追加したものを含む) に対してテストが実行されます。 + + + +
+ +💡 Vitest アドオンは、ここで紹介したもの以外にも、さまざまな種類のテストを実行できます。詳しくは[公式ドキュメント](https://storybook.js.org/docs/writing-tests/integrations/vitest-addon)を読むことをお勧めします。 + +
+ +これで、手作業でのチェックを必要とせずに UI テストを自動化するツールが揃いました。アプリケーションの開発を続けていく中で、UI の一貫性と機能性を維持するための優れた方法です。さらに、テストが失敗した場合はすぐに通知されるので、問題を素早く簡単に修正できます。 + +## コンポーネント駆動開発 + +まず、一番下の `Task` から始めて、`TaskList` を作り、画面全体の UI ができました。`InboxScreen` では繋がれたコンポーネントを含み、一緒にストーリーも作成しました。 + + + +[**コンポーネント駆動開発**](https://www.componentdriven.org/) (CDD) はコンポーネント階層を上がるごとに少しずつ複雑性を拡張します。利点としては、開発プロセスに集中できること、UI の組み合わせの網羅性を向上できること、が挙げられます。要するに、CDD によって、高品質で複雑な UI を作ることができます。 + +まだ終わりではありません。UI を作成しても作業は終わりません。長期間にわたり耐久性を維持できるようにしなければなりません。 + +
+💡 Git へのコミットを忘れずに行ってください! +
diff --git a/content/intro-to-storybook/vue/ja/simple-component.md b/content/intro-to-storybook/vue/ja/simple-component.md new file mode 100644 index 000000000..616d0a8f6 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/simple-component.md @@ -0,0 +1,407 @@ +--- +title: '単純なコンポーネントを作る' +tocTitle: '単純なコンポーネント' +description: '単純なコンポーネントを切り離して作りましょう' +commit: '18e218e' +--- + +それでは、[コンポーネント駆動開発](https://www.componentdriven.org/) (CDD) の手法にのっとって UI を作ってみましょう。コンポーネント駆動開発とは、UI を最初にコンポーネントから作り始めて、最後に画面を作り上げる「ボトムアップ」の開発プロセスです。CDD を用いれば、 UI を作る際に直面する複雑性を軽減できます。 + +## Task (タスク) + +![Task コンポーネントの 3 つの状態](/intro-to-storybook/task-states-learnstorybook-accessible.png) + +`Task` は今回作るアプリケーションのコアとなるコンポーネントです。タスクはその状態によって見た目が微妙に異なります。タスクにはチェックされた (または未チェックの) チェックボックスと、タスクについての説明と、リストの上部に固定したり解除したりするためのピン留めボタンがあります。これをまとめると、以下のプロパティが必要となります: + +- `title` – タスクを説明する文字列 +- `state` - タスクがどのリストに存在するか。またチェックされているかどうか。 + +`Task` の作成を始めるにあたり、事前に上記のそれぞれのタスクに応じたテスト用の状態を作成します。次いで、Storybook で、モックデータを使用し、コンポーネントを切り離して作ります。コンポーネントのそれぞれの状態について見た目を確認しながら進めます。 + +## セットアップする + +まずは、タスクのコンポーネントと、対応するストーリーファイル `src/components/Task.vue` と `src/components/Task.stories.ts` を作成しましょう。 + +`Task` の基本的な実装から始めます。`Task` は上述したプロパティと、タスクに対して実行できる 2 つの (リスト間を移動させる) アクションを引数として取ります。 + +```html:title=src/components/Task.vue + + + +``` + +上のコードは Todo アプリケーションの HTML を基にした `Task` の簡単なマークアップです。 + +下のコードは `Task` に対する 3 つのテスト用の状態をストーリーファイルに書くものです。 + +```ts:title=src/components/Task.stories.ts +import type { Meta, StoryObj } from '@storybook/vue3-vite' + +import { fn } from 'storybook/test' + +import Task from './Task.vue' + +export const TaskData = { + id: '1', + title: 'Test Task', + state: 'TASK_INBOX' as 'TASK_INBOX' | 'TASK_ARCHIVED' | 'TASK_PINNED', + events: { + onArchiveTask: fn(), + onPinTask: fn(), + }, +} + +const meta = { + component: Task, + title: 'Task', + tags: ['autodocs'], + //👇 Our exports that end in "Data" are not stories. + excludeStories: /.*Data$/, + args: { + ...TaskData.events, + }, +} satisfies Meta + +export default meta +type Story = StoryObj + +export const Default: Story = { + args: { + task: TaskData, + }, +} + +export const Pinned: Story = { + args: { + task: { + ...Default.args.task, + state: 'TASK_PINNED', + }, + }, +} + +export const Archived: Story = { + args: { + task: { + ...Default.args.task, + state: 'TASK_ARCHIVED', + }, + }, +} +``` + +
+ +💡 [**アクション**](https://storybook.js.org/docs/essentials/actions) は、UI コンポーネントを単独で構築する際にインタラクションを検証するのに役立ちます。アプリのコンテキストで使える関数や状態にアクセスできないことも多いため、`fn()` を使ってスタブとして差し込みましょう。 + +
+ +Storybook には基本となる 2 つの階層があります。コンポーネントとその子供となるストーリーです。各ストーリーはコンポーネントに連なるものだと考えてください。コンポーネントには必要なだけストーリーを記述できます。 + +- **コンポーネント** + - ストーリー + - ストーリー + - ストーリー + +Storybook にコンポーネントを認識させるには、以下の内容を含む `default export` を記述します: + +- `component` -- コンポーネント自体 +- `title` -- Storybook のサイドバーでコンポーネントをグループ化または分類するためのタイトル +- `tags` -- このコンポーネントのドキュメントを自動生成するためのタグ +- `excludeStories` -- ストーリーに必要な追加情報で、Storybook にはレンダリングしないもの +- `args` -- コンポーネントが期待するアクション [args](https://storybook.js.org/docs/essentials/actions#action-args) を定義し、カスタムイベントをモック化する + +ストーリーを定義するには、コンポーネント ストーリー フォーマット 3 ( [CSF3](https://storybook.js.org/docs/api/csf) )を使用してテストケースを構築します。このフォーマットは、各テストケースを簡潔に構築するために設計されています。各コンポーネントの状態を含むオブジェクトをエクスポートすることで、テストをより直感的に定義し、ストーリーをより効率的に作成・再利用できます。 + +Arguments (略して [`args`](https://storybook.js.org/docs/writing-stories/args)) を使用することで、コントロールアドオンを通して、Storybook を再起動することなく、コンポーネントを動的に編集することができるようになります。[`args`](https://storybook.js.org/docs/writing-stories/args) の値が変わるとコンポーネントもそれに合わせて変わります。 + +`fn()` を使うと、Storybook UI のアクションパネルにクリック時のコールバックが表示されます。つまり、ピン留めボタンを作成したとき、ボタンクリックが成功したかどうかを UI 上で確認できます。 + +コンポーネントのすべての変化形に同じアクションのセットを渡す必要があるため、それらを 1 つの `TaskData` 変数にまとめて、毎回ストーリー定義に渡すのが便利です。`TaskData` をまとめるもう 1 つの利点は、それを `export` して、このコンポーネントを再利用する別のコンポーネントのストーリーで使えることです。これについては後ほど説明します。 + +## 設定する + +作成したストーリーを認識させたり、CSS ファイル (`src/index.css`にあります) を Storybook 上で使用できるようにするため、Storybook の設定をいくつか変更する必要があります。 + +まず、設定ファイル (`.storybook/main.ts`) を以下のように変更してください。 + +```diff:title=.storybook/main.ts +import type { StorybookConfig } from '@storybook/vue3-vite' + +const config: StorybookConfig = { +- stories: ['../src/**/*.mdx', '../src/**/*.stories.@(js|jsx|ts|tsx)'], ++ stories: ['../src/components/**/*.stories.ts'], + staticDirs: ['../public'], + addons: [ + '@chromatic-com/storybook', + '@storybook/addon-docs', + '@storybook/addon-vitest', + ], + framework: { + name: '@storybook/vue3-vite', + options: {}, + }, +}; +export default config; +``` + +上記の変更が完了したら、`.storybook` フォルダー内の `preview.ts` を、以下のように変更してください。 + +```diff:title=.storybook/preview.ts +import type { Preview } from '@storybook/vue3-vite' + ++ import '../src/index.css' + +//👇 Configures Storybook to log the actions( onArchiveTask and onPinTask ) in the UI. +const preview: Preview = { + parameters: { + controls: { + matchers: { + color: /(background|color)$/i, + date: /Date$/, + }, + }, + }, +}; + +export default preview; +``` + +[`parameters`](https://storybook.js.org/docs/writing-stories/parameters) は Storybook の機能やアドオンの振る舞いをコントロールするのに使用します。今回はアプリケーションの CSS ファイルをインポートするために利用します。 + +Storybook のサーバーを再起動すると、タスクの 3 つの状態のテストケースが生成されているはずです: + + + +## 状態を作り出す + +ここまでで、Storybook のセットアップが完了し、スタイルをインポートし、テストケースを作りました。早速、デザインに合わせてコンポーネントの HTML を実装していきましょう。 + +今のところコンポーネントは簡素な状態です。まずはデザインを実現するために最低限必要なコードを書いてみましょう: + +```html:title=src/components/Task.vue + + + +``` + +追加したマークアップとインポートした CSS により以下のような UI ができます: + + + +## データ要件を明示する + +コンポーネントの構築を続けていく中で、TypeScript の型を定義して `Task` コンポーネントが期待するデータの形状を明示できます。こうすることで、エラーを早期に発見でき、複雑さが増してもコンポーネントが正しく使われるようになります。まず `src` フォルダに `types.ts` ファイルを作成し、既存の `TaskData` 型をそこに移動しましょう: + +```ts:title=src/types.ts +export type TaskData = { + id: string; + title: string; + state: 'TASK_ARCHIVED' | 'TASK_INBOX' | 'TASK_PINNED'; +}; +``` + +次に、`Task` コンポーネントを更新して、新しく作成した型を使用するようにします: + +```html:title=src/components/Task.vue + + + +``` + +## 完成! + +これでサーバーを起動したり、フロントエンドアプリケーションを起動したりすることなく、コンポーネントを作りあげることができました。次の章では、Taskbox の残りのコンポーネントを、同じように少しずつ作成します。 + +見た通り、コンポーネントを切り離して開発を始めるのは、迅速かつ簡単です。あらゆる状態を掘り下げてテストできるので、高品質で、バグが少なく、洗練された UI を作ることができることでしょう。 + +
+💡 Git へのコミットを忘れずに行ってください! +
diff --git a/content/intro-to-storybook/vue/ja/test.md b/content/intro-to-storybook/vue/ja/test.md new file mode 100644 index 000000000..35af38098 --- /dev/null +++ b/content/intro-to-storybook/vue/ja/test.md @@ -0,0 +1,203 @@ +--- +title: 'UI コンポーネントをテストする' +tocTitle: 'テスト' +description: 'UI コンポーネントのテスト手法について学びましょう' +--- + +Storybook のチュートリアルをテスト抜きには終われません。テストは高品質な UI を作成するのに必要なことです。疎結合なシステムにおいては、些細な変更で大きなリグレッション (手戻り) をもたらすことがあるのです。ここまでで、すでに 2 種類のテストについて学びました。 + +- **コンポーネントのテスト**では、Storybook と Vitest の連携機能を使い、実際のブラウザ環境でコンポーネントの描画や動作を自動的に検証します。 +- **インタラクションテスト**では、play 関数を使用し、コンポーネントが操作された際に期待通りの動作をすることを検証します。コンポーネントの利用中の振る舞いをテストするのに最適です。 + +## 「でも、見た目は大丈夫?」 + +残念ながら、前述のテスト方法だけでは UI のバグを防ぎきれません。UI というのは主観的でニュアンスの違いが多いため、テストが厄介なのです。手動テストは、その名の通り、手動です。UI のスナップショットテストでは多数の偽陽性を発生させてしまいます。ピクセルレベルの単体テストは価値があまりありません。Storybook のテスト戦略には視覚的なリグレッションテストが不可欠です。 + +## Storybook 向けのビジュアルテスト + +視覚的なリグレッションテスト (ビジュアルテスト) は、見た目の変更を検出するために設計されています。ビジュアルテストはコミット毎に各ストーリーのスクリーンショットを撮って、前のコミットと比較して変更点を探します。レイアウトや色、サイズ、コントラストといった表示要素の確認にとても適しています。 + + + +Storybook は視覚的なリグレッションテスト用の素晴らしいツールです。Storybook において、すべてのストーリーはテスト仕様となるからです。ストーリーを書いたり更新したりするたびに、仕様が無料でついてきます! + +視覚的なリグレッションテスト向けのツールは多々あります。Storybook のメンテナーが作成した無料のホスティングサービスである [**Chromatic**](https://www.chromatic.com/?utm_source=storybook_website&utm_medium=link&utm_campaign=storybook) がオススメです。Chromatic はクラウド環境上でビジュアルテストを光の速さで並列実行します。[前の章](/intro-to-storybook/vue/ja/deploy/)で見てきたように、Storybook をインターネット上に公開することもできます。 + +## UI の変更を検知する + +視覚的なリグレッションテストでは、新しく描画された UI コードのイメージが基準となるイメージと比較されます。UI の変更が検知されると、通知を受け取ることができます。 + +それでは、`Task` コンポーネントの背景を変更し、どう動くのか見てみましょう。 + +変更する前に新しいブランチを作成します。 + +```shell +git checkout -b change-task-background +``` + +`src/components/Task.vue` を以下のように変更します。 + +```diff:title=src/components/Task.vue + + + +``` + +これでタスクの背景色が変更されます。 + + + +![タスクの背景色の変更](/intro-to-storybook/chromatic-task-changes-non-react-9-0.png) + +この変更をステージングします。 + +```shell +git add . +``` + +コミットします。 + +```shell +git commit -m "change task background to red" +``` + +そして変更をリモートリポジトリにプッシュします。 + +```shell +git push -u origin change-task-background +``` + +最後に、ブラウザで GitHub のリポジトリを開き `change-task-background` ブランチのプルリクエストを作成します。 + +![GitHub にタスクの PR を作成する](/github/pull-request-background.png) + +プルリクエストに適切な説明を書き、`Create pull request` をクリックしてください。その後、ページの下部に表示された「🟡 UI Tests」の PR チェックをクリックしてください。 + +![GitHub にタスクの PR が作成された](/github/pull-request-background-ok.png) + +これで先のコミットによって検出された UI の変更を見られます。 + + + +![Chromatic が変更を検知した](/intro-to-storybook/chromatic-catch-changes.png) + +とてもたくさん変更されていますね!`Task` はコンポーネント階層で `PureTaskList` と `InboxScreen` の子供なので、少しの変更で雪だるま式に大規模なリグレッションが発生します。このような状況となるからこそ、他のテスト手法に加えてビジュアルテストが必要となるのです。 + +![UI のちょっとした変更で大きなリグレッションが発生](/intro-to-storybook/minor-major-regressions.gif) + +## 変更をレビューする + +視覚的なリグレッションテストはコンポーネントが意図せず変更されていないことを保障します。しかし、その変更が意図的であるかどうかを判別するのは、やはり人になります。 + +もし意図的な変更であるならば、基準を更新すれば、最新のストーリーが今後の比較に使用されるようになります。そうでなければ、修正が必要です。 + + + +モダンなアプリケーションはコンポーネントから作られていますので、コンポーネントのレベルでテストするのが重要です。そうすることで、他のコンポーネントの変更による影響を受けた画面や複合的なコンポーネントではなく、変化の原因であるコンポーネントを特定するのに役立ちます。 + +## 変更をマージする + +UI の変更をレビューしたら、その変更で意図せずバグを混入させていないことがわかっているので、自信をもってマージできます。赤色の背景が気に入ったのであれば、変更を受け入れ、そうでなければ元の状態に戻します。 + + + +![マージの準備ができた変更内容](/intro-to-storybook/chromatic-review-finished.png) + +Storybook はコンポーネントを**作る**のに役立ち、テストはコンポーネントを**保つ**のに役立ちます。このチュートリアルでは、手動テストとビジュアルテストの 2 種類の UI テストを取り上げました。設定が完了したように、どちらも CI に組み込んで自動化できます。これにより、バグの混入を心配することなくコンポーネントをリリースできます。ただし、コンポーネントのテスト方法はこれだけではありません。障害を持つユーザーを含むすべてのユーザーにとってコンポーネントがアクセシブルであることを確認する必要があります。つまり、アクセシビリティテストもワークフローに取り入れる必要があるのです。 diff --git a/gatsby-config.js b/gatsby-config.js index a5b7d1c18..a71c627ec 100644 --- a/gatsby-config.js +++ b/gatsby-config.js @@ -41,6 +41,7 @@ module.exports = { en: 10, es: 6.1, fr: 5.3, + ja: 10, pt: 5.3, it: 7.6, 'zh-CN': 8.1,