Testing
Unit and integration tests with Bun's built-in runner (bun test, bun:test); end-to-end tests with Playwright.
Testing
TL;DR
Use bun test with imports from bun:test—the runner documented at Writing tests (CLI flags, lifecycle hooks, mocks, snapshots, coverage). Use Playwright only for E2E in a real browser.
Do not default to Vitest or Jest for Manic apps—you already have a fast, native runner. Reach for Vitest/Jest only when you have a specific compatibility need.
What It Is
Testing patterns in Manic:
| Type | Tool | What It Tests |
|---|---|---|
| Unit | bun test | Components, hooks, utilities |
| Integration | bun test | API routes, state |
| E2E | Playwright | User flows, pages |
Prerequisites
- Project structure — where to put tests (
tests/,*.test.ts, etc.) - API routes — testing Hono handlers
Quick Start
Install tooling
bun add -d @testing-library/react @testing-library/jest-dom @playwright/testbun test— built-in runner; APIs and CLI options are covered in Bun’s test documentation (bun:test,--watch,--coverage, etc.).- Testing Library — render components and assert DOM (works with Bun’s runner).
- Playwright — browser E2E only.
Add a script in package.json:
{
"scripts": {
"test": "bun test",
"test:e2e": "playwright test"
}
}Test Examples
Example 1: Component Test
// tests/Button.test.tsx
import { describe, it, expect, mock } from 'bun:test';
import { render, screen, fireEvent } from '@testing-library/react';
import '@testing-library/jest-dom';
import { Button } from '../app/routes/~components/Button';
describe('Button', () => {
it('renders text', () => {
render(<Button>Click me</Button>);
expect(screen.getByText('Click me')).toBeInTheDocument();
});
it('handles click', () => {
const onClick = mock(() => {});
render(<Button onClick={onClick}>Click me</Button>);
fireEvent.click(screen.getByText('Click me'));
expect(onClick.mock.calls.length).toBe(1);
});
});Example 2: Hook Test
// tests/useCounter.test.ts
import { describe, it, expect } from 'bun:test';
import { renderHook, act } from '@testing-library/react';
import { useCounter } from '../app/routes/~hooks/useCounter';
describe('useCounter', () => {
it('increments counter', () => {
const { result } = renderHook(() => useCounter());
expect(result.current.count).toBe(0);
act(() => {
result.current.increment();
});
expect(result.current.count).toBe(1);
});
});Example 3: Programmatic navigation (navigate)
navigate() forwards to window.__MANIC_NAVIGATE__ when the client bundle is loaded. In unit tests, stub that hook and assert arguments:
// tests/navigate.test.ts
import { describe, it, expect, mock, beforeEach, afterEach } from 'bun:test';
import { navigate } from 'manicjs';
describe('navigate', () => {
const spy = mock<(to: string, opts?: { replace?: boolean }) => void>(() => {});
beforeEach(() => {
(globalThis as unknown as { window: Window }).window = {
__MANIC_NAVIGATE__: spy,
} as unknown as Window;
});
afterEach(() => {
spy.mockClear();
Reflect.deleteProperty(globalThis, 'window');
});
it('delegates to the Manic runtime', () => {
navigate('/about');
expect(spy.mock.calls.length).toBe(1);
expect(spy.mock.calls[0]?.[0]).toBe('/about');
});
it('passes replace', () => {
navigate('/login', { replace: true });
expect(spy.mock.calls[0]).toEqual(['/login', { replace: true }]);
});
});Example 4: API Route Test
Default-exported Hono apps implement the fetch API—invoke them like a Worker:
// tests/api/users.test.ts
import { describe, it, expect } from 'bun:test';
import users from '../../app/api/users/index';
describe('/api/users', () => {
it('returns users', async () => {
const req = new Request('http://localhost/api/users');
const res = await users.fetch(req);
expect(res.status).toBe(200);
});
it('creates user', async () => {
const req = new Request('http://localhost/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email: 'test@test.com' }),
});
const res = await users.fetch(req);
expect(res.status).toBe(201);
});
});Prefer bun test for these; no separate Node test runner required.
E2E Testing with Playwright
Config
// playwright.config.ts
import { defineConfig } from '@playwright/test';
export default defineConfig({
testDir: './e2e',
webServer: {
command: 'manic dev',
port: 6070,
reuseExistingServer: true,
},
});Test
// e2e/home.test.ts
import { test, expect } from '@playwright/test';
test('homepage loads', async ({ page }) => {
await page.goto('/');
await expect(page.locator('h1')).toContainText('Welcome');
});
test('navigation works', async ({ page }) => {
await page.goto('/');
await page.click('text=About');
await expect(page).toHaveURL('/about');
});
test('form submission', async ({ page }) => {
await page.goto('/contact');
await page.fill('input[name="email"]', 'test@test.com');
await page.fill('textarea[name="message"]', 'Hello');
await page.click('button[type="submit"]');
await expect(page.locator('text=Message sent')).toBeVisible();
});Running Tests
Unit / integration (bun test)
See Writing tests for the full CLI (filtering, timeouts, reporters) and bun:test APIs.
bun test
bun test --watchFilter by path or name:
bun test tests/api
bun test --test-name-pattern="Button"E2E (Playwright)
Playwright is a separate CLI; invoke it with Bun:
bunx playwright testCoverage (Bun)
bun test --coverageBest Practices
Test user-facing behavior — not implementation details.
Mock external dependencies — APIs, databases, etc. Use mock() / spyOn from bun:test, not Vitest’s vi.
Use realistic data — test with representative examples.
See also: