This document covers FlowStar's test setup, how to run each suite, and — most importantly — how Playwright visual snapshot baselines are managed.
| Suite | Runner | Command |
|---|---|---|
| Unit tests | Vitest | npm test |
| E2E / visual | Playwright | npm run test:e2e |
| Contract tests | Rust / cargo test |
cd contracts && cargo test |
npm test # single run
npm run test:watch # watch modeTests live in lib/__tests__/ and cover unlock math, formatters, and utility
functions.
npm test # single run
npm run test:watch # watch mode
### Where tests live
All unit tests must be placed under `__tests__/` at the repo root, following
the naming convention `**/*.{test,spec}.{ts,tsx}`. Files placed elsewhere
(e.g. alongside source files under `components/` or `lib/`) are **not
collected by Vitest** and will never run.
tests/ lib/ unlock-math.test.ts formatters.test.ts hooks/ use-stream.test.ts
### Runner: Vitest, not Jest
The project uses [Vitest](https://vitest.dev/). Use `vi` for all mocking and
spying — **not** `jest`:
```ts
import { describe, it, expect, vi } from "vitest";
// ✅ correct
const spy = vi.fn();
vi.spyOn(module, "method");
vi.useFakeTimers();
// ❌ wrong — jest is not installed
const spy = jest.fn();
npx vitest run __tests__/lib/unlock-math.test.tsStart the dev server in one terminal, then run Playwright in another:
# Terminal 1
npm run dev
# Terminal 2
npm run test:e2eOr build and serve in mock mode (no contract needed), which is what CI does:
npm run build # no contract env vars needed — falls back to mock mode
npm run start &
npm run test:e2ePlaywright uses pixel-diff screenshots to catch unintended UI regressions.
Baseline PNG files live in e2e/__screenshots__/. When a test runs, Playwright
compares the current screenshot against the committed baseline. If no baseline
exists, the test fails (it has nothing to compare against).
Baselines must be generated on Linux in CI, not on your local machine. This
is because Playwright renders fonts and anti-aliasing slightly differently
across operating systems. A baseline generated on Windows or macOS will produce
false-positive diff failures when CI runs on ubuntu-latest, causing tests to
fail for everyone else even when no real UI change was made.
Do not commit snapshot PNGs generated on Windows or macOS.
Regenerate baselines when you have intentionally changed the UI — for example:
- Updated a component's layout, colours, or typography
- Added a new visual test that has no existing baseline
- Changed global CSS that affects multiple pages
If you only changed non-visual logic (math, API calls, state), you should not need to update baselines.
Baselines are regenerated via the Update Playwright Snapshots GitHub Actions workflow. To trigger it:
- Go to the repository on GitHub.
- Open Actions → Update Playwright Snapshots.
- Click Run workflow.
- In the Branch to update snapshots on field, enter the branch you are working on (leave blank to use the default branch).
- Click Run workflow to confirm.
The workflow will:
- Check out your branch on
ubuntu-latest. - Install dependencies and Playwright browsers.
- Build the app in mock mode.
- Run
npx playwright test --update-snapshotsto regenerate all baseline PNGs. - Commit any new or changed files under
e2e/__screenshots__/back to your branch with the messagetest: update playwright visual snapshots [skip ci].
After the workflow completes, pull the changes locally:
git pullCI will now use the freshly committed Linux baselines for comparison.
The CI e2e job also runs with --update-snapshots. This means:
- If baseline PNGs are already committed, Playwright diffs against them normally.
- If a baseline is missing (e.g. you added a new visual test but haven't run the workflow yet), CI generates it in-place and commits it. The test does not fail on first run; it establishes the baseline so the next run can diff.
This means CI will never go red solely because a baseline PNG is missing — but it also means any newly generated baseline in CI reflects Linux rendering, which is correct and intentional.
| Situation | What to do |
|---|---|
| Intentional UI change on your branch | Trigger Update Playwright Snapshots workflow on your branch |
| New visual test with no baseline | Trigger the workflow, or let CI generate it on first run |
| Baseline generated on Windows / macOS | Do not commit it — trigger the workflow instead |
| Flaky diff on a pixel you didn't touch | Check if your branch has locally generated PNGs committed; replace via workflow |
cd contracts
cargo testTo run a single test:
cargo test test_withdraw_after_cliff44 tests cover the full stream lifecycle, authorization, cliff edge cases,
overdraw protection, integer math, and self-streams. See
CONTRACT_TESTING.md for details.
FlowStar has three separate test suites: Vitest (unit/component), Playwright
(end-to-end), and cargo test (the Soroban contract). This guide covers how
to run each, how to make sure a new test file is actually picked up, and the
coverage-threshold policy.
npm run test # run once
npm run test:watch # watch mode
npm run test:coverage # run once with coverage reportConfig: vitest.config.ts. Environment is jsdom.
Vitest is configured with:
include: ["__tests__/**/*.{test,spec}.{ts,tsx}"]A test file anywhere outside __tests__/ is silently excluded — it will
not fail, warn, or show up in coverage; it simply never runs. This actually
happened: a wallet-adapter test file sat outside __tests__/ for months
without anyone noticing, because there was no error to notice — the suite
just passed without it.
When adding a new test file:
- Put it under
__tests__/(subdirectories are fine, e.g.__tests__/hooks/use-webhooks.test.ts), matching the.test.ts/.test.tsx/.spec.ts/.spec.tsxnaming pattern. - Run
npm run testand confirm your new test's name actually appears in the output — don't just check that the run exits green. A file outside the glob produces the same green exit code as one that ran and passed.
vitest.config.ts's coverage.thresholds are a ratchet, not a target:
thresholds: {
statements: 13,
branches: 60,
functions: 40,
lines: 13,
},These were set from the actual coverage measured on 2026-07-18 and are a
floor test:coverage enforces — a PR that drops any of these numbers fails.
The policy:
- Never lower a threshold to make a failing PR pass. If your change legitimately reduces coverage (e.g. adding a large new untested module), add tests for the new code instead.
- Do raise a threshold when you add meaningful test coverage — bump the relevant number(s) up to (or just below) your new measured percentage in the same PR, so the improvement is locked in and can't silently regress later.
- Coverage is measured over
hooks/**,lib/**,components/**, andutils/**(seecoverage.include) — contract code and Next.jsapp/routes are not included in this threshold.
npm run test:e2e # run once (all configured browsers/devices)
npm run test:e2e:update # run once, updating snapshotsConfig: playwright.config.ts. Test files live under ./e2e. Runs against
Chrome, Firefox, Safari (WebKit), and mobile viewports (Pixel 5, iPhone 13) —
see the projects array in the config for the full list.
cd contracts/streaming
cargo testRuns the Soroban contract's Rust unit/integration tests. See
CONTRIBUTING.md's Rust section for cargo fmt/cargo clippy alongside
this.
| Suite | Command | Config | Test file location |
|---|---|---|---|
| Vitest | npm run test |
vitest.config.ts |
__tests__/**/*.{test,spec}.{ts,tsx} — outside this, silently excluded |
| Playwright | npm run test:e2e |
playwright.config.ts |
e2e/ |
| Contract | cargo test (from contracts/streaming) |
contracts/streaming/Cargo.toml |
alongside the contract source, standard Rust #[cfg(test)] |