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/svelteor any DOM-rendering test helper — tests do not mount.sveltecomponents. - No custom Vitest config. There is no
vitest.config.tsin either app; Vitest picks up plugins from the sharedvite.config.ts(@sveltejs/vite-plugin-svelte,@tailwindcss/vite). Notestblock 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 notest:run,test:coverage, ortest:uiscript. - 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.tstracker-frontend-svelte/src/lib/utils/campaignPermissions.test.tstracker-frontend-svelte/src/lib/utils/matchTrackers.test.tstracker-frontend-svelte/src/lib/utils/moveWheelTargets.test.tstracker-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:
- Prefer putting pure/testable logic in
packages/tracker-shared/src/if it's used by both apps, or in the app's ownsrc/lib/utils/if it's app-specific. - Add a co-located
*.test.tsfile and test the exported function(s) directly, following the existing factory-helper pattern. - If you need to test something that genuinely requires rendering a
.sveltecomponent 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.