仮想スクロール(@tanstack/react-virtual)導入でテストが落ちるようになった件について
はじめに
明細など「行が大量に並ぶ画面」で、表示件数が増えるほどレンダリングが重くなってきたため、@tanstack/react-virtualで仮想スクロールを導入しました。
こちらの記事で導入の背景など書いているので参考までに
その際に仮想スクロールの導入自体はうまくいったのですが、仮想スクロールを導入したテーブルがテスト環境(Vitest)で「何も描画されない」状態になり関連するテストが全て落ちるようになりました。
この記事では、その原因と解消した方法をまとめています。
今回の問題:テストが落ちるようになった
仮想スクロール導入後、Vitest で対象コンポーネントのテストを走らせると失敗しました。
症状はこんな感じです。
-
screen.getByText(...)が見つからない -
getAllByRole('row')が 0 件 - snapshot がほぼ空(コンテナだけで中身がない)
つまり、テスト上では描画されるはずの行が1件もレンダリングされない状態になっていました。
原因の整理:なぜ「何も描画されない」になるのか
@tanstack/react-virtual は、表示範囲(どの行を描画するか)を計算するために、DOM のサイズ情報を参照します。例えば以下のようなものです。
clientHeight / clientWidthoffsetHeight / offsetWidthscrollHeightgetBoundingClientRect()
ところが、Vitest の実行環境としてよく使われる jsdom は、実ブラウザのようなレイアウト計算をほぼ行いません。 そのため、上記の値が 0 扱いになったり、期待した値が取れなかったりします。
結果として、
- viewport の高さが 0 扱いになる
- 「表示できる行数」が 0 と判断される
-
virtualItemsが空になる -
virtualItems.map(...)が回らず、子要素が一切描画されない
という流れで「何も描画されない」状態が起きました。
解決策:テスト環境にモックを追加する
今回の解決策はシンプルで、テスト環境(jsdom)側で それっぽい寸法情報を返すようにモックすることでした。
以下を追加したら解消しました。
Object.defineProperties(HTMLElement.prototype, {
offsetHeight: {
configurable: true,
value: 140
},
offsetWidth: {
configurable: true,
value: 800
},
clientHeight: {
configurable: true,
value: 500
},
clientWidth: {
configurable: true,
value: 800
},
scrollHeight: {
configurable: true,
value: 1000
}
});
Element.prototype.getBoundingClientRect = vi.fn(() => ({
width: 800,
height: 500,
top: 0,
left: 0,
bottom: 500,
right: 800,
x: 0,
y: 0,
toJSON: () => {}
}));
ポイントはvirtualizer が計算を進められる最低限の前提を用意することなので、設定している値は適当です。ユニットテストでは目的がレイアウトの厳密さではなく、レンダリング成立やロジック検証になりやすいためここを固定値にしても困らないと思います。
まとめ
@tanstack/react-virtual は表示範囲の計算に DOM の寸法(clientHeight / offsetHeight / getBoundingClientRect() など)を使います。ところが Vitest の jsdom 環境ではレイアウト計算がほぼ行われず、寸法が取れないことで virtualItems が空になり、結果としてテスト上では「何も描画されない」状態になっていました。
対策として、テスト環境で寸法系プロパティと getBoundingClientRect() をモックして計算の前提を用意したところ解消しました。仮想スクロール導入後に DOM が空になる系のテスト失敗が出たら、まずは「テスト環境でサイズが取れているか」を疑うのが近道です。
Discussion
個人的にこういうときは無理にmockするよりは、Vitest Browser Modeを使っています。