Why We Skipped Vitest and Went All-in on Playwright
When Vitest failed with ERR_REQUIRE_ESM under Bun runtime, we chose Playwright for all testing — here's why

The Goal
Fable is a monorepo with two workspaces:
agent/ — Bun + TypeScript backendui/ — React + Vite frontendWe wanted to add tests. Specifically:
1. Unit tests for React components (StoryBook.tsx, LandingPage.tsx, etc.)
2. E2E tests for browser flows (navigate to /setup, fill form, submit, etc.)
Simple enough, right?
Attempt 1: Vitest
Setup
Vitest is the natural choice for Vite projects. We installed it:
cd ui
bun install -d vitest @testing-library/react @testing-library/jest-dom jsdomCreated vite.config.ts:
// ui/vite.config.ts
import { defineConfig } from "vitest/config";
import react from "@vitejs/plugin-react";
export default defineConfig({
plugins: [react()],
test: {
environment: "jsdom",
setupFiles: ["./src/test/setup.ts"],
},
});Created src/test/setup.ts:
// ui/src/test/setup.ts
import "@testing-library/jest-dom/vitest";Wrote our first test:
// ui/src/components/LandingPage.test.tsx
import { render, screen } from "@testing-library/react";
import LandingPage from "./LandingPage";
test("renders landing page", () => {
render(<LandingPage />);
expect(screen.getByText("Fable")).toBeInTheDocument();
});The Error
Ran the test:
cd ui
bun test run src/components/LandingPage.test.tsxOutput:
Error [ERR_REQUIRE_ESM]: require() of ES Module /path/to/vitest/dist/index.js not supported.Investigation
The issue: Bun's runtime doesn't play well with Vitest's ESM expectations.
Vitest expects to run under Node.js, where it can control the module system. Bun uses its own runtime, which has different ESM/CJS interop behavior.
We tried:
1. Using `bun test` instead of `vitest` — But bun test doesn't support jsdom environment properly.
2. Running Vitest with Node directly — node node_modules/.bin/vitest — But then it can't find Bun-specific imports in the codebase.
3. Configuring Vitest for ESM — Added "type": "module" to package.json, various test.deps configs — Still ERR_REQUIRE_ESM.
After 3 hours of debugging, we made a decision: skip Vitest.
Attempt 2: Playwright for E2E
Setup
Playwright was already partially set up in the project. We completed the setup:
cd ui
bun install -D @playwright/test
npx playwright install chromiumCreated playwright.config.ts:
// ui/playwright.config.ts
import { defineConfig } from "@playwright/test";
export default defineConfig({
webServer: {
command: "bun run dev:ui",
url: "http://localhost:3000",
reuseExistingServer: !process.env.CI,
},
use: {
baseURL: "http://localhost:3000",
},
projects: [
{
name: "chromium",
use: { ...devices["Desktop Chrome"] },
},
],
});Writing Tests
First, basic route tests:
// ui/e2e/routes.spec.ts
import { test, expect } from "@playwright/test";
test("loads landing page", async ({ page }) => {
await page.goto("/");
await expect(page.locator("h1")).toHaveText("Fable");
});
test("navigates to setup page", async ({ page }) => {
await page.goto("/");
await page.click("text=Start Story");
await expect(page).toHaveURL("/setup");
});
test("navigates to discover page", async ({ page }) => {
await page.goto("/");
await page.click("text=Discover");
await expect(page).toHaveURL("/discover");
});Then, navigation tests:
// ui/e2e/navigation.spec.ts
import { test, expect } from "@playwright/test";
test("browser back button works", async ({ page }) => {
await page.goto("/setup");
await page.click("text=Back");
await expect(page).toHaveURL("/");
});
test("deep link to story page", async ({ page }) => {
// This tests the route-based architecture
await page.goto("/story/abc123");
// Should either load the story or show 404
await expect(page.locator("body")).toBeVisible();
});The Win
Playwright tests actually ran:
cd ui
npx playwright test
# Output:
✓ e2e/routes.spec.ts:3 tests passed
✓ e2e/navigation.spec.ts:3 tests passed
✓ e2e/discover.spec.ts:2 tests passed
9 tests passed!Why Playwright Over Vitest?
1. It Actually Works
This is the most important reason. Playwright runs in a real browser (Chromium, Firefox, WebKit). It doesn't care about Bun vs Node ESM issues because it controls the browser directly.
2. Better Coverage for Our Use Case
We realized: E2E tests cover more user-facing scenarios than unit tests:
| Test Type | What it Catches |
| --------------------- | --------------------------------- |
| Unit test (Vitest) | Component renders correctly |
| E2E test (Playwright) | User can navigate through the app |
For a user-facing app like Fable, the second one matters more.
3. Real Browser Behavior
Playwright tests real browser behavior:
Unit tests with jsdom simulate these behaviors, but not always accurately.
4. Already Working
The E2E setup was already partially done. Vitest required fighting with ESM issues. The choice was clear.
What We Lost (and Why It's OK)
Lost: Fast Unit Tests for Components
With Vitest, we could write:
test("StoryBook renders messages", () => {
render(<StoryBook messages={mockMessages} />);
expect(screen.getByText("Once upon a time...")).toBeInTheDocument();
});With Playwright, we can't easily test individual components in isolation.
Why it's OK: Fable's components are mostly UI + API calls. The real behavior happens when the user interacts with the browser. E2E tests catch the important issues.
Lost: Quick Feedback Loop
Unit tests run in milliseconds. Playwright tests take seconds (browser startup, page navigation, etc.).
Why it's OK: We run E2E tests in CI/CD. For local development, we rely on the dev server's fast refresh.
Our Testing Strategy Now
Playwright E2E Tests (Primary)
// ui/e2e/routes.spec.ts
// Tests all routes load correctly
test("loads /setup page", async ({ page }) => {
await page.goto("/setup");
await expect(page.locator("h1")).toBeVisible();
});
// ui/e2e/navigation.spec.ts
// Tests browser navigation
test("back button from /setup goes to /", async ({ page }) => {
await page.goto("/setup");
await page.goBack();
await expect(page).toHaveURL("/");
});
// ui/e2e/discover.spec.ts
// Tests discover page functionality
test("shows public stories", async ({ page }) => {
await page.goto("/discover");
await expect(page.locator(".story-card")).toHaveCount(5);
});Manual Testing (Secondary)
For edge cases that are hard to automate:
Future: Firebase Emulator Tests
We might add integration tests with Firebase emulator:
// ui/e2e/integration.spec.ts
test("full story creation flow", async ({ page }) => {
// Sign in (with emulator)
await page.click("text=Sign In");
// Fill setup form
await page.fill("#age", "7");
await page.click("text=Next");
// Write story
await page.fill("#prompt", "Once upon a time...");
await page.click("text=Send");
// Wait for completion
await page.waitForURL("/story/**");
await expect(page.locator(".story-title")).toBeVisible();
});Lessons Learned
1. Don't Fight Your Tools
If a tool doesn't work after reasonable effort (3 hours in our case), switch. Don't waste days fighting ESM issues when there's a working alternative.
2. E2E Tests Are Better for User-Facing Apps
For apps where the user journey matters more than individual component logic:
Choose based on what matters for your app.
3. Playwright > Vitest for Bun Monorepos
| Criteria | Vitest | Playwright |
| ---------------- | --------------- | ------------------------ |
| Works with Bun | ❌ ESM issues | ✅ Runs in browser |
| Tests user flows | ⚠️ Simulated | ✅ Real browser |
| Setup effort | ❌ High (ESM) | ✅ Low (already working) |
| Test coverage | Component logic | User journeys |
4. It's OK to Skip "Best Practices"
The "best practice" says: write unit tests for all components. But if the tooling doesn't support your stack, it's OK to choose a different strategy.
The goal is tested code, not adherence to dogma.
5. Test the Important Stuff
We prioritized testing:
1. All routes load correctly ✅
2. Navigation works (back button, deep links) ✅
3. Critical user flows (story creation) ⚠️ (in progress)
We didn't prioritize:
The Takeaway
Vitest is great. But if you're using Bun and hit ERR_REQUIRE_ESM, don't bang your head against the wall.
Playwright might be the better choice for your stack anyway — real browser, real user flows, real confidence.
_Check more open source repos at: github.com/mnkrana/_