はじめに
Chakra UI でフォームをテストしていたとき、getByLabelText で要素が取得できずにテストが落ちるという問題に詰まりました。
ラベルのテキストは合っているはずなのに、なぜ?というやつです。
該当のコード
// CardForm.tsx
export function CardForm() {
return (
<form onSubmit={handleSubmit(onSubmit)} noValidate>
<VStack spacing={4} align="stretch">
<FormControl id="id" isInvalid={!!errors.id} isRequired>
<FormLabel>名刺ID</FormLabel>
<Input
{...register("id", {
required: true,
pattern: /^[a-zA-Z0-9]+$/i,
})}
type="text"
/>
{/* 以下省略... */}
)
}
// CardForm.spec.tsx
describe("UserCardForm", () => {
it("全項目を入力して登録ボタンを押すとHomeに遷移する", async () => {
render(
<ChakraProvider>
<CardForm />
</ChakraProvider>
);
const user = userEvent.setup();
await user.type(screen.getByRole("textbox", { name: "名刺ID" }), "testUser1");
// 以下省略...
})
何が起きていたか
Chakra UI の FormControl に isRequired を設定すると、FormLabel の末尾に必須インジケーター * が自動で付加されます。
今回のコードだと、実際にレンダリングされる HTML は以下のようになります。
<label for="id">
名刺ID
<span aria-hidden="true" role="presentation"> *</span>
</label>
aria-hidden="true" が付いているので「スクリーンリーダーには読まれない」のですが、textContent としては含まれます。
Testing Library の getByLabelText("名刺ID") はラベルの textContent 全体(「名刺ID *」)を見て完全一致(exact: true)で探すので、「名刺ID」 とは一致せずに見つけられない、という状況になっていました。
対処法
① getByRole を使う(おすすめ)
アクセシブルな名前(accessible name) でマッチングするため、aria-hidden な要素を除外して正しく要素を取得できます。
// テキスト入力・テキストエリア
screen.getByRole("textbox", { name: "名刺ID" })
// コンボボックス(react-select 系)
screen.getByRole("combobox", { name: "好きな技術" })
accessible name ベースで動くので、必須インジケーターの * を気にする必要がなくなります。個人的にはこれを第一選択にしています。
② exact: false を指定する
前方一致になるため "名刺ID *" にも "名刺ID" がマッチするようになります。
screen.getByLabelText("名刺ID", { exact: false })
シンプルでわかりやすいですが、意図しない要素にマッチしてしまう可能性がある点だけ注意です。
まとめ
| 方法 | 特徴 |
|---|---|
getByRole("textbox", { name: "..." }) |
accessible name ベースなので最も堅牢 |
getByLabelText("...", { exact: false }) |
シンプルだが意図しないマッチのリスクあり |
Testing Libraryには「文字から存在を確認する」だけでも似たようなAPIがたくさんありますので、どれが目的に合っているのかよく確認するのが大事だなと思いました。
今回のように UI ライブラリで必須フィールドを持つフォームをテストするときは、getByRole を使うのがおすすめです。