createRoutesStubは、自動生成される型定義にマッチしないケースが多い
ReactRouter(v7 / Framework mode)を愛用しています。Vitestでのテストもめっちゃ速いです。
今回どーしたもんだこれって思ったのが、Route.ComponentProps
を受け取る形のComponentを作ると、createRoutesStub
が実質使えない、です。ComponentPropsに必要な全てのパラメータを与える必要があります。ちょっと意味が。
export default function LoginComponent({ actionData }: Route.ComponentProps) {}
createRoutesStubの基本
その名前の通り、routes.ts
に書かれたルートの構造をスタブするものです。これにより、route単位のテストがかけるようになります。
loader
action
は、その都度指定しないと駄目です。loader: () => { return {items:[]}}
という感じで適当なデータを返してもいいし、実際に使ってるloaderをそのまま差し込むこともできます。ルート単位のE2E、アツいと思うんですよね。どうっすかね。
以下は、公式にも書かれているログインページのテストコードをちょっと改修したものです。
import {
render,
screen,
waitFor,
} from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { createRoutesStub } from "react-router";
import { it } from 'vitest';
import LoginComponent, { action } from "./login";
it("間違った情報でログインを実行", async () => {
const LOGIN_ERROR_MESSAGE = "ユーザー名とパスワードの組み合わせが誤っているようです";
const Stub = createRoutesStub([
{
path: "/login",
Component: LoginComponent,
// actionをそのまま差し込める
// mockにしたいなら、action: () => ({ errors: "ERROR"}})みたいな感じ。
action: action
},
]);
render(<Stub initialEntries={["/login"]} />);
const emailInput = screen.getByLabelText('メールアドレス');
const passwordInput = screen.getByLabelText('パスワード');
await userEvent.type(emailInput, "test@example.com");
await userEvent.type(passwordInput, "test");
userEvent.click(screen.getByText("ログイン"));
await waitFor(() => screen.findByText(LOGIN_ERROR_MESSAGE));
})
プロダクションで動かす loader
action
のロジックをそっくりテストすることができます。ルート単位でE2Eを書くことができるので、最も効率が良いテストがかけると思っています。パラメーター飛んできたけど検索条件に入ってねぇとか、DBに値が入ってねぇ or バリデーション通ってないとかをチェックするために、ルート単位で何パターンかのテストを書いて、最後は、PlayWrightで正常系シナリオ一本書いて終わり、というやり方を取っています。
Mockでダミー返して見た目のレンダリングやロジックだけ確認するテストは、プロダクトのデザインシステムが作られていない限り、必須ではないでしょう。受託では不要の長物だと思います。
LoginComponent
は useActionData
というHooksを使っているので、このように書きます。
export default function LoginComponent(){
const lastResult = useActionData<typeof action>()
}
以下のようなactionDataを受け取る定義になると、コンパイルエラーがめっちゃでます。
export default function LoginComponent({ actionData }: Route.ComponentProps) {}
// Error
Type '({ actionData }: ComponentProps) => Element' is not assignable to type 'ComponentType<{}> | null | undefined'.
Type '({ actionData }: ComponentProps) => Element' is not assignable to type 'FunctionComponent<{}>'.
Types of parameters '__0' and 'props' are incompatible.
Type '{}' is missing the following properties from type 'ComponentProps': params, loaderData, matches
params
loaderData
matches
の定義がないんだが?と言われてしまいます。ReactRouter v7が動的に差し込んでくれるものですけど、Vitest単体で見ると、「そんなもん知らん」という話のようです。
Routes.ComponentProps
を使ってpropsを受け取るコンポーネントを作ると、それらすべての値にMockが必要になるので、まじで耐えられへん。
全部にmockを差し込むってこういう感じのコードになる(動かないです)
仮に、ここまでやってRRv7の破壊的変更があったら差し替える可能性もある。Hooksでしれっと差し替えが効くのがよいし、Mockするなら、loader/actionの関数であるべきじゃん。
it("HOME画面初期表示", async () => {
const Stub = createRoutesStub([
{
path: "/",
Component: () => (
<HomeComponent
loaderData={{
categories: [
{ label: "test", value: 1 },
{ label: "test2", value: 2 },
],
searchResult: {
items: [],
total: 1,
},
}}
params={{}}
matches={[
{
id: "root",
params: {},
pathname: "/",
data: undefined,
handle: undefined,
},
]}
/>
),
},
]);
参考スレ
解決策:Hooksに戻した
テスト書きたいので、useLoaderData
とuseActionData
にして、しれっと状態を注入するHooksスタイルに戻るしかない。
パスパラメータを伴うRouteだと型定義が合わない
/detail/:itemId
のような定義をすると、自動生成された Loader / Actionの型定義を参照しない。
const Stub = createRoutesStub([
{
path: "/detail/:itemId",
loader: loader,
Component: DetailComponent,
},
],);
render(<Stub initialEntries={["/detail/658"]} />);
この時、loader
が型エラーをはく。Type 'LoaderFunctionArgs<any>' is not assignable to type 'LoaderArgs'
となる。RRv7のcontext, paramsなどをタイプセーフにしてくれる型定義が全部<any>で終わってしまう。
Route.LoaderArgs
を LoaderFuntionArgs
に書き換えればエラーは消えるけど、こんな素敵な型定義が消えてLoaderFunctionArgs<Context = any>
になるのはさみしい。
type Route.LoaderArgs = {
request: Request;
params: {
itemId: string;
} & {
[key: string]: string | undefined;
};
context: MiddlewareEnabled extends true ? unstable_RouterContextProvider : AppLoadContext;
}
参考スレ
解決策:華麗にスルー
一旦、赤波線を甘受して進めています。おーん。
createRemixStubの流れを受け継いでいるためか、型定義が厳密になったRRv7が作る定義と、うまく整合性が取れていないようです。Route.LoaderArgsを使わないで実装できるので、なんともかんとも。時間が経てば解決されるような気もする。動きはするから。
何かあれば追記します。
Discussion