Skip to main content

Testing Overview

This project uses a comprehensive testing strategy with Vitest for unit/integration tests and Playwright for end-to-end (E2E) tests. The testing suite ensures code quality, prevents regressions, and maintains reliability across the application.

Current Status​

Test Suite Status

All tests are running! As of January 2025, the test suite has achieved:

  • 1,488 total tests across 46 test files
  • 1,222 tests passing (82% success rate)
  • 266 tests failing (mostly minor issues)
  • ~11,000 lines of test code

Quick Start​

# Run tests in watch mode
npm run test

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

# Run tests with UI
npm run test:ui

# Run with coverage
npm run test:coverage

Testing Frameworks​

Vitest (Unit & Integration Tests)​

Framework: Vitest v4.0.17 Configuration: vitest.config.ts

Vitest is used for:

  • Component testing (React components)
  • Unit tests (utilities, helpers, functions)
  • Integration tests (hooks, data access, tRPC routers)
  • API testing (server endpoints)

Key Features:

  • Native ESM support
  • Fast test execution with workers
  • Built-in TypeScript support
  • jsdom environment for DOM testing
  • Coverage reporting with v8

Playwright (E2E Tests)​

Framework: Playwright Configuration: playwright.config.ts

Playwright is used for:

  • End-to-end user flows
  • Cross-browser testing (Chrome, Firefox, Safari)
  • Mobile viewport testing
  • Full integration testing

Key Features:

  • Multi-browser support
  • Mobile emulation
  • Automatic screenshots on failure
  • Trace recording for debugging
  • Parallel test execution

Test Structure​

whatsapp-message-AI-analysis-agent/
├── src/
│ ├── components/
│ │ ├── messages/
│ │ │ └── MultiCategoryPicker.test.tsx # Component tests
│ │ └── ui/
│ │ ├── button.test.tsx
│ │ ├── input.test.tsx
│ │ ├── label.test.tsx
│ │ └── textarea.test.tsx
│ ├── data-access/
│ │ ├── core/
│ │ │ └── users.test.ts # Data layer tests
│ │ └── messaging/
│ │ ├── categories.test.ts
│ │ └── messages.test.ts
│ ├── fn/
│ │ └── categories.test.ts # Business logic tests
│ ├── hooks/
│ │ ├── features/
│ │ │ ├── useFeatureVotes.test.tsx
│ │ │ └── useProfile.test.tsx
│ │ ├── messaging/
│ │ │ ├── useCategories.test.tsx
│ │ │ ├── useCategorization.test.tsx
│ │ │ ├── useConversations.test.tsx
│ │ │ └── useMessages.test.tsx
│ │ ├── subscriptions/
│ │ │ └── useSubscription.test.tsx
│ │ └── ui/
│ │ └── useTheme.test.tsx
│ ├── lib/
│ │ ├── categorization/
│ │ │ └── prompts.test.ts
│ │ ├── rate-limit-middleware.test.ts
│ │ ├── rate-limiter.test.ts
│ │ ├── search/
│ │ │ └── query-parser.test.ts
│ │ ├── roles.test.ts
│ │ └── utils.test.ts
│ ├── routes/
│ │ ├── dashboard.test.tsx
│ │ ├── import.test.tsx
│ │ ├── index.test.tsx
│ │ ├── messages.test.tsx
│ │ ├── settings.test.tsx
│ │ ├── sign-in.test.tsx
│ │ └── sign-up.test.tsx
│ ├── server/
│ │ └── routers/
│ │ ├── categories.test.ts
│ │ ├── categorization.test.ts
│ │ ├── conversations.test.ts
│ │ ├── feature-votes.test.ts
│ │ ├── feedback.test.ts
│ │ ├── messages.test.ts
│ │ ├── pipeline.test.ts
│ │ ├── qa.test.ts
│ │ └── search.test.ts
│ ├── test/
│ │ └── setup.tsx # Test setup configuration
│ └── utils/
│ ├── __tests__/
│ │ └── request-size-limit.test.ts
│ ├── auth.test.ts
│ ├── authMiddleware.test.ts
│ ├── error.test.ts
│ ├── logger.test.ts
│ ├── subscription.test.ts
│ └── whatsapp-parser.test.ts
├── tests/
│ ├── category-selection.spec.ts # Playwright E2E tests
│ └── cors.test.ts # CORS integration tests
└── vitest.config.ts

Test Coverage​

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/**",
],
all: true,
lines: 80, // Target: 80%
functions: 80, // Target: 80%
branches: 80, // Target: 80%
statements: 80, // Target: 80%
}

Coverage Goals​

Current targets:

  • 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
open coverage/index.html

Writing Tests​

Test Setup​

All tests use the global setup file: src/test/setup.tsx

import { expect, vi, afterEach } from "vitest";
import * as matchers from "@testing-library/jest-dom/matchers";

// Extend Vitest's expect with jest-dom matchers
expect.extend(matchers);

// Mock environment variables
process.env.DATABASE_URL = "postgresql://test:test@localhost:5432/test";
process.env.BETTER_AUTH_SECRET = "test-secret-for-testing";

// Mock TanStack Router
vi.mock("@tanstack/react-router", () => ({
createFileRoute: vi.fn(),
Link: vi.fn(),
useNavigate: vi.fn(),
useLocation: vi.fn(),
useSearch: vi.fn(),
useParams: vi.fn(),
useRouteContext: vi.fn(),
}));

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

Vitest is configured with globals: true, which provides describe, it, expect, etc. as global variables. Do not import these from vitest:

// ❌ Wrong - causes conflicts
import { describe, it, expect } from "vitest";

// ✅ Correct - use globals
describe("MyTest", () => {
it("should work", () => {
expect(true).toBe(true);
});
});

Component Testing Pattern​

Example: src/components/messages/MultiCategoryPicker.test.tsx

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 { MultiCategoryPicker } from "./MultiCategoryPicker";

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

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

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

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

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

describe("Dialog Interactions", () => {
it("should open dialog when trigger button is clicked", async () => {
render(
<MultiCategoryPicker
categories={mockCategories}
selectedCategoryIds={[]}
onSelect={onSelectMock}
/>
);

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

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

Hook Testing Pattern​

Example: src/hooks/messaging/useConversations.test.tsx

import { renderHook, waitFor, act } from "@testing-library/react";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";

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

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

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>
);
});

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

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

tRPC Router Testing Pattern​

Example: src/server/routers/messages.test.ts

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

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

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

beforeEach(() => {
vi.clearAllMocks();
// Setup mock returns
vi.mocked(createMessage).mockResolvedValue(mockMessage);
});

it("should create a message", async () => {
const caller = createCallerFactory()
.context<typeof createContext>()
.create(messagesRouter);

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

expect(result).toEqual(mockMessage);
});
});

Utility Testing Pattern​

Example: src/utils/whatsapp-parser.test.ts

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");
});
});
});

E2E Testing Pattern​

Example: tests/category-selection.spec.ts

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

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

test.describe("CategoryPicker", () => {
test("should display placeholder when no category is selected", async ({ page }) => {
const categoryPicker = page
.locator('[data-testid="message-row"]')
.filter({ hasText: "No category" })
.first()
.locator('[role="combobox"]');

await expect(categoryPicker).toBeVisible();
await expect(categoryPicker).toContainText("No category");
});

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

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

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

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

Test Results Breakdown​

Fully Passing Test Files (13 files)​

FileTestsStatus
src/utils/whatsapp-parser.test.ts59✓ All passing
src/hooks/messaging/useConversations.test.tsx38✓ All passing
src/lib/utils.test.ts25✓ All passing
src/lib/search/query-parser.test.ts65✓ All passing
src/components/ui/button.test.tsx61✓ All passing
src/components/ui/input.test.tsx61✓ All passing
src/components/ui/label.test.tsx65✓ All passing
src/utils/error.test.ts47✓ All passing
src/utils/logger.test.ts27✓ All passing
src/utils/subscription.test.ts46✓ All passing
src/fn/categories.test.ts48✓ All passing
src/hooks/ui/useTheme.test.tsx24✓ All passing
src/hooks/features/useProfile.test.tsx28✓ All passing

Total: 594 tests passing with 0 failures

Partially Passing Test Files​

Most test failures are minor issues:

  • toHaveFocus, toHaveValue - jest-dom matchers not properly set up
  • Mock assertion mismatches - Test expectations slightly off from implementation
  • Unhandled promises - Async test cleanup issues

Examples:

  • src/components/ui/textarea.test.tsx - 242/247 passing (98%)
  • src/data-access/messaging/messages.test.ts - 83/84 passing (99%)
  • src/server/routers/messages.test.ts - 42/43 passing (98%)

Files With More Failures​

Route test files have TanStack Router mock issues:

  • src/routes/dashboard.test.tsx
  • src/routes/import.test.tsx
  • src/routes/messages.test.tsx
  • src/routes/settings.test.tsx
  • src/routes/sign-in.test.tsx
  • src/routes/sign-up.test.tsx
  • src/routes/index.test.tsx

Troubleshooting​

Common Issues​

"No test suite found" Error​

Fixed

This issue has been resolved. Ensure you're not importing from vitest when globals: true is configured.

TanStack Router Mocking Issues​

Problem: createFileRoute is not a function in route tests.

Solution: The router is already mocked in src/test/setup.tsx. If you still have issues:

// In your test file, mock the route component
vi.mock("~/routes/dashboard", () => ({
DashboardPage: () => <div>Mock Dashboard</div>,
}));

jsdom Issues with Radix UI​

Problem: setPointerCapture not defined in jsdom.

Solution: Already mocked in src/test/setup.tsx:

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

Environment Variables Missing​

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

Solution: Already mocked 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";

Playwright Tests Timeout​

Problem: E2E tests timeout waiting for dev server.

Solution: Start dev server manually:

# Terminal 1: Start dev server
npm run dev

# Terminal 2: Run E2E tests
npm run test:e2e

Or configure webServer in playwright.config.ts to skip auto-start:

webServer: undefined, // Disable auto-start

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

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

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 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)

Test Distribution by Layer​

LayerTest FilesWorkingNeeds Fix
Components541
Data Access321
Hooks972
Lib/Utils761
Routes817
Server/tRPC1091
Functions110
E2E220

Remaining Work​

Immediate Actions​

  1. Fix jest-dom matchers (5 min) - Add proper import, fixes 24 tests
  2. Fix route mocks (30 min) - Mock components, fixes ~50 tests
  3. Fix mock assertions (1 hour) - Update expectations, fixes ~100 tests
  4. Fix async cleanup (30 min) - Add error handlers, fixes ~90 tests

Total: ~2-3 hours to reach ~95% passing (1400+ tests)

Long-term Improvements​

  1. Increase coverage - Aim for 80%+ coverage across all layers
  2. Add integration tests - Test cross-layer interactions
  3. Add visual regression tests - Use tools like Percy or Chromatic
  4. Add performance tests - Monitor render performance
  5. Set up CI testing - Automate tests in GitHub Actions
  6. Add mutation testing - Use Stryker to verify test quality

Resources​

Documentation​


Last Updated: January 2025 For questions or contributions, please refer to the contributing guide