Frontend Testing Guidelines
This document outlines the testing approach for the Asset360 v3 frontend, with specific guidance on handling tRPC calls in tests.
Core Testing Philosophy
Frontend Tests Focus on UI Behavior
Frontend tests should focus on:
- Component rendering and user interactions
- State management and context behavior
- UI logic and conditional rendering
- User experience flows
Backend Tests Handle API Logic
Backend tests should handle:
- tRPC procedure logic
- Data validation and transformation
- Business rule enforcement
- Database operations
tRPC Testing Strategy
❌ DO NOT Mock Complex tRPC Calls in Frontend Tests
Why not?
- Frontend tests become brittle and complex
- API integration is better tested in backend tests
- Mocking tRPC calls tightly couples frontend tests to API implementation details
- Complex mocks are hard to maintain and debug
✅ DO Use Simple Placeholders
Instead of complex tRPC mocks, use simple placeholders:
// ❌ WRONG - Complex tRPC mocking
const mockTrpc = {
system: {
organizations: {
list: {
queryOptions: vi.fn(() => ({
queryKey: ["system.organizations.list"],
queryFn: () =>
Promise.resolve({
organizations: [
{
id: "org-1",
name: "Test Organization",
// ... complex mock data
},
],
pagination: { page: 1, limit: 1000, total: 1, totalPages: 1 },
}),
})),
},
},
},
};
// ✅ CORRECT - Simple placeholders
vi.mock("@/lib/trpc", () => ({
useTRPC: () => ({
system: {
organizations: {
list: {
queryOptions: () => ({
queryKey: ["system.organizations.list"],
queryFn: () =>
Promise.resolve({
organizations: [],
pagination: { page: 1, limit: 1000, total: 0, totalPages: 0 },
}),
}),
},
},
},
tenant: {
fund: {
getAll: {
queryOptions: () => ({
queryKey: ["tenant.fund.getAll"],
queryFn: () => Promise.resolve([]),
}),
},
},
},
}),
}));
Test File Organization
Core Test Files
src/__tests__/test-utils.tsx- Centralized test utilities with simple tRPC placeholderssrc/__tests__/AuthContext.test.tsx- Authentication context testssrc/__tests__/OrganizationContext.placeholder.test.tsx- Organization context testssrc/__tests__/RoleGuard.test.tsx- Role-based access control testssrc/__tests__/BasicComponents.test.tsx- Basic component rendering tests
Test Utilities Pattern
// src/__tests__/test-utils.tsx
import { vi } from "vitest";
// Simple tRPC placeholders - no complex API mocking
vi.mock("@/lib/trpc", () => ({
useTRPC: () => ({
system: {
organizations: {
list: {
queryOptions: () => ({
queryKey: ["system.organizations.list"],
queryFn: () =>
Promise.resolve({
organizations: [],
pagination: { page: 1, limit: 1000, total: 0, totalPages: 0 },
}),
}),
},
},
},
tenant: {
fund: {
getAll: {
queryOptions: () => ({
queryKey: ["tenant.fund.getAll"],
queryFn: () => Promise.resolve([]),
}),
},
},
},
}),
}));
// Helper functions for creating test data
export function createTestUser(overrides = {}) {
return {
id: "user-1",
email: "[email protected]",
name: "Test User",
role: "ADMIN",
organizationId: "org-1",
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
organization: {
id: "org-1",
name: "Test Organization",
slug: "test-org",
description: "Test org description",
isActive: true,
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
},
...overrides,
};
}
export function createTestAuthState(overrides = {}) {
return {
state: {
user: createTestUser(),
isAuthenticated: true,
isSystemUser: false,
isLoading: false,
},
login: vi.fn(),
logout: vi.fn(),
refetchUser: vi.fn(),
...overrides,
};
}
Testing Patterns
1. Context Testing
Test React contexts with simple data and focus on state management:
describe("OrganizationContext", () => {
it("shows tenant user's organization from auth state", () => {
const mockAuthState = createTestAuthState({
state: {
user: createTestUser({
organization: {
id: "org-1",
name: "Tenant Organization",
slug: "tenant-org",
// ... other org fields
},
}),
isAuthenticated: true,
isSystemUser: false,
},
});
render(
<OrganizationProvider>
<TestComponent />
</OrganizationProvider>,
{ authState: mockAuthState }
);
expect(screen.getByTestId("current-org")).toHaveTextContent("Tenant Organization");
});
});
2. Component Testing
Test component behavior with mocked props and user interactions:
describe("RoleGuard", () => {
it("renders children when user has required capability", () => {
const systemAdmin = createTestUser({
role: ROLES.ADMIN,
organizationId: "system",
organization: {
id: "system",
name: "System",
slug: "system",
},
});
renderWithAuth(
<RoleGuard requiredCapability={CAPABILITIES.ACCESS_SYSTEM}>
<div data-testid="system">System Tools</div>
</RoleGuard>,
systemAdmin
);
expect(screen.getByTestId("system")).toBeInTheDocument();
});
});
3. Hook Testing
Test custom hooks with simple data and focus on hook behavior:
describe("useCapabilityCheck", () => {
it("returns access helpers for current user", () => {
const user = createTestUser({ role: ROLES.ADMIN });
renderWithAuth(
<TestComponent />,
{ authState: createTestAuthState({ state: { user } }) }
);
expect(screen.getByTestId("has-capability")).toHaveTextContent("true");
});
});
What NOT to Test in Frontend
❌ Don't Test API Integration
- tRPC procedure logic
- Data validation schemas
- Database queries
- External API calls
❌ Don't Test Business Logic
- Complex calculations
- Data transformations
- Business rule enforcement
- Domain-specific logic
❌ Don't Mock Complex API Responses
- Detailed mock data structures
- Complex pagination logic
- Error handling scenarios
- API response transformations
What TO Test in Frontend
✅ Do Test UI Behavior
- Component rendering
- User interactions (clicks, form submissions)
- State changes and updates
- Conditional rendering based on props/state
✅ Do Test Context Behavior
- State management
- Context provider/consumer patterns
- Hook behavior and return values
✅ Do Test User Experience
- Navigation flows
- Form validation feedback
- Loading states
- Error display
Test Configuration
Vitest Frontend Config
// vitest.frontend.config.ts
export default defineConfig({
test: {
include: [
// Core context and auth tests
"src/__tests__/BasicComponents.test.tsx",
"src/__tests__/RoleGuard.test.tsx",
"src/__tests__/OrganizationContext.placeholder.test.tsx",
"src/__tests__/AuthContext.test.tsx",
// Guard components
"src/components/guards/**/*.test.{ts,tsx}",
// Organization components
"src/components/organization/**/*.test.{ts,tsx}",
// Layout components
"src/components/layout/**/*.test.{ts,tsx}",
// UI components (basic ones first)
"src/components/ui/**/*.test.{ts,tsx}",
// Fund components (basic ones)
"src/components/funds/**/*.test.{ts,tsx}",
// Investor components (basic ones)
"src/components/investors/**/*.test.{ts,tsx}",
],
passWithNoTests: false,
// ... other config
},
});
Benefits of This Approach
🚀 Performance
- Faster test execution
- No complex async operations
- Minimal setup overhead
🔧 Maintainability
- Simple, readable tests
- Easy to understand and modify
- Less brittle test code
🎯 Focus
- Tests focus on what matters for frontend
- Clear separation of concerns
- Better test coverage of UI behavior
🐛 Debugging
- Easier to debug test failures
- Clear error messages
- Simple test data to reason about
Migration Guide
If you find complex tRPC mocks in existing tests:
- Identify the test purpose - What UI behavior is being tested?
- Replace complex mocks - Use simple placeholders from
test-utils.tsx - Focus on UI logic - Test component behavior, not API integration
- Use helper functions - Leverage
createTestUser()andcreateTestAuthState() - Verify test still passes - Ensure the test still validates the intended UI behavior
Examples of Good vs Bad Tests
❌ Bad Test (Complex tRPC Mocking)
// Don't do this - too complex and brittle
const mockTrpc = {
system: {
organizations: {
list: {
queryOptions: vi.fn(() => ({
queryKey: ["system.organizations.list"],
queryFn: () =>
Promise.resolve({
organizations: [
{
id: "org-1",
name: "Test Organization",
slug: "test-org",
description: "Test org description",
isActive: true,
createdAt: "2024-01-01T00:00:00.000Z",
updatedAt: "2024-01-01T00:00:00.000Z",
},
],
pagination: { page: 1, limit: 1000, total: 1, totalPages: 1 },
}),
})),
},
},
},
};
✅ Good Test (Simple Placeholder)
// Do this - simple and focused
it("shows tenant user's organization from auth state", () => {
const mockAuthState = createTestAuthState({
state: {
user: createTestUser({
organization: {
id: "org-1",
name: "Tenant Organization",
slug: "tenant-org",
},
}),
},
});
render(
<OrganizationProvider>
<TestComponent />
</OrganizationProvider>,
{ authState: mockAuthState }
);
expect(screen.getByTestId("current-org")).toHaveTextContent("Tenant Organization");
});
Conclusion
Frontend tests should be simple, fast, and focused on UI behavior. Use simple placeholders for tRPC calls and let backend tests handle API logic. This approach leads to more maintainable, reliable, and focused test suites.