tududi/docs/testing.md
Chris 3486541272
Add comprehensive LLM development documentation (#939)
* Increase coverage

* Add comprehensive LLM development documentation

- Add CLAUDE.md as main documentation index
- Create 8 detailed documentation files in docs/:
  - architecture.md: Tech stack, data models, auth system
  - directory-structure.md: Complete file tree with paths
  - backend-patterns.md: Module architecture and patterns
  - database.md: Models, migrations, and workflows
  - development-workflow.md: Setup and daily development
  - code-conventions.md: Style guide and best practices
  - testing.md: Test organization and patterns
  - common-tasks.md: How-to guides for frequent tasks
- Update .gitignore to allow project-level CLAUDE.md
- 4,285 lines of comprehensive documentation
- Organized for easy navigation with cross-links
- LLM-optimized with absolute paths and code examples

* fixup! Add comprehensive LLM development documentation
2026-03-14 02:54:59 +02:00

12 KiB

Testing Requirements

← Back to Index


Test Organization

/backend/tests/
├── unit/                      # Unit tests for isolated logic
│   ├── models/               # Model tests
│   │   ├── task.test.js
│   │   ├── project.test.js
│   │   ├── user.test.js
│   │   └── ...
│   ├── middleware/           # Middleware tests
│   │   ├── auth.test.js
│   │   └── authorize.test.js
│   ├── services/             # Service tests
│   │   ├── permissionsService.test.js
│   │   ├── applyPerms.test.js
│   │   └── ...
│   └── utils/                # Utility tests
│       ├── timezone-utils.test.js
│       ├── slug-utils.test.js
│       ├── attachment-utils.test.js
│       └── migration-utils.test.js
│
└── integration/              # Integration tests for API endpoints
    ├── tasks/
    │   ├── tasks.test.js
    │   ├── subtasks.test.js
    │   └── recurring.test.js
    ├── projects/
    │   └── projects.test.js
    ├── areas/
    ├── notes/
    ├── tags/
    ├── auth/
    ├── shares/
    └── ... (47+ test directories)

/e2e/tests/                   # E2E tests (Playwright)
├── login.spec.ts
├── tasks.spec.ts
├── projects.spec.ts
├── subtasks.spec.ts
└── ...

/frontend/__tests__/          # Frontend tests
├── setup.ts                 # Test configuration
└── components/
    └── ... (component tests)

Running Tests

Backend Tests

# Run all backend tests
npm test
# or
npm run backend:test

# Run specific test file
npm test -- backend/tests/unit/models/task.test.js

# Run with coverage
npm run test:coverage

# Watch mode (re-run on file changes)
npm run test:watch

Frontend Tests

# Run frontend tests
npm run frontend:test

# Watch mode
npm run frontend:test -- --watch

E2E Tests

# Headless mode (default)
npm run test:ui

# Headed mode (see browser)
npm run test:ui:headed

# Specific test file
npx playwright test e2e/tests/tasks.spec.ts

# Debug mode
npx playwright test --debug

Pre-Push Checks

# Run all checks before committing/pushing
npm run pre-push

# This runs:
# - ESLint checks
# - Prettier formatting
# - Backend tests
# - Type checking (if applicable)

Testing Requirements

For Bug Fixes

MUST include a test that would have caught the bug.

Process:

  1. Write failing test that demonstrates the bug
  2. Fix the bug
  3. Verify test now passes
  4. Submit PR with both test and fix

Example:

// Test for bug: completed tasks showing in Today view
it('should not return completed tasks in Today view', async () => {
  // Arrange - Create completed task
  await Task.create({
    name: 'Completed Task',
    status: 2, // completed
    due_date: new Date().toISOString().split('T')[0],
    user_id: user.id
  });

  // Act - Get today's tasks
  const response = await request(app)
    .get('/api/v1/tasks/today')
    .set('Cookie', authCookie);

  // Assert - No completed tasks
  expect(response.status).toBe(200);
  const completedTasks = response.body.filter(t => t.status === 2);
  expect(completedTasks.length).toBe(0);
});

For New Features

SHOULD include relevant tests covering:

  • Happy path (success case)
  • Common edge cases
  • Error conditions

Not required to test:

  • Every possible combination
  • Framework internals
  • Third-party library behavior

Test Patterns

Backend Integration Test

Arrange-Act-Assert Pattern:

// /backend/tests/integration/tasks/tasks.test.js
const request = require('supertest');
const app = require('../../../app');
const { Task, User } = require('../../../models');

describe('Task API', () => {
  let user;
  let authCookie;

  beforeEach(async () => {
    // Setup: Create user and authenticate
    user = await User.create({
      email: 'test@example.com',
      password: 'password123'
    });

    const res = await request(app)
      .post('/api/login')
      .send({ email: 'test@example.com', password: 'password123' });
    authCookie = res.headers['set-cookie'];
  });

  afterEach(async () => {
    // Cleanup
    await Task.destroy({ where: {} });
    await User.destroy({ where: {} });
  });

  it('should create task with valid data', async () => {
    // Arrange
    const taskData = {
      name: 'Test Task',
      priority: 1,
      due_date: '2026-03-15'
    };

    // Act
    const response = await request(app)
      .post('/api/v1/task')
      .set('Cookie', authCookie)
      .send(taskData);

    // Assert
    expect(response.status).toBe(201);
    expect(response.body.name).toBe('Test Task');
    expect(response.body.priority).toBe(1);

    // Verify in database
    const task = await Task.findOne({ where: { name: 'Test Task' } });
    expect(task).not.toBeNull();
    expect(task.user_id).toBe(user.id);
  });

  it('should return 400 for missing name', async () => {
    // Arrange
    const invalidData = { priority: 1 };

    // Act
    const response = await request(app)
      .post('/api/v1/task')
      .set('Cookie', authCookie)
      .send(invalidData);

    // Assert
    expect(response.status).toBe(400);
    expect(response.body.error).toBeDefined();
  });

  it('should return 404 for non-existent task', async () => {
    // Act
    const response = await request(app)
      .get('/api/v1/task/99999')
      .set('Cookie', authCookie);

    // Assert
    expect(response.status).toBe(404);
  });
});

Backend Unit Test

// /backend/tests/unit/utils/timezone-utils.test.js
const { getTodayBoundsInUTC } = require('../../../utils/timezone-utils');

describe('timezone-utils', () => {
  describe('getTodayBoundsInUTC', () => {
    it('should return UTC bounds for today in given timezone', () => {
      // Arrange
      const timezone = 'America/New_York';

      // Act
      const { startOfDay, endOfDay } = getTodayBoundsInUTC(timezone);

      // Assert
      expect(startOfDay).toBeInstanceOf(Date);
      expect(endOfDay).toBeInstanceOf(Date);
      expect(endOfDay.getTime()).toBeGreaterThan(startOfDay.getTime());
    });

    it('should handle invalid timezone gracefully', () => {
      // Arrange
      const invalidTimezone = 'Invalid/Timezone';

      // Act & Assert
      expect(() => getTodayBoundsInUTC(invalidTimezone)).not.toThrow();
    });
  });
});

Frontend Component Test

// /frontend/components/Task/__tests__/TaskItem.test.tsx
import { render, screen, fireEvent } from '@testing-library/react';
import { TaskItem } from '../TaskItem';
import { Task } from '../../../entities/Task';

describe('TaskItem', () => {
  const mockTask: Task = {
    id: 1,
    uid: 'test-uid-123',
    name: 'Test Task',
    completed: false,
    priority: 1,
    due_date: '2026-03-15'
  };

  it('renders task name', () => {
    // Act
    render(<TaskItem task={mockTask} onUpdate={jest.fn()} />);

    // Assert
    expect(screen.getByText('Test Task')).toBeInTheDocument();
  });

  it('shows priority badge', () => {
    // Act
    render(<TaskItem task={mockTask} onUpdate={jest.fn()} />);

    // Assert
    expect(screen.getByText('Medium')).toBeInTheDocument();
  });

  it('calls onUpdate when checkbox is clicked', () => {
    // Arrange
    const mockOnUpdate = jest.fn();
    render(<TaskItem task={mockTask} onUpdate={mockOnUpdate} />);

    // Act
    const checkbox = screen.getByRole('checkbox');
    fireEvent.click(checkbox);

    // Assert
    expect(mockOnUpdate).toHaveBeenCalledWith({
      ...mockTask,
      completed: true
    });
  });

  it('applies completed styling when task is done', () => {
    // Arrange
    const completedTask = { ...mockTask, completed: true };

    // Act
    render(<TaskItem task={completedTask} onUpdate={jest.fn()} />);

    // Assert
    const taskElement = screen.getByText('Test Task').closest('div');
    expect(taskElement).toHaveClass('line-through');
    expect(taskElement).toHaveClass('opacity-50');
  });
});

E2E Test (Playwright)

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

test.describe('Task Management', () => {
  test.beforeEach(async ({ page }) => {
    // Login before each test
    await page.goto('http://localhost:8080/login');
    await page.fill('input[name="email"]', 'test@example.com');
    await page.fill('input[name="password"]', 'password123');
    await page.click('button[type="submit"]');
    await page.waitForURL('**/tasks');
  });

  test('should create new task', async ({ page }) => {
    // Arrange
    await page.click('button:has-text("New Task")');

    // Act
    await page.fill('input[name="name"]', 'E2E Test Task');
    await page.selectOption('select[name="priority"]', '1');
    await page.fill('input[name="due_date"]', '2026-03-15');
    await page.click('button:has-text("Save")');

    // Assert
    await expect(page.locator('text=E2E Test Task')).toBeVisible();
  });

  test('should complete task', async ({ page }) => {
    // Arrange - Create a task first
    await page.click('button:has-text("New Task")');
    await page.fill('input[name="name"]', 'Task to Complete');
    await page.click('button:has-text("Save")');

    // Act - Complete the task
    const taskItem = page.locator('text=Task to Complete').locator('..');
    await taskItem.locator('input[type="checkbox"]').check();

    // Assert
    await expect(taskItem).toHaveClass(/line-through/);
  });

  test('should filter tasks by priority', async ({ page }) => {
    // Arrange - Create tasks with different priorities
    await createTask(page, 'High Priority Task', 2);
    await createTask(page, 'Low Priority Task', 0);

    // Act - Filter by high priority
    await page.selectOption('select[name="priority_filter"]', '2');

    // Assert
    await expect(page.locator('text=High Priority Task')).toBeVisible();
    await expect(page.locator('text=Low Priority Task')).not.toBeVisible();
  });
});

async function createTask(page, name: string, priority: number) {
  await page.click('button:has-text("New Task")');
  await page.fill('input[name="name"]', name);
  await page.selectOption('select[name="priority"]', priority.toString());
  await page.click('button:has-text("Save")');
  await page.waitForSelector(`text=${name}`);
}

Test Database

Backend tests use a separate test database:

  • Automatically created in test environment
  • Migrations run before tests
  • Database cleared between tests (in afterEach)
  • Configured in /backend/config/database.js

Example cleanup:

afterEach(async () => {
  // Clean up test data
  await Task.destroy({ where: {} });
  await Project.destroy({ where: {} });
  await User.destroy({ where: {} });
});

Mocking

Mock External Services

// Mock email service in tests
jest.mock('../../../services/emailService', () => ({
  sendEmail: jest.fn().mockResolvedValue(true)
}));

it('should send notification email', async () => {
  const emailService = require('../../../services/emailService');
  
  await taskService.create({ name: 'Task', notify: true }, userId);
  
  expect(emailService.sendEmail).toHaveBeenCalled();
});

Mock Frontend API Calls

import { rest } from 'msw';
import { setupServer } from 'msw/node';

const server = setupServer(
  rest.get('/api/v1/tasks', (req, res, ctx) => {
    return res(ctx.json([
      { id: 1, name: 'Mocked Task' }
    ]));
  })
);

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());

Coverage Goals

While not strictly enforced, aim for:

  • Critical paths: 80%+ coverage
  • Business logic: 70%+ coverage
  • UI components: 50%+ coverage

Run coverage report:

npm run test:coverage

# Open HTML report
open coverage/index.html

Before Submitting PR

All tests passing:

npm test
npm run test:ui

No linting errors:

npm run lint

Code formatted:

npm run format:fix

Run pre-push checks:

npm run pre-push

← Back to Index