Testing Libraryでラジオボタンのvalueを取得するテストを書きたい

ラジオボタンやセレクトボックスのように、valueを持つHTMLタグがある。 UIコンポーネントテストを書く際、valueが意図通り設定できていることをテストしたい場面がある。 ハマってしまいやすいのでメモする。

ラジオボタンの値を取得したい場合

こういう実装を書いて

<label>
  <input type="radio" name="extension" value="previous" />
  前泊のみ
</label>

こういうテストを書くと落ちる。

  it("前泊のみのラベルテキストとvalueがデグレードしていない", () => {
    const content = render(<StayExtensionOptionsRadio />);
    const previous = content.getByText("前泊のみ");
    expect(previous).toBeTruthy();
    expect(previous).toHaveAttribute("value", "previous"); // ここで落ちる
  });
 FAIL  src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx
  初期状態のテスト
    ✓ ラジオボタンは全部で4つ存在する (58 ms)
    ✕ 前泊のみのラベルテキストとvalueがデグレードしていない (9 ms)

  ● 初期状態のテスト › 前泊のみのラベルテキストとvalueがデグレードしていない

    expect(element).toHaveAttribute("value", "previous") // element.getAttribute("value") === "previous"

    Expected the element to have attribute:
      value="previous"
    Received:
      null

      14 |     const previous = content.getByText("前泊のみ");
      15 |     expect(previous).toBeTruthy();
    > 16 |     expect(previous).toHaveAttribute("value", "previous");
         |                      ^
      17 |   });
      18 | });
      19 |

      at Object.toHaveAttribute (src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx:16:22)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.859 s, estimated 1 s
Ran all test suites matching /src\/component\/StayExtensionOptionsRadio\/StayExtensionOptionsRadio.test.tsx/i.

Recievedがnullになっている。

expectの第一引数は対象の要素である。previousは何か?<lavel>である。labelのAttribute(属性)にvalueはあるか?

<label>←ここ
  <input type="radio" name="extension" value="previous" />
  前泊のみ ←ここ
</label>←ここ

…ない。ないんだからnullで帰ってくる。

どこにvalueはあるか?<input>である。ということは<input>を取得する必要がある。 ラジオボタンはWAI-ARIA1という規格でrole="radio"2 として定義されている。そのためgetByRole()3で要素を取得できる。

ラジオボタンを明示して要素を取得すればvalueを検証できるはず。という予想を立て、次のように修正した。 日和らず回してみるのが重要。

  it("前泊のみのラベルテキストとvalueがデグレードしていない", () => {
    const content = render(<StayExtensionOptionsRadio />);
    // ラベルのテキストを取得
    expect(content.getByText("前泊のみ")).toBeTruthy();
    // getByRoleでrole=radioを取得し、前泊のみの選択肢を取得している(つもり)
    expect(content.getByRole("radio", { name: "previous" })).toHaveAttribute("value", "previous");
  });

今度は次のようにエラーが出てきた。

➜  react-sandbox git:(65-radio-button) ✗ npm run test src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx

> reactjs-sandbox@1.0.0 test
> node scripts/test.js --watchAll=false src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx

 FAIL  src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx
  初期状態のテスト
    ✓ ラジオボタンは全部で4つ存在する (38 ms)
    ✕ 前泊のみのラベルテキストとvalueがデグレードしていない (38 ms)

  ● 初期状態のテスト › 前泊のみのラベルテキストとvalueがデグレードしていない

    TestingLibraryElementError: Unable to find an accessible element with the role "radio" and name "previous"

    Here are the accessible roles:

      radio:

      Name "前泊のみ":
      <input
        name="extension"
        type="radio"
        value="previous"
      />

      Name "後泊のみ":
      <input
        name="extension"
        type="radio"
        value="next"
      />

      Name "前泊と後泊":
      <input
        name="extension"
        type="radio"
        value="both"
      />

      Name "宿泊なし":
      <input
        name="extension"
        type="radio"
        value="none"
      />

      --------------------------------------------------

    Ignored nodes: comments, script, style
    <body>
      <div>
        <div>
          <label>
            <input
              name="extension"
              type="radio"
              value="previous"
            />
            前泊のみ
          </label>
          <label>
            <input
              name="extension"
              type="radio"
              value="next"
            />
            後泊のみ
          </label>
          <label>
            <input
              name="extension"
              type="radio"
              value="both"
            />
            前泊と後泊
          </label>
          <label>
            <input
              name="extension"
              type="radio"
              value="none"
            />
            宿泊なし
          </label>
        </div>
      </div>
    </body>

      13 |     const content = render(<StayExtensionOptionsRadio />);
      14 |     expect(content.getByText("前泊のみ")).toBeTruthy();
    > 15 |     expect(content.getByRole("radio", { name: "previous" })).toHaveAttribute("value", "previous");
         |                    ^
      16 |   });
      17 | });
      18 |

      at Object.getElementError (node_modules/@testing-library/dom/dist/config.js:37:19)
      at node_modules/@testing-library/dom/dist/query-helpers.js:76:38
      at node_modules/@testing-library/dom/dist/query-helpers.js:52:17
      at node_modules/@testing-library/dom/dist/query-helpers.js:95:19
      at Object.getByRole (src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx:15:20)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 passed, 2 total
Snapshots:   0 total
Time:        0.612 s, estimated 1 s
Ran all test suites matching /src\/component\/StayExtensionOptionsRadio\/StayExtensionOptionsRadio.test.tsx/i.

「TestingLibraryElementError: Unable to find an accessible element with the role "radio" and name "previous"」に注目する。Unableはできなかった、ということ。to findと続くため、「見つけられなかった」となる。つまり指定のRoleは見つからなかった。という意味である。

getByRoleのクエリに問題がある、とわかったため修正方法を検討する。次に注目すべきは「Here are the accessible roles:」の部分である。実際どのようにレンダリングされているかを教えてくれている。Nameの部分に注目する。

    Here are the accessible roles:
      radio:

      Name "前泊のみ": // ←Nameが{name: "xxx"}のnameに一致する
      <input
        name="extension"
        type="radio"
        value="previous"
      />

inputにもnameがあるため混乱しやすい。roleのNameとinputのnameは別物である。<input>name4はフォームコントロール要素として使われるため、WAI-ARIA Roleとは役割が異なる。

Nameの部分がWAI-ARIA Roleのnameとなるため、次のように修正する。

  it("前泊のみのラベルテキストとvalueがデグレードしていない", () => {
    const content = render(<StayExtensionOptionsRadio />);
    expect(content.getByText("前泊のみ")).toBeTruthy();
    // 出力結果Name "前泊のみ"を参考にクエリを修正
    expect(content.getByRole("radio", { name: "前泊のみ" })).toHaveAttribute("value", "previous");
  });

テストもオールグリーンになった。

➜  react-sandbox git:(65-radio-button) ✗ npm run test src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx

> reactjs-sandbox@1.0.0 test
> node scripts/test.js --watchAll=false src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx

 PASS  src/component/StayExtensionOptionsRadio/StayExtensionOptionsRadio.test.tsx
  初期状態のテスト
    ✓ ラジオボタンは全部で4つ存在する (37 ms)
    ✓ 前泊のみのラベルテキストとvalueがデグレードしていない (13 ms)

Test Suites: 1 passed, 1 total
Tests:       2 passed, 2 total
Snapshots:   0 total
Time:        0.91 s, estimated 1 s
Ran all test suites matching /src\/component\/StayExtensionOptionsRadio\/StayExtensionOptionsRadio.test.tsx/i.