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
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
- Unit/Integration Tests
- E2E Tests
# 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
# Run E2E tests
npm run test:e2e
# Run E2E tests with UI
npm run test:e2e:ui
# Run E2E tests in headed mode
npm run test:e2e:headed
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(() => {});
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)
| File | Tests | Status |
|---|---|---|
src/utils/whatsapp-parser.test.ts | 59 | ✓ All passing |
src/hooks/messaging/useConversations.test.tsx | 38 | ✓ All passing |
src/lib/utils.test.ts | 25 | ✓ All passing |
src/lib/search/query-parser.test.ts | 65 | ✓ All passing |
src/components/ui/button.test.tsx | 61 | ✓ All passing |
src/components/ui/input.test.tsx | 61 | ✓ All passing |
src/components/ui/label.test.tsx | 65 | ✓ All passing |
src/utils/error.test.ts | 47 | ✓ All passing |
src/utils/logger.test.ts | 27 | ✓ All passing |
src/utils/subscription.test.ts | 46 | ✓ All passing |
src/fn/categories.test.ts | 48 | ✓ All passing |
src/hooks/ui/useTheme.test.tsx | 24 | ✓ All passing |
src/hooks/features/useProfile.test.tsx | 28 | ✓ 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.tsxsrc/routes/import.test.tsxsrc/routes/messages.test.tsxsrc/routes/settings.test.tsxsrc/routes/sign-in.test.tsxsrc/routes/sign-up.test.tsxsrc/routes/index.test.tsx
Troubleshooting
Common Issues
"No test suite found" Error
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
describeblocks 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()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
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 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
| Layer | Test Files | Working | Needs Fix |
|---|---|---|---|
| Components | 5 | 4 | 1 |
| Data Access | 3 | 2 | 1 |
| Hooks | 9 | 7 | 2 |
| Lib/Utils | 7 | 6 | 1 |
| Routes | 8 | 1 | 7 |
| Server/tRPC | 10 | 9 | 1 |
| Functions | 1 | 1 | 0 |
| E2E | 2 | 2 | 0 |
Remaining Work
Immediate Actions
- Fix jest-dom matchers (5 min) - Add proper import, fixes 24 tests
- Fix route mocks (30 min) - Mock components, fixes ~50 tests
- Fix mock assertions (1 hour) - Update expectations, fixes ~100 tests
- Fix async cleanup (30 min) - Add error handlers, fixes ~90 tests
Total: ~2-3 hours to reach ~95% passing (1400+ tests)
Long-term Improvements
- Increase coverage - Aim for 80%+ coverage across all layers
- Add integration tests - Test cross-layer interactions
- Add visual regression tests - Use tools like Percy or Chromatic
- Add performance tests - Monitor render performance
- Set up CI testing - Automate tests in GitHub Actions
- Add mutation testing - Use Stryker to verify test quality
Resources
Documentation
- Vitest Documentation
- Playwright Documentation
- Testing Library
- React Testing Library
- TanStack Query Testing
Related Project Docs
- Architecture - Architecture overview
- Authentication - How authentication works
- Subscriptions - User plans and subscriptions
- TanStack Start - Technical implementation details
Last Updated: January 2025 For questions or contributions, please refer to the contributing guide