Files
desa-darmasaba/docs/TESTING.md
nico 6ed2392420 Add comprehensive testing suite and fix QC issues
- Add 115+ unit, component, and E2E tests
- Add Vitest configuration with coverage thresholds
- Add validation schema tests (validations.test.ts)
- Add sanitizer utility tests (sanitizer.test.ts)
- Add WhatsApp service tests (whatsapp.test.ts)
- Add component tests for UnifiedTypography and UnifiedSurface
- Add E2E tests for admin auth and public pages
- Add testing documentation (docs/TESTING.md)
- Add sanitizer and WhatsApp utilities
- Add centralized validation schemas
- Refactor state management (admin/public separation)
- Fix security issues (OTP via POST, session password validation)
- Update AGENTS.md with testing guidelines

Test Coverage: 50%+ target achieved
All tests passing: 115/115

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
2026-03-09 14:05:03 +08:00

12 KiB

Testing Guide - Desa Darmasaba

Overview

This document provides comprehensive testing guidelines for the Desa Darmasaba project. The project uses a multi-layered testing strategy including unit tests, component tests, and end-to-end (E2E) tests.

Testing Stack

Layer Tool Purpose
Unit Tests Vitest Testing utility functions, validation schemas, services
Component Tests Vitest + React Testing Library Testing React components in isolation
E2E Tests Playwright Testing complete user flows in real browsers
API Mocking MSW (Mock Service Worker) Mocking API responses for unit/component tests

Test Structure

__tests__/
├── api/                    # API integration tests
│   └── fileStorage.test.ts
├── components/             # Component tests
│   └── admin/
│       ├── UnifiedTypography.test.tsx
│       └── UnifiedSurface.test.tsx
├── e2e/                    # End-to-end tests
│   ├── admin/
│   │   └── auth.spec.ts
│   └── public/
│       └── pages.spec.ts
├── lib/                    # Unit tests for utilities
│   ├── validations.test.ts
│   ├── sanitizer.test.ts
│   └── whatsapp.test.ts
├── mocks/                  # MSW mocks for API
│   ├── handlers.ts
│   └── server.ts
└── setup.ts                # Test setup and configuration

Running Tests

All Tests

bun run test

Unit Tests Only

bun run test:api

E2E Tests Only

bun run test:e2e

Tests with Coverage

bun run test:api --coverage

Run Specific Test File

bunx vitest run __tests__/lib/validations.test.ts

Run Tests in Watch Mode

bunx vitest

Run E2E Tests with UI

bun run test:e2e --ui

Writing Tests

Unit Tests (Vitest)

Unit tests should test pure functions, validation schemas, and utilities in isolation.

// __tests__/lib/example.test.ts
import { describe, it, expect } from 'vitest';
import { exampleFunction } from '@/lib/example';

describe('exampleFunction', () => {
  it('should return expected value for valid input', () => {
    const result = exampleFunction('valid-input');
    expect(result).toBe('expected-output');
  });

  it('should handle edge cases', () => {
    expect(() => exampleFunction('')).toThrow();
    expect(() => exampleFunction(null)).toThrow();
  });
});

Component Tests (React Testing Library)

Component tests should test React components in isolation with mocked dependencies.

// __tests__/components/Example.test.tsx
import { describe, it, expect } from 'vitest';
import { render, screen, fireEvent } from '@testing-library/react';
import { MantineProvider, createTheme } from '@mantine/core';
import { ExampleComponent } from '@/components/Example';

function renderWithMantine(ui: React.ReactElement) {
  const theme = createTheme();
  return render(ui, {
    wrapper: ({ children }) => (
      <MantineProvider theme={theme}>{children}</MantineProvider>
    ),
  });
}

describe('ExampleComponent', () => {
  it('should render with props', () => {
    renderWithMantine(<ExampleComponent title="Test Title" />);
    expect(screen.getByText('Test Title')).toBeInTheDocument();
  });

  it('should handle user interactions', async () => {
    const onClick = vi.fn();
    renderWithMantine(<ExampleComponent onClick={onClick} />);
    
    fireEvent.click(screen.getByRole('button'));
    expect(onClick).toHaveBeenCalledTimes(1);
  });
});

E2E Tests (Playwright)

E2E tests should test complete user flows in a real browser environment.

// __tests__/e2e/example.spec.ts
import { test, expect } from '@playwright/test';

test.describe('Feature Name', () => {
  test.beforeEach(async ({ page }) => {
    // Setup before each test
    await page.goto('/starting-page');
  });

  test('should complete user flow', async ({ page }) => {
    // Fill form
    await page.fill('input[name="email"]', 'user@example.com');
    await page.click('button[type="submit"]');

    // Wait for navigation
    await page.waitForURL('/success');

    // Verify result
    await expect(page.getByText('Success!')).toBeVisible();
  });

  test('should handle errors gracefully', async ({ page }) => {
    // Submit invalid data
    await page.click('button[type="submit"]');

    // Verify error message
    await expect(page.getByText('Validation error')).toBeVisible();
  });
});

API Mocking (MSW)

Use MSW to mock API responses for unit and component tests.

// __tests__/mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
  http.get('/api/example', () => {
    return HttpResponse.json({
      data: [{ id: '1', name: 'Item 1' }],
    });
  }),

  http.post('/api/example', async ({ request }) => {
    const body = await request.json();
    return HttpResponse.json({
      data: { id: '2', ...body },
      status: 201,
    });
  }),
];

Test Coverage Goals

Current coverage thresholds (configured in vitest.config.ts):

Metric Target
Branches 50%
Functions 50%
Lines 50%
Statements 50%

Critical Files Priority

Focus testing efforts on these critical files first:

  1. Validation & Security

    • src/lib/validations/index.ts
    • src/lib/sanitizer.ts
    • src/lib/whatsapp.ts
    • src/lib/session.ts
  2. Core Utilities

    • src/lib/api-fetch.ts
    • src/lib/prisma.ts
    • src/utils/themeTokens.ts
  3. Shared Components

    • src/components/admin/UnifiedTypography.tsx
    • src/components/admin/UnifiedSurface.tsx
    • src/components/admin/UnifiedCard.tsx
  4. State Management

    • src/state/darkModeStore.ts
    • src/state/admin/*.ts
    • src/state/public/*.ts
  5. API Routes

    • src/app/api/[[...slugs]]/_lib/auth/**
    • src/app/api/[[...slugs]]/_lib/desa/**

Testing Conventions

Naming Conventions

  • Unit/Component Tests: *.test.ts or *.test.tsx
  • E2E Tests: *.spec.ts
  • Test Files: Match source file name (e.g., sanitizer.tssanitizer.test.ts)
  • Test Directories: Mirror source structure under __tests__/

Describe Blocks

Use nested describe blocks to organize tests logically:

describe('FeatureName', () => {
  describe('functionName', () => {
    describe('when valid input', () => {
      it('should return expected result', () => {});
    });

    describe('when invalid input', () => {
      it('should throw error', () => {});
    });
  });
});

Test Descriptions

  • Use clear, descriptive test names
  • Follow pattern: should [expected behavior] when [condition]
  • Avoid vague descriptions like "works correctly"

Assertions

  • Use specific matchers (toBe, toEqual, toContain)
  • Test both success and failure cases
  • Test edge cases (empty input, null, undefined, max values)

Setup and Teardown

describe('ComponentName', () => {
  beforeEach(() => {
    // Reset mocks, state
    vi.clearAllMocks();
  });

  afterEach(() => {
    // Cleanup
    vi.restoreAllMocks();
  });

  // ... tests
});

Mocking Guidelines

Mock External Services

// Mock fetch API
global.fetch = vi.fn();

// Mock modules
vi.mock('@/lib/prisma', () => ({
  default: {
    berita: {
      findMany: vi.fn(),
      create: vi.fn(),
    },
  },
}));

Mock Environment Variables

const originalEnv = process.env;

beforeEach(() => {
  process.env = {
    ...originalEnv,
    TEST_VAR: 'test-value',
  };
});

afterEach(() => {
  process.env = originalEnv;
});

Mock Date/Time

const mockDate = new Date('2024-01-01T00:00:00Z');
vi.useFakeTimers();
vi.setSystemTime(mockDate);

// ... tests

vi.useRealTimers();

E2E Testing Best Practices

Test User Flows, Not Implementation

Good:

test('user can login and view dashboard', async ({ page }) => {
  await page.goto('/admin/login');
  await page.fill('input[name="nomor"]', '08123456789');
  await page.click('button[type="submit"]');
  await expect(page).toHaveURL('/admin/dashboard');
});

Bad:

test('login form submits to API', async ({ page }) => {
  // Don't test internal implementation details
});

Use Data Attributes for Selectors

// In component
<button data-testid="submit-button">Submit</button>

// In test
await page.getByTestId('submit-button').click();

Handle Async Operations

// Wait for specific element
await page.waitForSelector('.loaded-content');

// Wait for navigation
await page.waitForNavigation();

// Wait for network request
await page.waitForResponse('/api/data');

Skip Tests Appropriately

// Skip in CI
test.skip(process.env.CI === 'true', 'Skip in CI environment');

// Skip with reason
test.skip(true, 'Feature not yet implemented');

// Conditional skip
test.skip(!hasValidCredentials, 'Requires valid credentials');

Continuous Integration

GitHub Actions Workflow

Tests run automatically on:

  • Pull requests
  • Push to main branch
  • Manual trigger

Test Requirements

  • All new features must include tests
  • Bug fixes should include regression tests
  • Coverage should not decrease significantly

Debugging Tests

Vitest Debug Mode

bunx vitest --reporter=verbose

Playwright Debug Mode

PWDEBUG=1 bun run test:e2e

Playwright Trace Viewer

bun run test:e2e --trace on
bunx playwright show-trace

Common Patterns

Testing Validation Schemas

describe('validationSchema', () => {
  it('should accept valid data', () => {
    const result = validationSchema.safeParse(validData);
    expect(result.success).toBe(true);
  });

  it('should reject invalid data', () => {
    const result = validationSchema.safeParse(invalidData);
    expect(result.success).toBe(false);
    if (!result.success) {
      expect(result.error.errors[0].message).toContain('error message');
    }
  });
});

Testing Async Functions

it('should fetch data successfully', async () => {
  const result = await fetchData();
  expect(result).toEqual(expectedData);
});

it('should handle errors', async () => {
  await expect(asyncFunction()).rejects.toThrow('error message');
});

Testing Hooks

import { renderHook, act } from '@testing-library/react';

it('should update state', () => {
  const { result } = renderHook(() => useCustomHook());

  act(() => {
    result.current.setValue('new value');
  });

  expect(result.current.value).toBe('new value');
});

Troubleshooting

Common Issues

Issue: Tests fail with "Cannot find module" Solution: Check import paths, ensure @/ alias is configured in vitest.config.ts

Issue: Mantine components throw errors Solution: Wrap components with MantineProvider in test setup

Issue: Tests fail in CI but pass locally Solution: Check for environment-specific code, use proper mocking

Issue: E2E tests timeout Solution: Increase timeout, check for async operations, use proper waits

Getting Help

Resources

Maintenance

Regular Tasks

  • Update test dependencies monthly
  • Review and update test coverage goals quarterly
  • Remove deprecated test patterns
  • Add tests for newly discovered edge cases
  • Document common testing patterns

Deprecation Policy

When refactoring code:

  1. Keep existing tests passing
  2. Update tests to match new implementation
  3. Remove tests for removed functionality
  4. Update this documentation

Last Updated: March 9, 2026 Version: 1.0.0 Maintained By: Development Team