Testing

July 24, 2026 · View on GitHub

This repo has four test categories. They differ in what they are allowed to fake, how fast they run, and when CI runs them. For the full lifecycle zero-mock policy and a per-spec inventory, see e2e/README.md.

The four categories

CategoryWhat it testsMay fakeSpeedRuns in CI
UnitIndividual modules/components in isolation (Vitest, happy-dom)Anything — mocks encouragedSecondsEvery PR
IntegrationMultiple main-process modules against real subprocesses/disk (Vitest, node, serial)External servicesTens of secondsEvery PR
E2EThe real built app driven by Playwright, per platformSynthetic state via dev hooks; may assert on IPC dispatch instead of real side effectsMinutesEvery PR (@windows / @macos / @linux)
LifecycleFull user flows with zero mocking: real clicks, real downloads, real git, real diskNothing during app execution (staging fixtures and stubbed native OS dialogs only)Up to hours (~500 MB real install)Nightly + manual (lifecycle.yml)

Where they live:

  • Unit: src/**/*.test.ts (excluding *.integration.test.ts)
  • Integration: src/**/*.integration.test.ts
  • E2E + Lifecycle: e2e/*.test.ts, selected by tag in the test title (@windows @macos @linux vs @lifecycle), mapped to Playwright projects in playwright.config.ts

Running tests

# Unit
pnpm test                    # run once
pnpm run test:watch          # watch mode
pnpm exec vitest run src/main/lib/desktopDetect.test.ts   # one file

# Integration
pnpm run test:integration

# E2E (build first — Playwright drives the built app)
pnpm run build
pnpm run test:e2e:windows    # or :macos / :linux
pnpm exec playwright test --project=windows chooser.test.ts   # one spec

# Lifecycle (real installs/downloads — see warning below)
pnpm run build
pnpm run test:e2e:lifecycle
pnpm exec playwright test --project=lifecycle lifecycle-copy.test.ts   # one spec

Warning

pnpm run test:e2e with no --project runs all projects, including lifecycle — which downloads ~500 MB and performs real installs. Use the per-platform or per-project scripts unless that is what you want.

The lifecycle harness prints the per-run profile directory ([lifecycle-harness] fresh profile dir: …); re-export it as LIFECYCLE_REUSE_DIR to re-run individual tests against that profile.

Which category does my test belong in?

  • Testing a function, composable, or component contract → unit.
  • Testing several main-process modules working together against real subprocesses or the filesystem → integration.
  • Driving the built app's UI, but seeding synthetic state via dev hooks (seedRunningSession, setAppUpdateState, …) or asserting that an IPC was dispatched → E2E, tagged @windows @macos @linux.
  • Validating a complete user flow the way a human release tester would — real clicks on real controls, real downloads, real side effects on disk → lifecycle, tagged @lifecycle. The flow under test must be triggered through the real UI control, never by calling window.api directly; a broken button must fail the test. See the policy in e2e/README.md before writing one.

Before every commit

pnpm run typecheck
pnpm run lint
pnpm run build
pnpm run test

Typecheck and lint are enforced by a husky pre-commit hook. Flaky tests are not acceptable — fix them when discovered (see AGENTS.md).

CI mapping

WorkflowTriggerWhat runs
ci.ymlEvery PRUnit + integration on Linux; E2E per platform (test:e2e:macos / :windows / :linux)
lifecycle.ymlNightly + manual dispatchThe whole lifecycle Playwright project

Playwright traces, videos, and screenshots for failures are uploaded as CI artifacts (playwright-report/, test-results/).