Skip to main content
Version: v2

Testing

modernGraphTool uses Vitest with Playwright browser mode. Specs are co-located next to the code they cover as *.spec.ts.

Running tests

CommandWhat it does
npm run testFull suite — both the client and server projects
npm run test:coverageFull suite + v8 coverage report into coverage/
npm run test:smokeBoots the built dist/ in a browser (run build first)

The two projects

The filename decides which project runs a spec:

  • *.svelte.spec.ts → the client project, in real Chromium via Playwright
  • everything else → the server project, in node

Most specs drive stores and services directly. Component specs mount with render() from vitest-browser-svelte and query through page.getBy* from vitest/browser.

Where the gotchas are documented

Layer-specific testing traps live next to the code, in per-directory AGENTS.md files — component and boot-test traps in src/lib/components/, d3/rAF traps in src/lib/graph/, and the fake-device fixtures in src/lib/device-peq/. They are kept there rather than here so they stay accurate as the code moves.

Coverage

The configuration lives in vite.config.ts under test.coverage, and two settings there are load-bearing:

  • The include glob covering all of src is what makes files that no test imports count at all. Without it the ~25 device-peq/handlers and connectors modules vanish from the denominator entirely rather than showing as the 0% they were, which made the reported number about 10 points optimistic. (Vitest 4 removed the old coverage.all flag — include now does that job, and passing all is a type error.)
  • exclude omits the Paraglide output. src/lib/paraglide/ is generated by the Paraglide Vite plugin and is gitignored; counting it added ~3800 machine-written statements — a third of the total — and dragged the figure well below what the hand-written source actually is. src/lib/types/** (type-only) and src/routes/** (shells covered by the boot tests) are excluded for the same "not meaningfully coverable" reason.
thresholds is a ratchet, not a target

It holds the measured numbers as of the last improvement. Raise it when coverage goes up; never lower it to turn a red run green. CI runs coverage on the Linux leg only, and it blocks — a PR that falls below the ratchet has removed coverage that used to exist.

Smoke test

npm run build && npm run test:smoke

The unit suite mounts components against source, so nothing in it ever loads what the build emits. An adapter-static misconfiguration, a defaults/ file the Vite plugin stopped copying, an asset-URL regression, or a worker that fails to resolve once bundled will all ship a blank page with the whole suite green.

scripts/smoke-dist.js serves dist/ over real HTTP — including the SPA fallback that .htaccess provides — boots it in Playwright chromium, and fails on a console error, an uncaught exception, a 4xx, a graph that never draws, or a ?share= link that lands no curve.

Two things to keep in mind when extending it:

  • A 200 does not prove a file exists. The SPA fallback answers anything not on disk with index.html, on the real Apache host as much as here, so asset checks compare the body against the HTML shell rather than trusting the status.
  • It runs in the build CI job, after npm run build, because it needs the built output.

Continuous integration

.github/workflows/ci.yml runs on every PR and every push to main: lintchecktest on both ubuntu-latest and windows-latest, plus a build of the app, the CDN distribution and the docs site on Linux.

The Windows leg is not redundant. Git for Windows sets core.autocrlf=true in its system config, so before .gitattributes pinned every text file to eol=lf, the same commit was Prettier-clean on macOS and dirty on Windows. The Working tree must be clean step guards against that regressing: it fails if a checkout plus install leaves anything modified, or if CRLF ever reaches the index.

npm run lint must exit 0. ESLint warnings (mostly device-peq protocol constants kept for reference) do not fail the build. Rule overrides live in eslint.config.js and each carries a comment explaining why — notably no-useless-assignment is off for *.svelte because it misreads $bindable() prop defaults, and no-explicit-any is off under docs/ because the config migration tool parses arbitrary operator-authored config.