1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

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.ts
    import solid from "vite-plugin-solid"
    import { defineConfig } from "vitest/config"
    
    export default defineConfig({
      plugins: [solid()],
      resolve: {
        conditions: ["development", "browser"],
      },
    })
    
  • テストを書く

    Counter.spec.tsx
    import { 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());
});

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?