Guide · @staticflux/testkit

Testing with @staticflux/testkit

DI makes everything testable: no global singletons to patch, every browser API behind an injectable port, and telemetry you can assert on.

Test a unit

packages/testkit/AGENTS.md
const t = createTestInjector({
  units: [docsUnit, dataClientUnit, epochSourceUnit, pauseControlUnit],
  overrides: [mockTransportUnit((m) => m.route(DocGet, ({ id }) => ({ id, title: 'Draft' })))],
});
const docs = await t.get(DocsToken);
expect((await docs.load('42')).ok).toBe(true);
expect(t.spans({ name: 'data/request', attrs: { endpoint: 'docs/get' } })).toHaveLength(1);
t.expectChildSpan({ name: 'di.resolve', attrs: { unit: 'docs/unit' } }, { name: 'di.resolve', attrs: { unit: 'data/client-unit' } });
t.expectNoViolations();
await t.dispose();

Overrides bind at experiment precedence, so they beat app bindings and variants; provideValue(Token, fake) for plain values. Telemetry defaults to debug so verbose DI spans are visible. Assert span structure and attributes, never durations.

Test a feature: boot, routing, session

TypeScript
const t = await bootTestApp({
  units: [...runtimeUnits, ...studioUnits],
  routes: [home, doc],
  initialUrl: '/doc/42',
  session: { initial: { user: 'ada', roles: ['editor'], plan: 'pro' } },
  time: true,                              // fake time: frames, back-offs, session expiry
});
expect(t.router?.current?.route).toBe(doc);
await t.session?.switchAccount('bob');     // principalChanged + requiresEpochRefresh
await t.time?.advance(1_000);              // timers only elapse here
t.expectChildSpan('app.boot', 'boot.critical');
await t.shutdown();

Fake time, fake platform, fake everything

  • createFakeClock, createFakeFrames, createFakeIdle, createFakeTime() (with schedulerEnv for the runtime frame scheduler) — flush(), advance(ms), runAll() with runaway detection.
  • createFakeSession over the real session store: login, logout, expire, switchAccount, revoke; multi-tab with a shared createMemoryChannelHub().
  • Re-exported fakes from their packages: mock transport, memory / fake IndexedDB / OPFS / cookie stores, fake platform env, in-memory worker port pairs (freeze, thaw, crash), memory histories.
  • Components: @staticflux/ui/testing (mountTestRoot, manualVisibility, settle) with a test injector.

Contract suites

TypeScript
runTransportContract('fetch', (fixture) => createFetchTransport({ baseUrl: '/api', fetch: fakeFetch(fixture) }), { describe, it });
runHistoryContract('router memory', (url) => createRouterMemoryHistory({ initial: url }), { describe, it }, { level: 'adapter' });
runStoreContract('memory', () => createMemoryStore(), { describe, it });

Every implementation of a contract runs the same shared suite; a new adapter is not done until it passes. Never import testkit from app or package source — only from *.test.ts.

TypeScript
for (const medium of availablePipeMedia()) runDeltaStreamContract(medium, { describe, it });   // local · shared · message
runRasterBackendContract('memory', () => ({ backend: createMemoryRasterBackend({ tileSize: 64 }), scene, tolerance: 0 }), { describe, it });
runTextInputContract('hidden textarea', subject, { describe, it });

The same holds for the newer ports: pipes on every medium and link transport, runDocumentCrdtContract (N-peer convergence), runDocumentRepoContract, runUndoServiceContract, runBlobStoreContract, runSpecRuntimeContract, runThemeContract, and runSequencingOracleContract for a whole editor ↔ render pipeline in every topology.

Shell
npx vitest run packages/router     # one package
npx vitest run apps/studio         # boots the whole example app per test (happy-dom)
pnpm test                          # everything

AGENTS.md on GitHub: @staticflux/testkit · @staticflux/ui