Test worlds
A session inside one daemon shares the daemon’s clock, Rules and Functions. For parallel tests that change any of them, the fireemu/testing helper starts independent worlds, each with its own daemon, ports, clock and data, even with the same project ID. A world pins its clock start, so its clock stands still and a test moves it by hand (see Time and scheduled functions).
Start a world
Section titled “Start a world”import { withTestWorld } from 'fireemu/testing';
await withTestWorld( { projectId: 'demo-parallel', clockStart: '2026-01-31T23:59:00Z', functionsSource: './functions', services: ['auth', 'firestore', 'storage', 'functions'], }, async (world) => { await world.clock.advance(59999); // milliseconds: one millisecond before midnight await world.run(process.execPath, ['./trial-before-expiry.mjs']); await world.clock.advance(1); await world.run(process.execPath, ['./trial-expired.mjs']); },);Separate worlds isolate their clocks, Rules, Functions and service state. The fireemu/control client also works in fireemu exec commands and shares URL resolution and authentication with world.control(), so callers do not need to concatenate control URLs (see the test worlds guide).
world.runlaunches a command with the world’s project and emulator hosts.world.reset()starts the world again from its creation snapshot.world.dispose()stops the world and removes its files.withTestWorlddisposes it for you, also when the callback throws.
Inspect a world in the UI
Section titled “Inspect a world in the UI”To inspect the same world in the debug UI, pass ui: true to createTestWorld() and open world.uiUrl. The UI is disabled by default. Each enabled world receives its own loopback port and shares its live Firestore, Auth, Storage and Functions state. Keep the world alive while inspecting it; dispose() also stops its UI. After reset(), read world.uiUrl again and reopen the page. See test worlds for an interactive example.
Export and import
Section titled “Export and import”Enable hub: true to export a test world and import its data into a fresh one; see Export independent test worlds.
Snapshots and timeouts
Section titled “Snapshots and timeouts”snapshotTimeoutMs bounds the initial input snapshot and first generation preparation together, and each reset preparation separately. Its default is 60,000 ms; it accepts integers from 1 through 2,147,483,647. startupTimeoutMs separately defaults to 60,000 ms for daemon readiness after preparation.
- Timeout or cancellation stops file copying, waits for active copies to settle and cleans up partial snapshots, including read-only copied directories.
- Configuration files must resolve to regular files. Copied trees can contain directories, regular files and supported dependency links, but other filesystem types are refused.
- If preparation times out, narrow unused dependencies or raise
snapshotTimeoutMs; raising the readiness timeout does not help. - Reset retires the previous generation first, so a failed preparation does not keep the old daemon running.
Dependency snapshots and global NODE_PATH
Section titled “Dependency snapshots and global NODE_PATH”Functions worlds snapshot local sources and dependencies so caller edits do not affect reset. Ambient NODE_PATH is cleared by default (since v0.27.0), while ordinary parent environment variables stay inherited. Local sources and ancestor node_modules are still snapshotted.
- To retain globals, pass
env: { ...process.env }, or supply a narrowedenv.NODE_PATHwith only the directories you need. An environment object containingNODE_PATHis an opt-in even when constructed with a spread. nodePathExcludesomits exact unused package names, including scoped names, before following their links. Required dependency failures are still errors.- Before v0.27.0, worlds also copied inherited
NODE_PATHdirectories, including linked global development repositories, so an unused linked repository could slow startup or fail copying before tests began. On those versions, passenv: { ...process.env, NODE_PATH: '' }for local-only dependencies.
See dependency snapshot limits.
Wait for runnable work
Section titled “Wait for runnable work”POST /v1/sessions/default:awaitIdle accepts { "scope": "runnable" } (since v0.26.0). In a world:
await world.control('POST', 'sessions/default:awaitIdle', { scope: 'runnable' });This excludes future task schedules and future retry backoff from the wait. The default scope still waits for all work. Due tasks blocked by rate or concurrency limits and active HTTP requests remain pending. Advance the applicable clock or wait for the active work; diagnostics identify the waiting phase and deadline. This does not wait for arbitrary user-created JavaScript timers. See idle scopes and task clocks.