2 min read

preact で Playwright を利用したコンポーネントテストをする

React から Preact に切り替える場合のデメリットとして公式 Playwright が提供するコンポーネントテスト (以降 CT) が利用できなくなるというものがありました。

一応 Preact 側で Preact playwright component testing を提供してくれていますが、メンテナンスが止まっています。最初はこれを fork して自前で運用することを考えていたのですが Vitest のBrowse Mode で Playwright を利用した CT が実現できそうとういことに気付きました。

Vitest Browse Mode

Browse Mode については公式ページをどうぞ。

Vitest
Next generation testing framework powered by Vite

pnpm add -D vitest @vitest/browser-playwright Vitest ではBrowse Mode 利用時のバックエンドに Playwright を指定することができます。

vitest-browse-preact

さらに vitest-browse-preact を Preact の中の人が提供してくれています。これ公式で提供してくれていい気がしますが ... 。

import preact from "@preact/preset-vite";
import tailwindcss from "@tailwindcss/vite";
import { playwright } from "@vitest/browser-playwright";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [preact(), tailwindcss()],
  optimizeDeps: {
    include: ["vitest-browser-preact"],
  },
  test: {
    globals: true,
    include: ["src/**/*.ct.tsx"],
    exclude: ["node_modules", "dist", ".git", ".cache"],
    setupFiles: ["vitest-browser-preact"],
    browser: {
      enabled: true,
      provider: playwright(),
      headless: true,
      instances: [{ browser: "chromium", headless: true }],
    },
  },
});

https://github.com/shiguredo/sora-devtools/blob/develop/vitest.ct.config.ts

import { assert, test } from "vitest";
import { render } from "vitest-browser-preact";
import { FormLabel } from "./FormLabel";

test("FormLabel: children をレンダリングする", async () => {
  const screen = render(<FormLabel>テストラベル</FormLabel>);
  const label = screen.getByText("テストラベル");
  assert.isNotNull(label.element());
});

test("FormLabel: htmlFor 属性を設定する", async () => {
  const screen = render(<FormLabel htmlFor="test-input">ラベル</FormLabel>);
  const label = screen.getByText("ラベル");
  assert.equal(label.element().getAttribute("for"), "test-input");
});

test("FormLabel: デフォルト className に me-2 を含む", async () => {
  const screen = render(<FormLabel>ラベル</FormLabel>);
  const label = screen.getByText("ラベル");
  assert.include(label.element().className, "me-2");
});

test("FormLabel: カスタム className をマージする", async () => {
  const screen = render(<FormLabel className="custom-class">ラベル</FormLabel>);
  const label = screen.getByText("ラベル");
  const className = label.element().className;
  assert.include(className, "me-2");
  assert.include(className, "custom-class");
});

https://github.com/shiguredo/sora-devtools/blob/develop/src/components/ui/FormLabel.ct.tsx

$ pnpm test:ct

> vitest run --config vitest.ct.config.ts


 RUN  v4.0.16`

 ✓  chromium  src/components/ui/FormLabel.ct.tsx (4 tests) 14ms
   ✓ FormLabel: children をレンダリングする 12ms
   ✓ FormLabel: htmlFor 属性を設定する 1ms
   ✓ FormLabel: デフォルト className に me-2 を含む 1ms
   ✓ FormLabel: カスタム className をマージする 0ms

 Test Files  1 passed (1)
      Tests  4 passed (4)
   Start at  11:10:22
   Duration  551ms (transform 0ms, setup 5ms, import 57ms, tests 14ms, environment 0ms)

無事 Playwright CT で書いていたテストが Vitest Browse Mode (Playwright) へ移行できました。

雑感

CT は人間が書くのは現実的ではないのですが LLM に書いてもらう事でコストは減らせます。CT があるだけでデグレをかなり防げるようになります。おすすめです。

Preact は React ほど高機能は必要なく、状態管理ライブラリとルーターを公式で提供してくれつつ Oxc の React 機能も利用できるというメリットがあるので大変おすすめです。サイズが小さいことで LLM のコンテキスト利用を減らせるというのもあります。

規模の小さい SPA しか作らないので preact + signals + preact-iso + tailwind + vite + vitest + playwright + oxc は 2026 年 1 月時点では自分がとても気に入ってる構成です。