Skip to main content

Testing Guide

HostMetrics uses Vitest for unit and integration tests and Playwright for end-to-end browser tests.

Test Stack

ToolPurposeConfig File
VitestUnit and integration testsvitest.config.ts
PlaywrightEnd-to-end browser testsplaywright.config.ts
jsdomDOM environment for VitestSet in Vitest config

Directory Structure

src/__tests__/
├── unit/                    # Unit tests for hooks, utils, pure functions
│   ├── useKPIs.test.ts
│   ├── calculations.test.ts
│   └── ...
├── integration/             # Integration tests (DB + component)
│   └── ...
└── mocks/
    └── supabase.ts          # Shared Supabase mock

e2e/
├── specs/                   # Playwright test specs
│   ├── dashboard.spec.ts
│   ├── trips.spec.ts
│   └── ...
├── auth.setup.ts            # Authentication setup
└── test-data.ts             # Test fixture data

Running Tests

Unit Tests

# Run all unit tests in watch mode
npm run test

# Run all unit tests once (CI mode)
npm run test -- --run

# Run only unit tests
npm run test -- --run "src/__tests__/unit/"

# Run a specific test file
npm run test -- --run "src/__tests__/unit/useKPIs.test.ts"

# Run with coverage report
npm run test -- --run --coverage

End-to-End Tests

# Run all E2E tests
npx playwright test

# Run a specific spec file
npx playwright test e2e/specs/dashboard.spec.ts

# Run in headed mode (visible browser)
npx playwright test --headed

# Run in a specific browser
npx playwright test --project=chromium

# Open the Playwright UI
npx playwright test --ui

Vitest Configuration

Key settings from vitest.config.ts:
  • Environment: jsdom (simulates browser DOM)
  • Coverage thresholds: 80% for branches, functions, lines, and statements
  • Path aliases: @/ maps to src/ (mirrors the app’s tsconfig)
  • Setup files: Global test setup for mocks and environment

Test File Naming

What you’re testingFile name patternLocation
ComponentComponentName.test.tsxsrc/__tests__/unit/
HookhookName.test.tssrc/__tests__/unit/
Utility functionutilName.test.tssrc/__tests__/unit/
E2E user flowfeature.spec.tse2e/specs/

Mock Patterns

Mocking the Supabase Client

The shared Supabase mock lives at src/__tests__/mocks/supabase.ts. Use it in unit tests:
import { vi, describe, it, expect, beforeEach } from "vitest";

// Mock the Supabase client module
vi.mock("@/lib/db/_client", () => ({
  getClient: () => mockSupabaseClient,
  getCurrentUserId: () => "test-user-id",
}));

const mockSupabaseClient = {
  from: vi.fn().mockReturnThis(),
  select: vi.fn().mockReturnThis(),
  eq: vi.fn().mockReturnThis(),
  order: vi.fn().mockReturnThis(),
  single: vi.fn().mockResolvedValue({
    data: { id: "trip-1", reservation_id: "RES-001" },
    error: null,
  }),
};

Mocking Hooks in Component Tests

When testing components, mock the hooks they depend on:
import { vi } from "vitest";
import { render, screen } from "@testing-library/react";
import { DashboardStats } from "@/components/dashboard/DashboardStats";

vi.mock("@/hooks/useKPIs", () => ({
  useKPIs: () => ({
    kpis: {
      totalEarnings: 15000,
      totalExpenses: 5000,
      netProfit: 10000,
      profitMargin: 66.7,
    },
    isLoading: false,
    error: null,
  }),
}));

describe("DashboardStats", () => {
  it("renders KPI values", () => {
    render(<DashboardStats />);
    expect(screen.getByText("$15,000.00")).toBeInTheDocument();
  });
});

Mocking Next.js Router

vi.mock("next/navigation", () => ({
  useRouter: () => ({
    push: vi.fn(),
    replace: vi.fn(),
    back: vi.fn(),
  }),
  usePathname: () => "/dashboard",
  useSearchParams: () => new URLSearchParams(),
}));

Writing Unit Tests

Follow this structure for hook tests:
import { describe, it, expect, beforeEach, vi } from "vitest";
import { renderHook, waitFor } from "@testing-library/react";
import { useBookings } from "@/hooks/useBookings";

vi.mock("@/lib/db/_client", () => ({
  getClient: () => mockClient,
  getCurrentUserId: vi.fn().mockResolvedValue("test-user-id"),
}));

describe("useBookings", () => {
  beforeEach(() => {
    vi.clearAllMocks();
  });

  it("fetches bookings on mount", async () => {
    const { result } = renderHook(() => useBookings());

    await waitFor(() => {
      expect(result.current.isLoading).toBe(false);
    });

    expect(result.current.bookings).toHaveLength(3);
  });

  it("filters by status", async () => {
    const { result } = renderHook(() =>
      useBookings({ status: "completed" })
    );

    await waitFor(() => {
      expect(result.current.bookings).toHaveLength(2);
    });
  });
});

Playwright E2E Tests

Browser Projects

Playwright is configured with 5 browser projects:
ProjectBrowserViewport
chromiumChromiumDesktop
firefoxFirefoxDesktop
webkitWebKit/SafariDesktop
mobile-chromeMobile Chrome390x844
mobile-safariMobile Safari390x844

Auth Setup

E2E tests authenticate before running specs using auth.setup.ts:
// e2e/auth.setup.ts
import { test as setup } from "@playwright/test";

setup("authenticate", async ({ page }) => {
  await page.goto("/login");
  await page.fill('[name="email"]', process.env.TEST_EMAIL!);
  await page.fill('[name="password"]', process.env.TEST_PASSWORD!);
  await page.click('button[type="submit"]');
  await page.waitForURL("/dashboard");

  // Save auth state for reuse
  await page.context().storageState({ path: "e2e/.auth/state.json" });
});

Writing E2E Specs

import { test, expect } from "@playwright/test";

test.describe("Dashboard", () => {
  test("displays KPI cards", async ({ page }) => {
    await page.goto("/dashboard");
    await expect(page.getByText("Total Earnings")).toBeVisible();
    await expect(page.getByText("Net Profit")).toBeVisible();
  });

  test("navigates to trip details", async ({ page }) => {
    await page.goto("/bookings");
    await page.click("text=RES-001");
    await expect(page).toHaveURL(/\/bookings\//);
  });
});

Pre-Commit Test Commands

Before creating a PR, always run:
# Unit tests (must pass)
npm run test -- --run

# Or specifically unit tests only
npm run test -- --run "src/__tests__/unit/"
See the PR Checklist for the full verification workflow.