Skip to Content

Frontend Testing

This guide covers testing strategies and implementation for the Rhesis frontend application.

Testing Framework The frontend uses Jest as the test runner and React Testing Library for component testing, providing comprehensive unit testing capabilities.

Quick Start

Running Tests

test-commands.sh
# Navigate to frontend directory
cd apps/frontend

# Run all tests
npm test

# Run tests in watch mode
npm run test:watch

# Run tests with coverage
npm run test:coverage

# Run tests in CI mode
npm test:ci

Test Coverage Target coverage thresholds: 70% for branches, functions, lines, and statements

Testing Configuration

Jest Setup

The testing framework is configured in jest.config.js:

jest.config.js
const nextJest = require('next/jest')

const createJestConfig = nextJest({
dir: './',
})

const customJestConfig = {
setupFilesAfterEnv: ['/jest.setup.js'],
testEnvironment: 'jsdom',
moduleNameMapper: {
  '^@/(.*)$': '/src/$1',
},
collectCoverageFrom: [
  'src/**/*.{js,jsx,ts,tsx}',
  '!src/**/*.d.ts',
  '!src/**/index.ts',
  '!src/app/layout.tsx',
  '!src/app/page.tsx',
  '!src/auth.ts',
  '!src/middleware.ts',
],
coverageThreshold: {
  global: {
    branches: 70,
    functions: 70,
    lines: 70,
    statements: 70,
  },
},
}

module.exports = createJestConfig(customJestConfig)

Test Environment Setup

The test environment is configured in jest.setup.js:

import '@testing-library/jest-dom' // Set environment variables for tests process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8080/api/v1' // Mock Next.js router jest.mock('next/router', () => ({ useRouter() { return { route: '/', pathname: '', query: {}, asPath: '', push: jest.fn(), events: { on: jest.fn(), off: jest.fn(), }, isFallback: false, } }, })) // Mock browser APIs Object.defineProperty(window, 'matchMedia', { writable: true, value: jest.fn().mockImplementation(query => ({ matches: false, media: query, onchange: null, addListener: jest.fn(), removeListener: jest.fn(), addEventListener: jest.fn(), removeEventListener: jest.fn(), dispatchEvent: jest.fn(), })), }) global.ResizeObserver = jest.fn().mockImplementation(() => ({ observe: jest.fn(), unobserve: jest.fn(), disconnect: jest.fn(), }))

Writing Tests

Component Testing

Use React Testing Library to test component behavior and user interactions:

import { render, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import BaseDrawer from '../BaseDrawer'; import '@testing-library/jest-dom'; describe('BaseDrawer', () => { const mockOnClose = jest.fn(); const mockOnSave = jest.fn(); afterEach(() => { jest.clearAllMocks(); }); it('renders with open state', () => { render( <BaseDrawer open={true} onClose={mockOnClose} title="Test Drawer"> <div>Test Content</div> </BaseDrawer> ); expect(screen.getByText('Test Drawer')).toBeInTheDocument(); expect(screen.getByText('Test Content')).toBeInTheDocument(); }); it('calls onClose when Cancel button is clicked', async () => { const user = userEvent.setup(); render( <BaseDrawer open={true} onClose={mockOnClose} title="Test Drawer"> <div>Test Content</div> </BaseDrawer> ); await user.click(screen.getByRole('button', { name: /cancel/i })); expect(mockOnClose).toHaveBeenCalledTimes(1); }); it('disables buttons when loading is true', () => { render( <BaseDrawer open={true} onClose={mockOnClose} onSave={mockOnSave} title="Test Drawer" loading={true} > <div>Test Content</div> </BaseDrawer> ); expect(screen.getByRole('button', { name: /cancel/i })).toBeDisabled(); expect(screen.getByRole('button', { name: /save changes/i })).toBeDisabled(); }); });

Hook Testing

Test custom hooks using @testing-library/react-hooks:

import { renderHook, act } from '@testing-library/react' import { useTasks } from '../useTasks' import { TasksClient } from '@/utils/api-client/tasks-client' // Mock the API client jest.mock('@/utils/api-client/tasks-client') const mockTasksClient = TasksClient as jest.Mocked<typeof TasksClient> describe('useTasks', () => { beforeEach(() => { jest.clearAllMocks() }) it('should fetch tasks on mount', async () => { const mockTasks = [{ id: '1', name: 'Test Task' }] mockTasksClient.getTasks.mockResolvedValue(mockTasks) const { result } = renderHook(() => useTasks()) await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)) }) expect(result.current.tasks).toEqual(mockTasks) expect(result.current.isLoading).toBe(false) }) it('should handle errors correctly', async () => { const mockError = new Error('API Error') mockTasksClient.getTasks.mockRejectedValue(mockError) const { result } = renderHook(() => useTasks()) await act(async () => { await new Promise(resolve => setTimeout(resolve, 0)) }) expect(result.current.error).toBe('API Error') expect(result.current.isLoading).toBe(false) }) })

Utility Function Testing

Test utility functions with Jest:

import { formatDate } from '../date-utils' describe('formatDate', () => { it('formats ISO date string correctly', () => { const isoDate = '2024-01-15T10:30:00Z' const formatted = formatDate(isoDate) expect(formatted).toBe('Jan 15, 2024') }) it('handles invalid date gracefully', () => { const invalidDate = 'invalid-date' const formatted = formatDate(invalidDate) expect(formatted).toBe('Invalid Date') }) it('formats with custom locale', () => { const isoDate = '2024-01-15T10:30:00Z' const formatted = formatDate(isoDate, 'en-GB') expect(formatted).toBe('15 Jan 2024') }) })

Test Utilities

Custom Test Utils

Use custom test utilities for consistent test setup:

// src/__mocks__/test-utils.tsx import { render, RenderOptions } from '@testing-library/react'; import { ReactElement } from 'react'; import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; // Mock QueryClient for testing const createTestQueryClient = () => new QueryClient({ defaultOptions: { queries: { retry: false, }, }, }); export const renderWithProviders = ( ui: ReactElement, options?: RenderOptions ) => { const queryClient = createTestQueryClient(); const providers = ({ children }: { children: React.ReactNode }) => ( <QueryClientProvider client={queryClient}> {children} </QueryClientProvider> ); return render(ui, { wrapper: providers, ...options }); }; export * from '@testing-library/react';

Mock Implementations

Create mock implementations for API clients:

// Mock API client jest.mock('@/utils/api-client/tasks-client', () => ({ TasksClient: jest.fn().mockImplementation(() => ({ getTasks: jest.fn(), createTask: jest.fn(), updateTask: jest.fn(), deleteTask: jest.fn(), })), }))

Testing Best Practices

Test Structure

✅ Good Practices

// Test user behavior, not implementation
expect(screen.getByRole('button')).toBeInTheDocument();

// Use semantic queries
screen.getByRole('button', { name: /submit/i });

// Test error states
expect(screen.getByText(/error/i)).toBeInTheDocument();

❌ Avoid These

// Don't test implementation details
expect(wrapper.find('.my-button')).toHaveLength(1);

// Avoid fragile queries
screen.getByClassName('button-submit');

// Don't test internal state
expect(component.state.isLoading).toBe(true);

Test Organization

Test Organization
└── 
src
    ├── 
components
└── 
Button
├── 
Button.tsx
├── 
Button.test.tsx# Co-located tests
└── 
index.ts
    ├── 
hooks
├── 
useAuth.ts
└── 
useAuth.test.ts# Hook tests
    ├── 
utils
├── 
formatDate.ts
└── 
formatDate.test.ts# Utility tests
    └── 
__tests__
        ├── 
integration# Integration tests
        ├── 
setup# Test setup files
        └── 
README.md# Test documentation

Coverage Guidelines

Unit Tests

Component and utility function testing

Target: 80%+

Integration Tests

Hook and API integration testing

Target: 70%+

Critical Paths

Authentication and core workflows

Target: 90%+

Edge Cases

Error handling and boundary conditions

Target: 60%+

Integration with CI/CD

Pre-commit Hooks

Tests run automatically before commits through the validation script:

# apps/frontend/scripts/validate.sh echo "🔍 Running tests..." npm test -- --passWithNoTests --watchAll=false TEST_EXIT_CODE=$? # Validation continues with other checks... if [ $TEST_EXIT_CODE -ne 0 ]; then echo "✗ Tests failed" exit 1 fi

GitHub Actions

Automated testing in CI/CD pipeline:

# .github/workflows/frontend.yml test: runs-on: ubuntu-latest steps: - name: Checkout repository uses: actions/checkout@v4 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: '18' cache: 'npm' cache-dependency-path: 'apps/frontend/package-lock.json' - name: Install dependencies run: npm ci working-directory: ./apps/frontend - name: Run tests run: npm run test:ci working-directory: ./apps/frontend - name: Upload coverage to Codecov uses: codecov/codecov-action@v3 with: file: ./apps/frontend/coverage/lcov.info

Troubleshooting

Common Issues

Environment Variables Missing

# Add to jest.setup.js
process.env.NEXT_PUBLIC_API_BASE_URL = 'http://localhost:8080/api/v1';

Browser APIs Not Available

# Mock in jest.setup.js
global.matchMedia = jest.fn().mockImplementation(query => ({ ... }));
global.ResizeObserver = jest.fn();

Async Operations Not Awaited

// Use act() for async operations
await act(async () => {
await new Promise(resolve => setTimeout(resolve, 0));
});

Debug Tips

# Run tests with verbose output npm test -- --verbose # Run specific test file npm test Button.test.tsx # Run tests matching pattern npm test -- --testNamePattern="should render" # Debug failing test npm test -- --detectOpenHandles