@triggery/testing 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md ADDED
@@ -0,0 +1,9 @@
1
+ # @triggery/testing
2
+
3
+ ## 0.1.0
4
+
5
+ First public preview release.
6
+
7
+ Testing utilities for Triggery — isolated runtime, mock conditions/actions, fake scheduler
8
+
9
+ See the [repository-level CHANGELOG](../../CHANGELOG.md#010--2026-05-16) for the full set of packages and the umbrella feature list. Future entries on this file are appended automatically by changesets.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Aleksey Skhomenko
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,20 @@
1
+ # @triggery/testing
2
+
3
+ Testing utilities for [Triggery](https://github.com/triggeryjs/triggery).
4
+
5
+ Provides:
6
+
7
+ - `createTestRuntime({ triggers })` — isolated runtime for tests.
8
+ - `mockCondition(name, value)` / `mockAction(name, fn)` — replace ports without rendering React.
9
+ - `fakeScheduler` — control time (advance, flush).
10
+ - Vitest and Jest adapters.
11
+
12
+ ## Install
13
+
14
+ ```bash
15
+ pnpm add -D @triggery/testing
16
+ ```
17
+
18
+ ## License
19
+
20
+ MIT © Aleksey Skhomenko
@@ -0,0 +1,110 @@
1
+ import { Runtime, TriggerSchema, ConditionKey, Trigger, ConditionMap, RegistrationToken, ActionKey, ActionFn, ActionMap, RuntimeOptions } from '@triggery/core';
2
+
3
+ /**
4
+ * Deterministic time controller for tests. Replaces global `setTimeout` /
5
+ * `clearTimeout` with a virtual clock — pending timers don't fire until you
6
+ * call `advance(ms)` or `flushAll()`. Useful for testing the
7
+ * `actions.debounce / throttle / defer` wrappers without `await new
8
+ * Promise(setTimeout, …)` flakes.
9
+ *
10
+ * Test-runner agnostic — no dependency on Vitest's `vi.useFakeTimers()` so it
11
+ * also works in plain `node:test`, Jest, etc.
12
+ *
13
+ * @example
14
+ * ```ts
15
+ * const ft = createFakeScheduler();
16
+ * ft.install();
17
+ * try {
18
+ * rt.fire('tick');
19
+ * await ft.advance(500); // run all timers due within 500ms
20
+ * expect(action).toHaveBeenCalledTimes(1);
21
+ * } finally {
22
+ * ft.uninstall();
23
+ * }
24
+ * ```
25
+ */
26
+ interface FakeScheduler {
27
+ /** Replace globalThis.setTimeout / clearTimeout with the fake controller. */
28
+ install(): void;
29
+ /** Restore the real timer functions. Safe to call multiple times. */
30
+ uninstall(): void;
31
+ /** Current virtual clock value, in ms since install. */
32
+ now(): number;
33
+ /**
34
+ * Advance the virtual clock by `ms` and run every timer that becomes due in
35
+ * that window. Returns a promise that resolves after pending microtasks are
36
+ * drained, so callers can `await ft.advance(N)` and then assert.
37
+ */
38
+ advance(ms: number): Promise<void>;
39
+ /**
40
+ * Run every pending timer (regardless of its scheduled time). Useful for
41
+ * "give up on the clock, just see what eventually happens".
42
+ */
43
+ flushAll(): Promise<void>;
44
+ /** Number of timers still pending. */
45
+ pending(): number;
46
+ }
47
+ declare function createFakeScheduler(): FakeScheduler;
48
+
49
+ /**
50
+ * @triggery/testing — testing utilities for Triggery.
51
+ *
52
+ * The kit lets you write trigger tests without React, mocking conditions and
53
+ * actions against an isolated runtime instead of relying on `getDefaultRuntime`.
54
+ *
55
+ * @example
56
+ * ```ts
57
+ * import { createTrigger } from '@triggery/core';
58
+ * import { createTestRuntime } from '@triggery/testing';
59
+ *
60
+ * const rt = createTestRuntime();
61
+ * const t = createTrigger<{
62
+ * events: { tick: number };
63
+ * conditions: { enabled: boolean };
64
+ * actions: { log: number };
65
+ * }>(
66
+ * {
67
+ * id: 'demo',
68
+ * events: ['tick'],
69
+ * required: ['enabled'],
70
+ * handler: ({ event, conditions, actions }) => {
71
+ * if (!conditions.enabled) return;
72
+ * actions.log?.(event.payload);
73
+ * },
74
+ * },
75
+ * rt,
76
+ * );
77
+ *
78
+ * rt.mockCondition(t, 'enabled', true);
79
+ * const log = vi.fn();
80
+ * rt.mockAction(t, 'log', log);
81
+ *
82
+ * rt.fireSync('tick', 42);
83
+ * expect(log).toHaveBeenCalledWith(42);
84
+ * ```
85
+ */
86
+
87
+ type TestRuntimeOptions = RuntimeOptions;
88
+ type TestRuntime = Runtime & {
89
+ /**
90
+ * Register a condition for a trigger.
91
+ *
92
+ * Accepts either a static value or a zero-argument getter. When the
93
+ * condition's value type is itself a zero-argument function, pass an
94
+ * explicit getter so the runtime knows which one you mean — otherwise the
95
+ * heuristic below would call your value as if it were the getter.
96
+ */
97
+ mockCondition<S extends TriggerSchema, K extends ConditionKey<S>>(trigger: Trigger<S>, name: K, valueOrGetter: ConditionMap<S>[K] | (() => ConditionMap<S>[K])): RegistrationToken;
98
+ /** Register an action handler — typically a `vi.fn()`. */
99
+ mockAction<S extends TriggerSchema, K extends ActionKey<S>>(trigger: Trigger<S>, name: K, handler: ActionFn<ActionMap<S>[K]>): RegistrationToken;
100
+ /**
101
+ * Flush pending microtasks. The default scheduler uses `queueMicrotask` —
102
+ * after `rt.fire(...)` you await `flushMicrotasks()` before asserting.
103
+ *
104
+ * `fireSync` does not need this: it runs handlers immediately.
105
+ */
106
+ flushMicrotasks(): Promise<void>;
107
+ };
108
+ declare function createTestRuntime(options?: TestRuntimeOptions): TestRuntime;
109
+
110
+ export { type FakeScheduler, type TestRuntime, type TestRuntimeOptions, createFakeScheduler, createTestRuntime };
package/dist/index.js ADDED
@@ -0,0 +1,113 @@
1
+ import { createRuntime } from '@triggery/core';
2
+
3
+ // src/index.ts
4
+
5
+ // src/fakeScheduler.ts
6
+ function createFakeScheduler() {
7
+ const timers = /* @__PURE__ */ new Map();
8
+ let now = 0;
9
+ let counter = 0;
10
+ let installed = false;
11
+ let originalSetTimeout = null;
12
+ let originalClearTimeout = null;
13
+ function install() {
14
+ if (installed) return;
15
+ installed = true;
16
+ originalSetTimeout = globalThis.setTimeout;
17
+ originalClearTimeout = globalThis.clearTimeout;
18
+ globalThis.setTimeout = ((fn, ms = 0) => {
19
+ const id = ++counter;
20
+ timers.set(id, { id, fn, runAt: now + Math.max(0, ms) });
21
+ return id;
22
+ });
23
+ globalThis.clearTimeout = ((handle) => {
24
+ if (handle == null) return;
25
+ timers.delete(handle);
26
+ });
27
+ }
28
+ function uninstall() {
29
+ if (!installed) return;
30
+ installed = false;
31
+ if (originalSetTimeout) globalThis.setTimeout = originalSetTimeout;
32
+ if (originalClearTimeout) globalThis.clearTimeout = originalClearTimeout;
33
+ originalSetTimeout = null;
34
+ originalClearTimeout = null;
35
+ timers.clear();
36
+ now = 0;
37
+ counter = 0;
38
+ }
39
+ async function advance(ms) {
40
+ if (ms < 0) throw new Error("[triggery/testing] advance(): ms must be >= 0");
41
+ const target = now + ms;
42
+ while (true) {
43
+ const due = [...timers.values()].filter((t) => t.runAt <= target);
44
+ if (due.length === 0) break;
45
+ due.sort((a, b) => a.runAt - b.runAt || a.id - b.id);
46
+ const next = due[0];
47
+ timers.delete(next.id);
48
+ now = next.runAt;
49
+ try {
50
+ next.fn();
51
+ } catch (err) {
52
+ console.error("[triggery/testing] fakeScheduler timer threw:", err);
53
+ }
54
+ }
55
+ now = target;
56
+ await Promise.resolve();
57
+ await Promise.resolve();
58
+ }
59
+ async function flushAll() {
60
+ while (timers.size > 0) {
61
+ const sorted = [...timers.values()].sort((a, b) => a.runAt - b.runAt || a.id - b.id);
62
+ const next = sorted[0];
63
+ timers.delete(next.id);
64
+ now = Math.max(now, next.runAt);
65
+ try {
66
+ next.fn();
67
+ } catch (err) {
68
+ console.error("[triggery/testing] fakeScheduler timer threw:", err);
69
+ }
70
+ }
71
+ await Promise.resolve();
72
+ await Promise.resolve();
73
+ }
74
+ return {
75
+ install,
76
+ uninstall,
77
+ now: () => now,
78
+ advance,
79
+ flushAll,
80
+ pending: () => timers.size
81
+ };
82
+ }
83
+
84
+ // src/index.ts
85
+ var isLikelyGetter = (fn) => fn.length === 0;
86
+ function createTestRuntime(options = {}) {
87
+ const runtime = createRuntime(options);
88
+ const mockCondition = (trigger, name, valueOrGetter) => {
89
+ const getter = typeof valueOrGetter === "function" && isLikelyGetter(valueOrGetter) ? valueOrGetter : () => valueOrGetter;
90
+ return runtime.registerCondition(trigger.id, name, getter);
91
+ };
92
+ const mockAction = (trigger, name, handler) => {
93
+ return runtime.registerAction(
94
+ trigger.id,
95
+ name,
96
+ handler
97
+ );
98
+ };
99
+ const flushMicrotasks = async () => {
100
+ await Promise.resolve();
101
+ await Promise.resolve();
102
+ };
103
+ return {
104
+ ...runtime,
105
+ mockCondition,
106
+ mockAction,
107
+ flushMicrotasks
108
+ };
109
+ }
110
+
111
+ export { createFakeScheduler, createTestRuntime };
112
+ //# sourceMappingURL=index.js.map
113
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/fakeScheduler.ts","../src/index.ts"],"names":[],"mappings":"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","file":"index.js","sourcesContent":["/**\n * Deterministic time controller for tests. Replaces global `setTimeout` /\n * `clearTimeout` with a virtual clock — pending timers don't fire until you\n * call `advance(ms)` or `flushAll()`. Useful for testing the\n * `actions.debounce / throttle / defer` wrappers without `await new\n * Promise(setTimeout, …)` flakes.\n *\n * Test-runner agnostic — no dependency on Vitest's `vi.useFakeTimers()` so it\n * also works in plain `node:test`, Jest, etc.\n *\n * @example\n * ```ts\n * const ft = createFakeScheduler();\n * ft.install();\n * try {\n * rt.fire('tick');\n * await ft.advance(500); // run all timers due within 500ms\n * expect(action).toHaveBeenCalledTimes(1);\n * } finally {\n * ft.uninstall();\n * }\n * ```\n */\nexport interface FakeScheduler {\n /** Replace globalThis.setTimeout / clearTimeout with the fake controller. */\n install(): void;\n /** Restore the real timer functions. Safe to call multiple times. */\n uninstall(): void;\n /** Current virtual clock value, in ms since install. */\n now(): number;\n /**\n * Advance the virtual clock by `ms` and run every timer that becomes due in\n * that window. Returns a promise that resolves after pending microtasks are\n * drained, so callers can `await ft.advance(N)` and then assert.\n */\n advance(ms: number): Promise<void>;\n /**\n * Run every pending timer (regardless of its scheduled time). Useful for\n * \"give up on the clock, just see what eventually happens\".\n */\n flushAll(): Promise<void>;\n /** Number of timers still pending. */\n pending(): number;\n}\n\ninterface PendingTimer {\n id: number;\n fn: () => void;\n runAt: number;\n}\n\ntype TimerHandle = ReturnType<typeof globalThis.setTimeout>;\n\nexport function createFakeScheduler(): FakeScheduler {\n const timers = new Map<number, PendingTimer>();\n let now = 0;\n let counter = 0;\n let installed = false;\n let originalSetTimeout: typeof globalThis.setTimeout | null = null;\n let originalClearTimeout: typeof globalThis.clearTimeout | null = null;\n\n function install(): void {\n if (installed) return;\n installed = true;\n originalSetTimeout = globalThis.setTimeout;\n originalClearTimeout = globalThis.clearTimeout;\n globalThis.setTimeout = ((fn: () => void, ms: number = 0): TimerHandle => {\n const id = ++counter;\n timers.set(id, { id, fn, runAt: now + Math.max(0, ms) });\n return id as unknown as TimerHandle;\n }) as typeof globalThis.setTimeout;\n globalThis.clearTimeout = ((handle: TimerHandle | undefined): void => {\n if (handle == null) return;\n timers.delete(handle as unknown as number);\n }) as typeof globalThis.clearTimeout;\n }\n\n function uninstall(): void {\n if (!installed) return;\n installed = false;\n if (originalSetTimeout) globalThis.setTimeout = originalSetTimeout;\n if (originalClearTimeout) globalThis.clearTimeout = originalClearTimeout;\n originalSetTimeout = null;\n originalClearTimeout = null;\n timers.clear();\n now = 0;\n counter = 0;\n }\n\n async function advance(ms: number): Promise<void> {\n if (ms < 0) throw new Error('[triggery/testing] advance(): ms must be >= 0');\n const target = now + ms;\n // Drain in-order across the advance; new timers scheduled during a callback\n // count if their runAt falls within `target`.\n while (true) {\n const due = [...timers.values()].filter((t) => t.runAt <= target);\n if (due.length === 0) break;\n due.sort((a, b) => a.runAt - b.runAt || a.id - b.id);\n const next = due[0] as PendingTimer;\n timers.delete(next.id);\n now = next.runAt;\n try {\n next.fn();\n } catch (err) {\n // eslint-disable-next-line no-console -- surface to test output\n console.error('[triggery/testing] fakeScheduler timer threw:', err);\n }\n }\n now = target;\n // Drain microtasks twice — handlers may queue follow-ups.\n await Promise.resolve();\n await Promise.resolve();\n }\n\n async function flushAll(): Promise<void> {\n while (timers.size > 0) {\n const sorted = [...timers.values()].sort((a, b) => a.runAt - b.runAt || a.id - b.id);\n const next = sorted[0] as PendingTimer;\n timers.delete(next.id);\n now = Math.max(now, next.runAt);\n try {\n next.fn();\n } catch (err) {\n // eslint-disable-next-line no-console -- surface to test output\n console.error('[triggery/testing] fakeScheduler timer threw:', err);\n }\n }\n await Promise.resolve();\n await Promise.resolve();\n }\n\n return {\n install,\n uninstall,\n now: () => now,\n advance,\n flushAll,\n pending: () => timers.size,\n };\n}\n","/**\n * @triggery/testing — testing utilities for Triggery.\n *\n * The kit lets you write trigger tests without React, mocking conditions and\n * actions against an isolated runtime instead of relying on `getDefaultRuntime`.\n *\n * @example\n * ```ts\n * import { createTrigger } from '@triggery/core';\n * import { createTestRuntime } from '@triggery/testing';\n *\n * const rt = createTestRuntime();\n * const t = createTrigger<{\n * events: { tick: number };\n * conditions: { enabled: boolean };\n * actions: { log: number };\n * }>(\n * {\n * id: 'demo',\n * events: ['tick'],\n * required: ['enabled'],\n * handler: ({ event, conditions, actions }) => {\n * if (!conditions.enabled) return;\n * actions.log?.(event.payload);\n * },\n * },\n * rt,\n * );\n *\n * rt.mockCondition(t, 'enabled', true);\n * const log = vi.fn();\n * rt.mockAction(t, 'log', log);\n *\n * rt.fireSync('tick', 42);\n * expect(log).toHaveBeenCalledWith(42);\n * ```\n */\n\nimport type {\n ActionFn,\n ActionKey,\n ActionMap,\n ConditionGetter,\n ConditionKey,\n ConditionMap,\n RegistrationToken,\n Runtime,\n RuntimeOptions,\n Trigger,\n TriggerSchema,\n UntypedActionFn,\n} from '@triggery/core';\nimport { createRuntime } from '@triggery/core';\n\nexport { createFakeScheduler, type FakeScheduler } from './fakeScheduler.ts';\n\nexport type TestRuntimeOptions = RuntimeOptions;\n\nexport type TestRuntime = Runtime & {\n /**\n * Register a condition for a trigger.\n *\n * Accepts either a static value or a zero-argument getter. When the\n * condition's value type is itself a zero-argument function, pass an\n * explicit getter so the runtime knows which one you mean — otherwise the\n * heuristic below would call your value as if it were the getter.\n */\n mockCondition<S extends TriggerSchema, K extends ConditionKey<S>>(\n trigger: Trigger<S>,\n name: K,\n valueOrGetter: ConditionMap<S>[K] | (() => ConditionMap<S>[K]),\n ): RegistrationToken;\n\n /** Register an action handler — typically a `vi.fn()`. */\n mockAction<S extends TriggerSchema, K extends ActionKey<S>>(\n trigger: Trigger<S>,\n name: K,\n handler: ActionFn<ActionMap<S>[K]>,\n ): RegistrationToken;\n\n /**\n * Flush pending microtasks. The default scheduler uses `queueMicrotask` —\n * after `rt.fire(...)` you await `flushMicrotasks()` before asserting.\n *\n * `fireSync` does not need this: it runs handlers immediately.\n */\n flushMicrotasks(): Promise<void>;\n};\n\ntype AnyFn = (...args: never[]) => unknown;\nconst isLikelyGetter = (fn: AnyFn): boolean => fn.length === 0;\n\nexport function createTestRuntime(options: TestRuntimeOptions = {}): TestRuntime {\n const runtime = createRuntime(options);\n\n const mockCondition = <S extends TriggerSchema, K extends ConditionKey<S>>(\n trigger: Trigger<S>,\n name: K,\n valueOrGetter: ConditionMap<S>[K] | (() => ConditionMap<S>[K]),\n ): RegistrationToken => {\n const getter: ConditionGetter =\n typeof valueOrGetter === 'function' && isLikelyGetter(valueOrGetter as AnyFn)\n ? (valueOrGetter as ConditionGetter)\n : () => valueOrGetter;\n return runtime.registerCondition(trigger.id, name as string, getter);\n };\n\n const mockAction = <S extends TriggerSchema, K extends ActionKey<S>>(\n trigger: Trigger<S>,\n name: K,\n handler: ActionFn<ActionMap<S>[K]>,\n ): RegistrationToken => {\n return runtime.registerAction(\n trigger.id,\n name as string,\n handler as unknown as UntypedActionFn,\n );\n };\n\n const flushMicrotasks = async (): Promise<void> => {\n // Two rounds: the first drains the queue we know about, the second picks\n // up follow-up microtasks queued by handlers that already ran.\n await Promise.resolve();\n await Promise.resolve();\n };\n\n return {\n ...runtime,\n mockCondition,\n mockAction,\n flushMicrotasks,\n };\n}\n"]}
package/package.json ADDED
@@ -0,0 +1,71 @@
1
+ {
2
+ "name": "@triggery/testing",
3
+ "version": "0.1.0",
4
+ "description": "Testing utilities for Triggery — isolated runtime, mock conditions/actions, fake scheduler",
5
+ "license": "MIT",
6
+ "author": "Aleksey Skhomenko",
7
+ "homepage": "https://triggeryjs.github.io/triggery",
8
+ "repository": {
9
+ "type": "git",
10
+ "url": "git+https://github.com/triggeryjs/triggery.git",
11
+ "directory": "packages/testing"
12
+ },
13
+ "bugs": "https://github.com/triggeryjs/triggery/issues",
14
+ "funding": [
15
+ {
16
+ "type": "patreon",
17
+ "url": "https://www.patreon.com/triggery"
18
+ },
19
+ {
20
+ "type": "boosty",
21
+ "url": "https://boosty.to/triggery"
22
+ }
23
+ ],
24
+ "keywords": [
25
+ "triggery",
26
+ "testing",
27
+ "vitest",
28
+ "jest",
29
+ "mocking"
30
+ ],
31
+ "type": "module",
32
+ "main": "./dist/index.js",
33
+ "module": "./dist/index.js",
34
+ "types": "./dist/index.d.ts",
35
+ "exports": {
36
+ ".": {
37
+ "source": "./src/index.ts",
38
+ "types": "./dist/index.d.ts",
39
+ "import": "./dist/index.js",
40
+ "default": "./dist/index.js"
41
+ },
42
+ "./package.json": "./package.json"
43
+ },
44
+ "files": [
45
+ "dist",
46
+ "README.md",
47
+ "LICENSE",
48
+ "CHANGELOG.md"
49
+ ],
50
+ "sideEffects": false,
51
+ "publishConfig": {
52
+ "access": "public"
53
+ },
54
+ "peerDependencies": {
55
+ "@triggery/core": "0.1.0"
56
+ },
57
+ "devDependencies": {
58
+ "tsup": "^8.5.1",
59
+ "typescript": "^6.0.3",
60
+ "vitest": "^4.1.6",
61
+ "@triggery/core": "0.1.0"
62
+ },
63
+ "scripts": {
64
+ "build": "tsup",
65
+ "dev": "tsup --watch",
66
+ "test": "vitest run --passWithNoTests",
67
+ "test:watch": "vitest --passWithNoTests",
68
+ "test:coverage": "vitest run --coverage --passWithNoTests",
69
+ "clean": "rm -rf dist *.tsbuildinfo"
70
+ }
71
+ }