Skip to content

Frontend Testing with Vitest

This document explains how the tracker-frontend-svelte and tracker-admin-svelte projects are tested today.

Overview

Both frontend projects use Vitest for unit testing, run via the test script in each app's package.json ("test": "vitest"). There is currently:

  • No component-testing library. Neither app depends on @testing-library/svelte or any DOM-rendering test helper — tests do not mount .svelte components.
  • No custom Vitest config. There is no vitest.config.ts in either app; Vitest picks up plugins from the shared vite.config.ts (@sveltejs/vite-plugin-svelte, @tailwindcss/vite). No test block is configured there, so Vitest runs with its defaults (no jsdom environment, no setup file, no coverage thresholds).
  • No coverage or UI scripts. Only npm run test (Vitest's default watch mode) exists — there is no test:run, test:coverage, or test:ui script.
  • No MSW or mocked API layer. Tests exercise pure functions directly rather than mocking fetch/the API client.

In practice, what exists today is plain Vitest unit tests over utility functions — logic that has been extracted out of components (shared logic lives in packages/tracker-shared/src/, e.g. campaignGrouping.ts, campaignPermissions.ts, matchTrackers.ts, moveWheelTargets.ts, trackerDisplay.ts) rather than tests of the components/pages themselves.

Quick Start

Running Tests

# tracker-frontend-svelte
cd tracker-frontend-svelte
npm run test              # runs `vitest` (watch mode by default)
npx vitest run            # run once, non-interactively (e.g. in CI)

# tracker-admin-svelte
cd tracker-admin-svelte
npm run test
npx vitest run

In Development Containers

# Frontend container
docker compose exec frontend-svelte-dev npm run test

# Admin container
docker compose exec admin-svelte-dev npm run test

There is no run_frontend_tests.sh script today that runs both projects at once — run each app's tests separately as shown above.

Where the tests live

Test files sit alongside the code they test, using the *.test.ts suffix, for example:

  • tracker-frontend-svelte/src/lib/utils/campaignGrouping.test.ts
  • tracker-frontend-svelte/src/lib/utils/campaignPermissions.test.ts
  • tracker-frontend-svelte/src/lib/utils/matchTrackers.test.ts
  • tracker-frontend-svelte/src/lib/utils/moveWheelTargets.test.ts
  • tracker-frontend-svelte/src/lib/utils/trackerDisplay.test.ts

Several of these test files import the actual logic under test from the shared tracker-shared package (e.g. import { groupCampaigns } from "tracker-shared/campaignGrouping") rather than from local app code — the tests live in the app that most directly depends on the behavior, even though the implementation is shared.

Writing Tests

Basic Test Structure

Tests use plain Vitest — describe/it/expect — against exported functions, with no rendering involved. For example (adapted from campaignGrouping.test.ts):

import type { ProductionRun } from "tracker-shared/api/production-runs";
import { groupCampaigns } from "tracker-shared/campaignGrouping";
import { describe, expect, it } from "vitest";

function makeCampaign(overrides: Partial<ProductionRun> = {}): ProductionRun {
  return {
    id: 1,
    start_date: new Date().toISOString(),
    end_date: null,
    brand_id: 1,
    parent_id: null,
    description: "Campaign",
    removal_date: null,
    image_url: null,
    tracker_count: 0,
    children_count: 0,
    ...overrides,
  };
}

describe("groupCampaigns", () => {
  it("returns an empty array for no campaigns", () => {
    expect(groupCampaigns([], null)).toEqual([]);
  });

  it("nests children under their parent and sorts both alphabetically", () => {
    const parentA = makeCampaign({ id: 1, description: "Zebra" });
    const parentB = makeCampaign({ id: 2, description: "Alpha" });
    const groups = groupCampaigns([parentA, parentB], null);

    expect(groups.map((g) => g.parent.description)).toEqual(["Alpha", "Zebra"]);
  });
});

The pattern throughout the existing suite is: build small factory helpers for the domain object under test (like makeCampaign above), then assert directly on the function's return value. There is no render, screen, or fireEvent anywhere in the current test suite.

Mocking

import { vi } from "vitest";

// Mock a module
vi.mock("tracker-shared/api/client", () => ({
  request: vi.fn(() => Promise.resolve({ data: "mocked" })),
}));

// Mock environment variables
vi.stubEnv("VITE_API_URL", "http://test-api.com");

Since there is no global mock setup file, any mocks needed by a given test (e.g. localStorage, fetch) must be set up within that test file itself.

Debug Mode

# Run tests matching a name pattern
npx vitest run --grep "groupCampaigns"

# Run a specific test file
npx vitest run src/lib/utils/campaignGrouping.test.ts

# Verbose reporter
npx vitest run --reporter=verbose

Adding New Tests

If you add logic worth unit testing:

  1. Prefer putting pure/testable logic in packages/tracker-shared/src/ if it's used by both apps, or in the app's own src/lib/utils/ if it's app-specific.
  2. Add a co-located *.test.ts file and test the exported function(s) directly, following the existing factory-helper pattern.
  3. If you need to test something that genuinely requires rendering a .svelte component or driving DOM interactions, there is no existing setup for that today (no @testing-library/svelte, no jsdom environment) — that would need to be introduced first rather than assumed to already work.

For more on Vitest itself, see the Vitest documentation.