Testing
Foundry uses Vitest for unit and integration tests, convex-test for Convex backend testing, and Playwright for end-to-end tests.
Running tests
Section titled “Running tests”# Run all tests oncebun run test
# Watch mode (re-runs on file changes)bun run test:watch
# Coverage report for the web appbun run test:coverage:web
# Type checking across all workspacesbun run typecheck
# End-to-end testsbun run test:e2e
# E2E with visible browserbun run test:e2e:headed
# E2E with Playwright UIbun run test:e2e:uiUnit tests with Vitest
Section titled “Unit tests with Vitest”File placement
Section titled “File placement”Place test files adjacent to the code they test, using the .test.ts or .test.tsx extension:
packages/ui/src/tasks/ TaskBoard.tsx TaskBoard.test.tsx TaskCard.tsx TaskCard.test.tsxFrontend component tests
Section titled “Frontend component tests”Use @testing-library/react and @testing-library/user-event for component tests:
import { render, screen } from "@testing-library/react";import userEvent from "@testing-library/user-event";import { describe, expect, it } from "vitest";import { TaskCard } from "./TaskCard";
describe("TaskCard", () => { it("renders the task title", () => { render(<TaskCard task={mockTask} />); expect(screen.getByText("Migrate catalog data")).toBeInTheDocument(); });
it("calls onClick when clicked", async () => { const user = userEvent.setup(); const onClick = vi.fn(); render(<TaskCard task={mockTask} onClick={onClick} />); await user.click(screen.getByRole("article")); expect(onClick).toHaveBeenCalledOnce(); });});Utility and logic tests
Section titled “Utility and logic tests”Pure function tests do not need any React testing setup:
import { describe, expect, it } from "vitest";import { normalizeStatus } from "./normalizeStatus";
describe("normalizeStatus", () => { it("normalizes mixed-case status strings", () => { expect(normalizeStatus("In Progress")).toBe("in_progress"); expect(normalizeStatus("IN_PROGRESS")).toBe("in_progress"); expect(normalizeStatus("inProgress")).toBe("in_progress"); });});Convex backend tests
Section titled “Convex backend tests”Use convex-test to test queries, mutations, and actions against an in-memory Convex backend:
import { convexTest } from "convex-test";import { describe, expect, it } from "vitest";import { api } from "./_generated/api";import schema from "./schema";
describe("requirements.listByProgram", () => { it("returns requirements for the given program", async () => { const t = convexTest(schema); await t.run(async (ctx) => { // Seed test data const programId = await ctx.db.insert("programs", { orgId: "org_test", name: "Test Program", status: "active", }); await ctx.db.insert("requirements", { orgId: "org_test", programId, title: "Migrate catalog", status: "draft", });
// Test the query const results = await ctx.query(api.requirements.listByProgram, { orgId: "org_test", programId, }); expect(results).toHaveLength(1); expect(results[0].title).toBe("Migrate catalog"); }); });});Testing mutations with auth
Section titled “Testing mutations with auth”Convex mutations require authentication context. Use convex-test identity helpers:
const t = convexTest(schema);await t.run(async (ctx) => { // Set up authenticated identity const asUser = t.withIdentity({ subject: "user_123", issuer: "https://clerk.example.com", });
await asUser.mutation(api.tasks.create, { orgId: "org_test", programId, title: "New task", });});End-to-end tests with Playwright
Section titled “End-to-end tests with Playwright”Install Playwright browsers (first time only):
bunx playwright installWriting E2E tests
Section titled “Writing E2E tests”Playwright tests live in the e2e/ directory or alongside features with .e2e.ts extension:
import { expect, test } from "@playwright/test";
test("programs page loads after sign-in", async ({ page }) => { // Navigate to the app await page.goto("http://localhost:3000");
// Sign in (assumes Clerk test mode) await page.click('button:has-text("Continue")');
// Verify programs page await expect(page.getByRole("heading", { name: "Programs" })).toBeVisible();});Running E2E tests
Section titled “Running E2E tests”E2E tests require the full dev stack running (at minimum Next.js + Convex):
# Start the dev stack in one terminalbun run dev:zellij
# Run E2E tests in anotherbun run test:e2e
# Run with visible browser for debuggingbun run test:e2e:headed
# Run with Playwright UI for interactive debuggingbun run test:e2e:uiTest guidelines
Section titled “Test guidelines”- Name tests descriptively. Use the pattern “it [does thing] when [condition]”.
- Test behavior, not implementation. Assert on user-visible output, not internal state.
- Keep tests independent. Each test should set up its own data. Do not rely on test execution order.
- Mock external services. AI calls, webhook handlers, and external APIs should be mocked in unit tests.
- Use the
noExplicitAnyoverride. Test files havenoExplicitAnydisabled in the Biome config — useanywhere it reduces test boilerplate. - Cover edge cases. Empty arrays, null values, unauthorized access, and malformed input are high-value test targets.
Coverage
Section titled “Coverage”Generate a coverage report:
bun run test:coverage:webThis produces an HTML report you can open in a browser. Focus coverage efforts on:
- Convex queries and mutations (data integrity)
- Row-level security (
assertOrgAccessenforcement) - State machine transitions (sandbox lifecycle)
- AI response normalization (lenient enum parsing)