React-Hook-Form で作成した入力フォームにバリデーションエラーが表示されることのテストを行います。
開発環境
node: 18.14.0
react: 18.2.0
typescript: 5.1.6
vite: 4.4.2
jest: 29.6.1
testing-library/react: 14.0.0
react-hook-form: 7.45.1
zod: 3.21.4
storybook: 7.0.26
vite-cli から react-ts のテンプレートを使って react 環境を作りました。
コード
仕様
フィールドの最大・最小文字数を定義します。
- タイトル
- 必須 ( 最小 1 文字 )
- 最大 25 文字
入力フォームの実装
import { useForm } from "react-hook-form";
export const CreatePostForm: React.FC = () => {
const {
register,
formState: { errors },
} = useForm({
defaultValues: {
title: "",
},
});
return (
<div>
<form>
<input
type="text"
aria-label="タイトル"
aria-invalid={!!errors.title}
{...register("title")}
/>
// バリデーションエラー時に表示するエラーメッセージ
{errors.title && (
<p role="alert">
{errors.title?.message}
</p>
)}
</form>
</div>
);
};
この状態だとバリデーションルール、エラーメッセージが定義されていないため、上記の仕様をもとに、 zod でバリデーションを作っていきます。
import z from "zod";
const schema = z
.object({
title: z
.string()
.min(1, "タイトルは必ず入力してください。")
.max(25, "タイトルは25文字以内で入力してください。")
})
.required()
.strict();
type Input = z.infer<typeof schema>;
先ほど作成した入力フォームに追加します。
useForm のオプションに {mode: "onBlur"}
を追加し、フォーカスが外れたときにバリデーションをかけるようにします。
export const CreatePostForm: React.FC = () => {
const errorMessageId = useId();
const {
register,
formState: { errors },
} = useForm<Input>({
resolver: zodResolver(schema),
defaultValues: {
title: "",
},
mode: "onBlur",
});
return (
<div>
<form>
<label>
タイトル:
<input
type="text"
aria-label="タイトル"
aria-errormessage={errorMessageId}
aria-invalid={!!errors.title}
{...register("title")}
/>
{errors.title && (
<p role="alert" id={errorMessageId}>
{errors.title?.message}
</p>
)}
</label>
</form>
</div>
);
};
また、上記 input タグの aria-errormessage と p タグの id
要素に生成した id
を含ませることで、input 要素とエラー文言要素を紐づかせ、テストコードで toHaveErrorMessage(errorMessage)
を参照できるようにします。こちらの記事を参考にさせていただいております。
Story の作成
Storybook を使って stories を作成します。
$ npx storybook@latest init
とコマンド入力すると Would you like to install it?
と聞かれるので y
と入力して、必要なライブラリをインストールします。
すべてインストールが済んだら Storybook の準備は完了です。
import type { Meta, StoryObj } from "@storybook/react";
import { CreatePostForm } from "./CreatePostForm";
const meta: Meta<typeof CreatePostForm> = {
component: CreatePostForm,
};
export default meta;
type Story = StoryObj<typeof CreatePostForm>;
export const EmptyTitle: Story = {
play: async ({
canvasElement,
}: {
canvasElement: HTMLElement;
}): Promise<void> => {
const canvas = within(canvasElement);
// タイトル入力欄にフォーカスを当てる
await canvas.getByRole("textbox", { name: "タイトル" }).focus();
// 何も入力せずにフォーカスを外す
await userEvent.tab();
},
};
テスト作成
Storybook をテストに使うので、以下のライブラリを追加します。
$ yarn add -D @storybook/jest @storybook/testing-library
テストファイルを作成します。
import { render, screen, waitFor } from "@testing-library/react";
import { composeStories } from "@storybook/react";
import * as stories from "./CreatePostForm.stories";
test("タイトルが入力されていない場合、エラーメッセージを表示する", async () => {
const { EmptyTitle } = composeStories(stories);
const { container } = render(<EmptyTitle />);
await EmptyTitle.play({ canvasElement: container });
const title = screen.getByRole("textbox", { name: "タイトル" });
await waitFor(() => {
expect(title).toBeInvalid();
});
expect(title).toHaveErrorMessage("タイトルは必ず入力してください。");
});
テストの実行結果は Pass となります。
同じ要領で 26 文字入力された場合のテストを書いていきます。
export const OverTitleLength: Story = {
play: async ({
canvasElement,
}: {
canvasElement: HTMLElement;
}): Promise<void> => {
const canvas = within(canvasElement);
await userEvent.type(
canvas.getByRole("textbox", { name: "タイトル" }),
"A".repeat(26)
);
await userEvent.tab();
},
};
test("タイトルに26文字入力された場合、エラーメッセージを表示する", async () => {
const { OverTitleLength } = composeStories(stories);
const { container } = render(<OverTitleLength />);
await OverTitleLength.play({ canvasElement: container });
const title = screen.getByRole("textbox", { name: "タイトル" });
await waitFor(() => {
expect(title).toBeInvalid();
});
expect(title).toHaveErrorMessage("タイトルは25文字以内で入力してください。");
});
テストの実行結果は Pass となります。