createFakeScheduler
Stable · since 0.1.0
A test-runner agnostic virtual clock. After install(), globalThis.setTimeout and clearTimeout are replaced so no real timers fire — instead, advance(ms) runs every callback that becomes due within the window. Useful for testing actions.debounce / throttle / defer chains without await new Promise(setTimeout, …) flakes.
No dependency on Vitest’s vi.useFakeTimers() — works in plain node:test, Jest, and bare Vitest alike.
Import
Section titled “Import”import { createFakeScheduler } from '@triggery/testing';
Signature
Section titled “Signature”function createFakeScheduler(): FakeScheduler;
interface FakeScheduler {
install(): void;
uninstall(): void;
now(): number;
advance(ms: number): Promise<void>;
flushAll(): Promise<void>;
pending(): number;
}Methods
Section titled “Methods”| Method | Description |
|---|---|
install() | Replace globalThis.setTimeout / clearTimeout with the fake controller. Idempotent. |
uninstall() | Restore the originals, clear pending timers, reset the virtual clock to 0. Safe to call multiple times. |
now() | Current virtual clock value, in ms since install. |
advance(ms) | Move the clock forward by ms and run every timer that becomes due in that window. Drains microtasks after. |
flushAll() | Run every pending timer regardless of scheduled time. Useful for “give up on the clock, see what eventually happens”. |
pending() | Number of timers still pending. |
Examples
Section titled “Examples”Debounce drains after the wait
Section titled “Debounce drains after the wait”import { createTrigger function createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>Create a trigger and register it in a runtime (the default runtime if none is passed). } from '@triggery/core';
import { createFakeScheduler function createFakeScheduler(): FakeScheduler , createTestRuntime function createTestRuntime(options?: TestRuntimeOptions): TestRuntime } from '@triggery/testing';
import { afterEach function afterEach<ExtraContext = object>(fn: AfterEachListener<ExtraContext>, timeout?: number): voidRegisters a callback function to be executed after each test within the current suite has completed.
This hook is useful for scenarios where you need to clean up or reset the test environment after each test runs, such as deleting temporary files, clearing test-specific database entries, or resetting mocked functions.
**Note:** The `afterEach` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file. , beforeEach function beforeEach<ExtraContext = object>(fn: BeforeEachListener<ExtraContext>, timeout?: number): voidRegisters a callback function to be executed before each test within the current suite.
This hook is useful for scenarios where you need to reset or reinitialize the test environment before each test runs, such as resetting database states, clearing caches, or reinitializing variables.
**Note:** The `beforeEach` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file. , expect const expect: ExpectStatic , it const it: TestAPIDefines a test case with a given name and test function. The test function can optionally be configured with test options. , vi const vi: VitestUtils } from 'vitest';
const ft const ft: FakeScheduler = createFakeScheduler function createFakeScheduler(): FakeScheduler ();
beforeEach beforeEach<object>(fn: BeforeEachListener<object>, timeout?: number): voidRegisters a callback function to be executed before each test within the current suite.
This hook is useful for scenarios where you need to reset or reinitialize the test environment before each test runs, such as resetting database states, clearing caches, or reinitializing variables.
**Note:** The `beforeEach` hooks are executed in the order they are defined one after another. You can configure this by changing the `sequence.hooks` option in the config file. (() => ft const ft: FakeScheduler .install FakeScheduler.install(): voidReplace globalThis.setTimeout / clearTimeout with the fake controller. ());
afterEach afterEach<object>(fn: AfterEachListener<object>, timeout?: number): voidRegisters a callback function to be executed after each test within the current suite has completed.
This hook is useful for scenarios where you need to clean up or reset the test environment after each test runs, such as deleting temporary files, clearing test-specific database entries, or resetting mocked functions.
**Note:** The `afterEach` hooks are running in reverse order of their registration. You can configure this by changing the `sequence.hooks` option in the config file. (() => ft const ft: FakeScheduler .uninstall FakeScheduler.uninstall(): voidRestore the real timer functions. Safe to call multiple times. ());
it it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)Defines a test case with a given name and test function. The test function can optionally be configured with test options. ('debounces keystrokes', async () => {
const rt const rt: TestRuntime = createTestRuntime function createTestRuntime(options?: TestRuntimeOptions): TestRuntime ();
const t const t: Trigger<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>
= createTrigger createTrigger<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>(config: CreateTriggerConfig<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>, runtime?: Runtime): Trigger<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>
Create a trigger and register it in a runtime (the default runtime if none is passed). <{
events events: {
keystroke: string;
}
: { keystroke keystroke: string : string };
actions actions: {
search: string;
}
: { search search: string : string };
}>(
{
id id: string : 'demo',
events events: readonly "keystroke"[] : ['keystroke'],
handler handler: TriggerHandler<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}, never>
({ event event: {
readonly name: "keystroke";
readonly payload: string;
}
, actions actions: ActionsCtx<{
search: string;
}>
}) {
actions actions: ActionsCtx<{
search: string;
}>
.debounce function debounce(ms: number): ActionsCtx<{
search: string;
}>
(300).search search?: ((payload: string) => void) | undefined ?.(event event: {
readonly name: "keystroke";
readonly payload: string;
}
.payload payload: string );
},
},
rt const rt: TestRuntime ,
);
const search const search: Mock<Procedure> = vi const vi: VitestUtils .fn VitestUtils.fn: <Procedure>(originalImplementation?: Procedure | undefined) => Mock<Procedure>Creates a spy on a function, though can be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with [methods](https://vitest.dev/api/mock).
If no function is given, mock will return `undefined`, when invoked. ();
rt const rt: TestRuntime .mockAction mockAction<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}, "search">(trigger: Trigger<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>, name: "search", handler: (payload: string) => void): RegistrationToken
Register an action handler — typically a `vi.fn()`. (t const t: Trigger<{
events: {
keystroke: string;
};
actions: {
search: string;
};
}>
, 'search', search const search: Mock<Procedure> );
rt const rt: TestRuntime .fireSync function fireSync(eventName: string, payload?: unknown): voidRun dispatch synchronously (for tests and benchmarks). ('keystroke', 'a');
rt const rt: TestRuntime .fireSync function fireSync(eventName: string, payload?: unknown): voidRun dispatch synchronously (for tests and benchmarks). ('keystroke', 'ab');
rt const rt: TestRuntime .fireSync function fireSync(eventName: string, payload?: unknown): voidRun dispatch synchronously (for tests and benchmarks). ('keystroke', 'abc');
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (299);
expect expect<Mock<Procedure>>(actual: Mock<Procedure>, message?: string): Assertion<Mock<Procedure>> (+1 overload) (search const search: Mock<Procedure> ).not not: Assertion<Mock<Procedure>> .toHaveBeenCalled JestAssertion<Mock<Procedure>>.toHaveBeenCalled: () => voidEnsures that a mock function is called.
Also under the alias `expect.toBeCalled`. ();
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (1);
expect expect<Mock<Procedure>>(actual: Mock<Procedure>, message?: string): Assertion<Mock<Procedure>> (+1 overload) (search const search: Mock<Procedure> ).toHaveBeenCalledExactlyOnceWith Assertion<Mock<Procedure>>.toHaveBeenCalledExactlyOnceWith: <[string]>(args_0: string) => voidEnsure that a mock function is called with specific arguments and called
exactly once. ('abc');
});Throttle window
Section titled “Throttle window”import { createFakeScheduler function createFakeScheduler(): FakeScheduler } from '@triggery/testing';
import { expect const expect: ExpectStatic , it const it: TestAPIDefines a test case with a given name and test function. The test function can optionally be configured with test options. } from 'vitest';
it it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)Defines a test case with a given name and test function. The test function can optionally be configured with test options. ('runs at most one timer per window', async () => {
const ft const ft: FakeScheduler = createFakeScheduler function createFakeScheduler(): FakeScheduler ();
ft const ft: FakeScheduler .install FakeScheduler.install(): voidReplace globalThis.setTimeout / clearTimeout with the fake controller. ();
try {
let calls let calls: number = 0;
setTimeout function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) (() => calls let calls: number ++, 100);
setTimeout function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) (() => calls let calls: number ++, 200);
setTimeout function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) (() => calls let calls: number ++, 300);
expect expect<number>(actual: number, message?: string): Assertion<number> (+1 overload) (ft const ft: FakeScheduler .pending FakeScheduler.pending(): numberNumber of timers still pending. ()).toBe JestAssertion<number>.toBe: <number>(expected: number) => voidChecks that a value is what you expect. It calls `Object.is` to compare values.
Don't use `toBe` with floating-point numbers. (3);
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (150);
expect expect<number>(actual: number, message?: string): Assertion<number> (+1 overload) (calls let calls: number ).toBe JestAssertion<number>.toBe: <number>(expected: number) => voidChecks that a value is what you expect. It calls `Object.is` to compare values.
Don't use `toBe` with floating-point numbers. (1);
await ft const ft: FakeScheduler .flushAll FakeScheduler.flushAll(): Promise<void>Run every pending timer (regardless of its scheduled time). Useful for
"give up on the clock, just see what eventually happens". ();
expect expect<number>(actual: number, message?: string): Assertion<number> (+1 overload) (calls let calls: number ).toBe JestAssertion<number>.toBe: <number>(expected: number) => voidChecks that a value is what you expect. It calls `Object.is` to compare values.
Don't use `toBe` with floating-point numbers. (3);
expect expect<number>(actual: number, message?: string): Assertion<number> (+1 overload) (ft const ft: FakeScheduler .pending FakeScheduler.pending(): numberNumber of timers still pending. ()).toBe JestAssertion<number>.toBe: <number>(expected: number) => voidChecks that a value is what you expect. It calls `Object.is` to compare values.
Don't use `toBe` with floating-point numbers. (0);
} finally {
ft const ft: FakeScheduler .uninstall FakeScheduler.uninstall(): voidRestore the real timer functions. Safe to call multiple times. ();
}
});import { createTrigger function createTrigger<S extends TriggerSchema>(config: CreateTriggerConfig<S>, runtime?: Runtime): Trigger<S>Create a trigger and register it in a runtime (the default runtime if none is passed). } from '@triggery/core';
import { createFakeScheduler function createFakeScheduler(): FakeScheduler , createTestRuntime function createTestRuntime(options?: TestRuntimeOptions): TestRuntime } from '@triggery/testing';
import { expect const expect: ExpectStatic , it const it: TestAPIDefines a test case with a given name and test function. The test function can optionally be configured with test options. , vi const vi: VitestUtils } from 'vitest';
it it<object>(name: string | Function, fn?: TestFunction<object> | undefined, options?: number): void (+1 overload)Defines a test case with a given name and test function. The test function can optionally be configured with test options. ('defers an action by a fixed delay', async () => {
const ft const ft: FakeScheduler = createFakeScheduler function createFakeScheduler(): FakeScheduler ();
ft const ft: FakeScheduler .install FakeScheduler.install(): voidReplace globalThis.setTimeout / clearTimeout with the fake controller. ();
try {
const rt const rt: TestRuntime = createTestRuntime function createTestRuntime(options?: TestRuntimeOptions): TestRuntime ();
const t const t: Trigger<{
events: {
ping: void;
};
actions: {
log: void;
};
}>
= createTrigger createTrigger<{
events: {
ping: void;
};
actions: {
log: void;
};
}>(config: CreateTriggerConfig<{
events: {
ping: void;
};
actions: {
log: void;
};
}>, runtime?: Runtime): Trigger<{
events: {
ping: void;
};
actions: {
log: void;
};
}>
Create a trigger and register it in a runtime (the default runtime if none is passed). <{
events events: {
ping: void;
}
: { ping ping: void : void };
actions actions: {
log: void;
}
: { log log: void : void };
}>(
{
id id: string : 'demo',
events events: readonly "ping"[] : ['ping'],
handler handler: TriggerHandler<{
events: {
ping: void;
};
actions: {
log: void;
};
}, never>
({ actions actions: ActionsCtx<{
log: void;
}>
}) {
actions actions: ActionsCtx<{
log: void;
}>
.defer function defer(ms: number): ActionsCtx<{
log: void;
}>
(500).log log?: (() => void) | undefined ?.();
},
},
rt const rt: TestRuntime ,
);
const log const log: Mock<Procedure> = vi const vi: VitestUtils .fn VitestUtils.fn: <Procedure>(originalImplementation?: Procedure | undefined) => Mock<Procedure>Creates a spy on a function, though can be initiated without one. Every time a function is invoked, it stores its call arguments, returns, and instances. Also, you can manipulate its behavior with [methods](https://vitest.dev/api/mock).
If no function is given, mock will return `undefined`, when invoked. ();
rt const rt: TestRuntime .mockAction mockAction<{
events: {
ping: void;
};
actions: {
log: void;
};
}, "log">(trigger: Trigger<{
events: {
ping: void;
};
actions: {
log: void;
};
}>, name: "log", handler: () => void): RegistrationToken
Register an action handler — typically a `vi.fn()`. (t const t: Trigger<{
events: {
ping: void;
};
actions: {
log: void;
};
}>
, 'log', log const log: Mock<Procedure> );
rt const rt: TestRuntime .fireSync function fireSync(eventName: string, payload?: unknown): voidRun dispatch synchronously (for tests and benchmarks). ('ping');
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (499);
expect expect<Mock<Procedure>>(actual: Mock<Procedure>, message?: string): Assertion<Mock<Procedure>> (+1 overload) (log const log: Mock<Procedure> ).not not: Assertion<Mock<Procedure>> .toHaveBeenCalled JestAssertion<Mock<Procedure>>.toHaveBeenCalled: () => voidEnsures that a mock function is called.
Also under the alias `expect.toBeCalled`. ();
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (1);
expect expect<Mock<Procedure>>(actual: Mock<Procedure>, message?: string): Assertion<Mock<Procedure>> (+1 overload) (log const log: Mock<Procedure> ).toHaveBeenCalledOnce Assertion<Mock<Procedure>>.toHaveBeenCalledOnce: () => voidAsserts that a mock function was called exactly once. ();
} finally {
ft const ft: FakeScheduler .uninstall FakeScheduler.uninstall(): voidRestore the real timer functions. Safe to call multiple times. ();
}
});Clock interrogation
Section titled “Clock interrogation”import { createFakeScheduler function createFakeScheduler(): FakeScheduler } from '@triggery/testing';
const ft const ft: FakeScheduler = createFakeScheduler function createFakeScheduler(): FakeScheduler ();
ft const ft: FakeScheduler .install FakeScheduler.install(): voidReplace globalThis.setTimeout / clearTimeout with the fake controller. ();
setTimeout function setTimeout<[]>(callback: () => void, delay?: number): NodeJS.Timeout (+1 overload)[MDN Reference](https://developer.mozilla.org/docs/Web/API/Window/setTimeout) (() => {}, 250);
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (ft const ft: FakeScheduler .now FakeScheduler.now(): numberCurrent virtual clock value, in ms since install. ()); // 0
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (ft const ft: FakeScheduler .pending FakeScheduler.pending(): numberNumber of timers still pending. ()); // 1
await ft const ft: FakeScheduler .advance FakeScheduler.advance(ms: number): Promise<void>Advance the virtual clock by `ms` and run every timer that becomes due in
that window. Returns a promise that resolves after pending microtasks are
drained, so callers can `await ft.advance(N)` and then assert. (250);
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (ft const ft: FakeScheduler .now FakeScheduler.now(): numberCurrent virtual clock value, in ms since install. ()); // 250
console var console: Console .log Console.log(...data: any[]): voidThe **`console.log()`** static method outputs a message to the console.
[MDN Reference](https://developer.mozilla.org/docs/Web/API/console/log_static) (ft const ft: FakeScheduler .pending FakeScheduler.pending(): numberNumber of timers still pending. ()); // 0
ft const ft: FakeScheduler .uninstall FakeScheduler.uninstall(): voidRestore the real timer functions. Safe to call multiple times. ();Related
Section titled “Related” createTestRuntime Pair this with the test runtime for trigger tests.
flushMicrotasks Drain microtasks (no timer interception).
createScheduler The runtime's real scheduler — what we're stand-in for.
Debouncing & throttling guide When to use debounce / throttle / defer in handlers.