Skip to main content

How to Test

This guide provides comprehensive instructions on how to write, run, and debug tests for the All My Chats application. We use Vitest for unit and integration tests, and Playwright for end-to-end (E2E) tests.

Quick Start​

Running Tests​

# Run all tests in watch mode (interactive)
npm run test

# Run all tests once (CI mode)
npm run test:run

# Run tests with UI
npm run test:ui

# Run tests with coverage report
npm run test:coverage

# Run E2E tests
npm run test:e2e

# Run E2E tests with UI
npm run test:e2e:ui

# Run E2E tests in headed mode (visible browser)
npm run test:e2e:headed

Test Files by Type​

Test TypeLocationFramework
Component Testssrc/components/**/*.test.tsxVitest
Hook Testssrc/hooks/**/*.test.tsxVitest
Utility Testssrc/utils/**/*.test.tsVitest
Data Access Testssrc/data-access/**/*.test.tsVitest
Router Testssrc/server/routers/**/*.test.tsVitest
Route Testssrc/routes/**/*.test.tsxVitest
E2E Teststests/**/*.spec.tsPlaywright

Testing Commands Reference​

Vitest Commands​

# Watch mode - best for development
npm run test

# Run specific test file
npm run test -- MultiCategoryPicker

# Run tests matching pattern
npm run test -- --grep "Component"

# Run tests in specific directory
npm run test -- src/components

# UI mode - interactive test runner
npm run test:ui

# Coverage report
npm run test:coverage

# View coverage report (opens HTML)
open coverage/index.html

Playwright Commands​

# Run all E2E tests
npm run test:e2e

# Run specific test file
npx playwright test category-selection

# Run tests in headed mode (visible browser)
npm run test:e2e:headed

# Run tests with UI
npm run test:e2e:ui

# Debug tests
npx playwright test --debug

# Run tests in specific browser
npx playwright test --project=chromium
npx playwright test --project=firefox
npx playwright test --project=webkit

Test File Structure and Conventions​

File Naming​

  • Test files use .test.ts or .test.tsx extension
  • Spec files (E2E) use .spec.ts extension
  • Place test files next to source files (co-located)
src/
├── components/
│ ├── messages/
│ │ ├── MultiCategoryPicker.tsx
│ │ └── MultiCategoryPicker.test.tsx # Component test
│ └── ui/
│ ├── button.tsx
│ └── button.test.tsx
├── hooks/
│ └── messaging/
│ ├── useConversations.ts
│ └── useConversations.test.tsx # Hook test
└── utils/
├── whatsapp-parser.ts
└── whatsapp-parser.test.ts # Utility test

Test File Template​

/**
* Component/Function Name Tests
*
* Brief description of what's being tested
*
* Test coverage:
* - Feature 1
* - Feature 2
* - Edge cases
*/

import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { YourComponent } from "./YourComponent";

// Mock dependencies
vi.mock("~/lib/some-dependency", () => ({
someFunction: vi.fn(),
}));

describe("YourComponent", () => {
beforeEach(() => {
// Setup before each test
vi.clearAllMocks();
});

afterEach(() => {
// Cleanup after each test
});

describe("Rendering", () => {
it("should render correctly", () => {
render(<YourComponent />);
expect(screen.getByText("Hello")).toBeInTheDocument();
});
});

describe("Interactions", () => {
it("should handle user interaction", async () => {
const user = userEvent.setup();
render(<YourComponent />);

const button = screen.getByRole("button");
await user.click(button);

expect(screen.getByText("Clicked")).toBeInTheDocument();
});
});
});

How to Write Different Types of Tests​

1. Unit Tests​

Unit tests test isolated functions or utilities in isolation.

When to use:

  • Testing pure functions
  • Testing utility functions
  • Testing business logic
  • Testing parsers/formatters

Example: WhatsApp Parser Test

import { describe, it, expect } from "vitest";
import { parseMessages } from "./whatsapp-parser";

describe("WhatsApp Parser", () => {
describe("Message Parsing", () => {
it("should parse standard WhatsApp message format", () => {
const text = `
12/31/23, 9:41 AM - John Doe: Hello world
`;

const result = parseMessages(text, "user-123");

expect(result).toHaveLength(1);
expect(result[0]).toMatchObject({
senderName: "John Doe",
content: "Hello world",
messageType: "text",
});
});

it("should handle media messages", () => {
const text = `
12/31/23, 9:41 AM - John Doe: <image omitted>
`;

const result = parseMessages(text, "user-123");

expect(result[0].messageType).toBe("media");
});

it("should handle empty input", () => {
const result = parseMessages("", "user-123");
expect(result).toHaveLength(0);
});
});
});

2. Component Tests​

Component tests test React components in isolation.

When to use:

  • Testing component rendering
  • Testing user interactions
  • Testing component state
  • Testing component props

Example: MultiCategoryPicker Test

import { render, screen, waitFor } from "@testing-library/react";
import userEvent from "@testing-library/user-event";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { MultiCategoryPicker } from "./MultiCategoryPicker";

// Mock data
const mockCategories = [
{ id: "cat-1", name: "Work", color: "#3b82f6" },
{ id: "cat-2", name: "Personal", color: "#10b981" },
];

describe("MultiCategoryPicker", () => {
let onSelectMock: ReturnType<typeof vi.fn>;

beforeEach(() => {
onSelectMock = vi.fn();
// Mock pointer capture for Radix UI
Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);
});

describe("Rendering", () => {
it("should display placeholder when no categories selected", () => {
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

expect(screen.getByText("Select categories...")).toBeInTheDocument();
});

it("should display selected categories as badges", () => {
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={["cat-1"]}
onSelect={onSelectMock}
/>
);

expect(screen.getByText("Work")).toBeInTheDocument();
});
});

describe("Interactions", () => {
it("should open dialog when trigger clicked", async () => {
const user = userEvent.setup();
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

const button = screen.getByRole("button");
await user.click(button);

expect(screen.getByRole("dialog")).toBeInTheDocument();
});

it("should call onSelect when category selected", async () => {
const user = userEvent.setup();
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

// Open dialog
await user.click(screen.getByRole("button"));

// Select category
const category = screen.getByText("Work");
await user.click(category);

expect(onSelectMock).toHaveBeenCalled();
});
});
});

3. Hook Tests​

Hook tests test custom React hooks in isolation.

When to use:

  • Testing custom hooks
  • Testing hook behavior with different inputs
  • Testing hook state changes
  • Testing hook side effects

Example: useConversations Hook Test

import { renderHook, waitFor, act } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { describe, it, expect, vi, beforeEach } from "vitest";
import { useConversations } from "./useConversations";

// Mock dependencies
vi.mock("~/lib/auth-client", () => ({
authClient: {
useSession: vi.fn(),
},
}));

vi.mock("~/fn/conversations", () => ({
createConversationFn: vi.fn(),
}));

vi.mock("~/queries/conversations", () => ({
getConversationsQuery: vi.fn(),
}));

describe("useConversations", () => {
let queryClient: QueryClient;
let wrapper: React.FC<{ children: React.ReactNode }>;

beforeEach(() => {
queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

vi.clearAllMocks();
});

it("should fetch conversations", async () => {
const { result } = renderHook(() => useConversations(), { wrapper });

await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});

it("should create conversation", async () => {
const mockCreate = vi.mocked(createConversationFn);
mockCreate.mockResolvedValueOnce({
id: "conv-1",
title: "Test Conversation",
});

const { result } = renderHook(() => useConversations(), { wrapper });

await act(async () => {
await result.current.createConversation({
title: "Test Conversation",
});
});

expect(mockCreate).toHaveBeenCalled();
});
});

4. Integration Tests​

Integration tests test multiple layers working together.

When to use:

  • Testing data access layer
  • Testing tRPC routers
  • Testing API endpoints
  • Testing database interactions

Example: tRPC Router Test

import { describe, it, expect, vi, beforeEach } from "vitest";
import { messagesRouter } from "./messages";
import { createCallerFactory } from "@trpc/server";

// Mock data access functions
vi.mock("~/data-access/messaging/messages", () => ({
findMessageById: vi.fn(),
findMessagesByUserId: vi.fn(),
createMessage: vi.fn(),
updateMessage: vi.fn(),
deleteMessage: vi.fn(),
}));

describe("messages router", () => {
const mockUserId = "test-user-id";
const mockCtx = {
userId: mockUserId,
isAuthenticated: true,
headers: new Headers(),
};

beforeEach(() => {
vi.clearAllMocks();
});

describe("create", () => {
it("should create a message", async () => {
const mockMessage = {
id: "msg-1",
content: "Test message",
senderName: "John Doe",
};

vi.mocked(createMessage).mockResolvedValueOnce(mockMessage);

const caller = createCallerFactory().context<typeof createContext>().create(messagesRouter);

const result = await caller.create(mockCtx, {
content: "Test message",
senderName: "John Doe",
});

expect(result).toEqual(mockMessage);
expect(createMessage).toHaveBeenCalledWith(mockUserId, expect.any(Object));
});

it("should throw error if not authenticated", async () => {
const unauthCtx = {
...mockCtx,
isAuthenticated: false,
userId: null,
};

const caller = createCallerFactory().context<typeof createContext>().create(messagesRouter);

await expect(
caller.create(unauthCtx, {
content: "Test",
senderName: "Test",
})
).rejects.toThrow();
});
});
});

5. E2E Tests​

E2E tests test the application from the user's perspective.

When to use:

  • Testing critical user journeys
  • Testing cross-page workflows
  • Testing real browser behavior
  • Testing authentication flows

Example: Category Selection E2E Test

import { test, expect } from "@playwright/test";

test.describe("Category Selection", () => {
test.beforeEach(async ({ page }) => {
await page.goto("/dashboard");
});

test("should select category from dropdown", async ({ page }) => {
// Wait for messages to load
await page.waitForSelector('[data-testid="message-row"]');

// Find first category picker
const categoryPicker = page.locator('[role="combobox"]').first();
await categoryPicker.click();

// Wait for dropdown
await page.waitForSelector('[role="option"]');

// Select first category
const categoryOption = page.locator('[role="option"]').first();
await categoryOption.click();

// Verify selection
await expect(categoryPicker).toContainText(categoryOption);
});

test("should display all messages", async ({ page }) => {
await page.waitForSelector('[data-testid="message-row"]');

const messageRows = page.locator('[data-testid="message-row"]');
const count = await messageRows.count();

expect(count).toBeGreaterThan(0);
});
});

Mocking Strategies​

Mocking External Dependencies​

Use vi.mock() at the top of your test file:

// Mock entire module
vi.mock("~/lib/auth-client", () => ({
authClient: {
useSession: vi.fn(() => ({
data: { user: { id: "test-user" } },
})),
},
}));

// Mock with partial implementation
vi.mock("~/lib/api-client", () => ({
apiClient: {
get: vi.fn(),
post: vi.fn(),
},
}));

Mocking React Query​

import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

const queryClient = new QueryClient({
defaultOptions: {
queries: { retry: false },
mutations: { retry: false },
},
});

const wrapper = ({ children }) => (
<QueryClientProvider client={queryClient}>
{children}
</QueryClientProvider>
);

// Use in test
renderHook(() => useYourHook(), { wrapper });

Mocking Environment Variables​

Already set up in src/test/setup.tsx:

process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
process.env.BETTER_AUTH_SECRET = "test-secret-for-testing";
process.env.R2_ACCESS_KEY_ID = "test";
process.env.R2_SECRET_ACCESS_KEY = "test";

Mocking Radix UI Components​

Already set up in src/test/setup.tsx:

Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);

Mocking TanStack Router​

Already set up in src/test/setup.tsx:

vi.mock("@tanstack/react-router", async () => {
const actual = await vi.importActual("@tanstack/react-router");
return {
...actual,
createFileRoute: vi.fn((path: string) => ({
component: vi.fn(),
loader: vi.fn(),
beforeLoad: vi.fn(),
})),
Link: ({ children, to, ...props }: any) => <a href={to} {...props}>{children}</a>,
useNavigate: () => vi.fn(),
useLocation: () => ({ pathname: "/", search: {} }),
};
});

Mocking tRPC Router​

vi.mock("~/lib/trpc", () => ({
trpc: {
messages: {
create: {
useMutation: () => ({
mutate: vi.fn(),
mutateAsync: vi.fn(),
}),
},
getAll: {
useQuery: () => ({
data: [],
isLoading: false,
error: null,
}),
},
},
},
}));

Common Testing Patterns​

1. Testing Async Operations​

import { waitFor } from "@testing-library/react";

it("should load data asynchronously", async () => {
const { result } = renderHook(() => useYourHook(), { wrapper });

// Wait for state to update
await waitFor(() => {
expect(result.current.data).toBeDefined();
});
});

2. Testing User Interactions​

import userEvent from "@testing-library/user-event";

it("should handle click", async () => {
const user = userEvent.setup();
const handleClick = vi.fn();

render(<button onClick={handleClick}>Click me</button>);

const button = screen.getByRole("button");
await user.click(button);

expect(handleClick).toHaveBeenCalledTimes(1);
});

3. Testing Form Submissions​

it("should submit form", async () => {
const user = userEvent.setup();
const handleSubmit = vi.fn();

render(
<form onSubmit={handleSubmit}>
<input name="email" type="email" />
<button type="submit">Submit</button>
</form>
);

await user.type(screen.getByRole("textbox"), "[email protected]");
await user.click(screen.getByRole("button", { name: /submit/i }));

expect(handleClick).toHaveBeenCalled();
});

4. Testing Loading States​

it("should show loading state", () => {
vi.mocked(getData).mockReturnValue(new Promise(() => {}));

const { result } = renderHook(() => useYourHook(), { wrapper });

expect(result.current.isLoading).toBe(true);
});

5. Testing Error States​

it("should handle errors", async () => {
vi.mocked(getData).mockRejectedValue(new Error("API Error"));

const { result } = renderHook(() => useYourHook(), { wrapper });

await waitFor(() => {
expect(result.current.error).toBeDefined();
});
});

Troubleshooting​

Common Issues and Solutions​

1. "No test suite found" Error​

Problem: Test file has describe blocks but Vitest can't find them.

Solution:

# Check if file has proper test structure
grep -n "describe" path/to/test.ts

# Ensure file exports something or has side effects

2. TanStack Router Mocking Issues​

Problem: createFileRoute is not a function in route tests.

Solution: Already set up in src/test/setup.tsx. If you still have issues:

// Add explicit mock in your test
vi.mock("@tanstack/react-router", () => ({
createFileRoute: vi.fn(),
}));

3. jsdom Issues with Radix UI​

Problem: setPointerCapture not defined in jsdom.

Solution: Already set up in src/test/setup.tsx. If you still have issues:

beforeEach(() => {
Element.prototype.setPointerCapture = vi.fn(() => true);
Element.prototype.releasePointerCapture = vi.fn(() => true);
});

4. Environment Variables Missing​

Problem: Tests fail due to missing process.env variables.

Solution: Add to src/test/setup.tsx:

process.env.YOUR_VAR = "test-value";

5. Playwright Tests Timeout​

Problem: E2E tests timeout waiting for dev server.

Solution 1: Start dev server manually:

# Terminal 1
npm run dev

# Terminal 2
npm run test:e2e

Solution 2: Set environment variable:

CI=true npm run test:e2e

6. Import Path Errors​

Problem: Cannot find module or path.

Solution: Ensure you're using the correct alias:

  • Use ~/ for src imports
  • Check vitest.config.ts for alias configuration

7. Test Coverage Not Generated​

Problem: Coverage report is empty or missing.

Solution:

# Run with coverage
npm run test:coverage

# Check if file is excluded from coverage
# See vitest.config.ts coverage.exclude

8. Tests Passing in Watch Mode but Failing in CI​

Problem: Tests behave differently in watch vs CI mode.

Solution:

# Run tests in CI mode locally
npm run test:run

# Check for race conditions or timing issues
# Use waitFor instead of hardcoded delays

Debugging Tips​

Use Vitest UI​

npm run test:ui

This provides an interactive UI to:

  • Run specific tests
  • View test results
  • Inspect coverage
  • Debug tests in browser

Debug Playwright Tests​

# Run with debug mode
npx playwright test --debug

# Run with headed mode
npm run test:e2e:headed

# Use Playwright Inspector
npx playwright test --debug

Console Logging​

// Console.log in tests
it("should do something", () => {
console.log("Debug info:", someValue);
// ...
});

// See logs in Vitest output

VS Code Debugging​

Create .vscode/launch.json:

{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug Vitest",
"program": "${workspaceFolder}/node_modules/vitest/vitest.mjs",
"args": ["run", "--reporter=verbose"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}

Best Practices​

1. Test Organization​

  • Co-location: Keep test files next to source files
  • Descriptive names: Use describe blocks to group related tests
  • One assertion per test: Keep tests focused and small
  • AAA pattern: Arrange, Act, Assert structure
describe("Component", () => {
it("should do something when condition met", () => {
// Arrange - Setup test data and conditions
const props = { value: "test" };

// Act - Execute the code being tested
render(<Component {...props} />);

// Assert - Verify expected outcomes
expect(screen.getByText("test")).toBeInTheDocument();
});
});

2. Mocking Strategy​

  • Mock external dependencies: Don't test third-party libraries
  • Mock data layer: Use vi.mock for database/network calls
  • Avoid over-mocking: Mock only what's necessary
  • Clear mocks: Use vi.clearAllMocks() in beforeEach

3. Component Testing​

  • Test user interactions: Use @testing-library/user-event
  • Test behavior, not implementation: Focus on what users see/do
  • Use data-testid: For elements without semantic meaning
  • Test edge cases: Empty states, loading, error states
// Good - Tests behavior
it("should display error message", () => {
render(<Form error="Invalid input" />);
expect(screen.getByText("Invalid input")).toBeInTheDocument();
});

// Bad - Tests implementation
it("should have error class", () => {
render(<Form error="Invalid input" />);
expect(container.firstChild).toHaveClass("error");
});

4. Hook Testing​

  • Use renderHook: From @testing-library/react
  • Wrap in QueryClient: For React Query hooks
  • Test async states: Use waitFor for async operations
  • Mock dependencies: Isolate the hook being tested

5. E2E Testing​

  • Test critical paths: Focus on important user journeys
  • Use data-testid: For reliable element selection
  • Wait for elements: Use waitForSelector properly
  • Avoid flaky tests: Use stable selectors, avoid hardcoded waits

Test Coverage​

Coverage Goals​

Current targets (configured in vitest.config.ts):

  • Line Coverage: 80%
  • Function Coverage: 80%
  • Branch Coverage: 80%
  • Statement Coverage: 80%

Generating Coverage​

# Generate coverage report
npm run test:coverage

# View HTML coverage report (macOS)
open coverage/index.html

# View HTML coverage report (Windows)
start coverage/index.html

# View HTML coverage report (Linux)
xdg-open coverage/index.html

Coverage Configuration​

Coverage is configured in vitest.config.ts:

coverage: {
provider: "v8",
reporter: ["text", "json", "html", "lcov"],
exclude: [
"node_modules/",
"src/test/",
"**/*.d.ts",
"**/*.config.*",
"**/mockData/**",
"src/routes/__root.tsx",
"src/examples/**",
"docs/**",
"temp/**",
"website/**",
],
all: true,
lines: 80,
functions: 80,
branches: 80,
statements: 80,
}

Resources​

Documentation​


Test Metrics​

Test Execution Time​

Typical execution times:

  • Unit tests: 1-5 seconds
  • Integration tests: 5-15 seconds
  • Full test suite: 30-60 seconds
  • E2E tests: 2-5 minutes (depending on complexity)

Current Test Status​

As of the latest test run:

Test SuiteTestsStatus
Hook Tests38 tests✓ Passing
Parser Tests59 tests✓ Passing
CORS Tests10 tests✓ Passing
E2E TestsMultiple✓ Passing

Last Updated: 2025-01-21 For questions or contributions, please refer to Contributing Guide