Skip to main content
CharleOS uses a comprehensive testing strategy with Vitest for unit tests and Cypress for E2E tests. This ensures code quality and prevents regressions.

Testing Stack

Unit Tests

Vitest - Fast unit testing for utilities and business logic
  • Tests in __tests__/
  • Run with npm run test
  • Coverage reports included

E2E Tests

Cypress - Browser automation for critical user flows
  • Tests in cypress/e2e/
  • Run with npm run test:e2e
  • Dedicated test users for isolation

Test Coverage

What We Test

Unit Tests (__tests__/):
  • Business logic (billing calculations, time utilities)
  • Data transformations
  • Helper functions
  • Type utilities
E2E Tests (cypress/e2e/):
  • Authentication flows
  • Critical user journeys (create task, approve quote)
  • Client portal functionality
  • Role-based access control
What We Don’t Test:
  • UI styling/layout
  • Third-party libraries
  • Database migrations
  • Simple CRUD operations

Running Tests

Unit Tests

# Run all unit tests
npm run test

# Run in watch mode (auto-rerun on changes)
npm run test:watch

# Run with coverage report
npm run test:coverage

# Run specific test file
npm run test __tests__/billing.test.ts

E2E Tests

# Seed test data first (if not already seeded)
npm run db:seed:e2e

# Run E2E tests (headless)
npm run test:e2e

# Run with Cypress UI (interactive)
npm run test:e2e:open

# Run specific test file
npx cypress run --spec "cypress/e2e/auth.cy.ts"

Unit Test Examples

Testing Utilities

// __tests__/billing.test.ts
import { describe, it, expect } from "vitest";
import { calculateBillable } from "@/lib/utils/billing";

describe("calculateBillable", () => {
  it("returns minimum when actual is below minimum", () => {
    const result = calculateBillable({
      actual: 100,
      average: 150,
      maximum: 200,
    });

    expect(result).toBe(150);  // Uses average (minimum)
  });

  it("returns maximum when actual exceeds maximum", () => {
    const result = calculateBillable({
      actual: 250,
      average: 150,
      maximum: 200,
    });

    expect(result).toBe(200);  // Capped at maximum
  });

  it("returns actual when within range", () => {
    const result = calculateBillable({
      actual: 175,
      average: 150,
      maximum: 200,
    });

    expect(result).toBe(175);  // Uses actual
  });
});

Testing Date Utilities

// __tests__/working-days.test.ts
import { describe, it, expect } from "vitest";
import { calculateWorkingDays, isWorkingDay } from "@/lib/utils/working-days";

describe("isWorkingDay", () => {
  it("returns true for Monday-Friday", () => {
    const monday = new Date("2024-01-22");  // Monday
    expect(isWorkingDay(monday)).toBe(true);
  });

  it("returns false for Saturday-Sunday", () => {
    const saturday = new Date("2024-01-20");  // Saturday
    expect(isWorkingDay(saturday)).toBe(false);
  });
});

describe("calculateWorkingDays", () => {
  it("counts working days correctly", () => {
    const startDate = new Date("2024-01-22");  // Monday
    const endDate = new Date("2024-01-26");    // Friday

    const workingDays = calculateWorkingDays(startDate, endDate);

    expect(workingDays).toBe(5);  // Mon-Fri
  });

  it("excludes weekends", () => {
    const startDate = new Date("2024-01-22");  // Monday
    const endDate = new Date("2024-01-28");    // Sunday (next week)

    const workingDays = calculateWorkingDays(startDate, endDate);

    expect(workingDays).toBe(5);  // Only Mon-Fri
  });
});

E2E Test Examples

Authentication Flow

// cypress/e2e/auth.cy.ts
describe("Authentication", () => {
  it("redirects unauthenticated users to sign-in", () => {
    cy.visit("/clients");
    cy.url().should("include", "sign-in");
  });

  describe("Authenticated - Admin", () => {
    beforeEach(() => {
      cy.loginAsAdmin();
    });

    it("can access dashboard", () => {
      cy.visit("/");
      cy.url().should("not.include", "sign-in");
    });

    it("can access admin settings", () => {
      cy.visit("/admin/settings");
      cy.url().should("include", "/admin/settings");
    });
  });
});

Test Configuration

Vitest Config

// vitest.config.ts
import { defineConfig } from "vitest/config";

export default defineConfig({
  test: {
    globals: true,
    environment: "node",
    include: ["__tests__/**/*.test.ts"],
    setupFiles: ["__tests__/setup.ts"],
    coverage: {
      provider: "v8",
      reporter: ["text", "json", "html"],
      include: ["lib/**/*.ts", "app/api/**/*.ts"],
      exclude: ["lib/db/**", "lib/auth*.ts"],
      thresholds: {
        statements: 60,
        branches: 50,
        functions: 60,
        lines: 60,
      },
    },
  },
});

Cypress Config

// cypress.config.ts
import { defineConfig } from "cypress";

export default defineConfig({
  e2e: {
    baseUrl: "http://localhost:3000",
    supportFile: "cypress/support/e2e.ts",
  },
});

CI/CD Integration

Tests run automatically on every pull request:
# .github/workflows/ci.yml
jobs:
  unit-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Run unit tests
        run: npm run test:coverage

      - name: Upload coverage
        uses: codecov/codecov-action@v3

  e2e-tests:
    runs-on: ubuntu-latest
    steps:
      - name: Seed E2E data
        run: npm run db:seed:e2e

      - name: Run E2E tests
        run: npm run test:e2e

Best Practices

Focus on behavior, not internal details:
// Good: Test behavior
it("calculates billable time correctly", () => {
  const result = calculateBillable({ actual: 175, average: 150, max: 200 });
  expect(result).toBe(175);
});

// Bad: Test implementation
it("uses Math.min and Math.max", () => {
  // Testing internal implementation details
});
Unit tests should run in milliseconds:
  • No database calls in unit tests
  • Mock external dependencies
  • Use in-memory data
// Good: Clear and specific
it("returns average when actual is below average", () => { ... });

// Bad: Vague
it("works correctly", () => { ... });
Don’t just test the happy path:
describe("calculateWorkingDays", () => {
  it("handles same start and end date");
  it("handles start date after end date");
  it("handles weekends");
  it("handles single day");
  it("handles bank holidays");
});

Debugging Tests

Vitest

# Run tests with debugging
npm run test -- --inspect-brk

# Run specific test
npm run test -- -t "calculates billable time"

# Show console.log output
npm run test -- --reporter=verbose

Cypress

# Run with Cypress UI
npm run test:e2e:open

# Run specific test
npx cypress run --spec "cypress/e2e/auth.cy.ts"

# Run in headed mode
npx cypress run --headed

Coverage Reports

# Generate coverage report
npm run test:coverage

# Open HTML report
open coverage/index.html
Coverage Thresholds:
  • Statements: 60%
  • Branches: 50%
  • Functions: 60%
  • Lines: 60%

Unit Tests

Writing unit tests with Vitest

E2E Tests

Writing E2E tests with Cypress

CI/CD

Automated testing in CI