preact で Playwright を利用したコンポーネントテストをする
React から Preact に切り替える場合のデメリットとして公式 Playwright が提供するコンポーネントテスト (以降 CT) が利用できなくなるというものがありました。
一応 Preact 側で Preact playwright component testing を提供してくれていますが、メンテナンスが止まっています。最初はこれを fork して自前で運用することを考えていたのですが Vitest のBrowse Mode で Playwright を利用した CT が実現できそうとういことに気付きました。
Vitest Browse Mode
Browse Mode については公式ページをどうぞ。

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 月時点では自分がとても気に入ってる構成です。
