@testing-libraryで改行など空白文字を含む要素をgetByTextしても取得できない時はnormalizerオプションを見直してみよう

# TL;DR

[testing-library](https://testing-library.com)の`getByText`をはじめとした`ByText`系のクエリーや、[@testing-library/jest-dom](https://github.com/testing-library/jest-dom)の`toHaveTextContent`マッチャーは空白文字を正規化するので、必要に応じて正規化オプションを設定しましょう。

```tsx:言いたいことをコードで表すと…
import { it, expect, afterEach } from "vitest";
import "@testing-library/jest-dom/vitest";
import {
  render,
  screen,
  getDefaultNormalizer,
} from "@testing-library/react";

const App: FC = () => {
  return (
    <div>
      <textarea
        defaultValue={
          "textareaタグではこのように↓\n↑改行は改行として扱われます"
        }
      />
    </div>
  );
};

it("textareaタグでgetByTextできるようにnormalizerを設定する", () => {
  render(<App />);
  expect(
    screen.getByText(
      "textareaタグではこのように↓\n↑改行は改行として扱われます",
      {
        normalizer: getDefaultNormalizer({ collapseWhitespace: false }),
      },
    ),
  ).toBeInTheDocument();
});

it("textareaの中身を見る", () => {
  render(<App />);
  // toHaveTextContentで比較すると正規化された状態でテストされる
  expect(screen.getByRole("textbox")).toHaveTextContent(
    "textareaタグではこのように↓ ↑改行は改行として扱われます",
  );
  // toHaveValueで比較すると正規化されない状態でテストされる
  expect(screen.getByRole("textbox")).toHaveValue(
    "textareaタグではこのように↓\n↑改行は改行として扱われます",
  );
  // toHaveTextContentで比較するとき正規化する内容をカスタマイズしてテストする
  expect(screen.getByRole("textbox")).toHaveTextContent(
    "textareaタグではこのように↓\n↑改行は改行として扱われます",
    { normalizeWhitespace: false },
  );
});
```

------

# はじめに

`@testing-library`で`getByText`によって要素を取得するときに、空白文字が正規化されていることに気がついたので、その動作を調べて[CodeSandbox](https://codesandbox.io)にサンプルコードを用意しました。

# `@testing-library`で`getByText`は空白文字をどのように正規化しているか

`@testing-library`の`getByText`などの`ByText`系のクエリーは、文字列内の複数の隣接する空白文字を1つのスペースとみなして要素を検索しています。（公式ドキュメント: [About Queries#Normalization | Testing Library](https://testing-library.com/docs/queries/about/#normalization)）


例えば以下のような`React`エレメントについて、

```tsx
<p>{"A↓\n\n\n↑B"}</p>
```

次の`getByText`を実行しても要素を取得できません。

```ts
conse elem = screen.getByText("A↓\n\n\n↑B"); // 見つからずに例外が排出される
```

:::note
なお`getByText`を実行しているので、見つからない場合は例外(`TestingLibraryElementError`)が投げられ、そのためにテストが失敗します。

`@testing-library`のクエリの種類については以下を参照してください。

About Queries | Testing Library
https://testing-library.com/docs/queries/about

:::


しかし次の`getByText`では要素を取得することができます。

```ts
// 正規化されることを見越して連続した空白を1つのスペースとして要素を取得する
conse elem = screen.getByText("A↓ ↑B"); 
expect(elem).toBeInTheDocument(); // OK
```

このことから、`@testing-library`では空白文字について正規化を行なった上で、要素を取得していることが確認できます。ちなみに`@testing-library`のソースコード上ではこの箇所が処理を担っています。

https://github.com/testing-library/dom-testing-library/blob/5c7f2e836799873edd700b42df56b9264a457cc8/src/matches.ts#L65-L77

ところで`p`タグの中に改行`\n`を入れても、ブラウザで表示する時には改行されず一つの空白として表示されます。改行したいときは`<br>`タグを使う必要がありますね。`@testing-library`の空白文字の正規化の仕様は、ブラウザの仕様に則したものであると言えるでしょう。

![スクリーンショット 2024-04-17 0.21.06.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/403150/5d919bfc-130e-b797-dda8-d708261b2bfb.png)

# `getByText`の正規化をカスタマイズする

`@testing-library`における空白の取り扱い方はカスタマイズできます。（公式ドキュメント: [About Queries#Normalization | Testing Library](https://testing-library.com/docs/queries/about/#normalization)）

やり方は`getByText`の第二引数のオプションに`normalizer`を指定することです。`normalizer`には正規化前の文字列を受け取って、正規化した文字列を返す関数`(text: string) => string`を指定します。

今回は公式ドキュメントのサンプルにあるように、デフォルトの`normalizer`を取得する関数を`getDefaultNormalizer`を使って`getByText`の正規化をカスタマイズします。`getDefaultNormalizer`は現在2つのオプション`trim`と`collapseWhitespace`を指定できます。

これらを使って改行を改行のまま要素を取得できる`getByText`を作ってみると次のように書けます。

```ts
import { screen, getDefaultNormalizer } from "@testing-library/react";

const elem = screen.getByText("A↓\n\n\n↑B", {
  normalizer: getDefaultNormalizer({
    /* 前後の空白を切り詰めるか */
    trim: false,
    /* 複数の空白を1つのスペースに置き換えるか */
    collapseWhitespace: false,
  }),
});

expect(elem).toBeInTheDocument(); // OK
```

## これの使い所

改行が入っている`p`タグを`getByText`で引いてきたいというケースは少ないかもしれません。前述の通りブラウザで表示する時には改行は空白にまとめられます。また、ソースコードの`JSX`中で改行をしてもレンダリングまでに複数の空白は1つのスペースにまとめられるため、ここまで示してきた例では`React`の`p`の直下に文字列を入れるという不自然な書き方で複数の空白が正規化される様子を確認してきました。

一方、`getByText`は`textarea`の`value`に応じて要素を引いてくることもできるので、サンプルコードでは`textarea`を取得する例を示しました。しかし、`textarea`についても要素を引くのに、`getByRole("textbox")`を用いて、取得後その中身をテストした方がいいかもしれません。

ところで、`textbox`の中身を評価する際にも、ここまで`getByText`の例で見てきたような空白の正規化が行われます。次章で説明します。

## `@testing-library/jest-dom`の`toHaveTextContent`も空白文字の正規化を行なっている

`@testing-library/jest-dom`の`toHaveTextContent`もデフォルトでは連続する空白文字を1つのスペースとみなして評価します。`toHaveTextContent`の引数にはオプションとして`normalizeWhitespace`を渡すことができ、これを無効にすることができます。（公式ドキュメント: [testing-library/jest-dom#toHaveTextContent](https://github.com/testing-library/jest-dom?tab=readme-ov-file#tohavetextcontent)）

`@testing-library/jest-dom`のソースコード上ではこの箇所が処理を担っています。

https://github.com/testing-library/jest-dom/blob/bd82f64592696c9710c59becaf1adfeb1e4c6169/src/utils.js#L167-L169

例えば変数`elem`に次のような要素をが入っている時、
```tsx
<textarea defaultValue={"A↓\n\n\n↑B"} />
// ↑を変数elemに入れる
const elem = getByRole("textbox");
```

`toHaveTextContent`が空白文字を正規化することを前提として次のようなテストを書くことができますが、
```tsx
expect(elem).toHaveTextContent("A↓ ↑B");
```

`textarea`では改行がそのまま表示されるので、ユーザーに見えているものとは違った内容でテストしていると言えます。

このような例の場合、`@testing-library/jest-dom`の`toHaveValue`という、`textarea`や`input`の`value`の内容をテストするマッチャーが適しているでしょう。
```tsx
expect(elem).toHaveValue("A↓\n\n\n↑B");
``` 

なお、`toHaveTextContent`で比較するとき正規化する内容をカスタマイズしてテストすることも可能で次のように書くことができます。

```tsx
expect(elem).toHaveTextContent("A↓\n\n\n↑B", { normalizeWhitespace: false });
```

# CodeSandboxによるサンプルコード

https://codesandbox.io/embed/testing-library-getbytext-normalizer-example-k3j75m

:::note

CodeSandboxのcloud sandboxのqiitaへの埋め込みは以下を参考にしました

https://qiita.com/wataru86/items/c8b9b8a1262a888889fc
:::

## サンプルコードについて

CodeSandboxのDevboxのテンプレート`React (Vite + TS)`に[Vitest](https://vitest.dev)を導入してテストを書いてあります。

## サンプルコード見どころ
* ソースコード
  * `/src/App.test.tsx` : テストコードが書いてあります
  * `/src/App.tsx` : テスト対象のコンポーネントが書いてあります
  * `package.json` : 今回検証に使ったライブラリのバージョンが指定されています
* コンソールのタブ
  * `dev:5173` : テスト対象のコンポーネントのレンダリング結果が表示されています
  * `test` : `Vitest`のテスト結果が表示されています