Bridge Training
Contributing

Testing

How to write and organize tests across the monorepo.

Overview

All packages use Vitest as the test runner. Tests live in a __tests__/ directory at the root of each package, mirroring the src/ folder structure.

packages/bridge-core/
├── src/
│   ├── auction/
│   ├── hands/
│   └── play/
└── __tests__/
    ├── auction/
    │   ├── auction.test.ts
    │   └── auction-utils.test.ts
    ├── hands/
    │   └── hands.test.ts
    └── play/
        └── play-utils.test.ts

The same convention applies to apps/app, packages/bridge-react, and all other testable packages.

Running tests

# From the root — run all tests across the monorepo
pnpm test

# From a specific package
cd packages/bridge-core
pnpm test                                            # All tests
pnpm test -- __tests__/auction/auction.test.ts       # Single file
pnpm test:watch                                      # Watch mode
pnpm test:ui                                         # Vitest UI (browser)

Vitest configuration

Each package has its own vitest.config.ts that uses shared helpers from @workspace/config/vitest:

Pure TypeScript packages (no DOM)

import { createSrcAlias, createTestConfig } from "@workspace/config/vitest";
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: createTestConfig(),
  resolve: { alias: createSrcAlias(import.meta.dirname) },
});

React packages (jsdom)

import react from "@vitejs/plugin-react";
import { createSrcAlias, createTestConfig } from "@workspace/config/vitest";
import { defineConfig } from "vitest/config";

export default defineConfig({
  plugins: [react()],
  test: createTestConfig({
    environment: "jsdom",
    setupFiles: ["./vitest.setup.ts"],
  }),
  resolve: { alias: createSrcAlias(import.meta.dirname) },
});

The vitest.setup.ts file in React packages mocks browser APIs that jsdom doesn't support (scrollIntoView, pointer capture, ResizeObserver, matchMedia) — required for Radix UI components.

TypeScript configuration

Each package has a dedicated tsconfig.test.json that includes both src and __tests__, and sets up the @/ path alias so tests can import source code cleanly:

{
  "extends": "@workspace/typescript-config/base.json",
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["./src/*"],
      "@workspace/bridge-core/*": ["./src/*"]
    }
  },
  "include": ["__tests__", "src"],
  "exclude": ["node_modules"]
}

Packages also map their own @workspace/<name>/* to ./src/* so that imports like @workspace/bridge-core/engine resolve to source during tests instead of built output.

React packages extend @workspace/typescript-config/react.json instead and also include vitest.setup.ts in the include array. When adding a new testable package, create a tsconfig.test.json following this pattern.

createTestConfig options

OptionDefaultDescription
environmentSet to "jsdom" for React/DOM tests
setupFilesArray of setup file paths
include__tests__/**/*.{test,spec}.*Test file glob patterns
coveragev8 providerSet to false to disable, or pass { include, exclude }

File naming

  • Test files: <name>.test.ts or <name>.test.tsx
  • Mirror the source path: src/auction/auction-utils.ts__tests__/auction/auction-utils.test.ts
  • Utility files: __tests__/test-utils.tsx for shared render helpers

Imports

All packages alias @/ to src/. Use @/ in test files instead of relative paths:

// Good
import { calculateContract } from "@/auction";
import { makeBidSequence } from "@/testing/make-deal";

// Avoid
import { calculateContract } from "../../src/auction";

The alias is configured via createSrcAlias(import.meta.dirname) in each vitest.config.ts.

Writing tests

Pure logic (bridge-core)

Import from vitest directly. Use the test factories from bridge-core/src/testing/make-deal to build domain objects:

import { describe, expect, it } from "vitest";
import { calculateContract } from "@/auction";
import { makeBidSequence } from "@/testing/make-deal";

describe("calculateContract", () => {
  it("derives the final contract and declarer", () => {
    const sequence = makeBidSequence("N / 1H pass 4H AP");
    const result = calculateContract(sequence);

    expect(result?.type).toBe("confirmed");
    expect(result?.value).toEqual({
      level: 4,
      suit: "H",
      trump: "H",
      declarer: "N",
      doubled: "none",
    });
  });
});

React components (bridge-react)

Use the custom render from __tests__/test-utils — it wraps components in BridgeProvider and BridgeLabelsProvider with English defaults:

import { fireEvent, screen } from "@testing-library/react";
import { describe, expect, it, vi } from "vitest";
import { Hand } from "@/components/hand/hand";
import { render } from "../test-utils";

describe("Hand", () => {
  it("renders player label", () => {
    render(<Hand cards={defaultCards} player="N" />);
    expect(screen.getByText("North")).toBeInTheDocument();
  });

  it("calls onCardsChange on blur", () => {
    const handleCardsChange = vi.fn();
    render(<Hand cards={defaultCards} onCardsChange={handleCardsChange} player="N" />);

    const input = screen.getAllByRole("textbox")[0];
    fireEvent.focus(input);
    fireEvent.change(input, { target: { value: "AKQJ" } });
    fireEvent.blur(input);

    expect(handleCardsChange).toHaveBeenCalledWith("S", ["A", "K", "Q", "J"]);
  });
});

You can customize the context via render(ui, { context, labels }):

render(<MyComponent />, {
  context: { teachingMode: "petit-bridge", locale: "fr" },
});

App-level tests

Tests in apps/app follow the same pattern. Domain test factories are available from @workspace/domain/testing (e.g., makeBoard).

The /testing export

Packages that provide test utilities expose them via a ./testing export in their package.json. This lets consumers import factories without reaching into internal paths:

import { makeDeal, makeBidSequence } from "@workspace/bridge-core/testing";
import { makeBoard, makeGroup } from "@workspace/domain/testing";
import { richText, fromString } from "@workspace/rich-text/testing";

When adding test utilities to a package, place them in src/testing/ and add a "./testing" entry to the package's exports field. See each package's documentation for the full list of available factories.

Coverage

Coverage uses the v8 provider with text, json, and html reporters. Source files in src/**/* are included, .d.ts files excluded.

cd packages/bridge-core
pnpm test -- --coverage

Best practices

  • Mirror the source tree in __tests__/ — makes files easy to find
  • Use test factories (makeDeal, makeBidSequence, etc.) instead of manually constructing domain objects
  • One assertion per test when possible — descriptive it("...") names replace comments
  • Use vi.fn() for spies and mocks, avoid mocking internal modules when you can test through the public API
  • Don't commit .only or .skip — the linter will catch it
  • Async tests: use async/await, not done callbacks

On this page