https://docs.solidjs.com/guides/testing#rendering-the-component を読めばいいだけなんですが、Router周りにバグあったのでその回避方法と、その他いくつか解説していきます。
前提
- "@solidjs/router": "^0.15.0"
- "@solidjs/start": "^1.0.10"
- "solid-js": "^1.9.3”
テスト環境構築
-
前提ライブラリインストール
npm i -D vitest jsdom @solidjs/testing-library @testing-library/user-event @testing-library/jest-dom
-
package.jsonに追記
package.json"scripts": { "test": "vitest" }
-
tsconfig.jsonに追記
tsconfig.json"compilerOptions": { // ... "jsx": "preserve", "jsxImportSource": "solid-js", "types": ["vite/client", "@testing-library/jest-dom"] }
-
solidstartを使っている場合はvitest.config.tsを作成する
vitest.config.tsimport solid from "vite-plugin-solid" import { defineConfig } from "vitest/config" export default defineConfig({ plugins: [solid()], resolve: { conditions: ["development", "browser"], }, })
-
テストを書く
Counter.spec.tsximport { test, expect } from "vitest" import { render, screen } from "@solidjs/testing-library" import userEvent from "@testing-library/user-event" import { Counter } from "./Counter" test("increments value", async () => { render(() => <Counter />) const counter = screen.getByRole('button') expect(counter).toHaveTextContent("1") await userEvent.click(counter) expect(counter).toHaveTextContent("2") })
Router(やそのほかProvider)を絡めたテスト
@solidjs/testing-libraryのrender
関数はlocationを取れるので以前のバージョンなら何もしなくてもRouterが絡むテストができたのですが、issueを見るとどうやらできなくなってるようなので 、Routerが絡むテストが書けるよう renderApp
関数を実装します。
テスト対象のコンポーネントは次のような Routerが絡まったコンポーネントです。
import { useParams, useSearchParams } from "@solidjs/router";
export default function Page() {
const params = useParams();
const [search, setSearch] = useSearchParams<{ query: string }>();
return (
<div>
<div>param: {params.id}</div>
<div>search: {search.query}</div>
<button
type="button"
onClick={() => {
setSearch({ query: search.query === "foo" ? "bar" : "foo" });
}}
>
Change Search
</button>
</div>
);
}
これが次のようにルーティングされてる想定です。
<Route path="/router/:id" component={Page} />
これを次のようにテストしたいです。
import { screen } from "@solidjs/testing-library";
import userEvent from "@testing-library/user-event";
import Page from "~/routes/router/[id]";
import { renderApp } from "~/test/renderApp";
test("increments value", async () => {
renderApp(() => <Page />, {
path: "/router/:id",
currentPath: "/router/1?query=foo",
});
expect(screen.getByText("param: 1")).toBeInTheDocument();
expect(screen.getByText("search: foo")).toBeInTheDocument();
const button = screen.getByText("Change Search");
await userEvent.click(button);
expect(screen.getByText("search: bar")).toBeInTheDocument();
await userEvent.click(button);
expect(screen.getByText("search: foo")).toBeInTheDocument();
});
このときrenderApp関数は次のように実装します。
import { MetaProvider } from "@solidjs/meta";
import {
MemoryRouter,
Route,
Router,
createMemoryHistory,
} from "@solidjs/router";
import { render } from "@solidjs/testing-library";
import type { ParentProps } from "solid-js";
import { args } from "valibot";
type UI = Parameters<typeof render>[0];
type Options = NotNull<Parameters<typeof render>[1]>;
type NotNull<T> = T extends null | undefined ? never : T;
export const renderApp = (
ui: UI,
options?: Omit<Options, "location"> & { path?: string; currentPath?: string },
) => {
const Wrapper =
options?.wrapper ??
((props: ParentProps) => {
return <>{props.children}</>;
});
const history = createMemoryHistory();
options?.currentPath &&
history.set({ value: options.currentPath, scroll: false, replace: true });
return render(ui, {
...options,
wrapper: (props) => {
return (
<MetaProvider>
<MemoryRouter history={history}>
<Route
path={options?.path ?? ""}
component={() => <Wrapper>{props.children}</Wrapper>}
/>
</MemoryRouter>
</MetaProvider>
);
},
});
};
fast-checkを使う
前提ライブラリインストール
npm i -D @fast-check/vitest
普通に使えました
import { fc, test } from "@fast-check/vitest";
import userEvent from "@testing-library/user-event";
import { renderApp } from "~/test/renderApp";
import Counter from "./Counter";
test.prop([fc.integer({ min: 1, max: 10 })], { numRuns: 25 })(
"increments value",
async (count) => {
const { getByRole } = renderApp(() => <Counter />);
const counter = getByRole("button");
expect(counter).toHaveTextContent("Clicks: 0");
for (let i = 0; i < count; i++) {
await userEvent.click(counter);
}
expect(counter).toHaveTextContent(`Clicks: ${count}`);
},
10000,
);
mswを使う
前提ライブラリインストール
npm i -D msw
vitest.config.ts
import path from "node:path";
import solid from "vite-plugin-solid";
import { defineConfig } from "vitest/config";
export default defineConfig({
...
test: {
...
setupFiles: ["src/test/setup.ts"],
},
});
src/test/msw.ts
import { setupServer } from "msw/node";
export const server = setupServer();
src/test/setup.ts
import { server } from "./msw";
beforeAll(() => {
server.listen();
});
afterEach(async () => {
server.resetHandlers();
});
afterAll(() => {
server.close();
});
テストは次の形
import { screen, waitFor } from "@solidjs/testing-library";
import { http, HttpResponse } from "msw";
import Page, { route } from "~/routes";
import { server } from "~/test/msw";
import { renderApp } from "~/test/renderApp";
function setupMSW() {
server.use(
http.all("https://jsonplaceholder.typicode.com/todos/1", () => {
return HttpResponse.json({
userId: 1,
id: 1,
title: "todo1",
completed: false,
});
}),
);
}
beforeEach(() => {
setupMSW();
});
it("increments value", async () => {
await route.preload();
renderApp(() => <Page />, {});
await waitFor(() => expect(screen.getByText("todo1")).toBeInTheDocument());
});