visible true

技術的なメモを書く

testing-library/reactでmaterial-uiのTextFieldの値をテストする

material-uiのTextFieldをtesting-library/reactでテストしようとすると、HTMLElementを取り出すところで苦労したのでメモを残す。

TextFieldに付与したaria-labelはinputのラッパー要素に付く

次のTextFieldがどこかのコンポーネントにあるとする。

<TextField
  name="name"
  label="name"
  aria-label="name"
  variant="outlined"
  value="title"
/>

実際にDOMにレンダリングすると次の構造になる。ルートの要素にaria-labelがついていることがわかる。

<div
  aria-label="name"
  class="MuiFormControl-root MuiTextField-root"
>
  <label
  class="MuiFormLabel-root MuiInputLabel-root MuiInputLabel-formControl MuiInputLabel-animated MuiInputLabel-shrink MuiInputLabel-outlined MuiFormLabel-filled"
  data-shrink="true"
  >
  name
  </label>
  <div
  class="MuiInputBase-root MuiOutlinedInput-root MuiInputBase-formControl"
  >
  <input
    aria-invalid="false"
    class="MuiInputBase-input MuiOutlinedInput-input"
    name="name"
    type="text"
    value="title"
  />
  <fieldset
    aria-hidden="true"
    class="PrivateNotchedOutline-root-1 MuiOutlinedInput-notchedOutline"
  >
    <legend
    class="PrivateNotchedOutline-legendLabelled-3 PrivateNotchedOutline-legendNotched-4"
    >
    <span>
      name
    </span>
    </legend>
  </fieldset>
  </div>
</div>

testing-libraryのgetByLabelText関数は、ラベル名でHTMLElementを取り出すので、input要素に直接アクセスできないことがわかる。解決策として2つの方法がある。

querySelector関数を使う

1つ目の方法はgetByLabelText関数でルート要素を取り出したあと、querySelector関数を使ってinput要素を探すというもの。

import { render, cleanup } from "@testing-library/react/pure";
import "@testing-library/jest-dom/extend-expect";
import { TextField } from "@material-ui/core";

describe("Material UIのTextFieldのテスト", () => {
  it("querySelectorを使ってvalueの値をチェックする", () => {
    const result = render(
      <TextField
        name="name"
        label="name"
        aria-label="name"
        variant="outlined"
        value="title"
      />
    );
    // input要素を取り出す。
    const input = result.getByLabelText("name").querySelector("input");
    expect(input?.value).toEqual("title");
  });
});

querySelector("input")の戻り値はHTMLInputElement | null なので適宜nullチェックを加える必要があるが、値を検証するだけならinput?.valueとするだけで十分。

getByDisplayValue関数を使う

2つめの方法は、getByDisplayValue関数を使ってvalue値からinput要素を取り出す。

describe("Material UIのTextFieldのテスト", () => {
  it("getByDisplayValueを使ってvalueの値をチェックする", () => {
    const result = render(
      <div>
        <div>title</div>
        <TextField
          name="name"
          label="name"
          aria-label="name"
          variant="outlined"
          value="title"
        />
      </div>
    );
    const input = result.getByDisplayValue("title");
    // elementが取り出せた時点で 'title' というvalueを持っていることがわかるので
    // このexpectは実際は不要
    expect(input.getAttribute("value")).toEqual("title");
  });
});

getByDisplayValue関数はinput要素系のための関数なので便利なのだが、値を使って取り出すので同じ値のinput要素があったりするとエラーになってしまう。

おわりに

個人的にはラベルを指定した上でquerySelector関数を使う方法が好きですが、しかしMaterial UIの内部構造を知った上で書く必要があるのでちょっとな〜という気はしますが仕方ない気もする。