@moku-labs/game 0.14.11 → 0.14.13
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/README.md +13 -12
- package/dist/app/page.d.mts +8 -3
- package/dist/app/page.mjs +57 -12
- package/dist/app/system.d.mts +5 -3
- package/dist/app/system.mjs +2 -0
- package/dist/app.d.mts +15 -7
- package/dist/app.mjs +131 -27
- package/dist/{assets-BrKSg1O6.mjs → assets-CEMfBogh.mjs} +471 -120
- package/dist/assets.d.mts +30 -13
- package/dist/assets.mjs +1 -1
- package/dist/cli.d.mts +105 -8
- package/dist/cli.mjs +1036 -128
- package/dist/{commands-d5Zlh9hf.d.mts → commands-DAETmK86.d.mts} +43 -7
- package/dist/{config-CdARrrhU.mjs → config-B87aFTW-.mjs} +25 -4
- package/dist/control.d.mts +130 -17
- package/dist/control.mjs +4 -4
- package/dist/{define-BN15lAWb.d.mts → define-BASd9ZUh.d.mts} +32 -4
- package/dist/dev-DbHgx-R-.mjs +89 -0
- package/dist/{fake-MpzwZhIZ.mjs → fake-6mazmHK3.mjs} +20 -2
- package/dist/features-DowEilZm.mjs +266 -0
- package/dist/{headless-CrhefcLu.d.mts → headless-cFveciuY.d.mts} +1 -1
- package/dist/hot.d.mts +4 -2
- package/dist/hot.mjs +7 -4
- package/dist/{index-0GIP3tgm.d.mts → index-DedbED3T.d.mts} +317 -69
- package/dist/index.d.mts +5 -6
- package/dist/index.mjs +3 -3
- package/dist/inspect.d.mts +12 -7
- package/dist/inspect.mjs +2 -2
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/lint.d.mts +8 -3
- package/dist/lint.mjs +432 -20
- package/dist/project.d.mts +15 -1
- package/dist/project.mjs +1 -1
- package/dist/{run-Bf2hKyjZ.mjs → run-BbipFRc6.mjs} +250 -51
- package/dist/{runtime-aLkoeoGj.d.mts → runtime-DoSlRx8X.d.mts} +1 -1
- package/dist/{session-DgukhR_7.mjs → session-CeKgO2hy.mjs} +30 -2
- package/dist/{sources-Duzj6lhV.mjs → sources-Cdvb0rOS.mjs} +33 -6
- package/dist/{src-Do697seR.mjs → src-BEbJ_sSx.mjs} +29154 -11121
- package/dist/testing.d.mts +119 -14
- package/dist/testing.mjs +97 -7
- package/dist/{types-BZa187kz.d.mts → types-7GXcXxxv.d.mts} +196 -242
- package/dist/{types-BT9S_Lo2.d.mts → types-BGcda5Le.d.mts} +4464 -735
- package/dist/{types-yiHtzg1Y.d.mts → types-C73hbmxi.d.mts} +129 -17
- package/dist/{types-Bc9AvZVO.d.mts → types-DngFQJro.d.mts} +43 -5
- package/dist/{types-kNiV7iFj.d.mts → types-GHE4J5Uf.d.mts} +1027 -192
- package/dist/{visual-dMW8iqAE.mjs → visual-2m4wJL_z.mjs} +111 -7
- package/dist/visual-_XhoGeIC.d.mts +345 -0
- package/dist/visual.d.mts +1 -332
- package/dist/visual.mjs +130 -20
- package/dist/{watch-CWI_on4T.mjs → watch-BEdrp80i.mjs} +149 -30
- package/llms.txt +103 -51
- package/package.json +8 -6
- package/dist/dev-Cc50Dk7H.mjs +0 -47
- package/dist/memory-DR1ibF_s.d.mts +0 -50
- package/dist/types-DMoi447V.d.mts +0 -656
package/dist/testing.d.mts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { t as createApp } from "./index-
|
|
3
|
-
import {
|
|
4
|
-
import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-
|
|
1
|
+
import { $n as NodeFx, In as FeaturePlugin, Jn as Descriptor, Qn as Hint, Rn as AnyFlow, Wn as NodeTable, ir as FakeClock, nr as ClockSource, ur as Json } from "./types-BGcda5Le.mjs";
|
|
2
|
+
import { t as createApp } from "./index-DedbED3T.mjs";
|
|
3
|
+
import { b as saveOf, v as WithGameState, y as memory } from "./types-C73hbmxi.mjs";
|
|
4
|
+
import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-cFveciuY.mjs";
|
|
5
5
|
import { AnyPluginInstance } from "@moku-labs/core";
|
|
6
6
|
|
|
7
7
|
//#region src/plugins/clock/fake.d.ts
|
|
@@ -26,6 +26,101 @@ import { AnyPluginInstance } from "@moku-labs/core";
|
|
|
26
26
|
*/
|
|
27
27
|
declare function fakeClock(start?: number): FakeClock;
|
|
28
28
|
//#endregion
|
|
29
|
+
//#region src/testing/fake-fx.d.ts
|
|
30
|
+
/**
|
|
31
|
+
* What a test tells `fakeFx`.
|
|
32
|
+
*
|
|
33
|
+
* @example
|
|
34
|
+
* ```ts
|
|
35
|
+
* const options: FakeFxOptions = {
|
|
36
|
+
* answer: descriptor => (descriptor.kind === "popup" ? { intent: "again" } : undefined),
|
|
37
|
+
* path: "board/place"
|
|
38
|
+
* };
|
|
39
|
+
* ```
|
|
40
|
+
*/
|
|
41
|
+
type FakeFxOptions = {
|
|
42
|
+
/**
|
|
43
|
+
* What an awaited effect resolves with: a value, or a promise of one. Called once for every
|
|
44
|
+
* `await fx(descriptor)`, at the call, with the descriptor itself. Without it every awaited
|
|
45
|
+
* effect resolves `undefined`. A promise that never settles holds the body at its `await`, as a
|
|
46
|
+
* real effect can. A throw and a rejection stand for a handler that fails, and end as they do
|
|
47
|
+
* in the game: the awaited effect rejects, except for a descriptor with `cosmetic: true`
|
|
48
|
+
* (`sfx`, `play`, `haptic`, `music`), which resolves `undefined`. The game also writes the warn
|
|
49
|
+
* entry `flow:fx-cosmetic-failed` there. The fake has no log and writes nothing.
|
|
50
|
+
*
|
|
51
|
+
* @example
|
|
52
|
+
* ```ts
|
|
53
|
+
* // The popup is answered, a sound never ends: a body that waits for the sound stays there.
|
|
54
|
+
* fakeFx({
|
|
55
|
+
* answer: descriptor =>
|
|
56
|
+
* descriptor.kind === "sfx" ? new Promise<never>(() => undefined) : { intent: "again" }
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
answer?: (descriptor: Descriptor) => unknown;
|
|
61
|
+
/**
|
|
62
|
+
* The path of the node under test. Only the error of a refused `fx.start` names it. `"test"`
|
|
63
|
+
* by default.
|
|
64
|
+
*/
|
|
65
|
+
path?: string;
|
|
66
|
+
};
|
|
67
|
+
/**
|
|
68
|
+
* What `fakeFx` returns: the `fx` for the node context, and the three lists it records into.
|
|
69
|
+
* Each list is one array for the whole life of the fake: a list read before the body ran holds
|
|
70
|
+
* the later calls too, and a test starts a list over with `list.length = 0`. The lists hold the
|
|
71
|
+
* objects the body passed, not copies. The holder is frozen, so a list cannot be replaced by
|
|
72
|
+
* another array the fake would not write to.
|
|
73
|
+
*
|
|
74
|
+
* @example
|
|
75
|
+
* ```ts
|
|
76
|
+
* const { fx, started }: FakeFx = fakeFx();
|
|
77
|
+
*
|
|
78
|
+
* fx.start({ kind: "sfx", payload: { key: "ui.tap" } });
|
|
79
|
+
* started; // [{ kind: "sfx", payload: { key: "ui.tap" } }]
|
|
80
|
+
* ```
|
|
81
|
+
*/
|
|
82
|
+
type FakeFx = {
|
|
83
|
+
/** The `fx` to put into the node context: `{ ...context, fx: fake.fx }`. */readonly fx: NodeFx; /** Every `await fx(descriptor)` in call order, recorded at the call, answered or not. */
|
|
84
|
+
readonly awaited: Descriptor[]; /** Every `fx.start(descriptor)` in call order. A refused start is not in it. */
|
|
85
|
+
readonly started: Descriptor[]; /** Every `fx.emit(hint)` in call order. */
|
|
86
|
+
readonly emitted: Hint[];
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Builds the `fx` of a node context for a unit test that calls `node.run(context)` itself, with no
|
|
90
|
+
* app and no runner. It records every call and plays no effect: `await fx(descriptor)` goes to
|
|
91
|
+
* `awaited` and resolves with `options.answer(descriptor)`, `fx.start(descriptor)` goes to
|
|
92
|
+
* `started`, `fx.emit(hint)` goes to `emitted`. `fx.start` refuses a descriptor with `answers`
|
|
93
|
+
* and a `guide` with the error the game throws. An `answer` that fails rejects the awaited
|
|
94
|
+
* effect, and resolves `undefined` for a `cosmetic` descriptor, as in the game. There is no
|
|
95
|
+
* handler, no log, no fast mode and no abort signal here: for those, walk the graph with
|
|
96
|
+
* `createHeadless`.
|
|
97
|
+
*
|
|
98
|
+
* @param options - The answer of an awaited effect, and the node path for the refusal message.
|
|
99
|
+
* @returns The `fx` and the lists `awaited`, `started` and `emitted`.
|
|
100
|
+
* @example
|
|
101
|
+
* ```ts
|
|
102
|
+
* // The body of the node "place" runs fx.start(sfx("ui.tap")), then pushes input.cell to
|
|
103
|
+
* // session.marks. `context` is the rest of its context: input, player, session, rng, out,
|
|
104
|
+
* // signal and now.
|
|
105
|
+
* const fx = fakeFx();
|
|
106
|
+
*
|
|
107
|
+
* await place.run?.({ ...context, fx: fx.fx });
|
|
108
|
+
*
|
|
109
|
+
* fx.started; // [{ kind: "sfx", payload: { key: "ui.tap", bus: "sfx" }, cosmetic: true }]
|
|
110
|
+
* fx.awaited; // []: the body waited for no effect
|
|
111
|
+
*
|
|
112
|
+
* // The node "retry" awaits a popup that takes answers. The test gives the player's pick.
|
|
113
|
+
* const picked = fakeFx({
|
|
114
|
+
* answer: descriptor => (descriptor.kind === "popup" ? { intent: "again" } : undefined)
|
|
115
|
+
* });
|
|
116
|
+
*
|
|
117
|
+
* await retry.run?.({ ...context, fx: picked.fx });
|
|
118
|
+
*
|
|
119
|
+
* picked.awaited; // [{ kind: "popup", payload: { name: "Retry" }, answers: ["again", "home"] }]
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
declare function fakeFx(options?: FakeFxOptions): FakeFx;
|
|
123
|
+
//#endregion
|
|
29
124
|
//#region src/testing/isolate.d.ts
|
|
30
125
|
/**
|
|
31
126
|
* A stub for one node or sub-flow of the isolated flow: the outcome it ends with, and its payload.
|
|
@@ -65,12 +160,12 @@ type StubsOf<Nodes extends NodeTable> = { readonly [Name in keyof Nodes & string
|
|
|
65
160
|
*
|
|
66
161
|
* @example
|
|
67
162
|
* ```ts
|
|
68
|
-
* const options: IsolateOptions<typeof boardFlow.nodes> = {
|
|
163
|
+
* const options: IsolateOptions<typeof boardFlow.nodes, Player> = {
|
|
69
164
|
* flow: boardFlow, shared: sharedLayer, stubs: { settings: stub("closed") }, player: fresh
|
|
70
165
|
* };
|
|
71
166
|
* ```
|
|
72
167
|
*/
|
|
73
|
-
type IsolateOptions<Nodes extends NodeTable> = {
|
|
168
|
+
type IsolateOptions<Nodes extends NodeTable, Player extends Json = Json, Session extends Json = Json> = {
|
|
74
169
|
/** The feature's flow. It becomes the one node of the harness main flow, named by `as`. */flow: AnyFlow & {
|
|
75
170
|
readonly nodes: Nodes;
|
|
76
171
|
};
|
|
@@ -87,9 +182,9 @@ type IsolateOptions<Nodes extends NodeTable> = {
|
|
|
87
182
|
as?: string; /** The shared layer, composed as `logicOnly` before the feature. */
|
|
88
183
|
shared?: FeaturePlugin; /** Node name of `flow` to the stub that replaces it. A node not listed runs for real. */
|
|
89
184
|
stubs?: StubsOf<Nodes>; /** More plugins the feature needs headless, for example a feature-own plugin. */
|
|
90
|
-
plugins?: readonly AnyPluginInstance[]; /** The player a new save starts from. */
|
|
91
|
-
player:
|
|
92
|
-
session?:
|
|
185
|
+
plugins?: readonly AnyPluginInstance[]; /** The player a new save starts from. Its type types `model.store.snapshot().player`. */
|
|
186
|
+
player: Player; /** The session at start. `{}` by default. Its type types `model.store.snapshot().session`. */
|
|
187
|
+
session?: Session; /** The rng seed. `1` by default. */
|
|
93
188
|
seed?: number;
|
|
94
189
|
};
|
|
95
190
|
/**
|
|
@@ -111,6 +206,12 @@ type IsolateSeams = {
|
|
|
111
206
|
* plugins on it.
|
|
112
207
|
*/
|
|
113
208
|
type IsolatedApp = ReturnType<typeof createApp>;
|
|
209
|
+
/**
|
|
210
|
+
* The app of an isolated feature with `model.store.snapshot()` typed by the player and session
|
|
211
|
+
* of the `isolate` call, as `WithGameState` types `game.headless()`. A call whose player and
|
|
212
|
+
* session are `Model.Json` reads a `Model.Json` snapshot, as before. Only the type.
|
|
213
|
+
*/
|
|
214
|
+
type IsolatedAppOf<Player, Session> = WithGameState<IsolatedApp, Player, Session>;
|
|
114
215
|
/**
|
|
115
216
|
* A stub: the replaced node or sub-flow ends at once with this outcome. Written as a sub-flow of
|
|
116
217
|
* one transit node, so a route can still substitute it for one visit:
|
|
@@ -128,14 +229,16 @@ type IsolatedApp = ReturnType<typeof createApp>;
|
|
|
128
229
|
declare function stub<const Name extends string>(outcome: Name, payload?: Json): Stub<Name>;
|
|
129
230
|
/**
|
|
130
231
|
* Composes one feature without the rest of the game: the shared layer and the feature as
|
|
131
|
-
* `logicOnly
|
|
232
|
+
* `logicOnly` (a parent feature is composed with its sub-features), the feature's flow inside a
|
|
233
|
+
* harness main flow `isolated`, and every stubbed node
|
|
132
234
|
* replaced by a sub-flow that ends with the stub's outcome. Every exit of the flow lands on the
|
|
133
235
|
* rest node `exited`, whose intent `again` enters the flow once more. Paths read as in the game:
|
|
134
236
|
* the harness holds the flow under `options.as`, its id by default. A node that is not stubbed
|
|
135
237
|
* runs for real, even when it belongs to a feature that is not composed.
|
|
136
238
|
*
|
|
137
239
|
* @param feature - The feature under test.
|
|
138
|
-
* @param options - Its flow, the shared layer, the stubs, the starting state.
|
|
240
|
+
* @param options - Its flow, the shared layer, the stubs, the starting state. The types of
|
|
241
|
+
* `player` and `session` type the app's `model.store.snapshot()`.
|
|
139
242
|
* @returns A factory of apps, not started; pass one to `createHeadless`.
|
|
140
243
|
* @throws {Error} For a stub key that is not a node of the flow, or an outcome the node does not declare.
|
|
141
244
|
* @example
|
|
@@ -144,11 +247,13 @@ declare function stub<const Name extends string>(outcome: Name, payload?: Json):
|
|
|
144
247
|
* const homeOnly = isolate(homeFeature, {
|
|
145
248
|
* flow: mainFlow, stubs: { info: stub("ok") }, player: startingPlayer, session: startingSession
|
|
146
249
|
* });
|
|
147
|
-
* const
|
|
250
|
+
* const app = homeOnly();
|
|
251
|
+
* const game = await createHeadless(app);
|
|
148
252
|
* const state = await game.walk([{ at: "main/home", intent: "info" }]);
|
|
149
253
|
* state.path; // "main/home"
|
|
254
|
+
* app.model.store.snapshot().player.count; // 0, typed number: startingPlayer types the snapshot
|
|
150
255
|
* ```
|
|
151
256
|
*/
|
|
152
|
-
declare function isolate<Nodes extends NodeTable>(feature: FeaturePlugin, options: IsolateOptions<Nodes>): (seams?: IsolateSeams) =>
|
|
257
|
+
declare function isolate<Nodes extends NodeTable, Player extends Json = Json, Session extends Json = Json>(feature: FeaturePlugin, options: IsolateOptions<Nodes, Player, Session>): (seams?: IsolateSeams) => IsolatedAppOf<Player, Session>;
|
|
153
258
|
//#endregion
|
|
154
|
-
export { type HeadlessApp, type HeadlessGame, type IsolateOptions, type IsolateSeams, type Repro, type ReproResult, type Stub, type StubsOf, createHeadless, fakeClock, isolate, memory, runRepro, saveOf, stepFrames, stub };
|
|
259
|
+
export { type FakeFx, type FakeFxOptions, type HeadlessApp, type HeadlessGame, type IsolateOptions, type IsolateSeams, type Repro, type ReproResult, type Stub, type StubsOf, createHeadless, fakeClock, fakeFx, isolate, memory, runRepro, saveOf, stepFrames, stub };
|
package/dist/testing.mjs
CHANGED
|
@@ -1,7 +1,93 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { $t as refuseStart, en as exit, p as flattenFeatures, rn as type, t as createApp } from "./src-BEbJ_sSx.mjs";
|
|
2
2
|
import { n as saveOf, t as memory } from "./memory-CWlqhS7l.mjs";
|
|
3
|
-
import { t as fakeClock } from "./fake-
|
|
3
|
+
import { t as fakeClock } from "./fake-6mazmHK3.mjs";
|
|
4
4
|
import { i as stepFrames, r as runRepro, t as createHeadless } from "./headless-BA6ffzqD.mjs";
|
|
5
|
+
//#region src/testing/fake-fx.ts
|
|
6
|
+
/** The node path a refused `fx.start` names when the test gives none. */
|
|
7
|
+
const defaultPath = "test";
|
|
8
|
+
/**
|
|
9
|
+
* Builds the `fx` of a node context for a unit test that calls `node.run(context)` itself, with no
|
|
10
|
+
* app and no runner. It records every call and plays no effect: `await fx(descriptor)` goes to
|
|
11
|
+
* `awaited` and resolves with `options.answer(descriptor)`, `fx.start(descriptor)` goes to
|
|
12
|
+
* `started`, `fx.emit(hint)` goes to `emitted`. `fx.start` refuses a descriptor with `answers`
|
|
13
|
+
* and a `guide` with the error the game throws. An `answer` that fails rejects the awaited
|
|
14
|
+
* effect, and resolves `undefined` for a `cosmetic` descriptor, as in the game. There is no
|
|
15
|
+
* handler, no log, no fast mode and no abort signal here: for those, walk the graph with
|
|
16
|
+
* `createHeadless`.
|
|
17
|
+
*
|
|
18
|
+
* @param options - The answer of an awaited effect, and the node path for the refusal message.
|
|
19
|
+
* @returns The `fx` and the lists `awaited`, `started` and `emitted`.
|
|
20
|
+
* @example
|
|
21
|
+
* ```ts
|
|
22
|
+
* // The body of the node "place" runs fx.start(sfx("ui.tap")), then pushes input.cell to
|
|
23
|
+
* // session.marks. `context` is the rest of its context: input, player, session, rng, out,
|
|
24
|
+
* // signal and now.
|
|
25
|
+
* const fx = fakeFx();
|
|
26
|
+
*
|
|
27
|
+
* await place.run?.({ ...context, fx: fx.fx });
|
|
28
|
+
*
|
|
29
|
+
* fx.started; // [{ kind: "sfx", payload: { key: "ui.tap", bus: "sfx" }, cosmetic: true }]
|
|
30
|
+
* fx.awaited; // []: the body waited for no effect
|
|
31
|
+
*
|
|
32
|
+
* // The node "retry" awaits a popup that takes answers. The test gives the player's pick.
|
|
33
|
+
* const picked = fakeFx({
|
|
34
|
+
* answer: descriptor => (descriptor.kind === "popup" ? { intent: "again" } : undefined)
|
|
35
|
+
* });
|
|
36
|
+
*
|
|
37
|
+
* await retry.run?.({ ...context, fx: picked.fx });
|
|
38
|
+
*
|
|
39
|
+
* picked.awaited; // [{ kind: "popup", payload: { name: "Retry" }, answers: ["again", "home"] }]
|
|
40
|
+
* ```
|
|
41
|
+
*/
|
|
42
|
+
function fakeFx(options = {}) {
|
|
43
|
+
const path = options.path ?? defaultPath;
|
|
44
|
+
const awaited = [];
|
|
45
|
+
const started = [];
|
|
46
|
+
const emitted = [];
|
|
47
|
+
/**
|
|
48
|
+
* Records one awaited effect and answers it. Both happen at the call. An `answer` that throws
|
|
49
|
+
* or rejects is a handler that fails: the effect rejects, never the call. A `cosmetic`
|
|
50
|
+
* descriptor swallows that failure and resolves `undefined`, as the gateway of the game does.
|
|
51
|
+
*
|
|
52
|
+
* @param descriptor - The descriptor the body awaits.
|
|
53
|
+
* @returns A promise of what `options.answer` gives for it.
|
|
54
|
+
*/
|
|
55
|
+
const run = async (descriptor) => {
|
|
56
|
+
awaited.push(descriptor);
|
|
57
|
+
try {
|
|
58
|
+
return await options.answer?.(descriptor);
|
|
59
|
+
} catch (error) {
|
|
60
|
+
if (descriptor.cosmetic !== true) throw error;
|
|
61
|
+
}
|
|
62
|
+
};
|
|
63
|
+
const fx = Object.assign(run, {
|
|
64
|
+
/**
|
|
65
|
+
* Records one hint. Nothing releases it: the test has no edge to commit.
|
|
66
|
+
*
|
|
67
|
+
* @param item - The hint.
|
|
68
|
+
*/
|
|
69
|
+
emit: (item) => {
|
|
70
|
+
emitted.push(item);
|
|
71
|
+
},
|
|
72
|
+
/**
|
|
73
|
+
* Records one started effect, unless the real `fx.start` would refuse it.
|
|
74
|
+
*
|
|
75
|
+
* @param descriptor - The descriptor the body starts.
|
|
76
|
+
* @throws {Error} When the descriptor has `answers` or is a `guide`.
|
|
77
|
+
*/
|
|
78
|
+
start: (descriptor) => {
|
|
79
|
+
refuseStart(descriptor, path);
|
|
80
|
+
started.push(descriptor);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
return Object.freeze({
|
|
84
|
+
fx,
|
|
85
|
+
awaited,
|
|
86
|
+
started,
|
|
87
|
+
emitted
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
//#endregion
|
|
5
91
|
//#region src/testing/isolate.ts
|
|
6
92
|
const noPayload = null;
|
|
7
93
|
/** Id of the harness main flow. */
|
|
@@ -149,14 +235,16 @@ function harnessOf(flow, name = flow.id) {
|
|
|
149
235
|
}
|
|
150
236
|
/**
|
|
151
237
|
* Composes one feature without the rest of the game: the shared layer and the feature as
|
|
152
|
-
* `logicOnly
|
|
238
|
+
* `logicOnly` (a parent feature is composed with its sub-features), the feature's flow inside a
|
|
239
|
+
* harness main flow `isolated`, and every stubbed node
|
|
153
240
|
* replaced by a sub-flow that ends with the stub's outcome. Every exit of the flow lands on the
|
|
154
241
|
* rest node `exited`, whose intent `again` enters the flow once more. Paths read as in the game:
|
|
155
242
|
* the harness holds the flow under `options.as`, its id by default. A node that is not stubbed
|
|
156
243
|
* runs for real, even when it belongs to a feature that is not composed.
|
|
157
244
|
*
|
|
158
245
|
* @param feature - The feature under test.
|
|
159
|
-
* @param options - Its flow, the shared layer, the stubs, the starting state.
|
|
246
|
+
* @param options - Its flow, the shared layer, the stubs, the starting state. The types of
|
|
247
|
+
* `player` and `session` type the app's `model.store.snapshot()`.
|
|
160
248
|
* @returns A factory of apps, not started; pass one to `createHeadless`.
|
|
161
249
|
* @throws {Error} For a stub key that is not a node of the flow, or an outcome the node does not declare.
|
|
162
250
|
* @example
|
|
@@ -165,16 +253,18 @@ function harnessOf(flow, name = flow.id) {
|
|
|
165
253
|
* const homeOnly = isolate(homeFeature, {
|
|
166
254
|
* flow: mainFlow, stubs: { info: stub("ok") }, player: startingPlayer, session: startingSession
|
|
167
255
|
* });
|
|
168
|
-
* const
|
|
256
|
+
* const app = homeOnly();
|
|
257
|
+
* const game = await createHeadless(app);
|
|
169
258
|
* const state = await game.walk([{ at: "main/home", intent: "info" }]);
|
|
170
259
|
* state.path; // "main/home"
|
|
260
|
+
* app.model.store.snapshot().player.count; // 0, typed number: startingPlayer types the snapshot
|
|
171
261
|
* ```
|
|
172
262
|
*/
|
|
173
263
|
function isolate(feature, options) {
|
|
174
264
|
const mainFlow = harnessOf(withStubs(options.flow, options.stubs ?? {}), options.as);
|
|
175
265
|
const plugins = [
|
|
176
266
|
...options.shared === void 0 ? [] : [options.shared.logicOnly],
|
|
177
|
-
feature.logicOnly,
|
|
267
|
+
...flattenFeatures([feature.logicOnly]),
|
|
178
268
|
...options.plugins ?? []
|
|
179
269
|
];
|
|
180
270
|
return (seams = {}) => createApp({
|
|
@@ -191,4 +281,4 @@ function isolate(feature, options) {
|
|
|
191
281
|
});
|
|
192
282
|
}
|
|
193
283
|
//#endregion
|
|
194
|
-
export { createHeadless, fakeClock, isolate, memory, runRepro, saveOf, stepFrames, stub };
|
|
284
|
+
export { createHeadless, fakeClock, fakeFx, isolate, memory, runRepro, saveOf, stepFrames, stub };
|