Advanced

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:

TypeToolWhat It Tests
Unitbun testComponents, hooks, utilities
Integrationbun testAPI routes, state
E2EPlaywrightUser flows, pages

Prerequisites


Quick Start

Install tooling

bun add -d @testing-library/react @testing-library/jest-dom @playwright/test
  • bun 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 --watch

Filter 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 test

Coverage (Bun)

bun test --coverage

Best 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:

On this page