createFakeScheduler
Стабильный · с 0.1.0
Независящий от тест-раннера виртуальный clock. После install() globalThis.setTimeout и clearTimeout подменяются, так что ни один реальный таймер не выстреливает — вместо этого advance(ms) гоняет каждый обратный вызов, который становится готов в окне. Полезно для тестов цепочек actions.debounce / throttle / defer без флака await new Promise(setTimeout, …).
Зависимости от vi.useFakeTimers() Vitest’а нет — работает одинаково в чистом node:test, Jest и обычном Vitest.
import { createFakeScheduler } from '@triggery/testing';
Сигнатура
Заголовок раздела «Сигнатура»function createFakeScheduler(): FakeScheduler;
interface FakeScheduler {
install(): void;
uninstall(): void;
now(): number;
advance(ms: number): Promise<void>;
flushAll(): Promise<void>;
pending(): number;
}| Метод | Описание |
|---|---|
install() | Подменить globalThis.setTimeout / clearTimeout на фейковый контроллер. Идемпотентно. |
uninstall() | Восстановить оригиналы, очистить ожидающие таймеры, сбросить виртуальные часы в 0. Безопасно звать многократно. |
now() | Текущее значение виртуальных часов в мс с момента install. |
advance(ms) | Сдвинуть часы вперёд на ms и прогнать каждый таймер, ставший готовым в этом окне. После — сливает микротаски. |
flushAll() | Прогнать каждый ожидающий таймер независимо от запланированного времени. Полезно для “забить на часы, посмотреть, что в итоге произойдёт”. |
pending() | Сколько таймеров ещё ожидает. |
Примеры
Заголовок раздела «Примеры»Debounce сливается после ожидания
Заголовок раздела «Debounce сливается после ожидания»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
Заголовок раздела «Окно throttle»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. ();
}
});Опрос часов
Заголовок раздела «Опрос часов»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. ();Замечания
Заголовок раздела «Замечания»См. также
Заголовок раздела «См. также» createTestRuntime Парься с тестовым рантаймом для тестов триггеров.
flushMicrotasks Слить микротаски (без перехвата таймеров).
createScheduler Настоящий планировщик рантайма — то, что мы подменяем.
Руководство по debounce и throttle Когда использовать debounce / throttle / defer в обработчиках.