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
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
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()(withschedulerEnvfor the runtime frame scheduler) —flush(),advance(ms),runAll()with runaway detection.createFakeSessionover the real session store:login,logout,expire,switchAccount,revoke; multi-tab with a sharedcreateMemoryChannelHub().- 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
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.
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.
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