jsdom Version 27.0.0のアップデートでVitestの単体テストが失敗するようになった
ことの発端
20205-09-13、jsdomがVersion 27.0.0にメジャーアップデートされた。
Release Version 27.0.0 · jsdom/jsdom
これにより、Vitestで書いている既存の単体テストが大量に失敗するようになった。
おそらく原因は、Version 27.0.0 のリリースノートに記載のある変更点、
Updated the user agent stylesheet to be derived from the HTML Standard, instead of from an old revision of Chromium.
ではないかと推測するが、憶測の域を出ない。
いずれにせよ失敗の原因をエラーログから判断して単体テストあるいはコンポーネントを修正する必要があった。
単体テストで失敗するようになった箇所
以下のようなHTMLを持つ要素があったとする。
<div role="cell">
<span>〒100-0005</span>
<span>東京都千代田区丸の内1-2-3</span>
<span>丸の内フロントビル 15階</span>
</div>
以前は以下のように、span要素ごとに1つの文字列の区切りとして扱うことが出来た。
getByRole('cell', { name: '〒100-0005 東京都千代田区丸の内1-2-3 丸の内フロントビル 15階' })
jsdom Version 27.0.0では以下のようにname値の解釈が変更になっていた。
getByRole('cell', { name: '〒100-0005東京都千代田区丸の内1-2-3丸の内フロントビル 15階' })
このようにしてテストを修復できたとしても、アクセシブルネームの値で本来繋がってほしくない単語同士が繋がってしまうので、HTMLの品質として不十分であると判断した。
そのうえで調査を進めていくと、以下のようにしてHTMLをspanからdivに置き換えるとテストはそのままで解決することがわかった。
<div role="cell">
<div>〒100-0005</div>
<div>東京都千代田区丸の内1-2-3</div>
<div>丸の内フロントビル 15階</div>
</div>
ブロック要素として区切ることで、HTML要素の区切りがそのままアクセシブルネームの単語の区切りとして解釈されるようだった。
jsdomのリリースノートからは、よりHTML標準に近い解釈をするような更新を行ったと受け取ることができる。
だとすると、今回のjsdomのアップデートに関わらず、コンポーネントのHTML構造とテストコードが元々問題を抱えていたともいえる。
具体的な対応
1. span要素をdiv要素に置き換える
上述した原因を直接解決する方法となる。
コンポーネントのHTML構造がそもそも期待するアクセシブルネームになっていなかったかもしれないという懸念を解決できるので最も安全そうに思えるが、インライン要素がブロック要素に変わることで画面に描画された際にレイアウト崩れを起こさないか注意する必要がある。
2. アクセシブルネームの単語の区切りがなくなっても自然な日本語表現になるようにする
上述の例とは別に、例えば以下のようにSVG要素とテキストが並んでいるボタンコンポーネントの場合、スピナーのSVGのtitleを処理中から(処理中)にすればボタン全体としてのラベルが日本語として意味が通じるようになるので、HTML構造に影響を及ぼすことなく修正が出来る。
Before
<button>
<svg class="spinner">
<title>処理中</title>
<path />
</svg>
<span>データを保存する</span>
</button>
After
<button>
<svg class="spinner">
<title>(処理中)</title>
<path />
</svg>
<span>データを保存する</span>
</button>
3. コンポーネントの修正が必要ない場合はgetByRoleのname値を更新する
以下のように元々のHTML構造も日本語の文章も問題がない場合、テストコードに記載しているgetByRoleのname値を更新するだけで対応できる。
<div>
<label for="name">
<span>データ入力</span>
<span>(必須)</span>
</label>
<input id="name" type="text" />
</div>
Before
getByRole('cell', { name: 'データ入力 (必須)' })
After
getByRole('cell', { name: 'データ入力(必須)' })
Discussion