PoC ルーティングライブラリの開発ログ

とりあえずコンセプトは「対応ブラウザのシェア割合とか気にせずに1回新しい API 使って実装してみようぜ」というもの。
お察しの通り、Chromium 系ブラウザ専用ライブラリになる予定。

その為にはまず現在のテストコードを実際のブラウザ上で動かすように変更する必要がある。
というわけで、現在の Vitest / happy-dom + React Testing Library という構成のテストを Vitest / Vitest Browser Mode に変更する。

ほとんどはそのまま vitest-browser-react からインポートするように書き換えるだけで済んだが、いくつか引っかかる点があった。
まず1つは、act
の警告。
ドキュメントに書いてある通りの文を setupFile に書いてみたが変わらず出てくる。
vitest-browser-react の README を見てみると、「うちは act
を直接エクスポートしないよ、だって poll
とかビルトインのリトライ機能使えばいいから」的なことが書いてある。
This library doesn't expose React's act and uses it only to flush operations happening as part of useEffect during initial rendering and unmounting. Other use cases are handled by CDP and expect.element which both have built-in retry-ability mechanism.
とりあえずそれに従って renderHook
を使わないテストケースでは expect.poll()
を使うようにすることで act
のインポートを完全に無くす事ができた。当然警告も出なくなった。

もう1つは、history.length
の上限。
history.length
の値が実際に増えたかどうかで処理が正しく実行されているか確認しているテストが数個あった。
ただ、Chromium の history.length
には上限があるようで、50になるとそれ以上増えなくなる。
全てのテストを一度で実行すると簡単に50に届いてしまい、そこから動かなくなるので、このテストが失敗する。
対処法としては普通に test.skipIf(history.length === 50)
と設定してお茶を濁した。

テストを動かしてみると、「Navigation is aborted」というエラーが出る。
おそらく Navigation.navigate()
の返り値である finished
を await していないせいだろうか。
とはいえ、wouter の navigate
関数を async function に変更するのもちょっとはばかれる。まあ別にいいと思うけど、とりあえず元の wouter の API はなるべく維持したい。非同期関数の扱いめんどくさいし
なので、とりあえず今回はフレームワーク側で非同期処理を隠蔽する。
React で非同期処理を扱う方法といえば。Suspense でもいいが、今回の場合は startTransition の方が適切だろう。
とはいえ React 18 の startTransition は非同期関数を渡せない。
なので React 19 に移行・動作確認をする必要がある。

Navigation API に書き換えている途中、<Redirect>
コンポーネントのテストが落ちるようになったことに気付いた。
<Redirect>
コンポーネントは内部の useEffect
で navigate()
関数を呼んでいるので、このコンポーネントをレンダーするだけで指定のページに移動させることができる。
調査してみると、どうやら event.intercept()
が想定通り動いてないようだった。本来全てのナビゲーションを intercept するはずなのだが、コンソールに404エラーが出てるログが表示されていた。
想定通り動いてないというより、呼ばれてすらいないように見える。例えば event.intercept()
の上の行に console.log()
を書いたり throw
してみたりしても何も起きない。
もしやと思い、<Redirect>
コンポーネントをレンダーする前に別の適当な要素をレンダーし、それを click()
してから <Redirect>
コンポーネントを rerender してみると、テストが通った。
明らかに何らかのユーザーインタラクションが無いと動作しない制限がかかっているパターンに見える。
困った事にこの挙動は github の README にも whatwg の仕様にも書かれていない。Chromium のバグトラッカーにもそれらしきものは見あたらない。
仕方がないのでとりあえず諦めることにした。
試しに、ブラウザの UserScript や適当なペライチ html ファイルを使って検証してみたが、そちらでは少なくとも event.canIntercept === false
の状態で呼ばれてはいた。挙動が異なるのは、vitest の Browser Mode では iframe 上でコードが実行される為だろうか。
正直、History API で出来ていたことが Navigation API では出来ないことには少し驚いた。
まあ XHR と fetch という前例はあるけども。

検証コードの例:
window.navigation.addEventListener("navigate", e => {
alert("canIntercept: " + e.canIntercept)
e.intercept({
handler() {
alert("Intercept handler")
}
})
})
window.navigation.navigate("/to/somewhere")

intercept 出来ないと何が問題なのか?それは、navigate()
による移動は必ず intercept しないと通常の MPA サイトでリンクをクリックした時のように cross-document な遷移をしてしまうから。
History API の pushState()
などは単純に URL を変えるだけだが、こちらは intercept しない限り普通に HTTP リクエストを送って移動しようとする。
SPA アプリなんかのようにどの URL からのアクセスでも同じファイルを返すようにすれば解決する話ではある。
ただ vitest の Browser Mode のようなテスト環境ではどう対処したらいいのかわからない。