@modular-react/journeys 0.1.0 → 1.0.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/LICENSE +21 -21
- package/README.md +2367 -1669
- package/dist/index.d.ts +686 -28
- package/dist/index.js +0 -0
- package/dist/index.js.map +1 -1
- package/dist/runtime-BUVl0_Ad.js +1422 -0
- package/dist/runtime-BUVl0_Ad.js.map +1 -0
- package/dist/testing.d.ts +155 -6
- package/dist/testing.js +62 -30
- package/dist/testing.js.map +1 -1
- package/package.json +5 -3
- package/dist/runtime-DyU_PmaC.js +0 -599
- package/dist/runtime-DyU_PmaC.js.map +0 -1
package/dist/testing.js
CHANGED
|
@@ -1,63 +1,95 @@
|
|
|
1
|
-
import { n as e, t } from "./runtime-
|
|
1
|
+
import { n as e, t } from "./runtime-BUVl0_Ad.js";
|
|
2
2
|
//#region src/simulate-journey.ts
|
|
3
|
-
function n(
|
|
4
|
-
let
|
|
3
|
+
function n(n, ...a) {
|
|
4
|
+
let o = a.length > 0 ? a[0] : void 0, s = a.length > 1 ? a[1] : void 0, c = [], l = (e) => {
|
|
5
|
+
c.push(e);
|
|
6
|
+
}, u = t([{
|
|
7
|
+
definition: n,
|
|
8
|
+
options: { onTransition: l }
|
|
9
|
+
}, ...(s?.children ?? []).map((e) => ({
|
|
5
10
|
definition: e,
|
|
6
|
-
options: { onTransition:
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
11
|
+
options: { onTransition: l }
|
|
12
|
+
}))]), d = u.start(n.id, o);
|
|
13
|
+
return r(u, i(u), e(u), c, d, n.id);
|
|
14
|
+
}
|
|
15
|
+
function r(e, t, n, i, a, o) {
|
|
16
|
+
function s() {
|
|
17
|
+
return t.inspect(a);
|
|
12
18
|
}
|
|
13
|
-
function
|
|
14
|
-
let
|
|
15
|
-
if (!
|
|
16
|
-
return
|
|
19
|
+
function c() {
|
|
20
|
+
let t = e.getInstance(a);
|
|
21
|
+
if (!t) throw Error(`[simulateJourney] instance ${a} not found`);
|
|
22
|
+
return t;
|
|
17
23
|
}
|
|
24
|
+
let l = null;
|
|
18
25
|
return {
|
|
19
|
-
journeyId:
|
|
20
|
-
instanceId:
|
|
26
|
+
journeyId: o,
|
|
27
|
+
instanceId: a,
|
|
21
28
|
get step() {
|
|
22
|
-
return
|
|
29
|
+
return s().step;
|
|
23
30
|
},
|
|
24
31
|
get currentStep() {
|
|
25
|
-
let e =
|
|
32
|
+
let e = s();
|
|
26
33
|
if (!e.step) throw Error(`[simulateJourney] no current step (status=${e.status}). Use \`step\` if a null step is expected.`);
|
|
27
34
|
return e.step;
|
|
28
35
|
},
|
|
29
36
|
get state() {
|
|
30
|
-
return
|
|
37
|
+
return s().state;
|
|
31
38
|
},
|
|
32
39
|
get history() {
|
|
33
|
-
return
|
|
40
|
+
return s().history;
|
|
34
41
|
},
|
|
35
42
|
get status() {
|
|
36
|
-
return
|
|
43
|
+
return s().status;
|
|
37
44
|
},
|
|
38
45
|
get transitions() {
|
|
39
|
-
return
|
|
46
|
+
return i;
|
|
40
47
|
},
|
|
41
48
|
get terminalPayload() {
|
|
42
|
-
return
|
|
49
|
+
return c().terminalPayload;
|
|
50
|
+
},
|
|
51
|
+
get activeChildId() {
|
|
52
|
+
return c().activeChildId;
|
|
53
|
+
},
|
|
54
|
+
get activeChild() {
|
|
55
|
+
let a = c().activeChildId;
|
|
56
|
+
if (!a) return l = null, null;
|
|
57
|
+
if (l && l.childId === a) return l.sim;
|
|
58
|
+
let o = e.getInstance(a);
|
|
59
|
+
if (!o) return l = null, null;
|
|
60
|
+
let s = r(e, t, n, i, a, o.journeyId);
|
|
61
|
+
return l = {
|
|
62
|
+
childId: a,
|
|
63
|
+
sim: s
|
|
64
|
+
}, s;
|
|
43
65
|
},
|
|
44
66
|
serialize() {
|
|
45
|
-
return
|
|
67
|
+
return c().serialize();
|
|
46
68
|
},
|
|
47
|
-
fireExit(e,
|
|
48
|
-
|
|
69
|
+
fireExit(e, n) {
|
|
70
|
+
t.fireExit(a, e, n);
|
|
49
71
|
},
|
|
50
72
|
goBack() {
|
|
51
|
-
|
|
73
|
+
t.goBack(a);
|
|
74
|
+
},
|
|
75
|
+
end(t) {
|
|
76
|
+
e.end(a, t);
|
|
77
|
+
},
|
|
78
|
+
completeChild(e) {
|
|
79
|
+
let t = c().activeChildId;
|
|
80
|
+
if (!t) throw Error(`[simulateJourney] completeChild() called on instance "${a}" but no child is in flight.`);
|
|
81
|
+
n.__synthesizeCompletion(t, e);
|
|
52
82
|
},
|
|
53
|
-
|
|
54
|
-
|
|
83
|
+
abortChild(e) {
|
|
84
|
+
let t = c().activeChildId;
|
|
85
|
+
if (!t) throw Error(`[simulateJourney] abortChild() called on instance "${a}" but no child is in flight.`);
|
|
86
|
+
n.__synthesizeAbort(t, e);
|
|
55
87
|
}
|
|
56
88
|
};
|
|
57
89
|
}
|
|
58
90
|
//#endregion
|
|
59
91
|
//#region src/testing.ts
|
|
60
|
-
function
|
|
92
|
+
function i(t) {
|
|
61
93
|
let n = e(t);
|
|
62
94
|
function r(e) {
|
|
63
95
|
let t = n.__getRecord(e);
|
|
@@ -97,6 +129,6 @@ function r(t) {
|
|
|
97
129
|
};
|
|
98
130
|
}
|
|
99
131
|
//#endregion
|
|
100
|
-
export {
|
|
132
|
+
export { i as createTestHarness, n as simulateJourney };
|
|
101
133
|
|
|
102
134
|
//# sourceMappingURL=testing.js.map
|
package/dist/testing.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"testing.js","names":[],"sources":["../src/simulate-journey.ts","../src/testing.ts"],"sourcesContent":["import { createJourneyRuntime } from \"./runtime.js\";\nimport { createTestHarness } from \"./testing.js\";\nimport type {\n AnyJourneyDefinition,\n JourneyDefinition,\n JourneyStep,\n ModuleTypeMap,\n SerializedJourney,\n TransitionEvent,\n} from \"./types.js\";\n\n/**\n * Headless simulator for a journey definition. Fires exits / goBack without\n * mounting React and exposes state / step / history / the recorded\n * `TransitionEvent` stream for assertions.\n *\n * Intended for pure-logic unit tests of transition graphs.\n */\nexport interface JourneySimulator<_TModules extends ModuleTypeMap, TState> {\n readonly journeyId: string;\n readonly instanceId: string;\n /** Current step — null once the journey completes or aborts. */\n readonly step: JourneyStep | null;\n /**\n * Same as `step`, but throws if the journey has terminated. Use this in\n * tests to skip optional chaining on the common \"still running\" path —\n * the throw spells out the unexpected status (`completed` / `aborted`)\n * and is far easier to debug than a `Cannot read property 'moduleId' of\n * null` thrown by an assertion line.\n */\n readonly currentStep: JourneyStep;\n readonly state: TState;\n readonly history: readonly JourneyStep[];\n readonly status: \"loading\" | \"active\" | \"completed\" | \"aborted\";\n /**\n * Every `TransitionEvent` the runtime has fired since the simulator\n * started. Useful for assertions on analytics rules without having to\n * attach an `onTransition` by hand.\n */\n readonly transitions: readonly TransitionEvent[];\n /**\n * Terminal payload from the `complete` / `abort` transition that ended\n * the journey. `undefined` while the journey is still active.\n */\n readonly terminalPayload: unknown;\n\n fireExit(name: string, output?: unknown): void;\n goBack(): void;\n end(reason?: unknown): void;\n /**\n * Serialize the simulator's current instance into the same blob shape\n * a persistence adapter would see. Useful for pinning the exact blob\n * shape tests expect to round-trip, and for asserting `rollbackSnapshots`\n * alignment with `history` without reaching into runtime internals.\n */\n serialize(): SerializedJourney<TState>;\n}\n\n/**\n * Headlessly drive a journey definition — see {@link JourneySimulator}.\n *\n * The second argument is the journey's `TInput`. When a journey declares\n * no input (`TInput extends void`), callers can omit it entirely:\n *\n * ```ts\n * simulateJourney(noInputJourney); // no input required\n * simulateJourney(inputJourney, { id: 1 }); // input required and typed\n * ```\n */\nexport function simulateJourney<TModules extends ModuleTypeMap, TState, TInput>(\n definition: JourneyDefinition<TModules, TState, TInput>,\n ...rest: [TInput] extends [void] ? [] | [input?: TInput] : [input: TInput]\n): JourneySimulator<TModules, TState> {\n const input = (rest.length > 0 ? rest[0] : undefined) as TInput;\n // Attach our own recorder on top of whatever `onTransition` the definition\n // declares — the runtime already invokes both (definition first, then\n // registration option), so this does not shadow the journey's own hook.\n const transitions: TransitionEvent[] = [];\n const runtime = createJourneyRuntime([\n {\n definition: definition as AnyJourneyDefinition,\n options: {\n onTransition: (ev) => {\n transitions.push(ev);\n },\n },\n },\n ]);\n const instanceId = runtime.start(definition.id, input);\n const harness = createTestHarness(runtime);\n\n function snapshot() {\n return harness.inspect<TState>(instanceId);\n }\n\n function instanceOrThrow() {\n const inst = runtime.getInstance(instanceId);\n if (!inst) throw new Error(`[simulateJourney] instance ${instanceId} not found`);\n return inst;\n }\n\n return {\n journeyId: definition.id,\n instanceId,\n get step() {\n return snapshot().step;\n },\n get currentStep() {\n const snap = snapshot();\n if (!snap.step) {\n throw new Error(\n `[simulateJourney] no current step (status=${snap.status}). Use \\`step\\` if a null step is expected.`,\n );\n }\n return snap.step;\n },\n get state() {\n return snapshot().state;\n },\n get history() {\n return snapshot().history;\n },\n get status() {\n return snapshot().status;\n },\n get transitions() {\n return transitions;\n },\n get terminalPayload() {\n return instanceOrThrow().terminalPayload;\n },\n serialize() {\n return instanceOrThrow().serialize() as SerializedJourney<TState>;\n },\n fireExit(name, output) {\n harness.fireExit(instanceId, name, output);\n },\n goBack() {\n harness.goBack(instanceId);\n },\n end(reason) {\n runtime.end(instanceId, reason);\n },\n };\n}\n","import type { InstanceId, JourneyRuntime, JourneyStatus, JourneyStep } from \"@modular-react/core\";\n\nimport { getInternals } from \"./runtime.js\";\n\nexport { simulateJourney } from \"./simulate-journey.js\";\nexport type { JourneySimulator } from \"./simulate-journey.js\";\n\n/**\n * Snapshot of the mutable runtime record for a single instance. Returned by\n * `JourneyTestHarness.inspect` so tests can assert on fields that the\n * public `JourneyInstance` surface intentionally does not expose (stepToken,\n * retryCount). Everything else is also available via `runtime.getInstance`.\n */\nexport interface InstanceSnapshot<TState = unknown> {\n readonly status: JourneyStatus;\n readonly step: JourneyStep | null;\n readonly state: TState;\n readonly history: readonly JourneyStep[];\n readonly stepToken: number;\n readonly retryCount: number;\n}\n\n/**\n * Test-only accessor that drives a runtime's internals from the outside —\n * fire exits, walk back, peek at per-instance state. Prefer\n * {@link simulateJourney} for pure-logic transition tests; use this when you\n * already have a live runtime (e.g. one produced by the registry) and need\n * to poke it from a test without mounting the outlet.\n *\n * The harness is the supported replacement for directly importing the\n * runtime's `__`-prefixed internals, which are kept off the public export\n * surface intentionally.\n */\nexport interface JourneyTestHarness {\n fireExit(id: InstanceId, name: string, output?: unknown): void;\n goBack(id: InstanceId): void;\n inspect<TState = unknown>(id: InstanceId): InstanceSnapshot<TState>;\n}\n\nexport function createTestHarness(runtime: JourneyRuntime): JourneyTestHarness {\n const internals = getInternals(runtime);\n\n function recordOrThrow(id: InstanceId) {\n const record = internals.__getRecord(id);\n if (!record) {\n throw new Error(\n `[@modular-react/journeys/testing] No instance with id \"${id}\". Pass the id returned by runtime.start(...).`,\n );\n }\n return record;\n }\n\n return {\n fireExit(id, name, output) {\n const record = recordOrThrow(id);\n const reg = internals.__getRegistered(record.journeyId);\n if (!reg) {\n throw new Error(\n `[@modular-react/journeys/testing] Journey \"${record.journeyId}\" is not registered with this runtime.`,\n );\n }\n // Calling fireExit on a loading instance is a silent no-op at the\n // runtime level (the runtime has no step to resolve against yet).\n // In tests this almost always indicates the caller forgot to await\n // the persistence load probe. Throw early so the test fails on the\n // offending call instead of on a later `expect(step?.entry)` read.\n if (record.status === \"loading\") {\n throw new Error(\n `[@modular-react/journeys/testing] fireExit(\"${name}\") called on instance \"${id}\" while status=loading. ` +\n `Await the runtime's async load probe (typically \\`await Promise.resolve()\\` a few times, or expose a subscribe hook in your test) before dispatching exits.`,\n );\n }\n if (record.status !== \"active\") {\n throw new Error(\n `[@modular-react/journeys/testing] fireExit(\"${name}\") called on terminal instance \"${id}\" (status=${record.status}).`,\n );\n }\n internals.__bindStepCallbacks(record, reg).exit(name, output);\n },\n goBack(id) {\n const record = recordOrThrow(id);\n const reg = internals.__getRegistered(record.journeyId);\n if (!reg) {\n throw new Error(\n `[@modular-react/journeys/testing] Journey \"${record.journeyId}\" is not registered with this runtime.`,\n );\n }\n if (record.status === \"loading\") {\n throw new Error(\n `[@modular-react/journeys/testing] goBack() called on instance \"${id}\" while status=loading. ` +\n `Await the runtime's async load probe before dispatching.`,\n );\n }\n const callbacks = internals.__bindStepCallbacks(record, reg);\n if (!callbacks.goBack) {\n // Silently no-oping here would quietly \"pass\" a test that expects\n // back navigation to work — the common `goBack walks back…` pattern\n // asserts state *after* the call, so a no-op masks the wiring bug.\n // Throw with context so the test fails on the offending call instead.\n const stepLabel = record.step\n ? `${record.step.moduleId}.${record.step.entry}`\n : \"(no step)\";\n throw new Error(\n `[@modular-react/journeys/testing] goBack is unavailable on instance \"${id}\" (step=${stepLabel}). ` +\n `The journey's transition must declare allowBack: true AND the current step must have at least one history entry.`,\n );\n }\n callbacks.goBack();\n },\n inspect<TState = unknown>(id: InstanceId): InstanceSnapshot<TState> {\n const record = recordOrThrow(id);\n // Snapshot — `history` is a live array on the runtime record and will\n // grow as the journey advances. Copy so assertions captured by the\n // caller stay stable when the next `fireExit` runs.\n return {\n status: record.status,\n step: record.step,\n state: record.state as TState,\n history: [...record.history],\n stepToken: record.stepToken,\n retryCount: record.retryCount,\n };\n },\n };\n}\n"],"mappings":";;AAqEA,SAAgB,EACd,GACA,GAAG,GACiC;CACpC,IAAM,IAAS,EAAK,SAAS,IAAI,EAAK,KAAK,KAAA,GAIrC,IAAiC,EAAE,EACnC,IAAU,EAAqB,CACnC;EACc;EACZ,SAAS,EACP,eAAe,MAAO;AACpB,KAAY,KAAK,EAAG;KAEvB;EACF,CACF,CAAC,EACI,IAAa,EAAQ,MAAM,EAAW,IAAI,EAAM,EAChD,IAAU,EAAkB,EAAQ;CAE1C,SAAS,IAAW;AAClB,SAAO,EAAQ,QAAgB,EAAW;;CAG5C,SAAS,IAAkB;EACzB,IAAM,IAAO,EAAQ,YAAY,EAAW;AAC5C,MAAI,CAAC,EAAM,OAAU,MAAM,8BAA8B,EAAW,YAAY;AAChF,SAAO;;AAGT,QAAO;EACL,WAAW,EAAW;EACtB;EACA,IAAI,OAAO;AACT,UAAO,GAAU,CAAC;;EAEpB,IAAI,cAAc;GAChB,IAAM,IAAO,GAAU;AACvB,OAAI,CAAC,EAAK,KACR,OAAU,MACR,6CAA6C,EAAK,OAAO,6CAC1D;AAEH,UAAO,EAAK;;EAEd,IAAI,QAAQ;AACV,UAAO,GAAU,CAAC;;EAEpB,IAAI,UAAU;AACZ,UAAO,GAAU,CAAC;;EAEpB,IAAI,SAAS;AACX,UAAO,GAAU,CAAC;;EAEpB,IAAI,cAAc;AAChB,UAAO;;EAET,IAAI,kBAAkB;AACpB,UAAO,GAAiB,CAAC;;EAE3B,YAAY;AACV,UAAO,GAAiB,CAAC,WAAW;;EAEtC,SAAS,GAAM,GAAQ;AACrB,KAAQ,SAAS,GAAY,GAAM,EAAO;;EAE5C,SAAS;AACP,KAAQ,OAAO,EAAW;;EAE5B,IAAI,GAAQ;AACV,KAAQ,IAAI,GAAY,EAAO;;EAElC;;;;ACxGH,SAAgB,EAAkB,GAA6C;CAC7E,IAAM,IAAY,EAAa,EAAQ;CAEvC,SAAS,EAAc,GAAgB;EACrC,IAAM,IAAS,EAAU,YAAY,EAAG;AACxC,MAAI,CAAC,EACH,OAAU,MACR,0DAA0D,EAAG,gDAC9D;AAEH,SAAO;;AAGT,QAAO;EACL,SAAS,GAAI,GAAM,GAAQ;GACzB,IAAM,IAAS,EAAc,EAAG,EAC1B,IAAM,EAAU,gBAAgB,EAAO,UAAU;AACvD,OAAI,CAAC,EACH,OAAU,MACR,8CAA8C,EAAO,UAAU,wCAChE;AAOH,OAAI,EAAO,WAAW,UACpB,OAAU,MACR,+CAA+C,EAAK,yBAAyB,EAAG,qLAEjF;AAEH,OAAI,EAAO,WAAW,SACpB,OAAU,MACR,+CAA+C,EAAK,kCAAkC,EAAG,YAAY,EAAO,OAAO,IACpH;AAEH,KAAU,oBAAoB,GAAQ,EAAI,CAAC,KAAK,GAAM,EAAO;;EAE/D,OAAO,GAAI;GACT,IAAM,IAAS,EAAc,EAAG,EAC1B,IAAM,EAAU,gBAAgB,EAAO,UAAU;AACvD,OAAI,CAAC,EACH,OAAU,MACR,8CAA8C,EAAO,UAAU,wCAChE;AAEH,OAAI,EAAO,WAAW,UACpB,OAAU,MACR,kEAAkE,EAAG,kFAEtE;GAEH,IAAM,IAAY,EAAU,oBAAoB,GAAQ,EAAI;AAC5D,OAAI,CAAC,EAAU,QAAQ;IAKrB,IAAM,IAAY,EAAO,OACrB,GAAG,EAAO,KAAK,SAAS,GAAG,EAAO,KAAK,UACvC;AACJ,UAAU,MACR,wEAAwE,EAAG,UAAU,EAAU,qHAEhG;;AAEH,KAAU,QAAQ;;EAEpB,QAA0B,GAA0C;GAClE,IAAM,IAAS,EAAc,EAAG;AAIhC,UAAO;IACL,QAAQ,EAAO;IACf,MAAM,EAAO;IACb,OAAO,EAAO;IACd,SAAS,CAAC,GAAG,EAAO,QAAQ;IAC5B,WAAW,EAAO;IAClB,YAAY,EAAO;IACpB;;EAEJ"}
|
|
1
|
+
{"version":3,"file":"testing.js","names":[],"sources":["../src/simulate-journey.ts","../src/testing.ts"],"sourcesContent":["import { createJourneyRuntime, getInternals } from \"./runtime.js\";\nimport { createTestHarness } from \"./testing.js\";\nimport type {\n AnyJourneyDefinition,\n InstanceId,\n JourneyDefinition,\n JourneyStep,\n ModuleTypeMap,\n SerializedJourney,\n TransitionEvent,\n} from \"./types.js\";\n\n/**\n * Headless simulator for a journey definition. Fires exits / goBack without\n * mounting React and exposes state / step / history / the recorded\n * `TransitionEvent` stream for assertions.\n *\n * Intended for pure-logic unit tests of transition graphs.\n */\nexport interface JourneySimulator<_TModules extends ModuleTypeMap, TState> {\n readonly journeyId: string;\n readonly instanceId: string;\n /** Current step — null once the journey completes or aborts. */\n readonly step: JourneyStep | null;\n /**\n * Same as `step`, but throws if the journey has terminated. Use this in\n * tests to skip optional chaining on the common \"still running\" path —\n * the throw spells out the unexpected status (`completed` / `aborted`)\n * and is far easier to debug than a `Cannot read property 'moduleId' of\n * null` thrown by an assertion line.\n */\n readonly currentStep: JourneyStep;\n readonly state: TState;\n readonly history: readonly JourneyStep[];\n readonly status: \"loading\" | \"active\" | \"completed\" | \"aborted\";\n /**\n * Every `TransitionEvent` the runtime has fired since the simulator\n * started. Useful for assertions on analytics rules without having to\n * attach an `onTransition` by hand.\n */\n readonly transitions: readonly TransitionEvent[];\n /**\n * Terminal payload from the `complete` / `abort` transition that ended\n * the journey. `undefined` while the journey is still active.\n */\n readonly terminalPayload: unknown;\n\n fireExit(name: string, output?: unknown): void;\n goBack(): void;\n end(reason?: unknown): void;\n /**\n * Serialize the simulator's current instance into the same blob shape\n * a persistence adapter would see. Useful for pinning the exact blob\n * shape tests expect to round-trip, and for asserting `rollbackSnapshots`\n * alignment with `history` without reaching into runtime internals.\n */\n serialize(): SerializedJourney<TState>;\n\n // -------------------------------------------------------------------------\n // Invoke / resume helpers — present on every simulator, no-op when no\n // child is in flight.\n // -------------------------------------------------------------------------\n\n /**\n * Active child instance id when this journey has invoked another. `null`\n * when the journey is not currently awaiting a child.\n */\n readonly activeChildId: InstanceId | null;\n\n /**\n * Sub-simulator for the currently-invoked child journey, if any. The\n * returned simulator drives the child via the same runtime — once the\n * child terminates, the parent's resume fires automatically and this\n * sim's `state` / `step` reflect the post-resume position.\n *\n * `null` when no child is in flight (parent has not invoked, or the\n * child has already resumed back into the parent).\n */\n readonly activeChild: JourneySimulator<ModuleTypeMap, unknown> | null;\n\n /**\n * Synthesize a `{ status: \"completed\" }` outcome on the active child\n * without enumerating its steps. Useful for unit-testing a parent's\n * resume handler in isolation. Throws if no child is in flight.\n */\n completeChild(payload: unknown): void;\n\n /**\n * Synthesize a `{ status: \"aborted\" }` outcome on the active child.\n * Throws if no child is in flight.\n */\n abortChild(reason?: unknown): void;\n}\n\n/**\n * Options for {@link simulateJourney}. Pass `children` to register\n * additional journey definitions reachable via `invoke` from the\n * primary journey, so the simulator can drive child sub-flows\n * end-to-end. The `mockChildOutcomes` shortcut is preferred when the\n * child's transition path is irrelevant to the test.\n */\nexport interface SimulateJourneyOptions {\n /**\n * Child journey definitions the primary journey can `invoke`. Without\n * this, an `invoke` from the primary will abort with\n * `invoke-unknown-journey`. Order does not matter; mutually-invoking\n * journeys all go in here.\n */\n readonly children?: readonly AnyJourneyDefinition[];\n}\n\n/**\n * Headlessly drive a journey definition — see {@link JourneySimulator}.\n *\n * The second argument is the journey's `TInput`. When a journey declares\n * no input (`TInput extends void`), callers can omit it entirely:\n *\n * ```ts\n * simulateJourney(noInputJourney); // no input required\n * simulateJourney(inputJourney, { id: 1 }); // input required and typed\n * ```\n *\n * Pass `options.children` when the primary journey can `invoke` child\n * journeys — every reachable child must be registered or the parent\n * will abort with `invoke-unknown-journey`.\n */\nexport function simulateJourney<TModules extends ModuleTypeMap, TState, TInput>(\n definition: JourneyDefinition<TModules, TState, TInput>,\n ...rest: [TInput] extends [void]\n ? [] | [input?: TInput] | [input: TInput, options: SimulateJourneyOptions]\n : [input: TInput] | [input: TInput, options: SimulateJourneyOptions]\n): JourneySimulator<TModules, TState> {\n const input = (rest.length > 0 ? rest[0] : undefined) as TInput;\n const options = (rest.length > 1 ? rest[1] : undefined) as SimulateJourneyOptions | undefined;\n // Attach our own recorder on top of whatever `onTransition` the definition\n // declares — the runtime already invokes both (definition first, then\n // registration option), so this does not shadow the journey's own hook.\n const transitions: TransitionEvent[] = [];\n const recorder = (ev: TransitionEvent) => {\n transitions.push(ev);\n };\n const registered = [\n {\n definition: definition as AnyJourneyDefinition,\n options: { onTransition: recorder },\n },\n ...(options?.children ?? []).map((child) => ({\n definition: child,\n options: { onTransition: recorder },\n })),\n ];\n const runtime = createJourneyRuntime(registered);\n const instanceId = runtime.start(definition.id, input);\n const harness = createTestHarness(runtime);\n const internals = getInternals(runtime);\n\n return wrapInstanceAsSim<TModules, TState>(\n runtime,\n harness,\n internals,\n transitions,\n instanceId,\n definition.id,\n );\n}\n\n/**\n * Build a simulator wrapper around a runtime instance. Used for both the\n * primary sim returned by `simulateJourney` and any child sims surfaced\n * via `sim.activeChild`. Sharing this constructor keeps the two surfaces\n * structurally identical so test code drives parent and child the same\n * way.\n */\nfunction wrapInstanceAsSim<TModules extends ModuleTypeMap, TState>(\n runtime: ReturnType<typeof createJourneyRuntime>,\n harness: ReturnType<typeof createTestHarness>,\n internals: ReturnType<typeof getInternals>,\n transitions: TransitionEvent[],\n instanceId: InstanceId,\n journeyId: string,\n): JourneySimulator<TModules, TState> {\n function snapshot() {\n return harness.inspect<TState>(instanceId);\n }\n function instanceOrThrow() {\n const inst = runtime.getInstance(instanceId);\n if (!inst) throw new Error(`[simulateJourney] instance ${instanceId} not found`);\n return inst;\n }\n // Cache `activeChild` sub-sims by child instance id so consecutive reads\n // of `sim.activeChild` return the same object reference. Without this\n // each getter access allocated a fresh wrapper, defeating consumers that\n // want to keep a stable reference to the child sim across assertions.\n let cachedChildSim: {\n childId: InstanceId;\n sim: JourneySimulator<ModuleTypeMap, unknown>;\n } | null = null;\n return {\n journeyId,\n instanceId,\n get step() {\n return snapshot().step;\n },\n get currentStep() {\n const snap = snapshot();\n if (!snap.step) {\n throw new Error(\n `[simulateJourney] no current step (status=${snap.status}). Use \\`step\\` if a null step is expected.`,\n );\n }\n return snap.step;\n },\n get state() {\n return snapshot().state;\n },\n get history() {\n return snapshot().history;\n },\n get status() {\n return snapshot().status;\n },\n get transitions() {\n return transitions;\n },\n get terminalPayload() {\n return instanceOrThrow().terminalPayload;\n },\n get activeChildId() {\n return instanceOrThrow().activeChildId;\n },\n get activeChild() {\n const inst = instanceOrThrow();\n const childId = inst.activeChildId;\n if (!childId) {\n cachedChildSim = null;\n return null;\n }\n if (cachedChildSim && cachedChildSim.childId === childId) {\n return cachedChildSim.sim;\n }\n const child = runtime.getInstance(childId);\n if (!child) {\n cachedChildSim = null;\n return null;\n }\n const sim = wrapInstanceAsSim<ModuleTypeMap, unknown>(\n runtime,\n harness,\n internals,\n transitions,\n childId,\n child.journeyId,\n );\n cachedChildSim = { childId, sim };\n return sim;\n },\n serialize() {\n return instanceOrThrow().serialize() as SerializedJourney<TState>;\n },\n fireExit(name, output) {\n harness.fireExit(instanceId, name, output);\n },\n goBack() {\n harness.goBack(instanceId);\n },\n end(reason) {\n runtime.end(instanceId, reason);\n },\n completeChild(payload) {\n const childId = instanceOrThrow().activeChildId;\n if (!childId) {\n throw new Error(\n `[simulateJourney] completeChild() called on instance \"${instanceId}\" but no child is in flight.`,\n );\n }\n internals.__synthesizeCompletion(childId, payload);\n },\n abortChild(reason) {\n const childId = instanceOrThrow().activeChildId;\n if (!childId) {\n throw new Error(\n `[simulateJourney] abortChild() called on instance \"${instanceId}\" but no child is in flight.`,\n );\n }\n // Symmetric with `completeChild`: synthesize the abort directly so\n // the parent's resume sees the reason as-is, with no wrapping.\n internals.__synthesizeAbort(childId, reason);\n },\n };\n}\n","import type { InstanceId, JourneyRuntime, JourneyStatus, JourneyStep } from \"@modular-react/core\";\n\nimport { getInternals } from \"./runtime.js\";\n\nexport { simulateJourney } from \"./simulate-journey.js\";\nexport type { JourneySimulator } from \"./simulate-journey.js\";\n\n/**\n * Snapshot of the mutable runtime record for a single instance. Returned by\n * `JourneyTestHarness.inspect` so tests can assert on fields that the\n * public `JourneyInstance` surface intentionally does not expose (stepToken,\n * retryCount). Everything else is also available via `runtime.getInstance`.\n */\nexport interface InstanceSnapshot<TState = unknown> {\n readonly status: JourneyStatus;\n readonly step: JourneyStep | null;\n readonly state: TState;\n readonly history: readonly JourneyStep[];\n readonly stepToken: number;\n readonly retryCount: number;\n}\n\n/**\n * Test-only accessor that drives a runtime's internals from the outside —\n * fire exits, walk back, peek at per-instance state. Prefer\n * {@link simulateJourney} for pure-logic transition tests; use this when you\n * already have a live runtime (e.g. one produced by the registry) and need\n * to poke it from a test without mounting the outlet.\n *\n * The harness is the supported replacement for directly importing the\n * runtime's `__`-prefixed internals, which are kept off the public export\n * surface intentionally.\n */\nexport interface JourneyTestHarness {\n fireExit(id: InstanceId, name: string, output?: unknown): void;\n goBack(id: InstanceId): void;\n inspect<TState = unknown>(id: InstanceId): InstanceSnapshot<TState>;\n}\n\nexport function createTestHarness(runtime: JourneyRuntime): JourneyTestHarness {\n const internals = getInternals(runtime);\n\n function recordOrThrow(id: InstanceId) {\n const record = internals.__getRecord(id);\n if (!record) {\n throw new Error(\n `[@modular-react/journeys/testing] No instance with id \"${id}\". Pass the id returned by runtime.start(...).`,\n );\n }\n return record;\n }\n\n return {\n fireExit(id, name, output) {\n const record = recordOrThrow(id);\n const reg = internals.__getRegistered(record.journeyId);\n if (!reg) {\n throw new Error(\n `[@modular-react/journeys/testing] Journey \"${record.journeyId}\" is not registered with this runtime.`,\n );\n }\n // Calling fireExit on a loading instance is a silent no-op at the\n // runtime level (the runtime has no step to resolve against yet).\n // In tests this almost always indicates the caller forgot to await\n // the persistence load probe. Throw early so the test fails on the\n // offending call instead of on a later `expect(step?.entry)` read.\n if (record.status === \"loading\") {\n throw new Error(\n `[@modular-react/journeys/testing] fireExit(\"${name}\") called on instance \"${id}\" while status=loading. ` +\n `Await the runtime's async load probe (typically \\`await Promise.resolve()\\` a few times, or expose a subscribe hook in your test) before dispatching exits.`,\n );\n }\n if (record.status !== \"active\") {\n throw new Error(\n `[@modular-react/journeys/testing] fireExit(\"${name}\") called on terminal instance \"${id}\" (status=${record.status}).`,\n );\n }\n internals.__bindStepCallbacks(record, reg).exit(name, output);\n },\n goBack(id) {\n const record = recordOrThrow(id);\n const reg = internals.__getRegistered(record.journeyId);\n if (!reg) {\n throw new Error(\n `[@modular-react/journeys/testing] Journey \"${record.journeyId}\" is not registered with this runtime.`,\n );\n }\n if (record.status === \"loading\") {\n throw new Error(\n `[@modular-react/journeys/testing] goBack() called on instance \"${id}\" while status=loading. ` +\n `Await the runtime's async load probe before dispatching.`,\n );\n }\n const callbacks = internals.__bindStepCallbacks(record, reg);\n if (!callbacks.goBack) {\n // Silently no-oping here would quietly \"pass\" a test that expects\n // back navigation to work — the common `goBack walks back…` pattern\n // asserts state *after* the call, so a no-op masks the wiring bug.\n // Throw with context so the test fails on the offending call instead.\n const stepLabel = record.step\n ? `${record.step.moduleId}.${record.step.entry}`\n : \"(no step)\";\n throw new Error(\n `[@modular-react/journeys/testing] goBack is unavailable on instance \"${id}\" (step=${stepLabel}). ` +\n `The journey's transition must declare allowBack: true AND the current step must have at least one history entry.`,\n );\n }\n callbacks.goBack();\n },\n inspect<TState = unknown>(id: InstanceId): InstanceSnapshot<TState> {\n const record = recordOrThrow(id);\n // Snapshot — `history` is a live array on the runtime record and will\n // grow as the journey advances. Copy so assertions captured by the\n // caller stay stable when the next `fireExit` runs.\n return {\n status: record.status,\n step: record.step,\n state: record.state as TState,\n history: [...record.history],\n stepToken: record.stepToken,\n retryCount: record.retryCount,\n };\n },\n };\n}\n"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@modular-react/journeys",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Typed, serializable workflows that compose multiple modules. A journey declares entry/exit transitions between modules and owns shared state; modules stay journey-unaware.",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -39,8 +39,9 @@
|
|
|
39
39
|
"vite": "^8.0.10",
|
|
40
40
|
"vite-plugin-dts": "^4.5.4",
|
|
41
41
|
"vitest": "^4.1.5",
|
|
42
|
-
"@modular-react/core": "
|
|
43
|
-
"@modular-react/react": "
|
|
42
|
+
"@modular-react/core": "2.0.0",
|
|
43
|
+
"@modular-react/react": "2.0.0",
|
|
44
|
+
"@modular-react/testing": "1.2.0"
|
|
44
45
|
},
|
|
45
46
|
"peerDependencies": {
|
|
46
47
|
"@modular-react/core": "^1.2.0",
|
|
@@ -52,6 +53,7 @@
|
|
|
52
53
|
"build": "vite build",
|
|
53
54
|
"dev": "vite build --watch",
|
|
54
55
|
"test": "vitest run",
|
|
56
|
+
"bench": "vitest bench --run bench",
|
|
55
57
|
"typecheck": "tsc --noEmit"
|
|
56
58
|
}
|
|
57
59
|
}
|