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 Type | Location | Framework |
|---|---|---|
| Component Tests | src/components/**/*.test.tsx | Vitest |
| Hook Tests | src/hooks/**/*.test.tsx | Vitest |
| Utility Tests | src/utils/**/*.test.ts | Vitest |
| Data Access Tests | src/data-access/**/*.test.ts | Vitest |
| Router Tests | src/server/routers/**/*.test.ts | Vitest |
| Route Tests | src/routes/**/*.test.tsx | Vitest |
| E2E Tests | tests/**/*.spec.ts | Playwright |
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.tsor.test.tsxextension - Spec files (E2E) use
.spec.tsextension - 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.tsfor 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
describeblocks 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()inbeforeEach
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
waitForfor 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
waitForSelectorproperly - 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
- Vitest Documentation
- Playwright Documentation
- Testing Library
- React Testing Library
- TanStack Query Testing
- tRPC Testing
Related Project Docs
- Architecture Overview - Architecture overview
- Contributing Guide - Contribution guidelines
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 Suite | Tests | Status |
|---|---|---|
| Hook Tests | 38 tests | ✓ Passing |
| Parser Tests | 59 tests | ✓ Passing |
| CORS Tests | 10 tests | ✓ Passing |
| E2E Tests | Multiple | ✓ Passing |
Last Updated: 2025-01-21 For questions or contributions, please refer to Contributing Guide