Frontend Testing¶
This guide covers testing strategies and practices for the Farm frontend.
Overview¶
Farm uses Vitest as the test runner with React Testing Library for component tests and jsdom as the browser environment.
Test Structure¶
web/
src/__tests__/
setup.ts # Global test setup (matchers, mocks)
lib/
api-client.test.ts # API client unit tests
ws-client.test.ts # WebSocket client unit tests
contexts/
auth-context.test.tsx # AuthProvider tests
components/
auth-guard.test.tsx # AuthGuard component tests
dashboard.test.tsx # Dashboard widget tests
pages/
login.test.tsx # Login page tests
catalog.test.tsx # Catalog page tests
deployments.test.tsx # Deployment matrix tests
teams.test.tsx # Teams page tests
queues.test.tsx # Queues page tests
observability.test.tsx # Observability page tests
Running Tests¶
All Tests¶
Watch Mode¶
With Coverage¶
Full Frontend Check¶
This runs lint, build, and tests.
Configuration¶
The Vitest configuration is in web/vitest.config.ts:
- Environment: jsdom (simulates browser DOM)
- Globals:
describe,it,expectavailable without imports - Path aliases:
@/maps tosrc/(matchingtsconfig.json) - Setup file:
src/__tests__/setup.tsruns before all tests - Coverage exclusions: Shadcn/ui generated components (
components/ui/)
Writing Tests¶
Test Setup¶
The global setup file (src/__tests__/setup.ts) provides:
@testing-library/jest-dommatchers (e.g.,toBeInTheDocument())- Auto-cleanup after each test
- Mocks for
next/navigation(useRouter,usePathname) - Mocks for
next-themes(useTheme) - Mocks for
sonner(toast) - Mock
sessionStorage
Testing API Client Functions¶
import { describe, it, expect, vi, beforeEach } from "vitest";
// Mock fetch globally
const mockFetch = vi.fn();
global.fetch = mockFetch;
import { catalog } from "@/lib/api-client";
describe("catalog API", () => {
beforeEach(() => {
vi.clearAllMocks();
});
it("should list components", async () => {
mockFetch.mockResolvedValueOnce({
ok: true,
status: 200,
json: () => Promise.resolve({ data: [], total: 0 }),
});
const result = await catalog.listComponents({ take: 10 });
expect(result.total).toBe(0);
});
});
Testing Components with Mocked API¶
import { describe, it, expect, vi } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
const mockListComponents = vi.fn();
vi.mock("@/lib/api-client", () => ({
catalog: { listComponents: (...args) => mockListComponents(...args) },
}));
import CatalogPage from "@/app/(protected)/catalog/page";
it("should display components in table", async () => {
mockListComponents.mockResolvedValue({
data: [{ id: "1", name: "auth-service", kind: "service" }],
total: 1,
});
render(<CatalogPage />);
await waitFor(() => {
expect(screen.getByText("auth-service")).toBeInTheDocument();
});
});
Testing User Interactions¶
import userEvent from "@testing-library/user-event";
it("should filter by search text", async () => {
const user = userEvent.setup();
// ... render component with data
await user.type(screen.getByPlaceholderText("Filter..."), "payment");
expect(screen.queryByText("auth-service")).not.toBeInTheDocument();
expect(screen.getByText("payment-api")).toBeInTheDocument();
});
Testing Auth Context¶
import { render, screen, act } from "@testing-library/react";
import { AuthProvider, useAuth } from "@/contexts/auth-context";
function TestConsumer() {
const { isAuthenticated, login } = useAuth();
return <span>{isAuthenticated ? "yes" : "no"}</span>;
}
it("should start unauthenticated", () => {
render(<AuthProvider><TestConsumer /></AuthProvider>);
expect(screen.getByText("no")).toBeInTheDocument();
});
Best Practices¶
Mock External Dependencies¶
Always mock API calls, WebSocket connections, and Next.js router at the module level using vi.mock(). This keeps tests fast and deterministic.
Use waitFor for Async Rendering¶
Components that fetch data in useEffect need waitFor to wait for state updates:
Test User-Visible Behavior¶
Focus on what the user sees and interacts with, not implementation details. Query elements by role, label text, or placeholder text rather than CSS classes or test IDs.
Test Error States¶
Always test what happens when API calls fail:
it("should show error on API failure", async () => {
mockApi.mockRejectedValue(new Error("Network error"));
render(<MyPage />);
await waitFor(() => {
expect(screen.getByText(/error/i)).toBeInTheDocument();
});
});
Keep Tests Independent¶
Each test should work in isolation. Use beforeEach with vi.clearAllMocks() to reset state between tests.
Current Coverage¶
| Area | Test File | Tests |
|---|---|---|
| API Client | api-client.test.ts | 40 |
| WebSocket Client | ws-client.test.ts | 4 |
| Auth Context | auth-context.test.tsx | 6 |
| Auth Guard | auth-guard.test.tsx | 5 |
| Dashboard Widgets | dashboard.test.tsx | 7 |
| Login Page | login.test.tsx | 5 |
| Catalog Page | catalog.test.tsx | 8 |
| Deployments Page | deployments.test.tsx | 7 |
| Teams Page | teams.test.tsx | 8 |
| Queues Page | queues.test.tsx | 6 |
| Observability Page | observability.test.tsx | 5 |
| Total | 11 files | 101 |