Skip to main content

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 placeholders
  • src/__tests__/AuthContext.test.tsx - Authentication context tests
  • src/__tests__/OrganizationContext.placeholder.test.tsx - Organization context tests
  • src/__tests__/RoleGuard.test.tsx - Role-based access control tests
  • src/__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:

  1. Identify the test purpose - What UI behavior is being tested?
  2. Replace complex mocks - Use simple placeholders from test-utils.tsx
  3. Focus on UI logic - Test component behavior, not API integration
  4. Use helper functions - Leverage createTestUser() and createTestAuthState()
  5. 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.