@moku-labs/game 0.7.1 → 0.9.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/README.md +12 -8
- package/dist/assets.d.mts +97 -33
- package/dist/assets.mjs +350 -110
- package/dist/{commands-mzE9PX0H.d.mts → commands-DuwCxK5t.d.mts} +5 -4
- package/dist/control.d.mts +4 -3
- package/dist/control.mjs +2 -2
- package/dist/{define-BbgXv4cy.d.mts → define-B6pHpdBi.d.mts} +4 -3
- package/dist/headless-BsNdJs9W.mjs +186 -0
- package/dist/headless-SCQXJxQf.d.mts +181 -0
- package/dist/hot.d.mts +4 -2
- package/dist/hot.mjs +8 -6
- package/dist/index-BO9kZzY3.d.mts +1611 -0
- package/dist/index.d.mts +6 -1596
- package/dist/index.mjs +4 -34334
- package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
- package/dist/inspect.d.mts +7 -6
- 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 +55 -4
- package/dist/lint.mjs +993 -40
- package/dist/project.d.mts +57 -41
- package/dist/project.mjs +994 -505
- package/dist/{run-Bcyg32M5.mjs → run-CofJEdYx.mjs} +4 -187
- package/dist/{runtime-HTlmRb81.d.mts → runtime-CdNJ0UBm.d.mts} +1 -1
- package/dist/{sources-DVWJ8nfr.mjs → sources-DNnzyCYf.mjs} +2 -2
- package/dist/src-536arXjV.mjs +34644 -0
- package/dist/testing.d.mts +85 -283
- package/dist/testing.mjs +158 -2109
- package/dist/{types-BMEn0tSC.d.mts → types-B0v5VVa3.d.mts} +10 -9
- package/dist/{types-ClF8say0.d.mts → types-CQueODzo.d.mts} +2 -2
- package/dist/{types-CYYKjvLn.d.mts → types-DUMoYPYe.d.mts} +5 -182
- package/dist/{types-CXyipKhE.d.mts → types-VZVWwqCg.d.mts} +42 -4
- package/dist/{types-BaUUcIaN.d.mts → types-xXfyk56Q.d.mts} +1 -1
- package/dist/{visual-DUvvPPCt.mjs → visual-dMW8iqAE.mjs} +8 -4
- package/dist/visual.d.mts +330 -0
- package/dist/visual.mjs +2142 -0
- package/llms.txt +59 -21
- package/package.json +7 -8
- package/dist/memory-CvgdnsQO.mjs +0 -259
package/dist/testing.d.mts
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import { $t as
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { t as commands } from "./commands-mzE9PX0H.mjs";
|
|
1
|
+
import { $t as PlayerStateProvider, Et as AnyFlow, Jt as Json, Mt as NodeTable, en as ProviderCall, gn as FakeClock, mn as ClockSource, mt as FeaturePlugin, tn as SaveDoc } from "./types-B0v5VVa3.mjs";
|
|
2
|
+
import { a as createHeadless, i as ReproResult, n as HeadlessGame, o as runRepro, r as Repro, s as stepFrames, t as HeadlessApp } from "./headless-SCQXJxQf.mjs";
|
|
3
|
+
import { t as createApp } from "./index-BO9kZzY3.mjs";
|
|
4
|
+
import { AnyPluginInstance } from "@moku-labs/core";
|
|
6
5
|
|
|
7
6
|
//#region src/plugins/clock/fake.d.ts
|
|
8
7
|
/**
|
|
@@ -73,326 +72,129 @@ declare function memory(fixture?: {
|
|
|
73
72
|
*/
|
|
74
73
|
declare function saveOf(player: Json, seed?: number): SaveDoc;
|
|
75
74
|
//#endregion
|
|
76
|
-
//#region src/
|
|
77
|
-
/** The base commands of the `/control` door, keyed by their short names. */
|
|
78
|
-
type Catalogue = typeof commands;
|
|
75
|
+
//#region src/testing/isolate.d.ts
|
|
79
76
|
/**
|
|
80
|
-
*
|
|
81
|
-
* `capture` (the runner captures by itself) and `debug` (debug drawing would enter a baseline). A
|
|
82
|
-
* command the catalogue gains is a step with no change to the runner.
|
|
77
|
+
* A stub for one node or sub-flow of the isolated flow: the outcome it ends with, and its payload.
|
|
83
78
|
*
|
|
84
79
|
* @example
|
|
85
80
|
* ```ts
|
|
86
|
-
* const
|
|
81
|
+
* const closed: Stub<"closed"> = stub("closed"); // { kind: "stub", outcome: "closed" }
|
|
87
82
|
* ```
|
|
88
83
|
*/
|
|
89
|
-
type
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
*
|
|
94
|
-
* @example
|
|
95
|
-
* ```ts
|
|
96
|
-
* // A save with a plank ready for the first order, entered at Home.
|
|
97
|
-
* const start: VisualStart = { player: { coins: 40, orders: [] }, checkpoint: "home" };
|
|
98
|
-
* ```
|
|
99
|
-
*/
|
|
100
|
-
type VisualStart = Omit<Repro, "route">;
|
|
101
|
-
/**
|
|
102
|
-
* One step of a visual test: a `/control` command by its short name with that command's input,
|
|
103
|
-
* typed from the catalogue, or a checkpoint by its name.
|
|
104
|
-
*
|
|
105
|
-
* @example
|
|
106
|
-
* ```ts
|
|
107
|
-
* // Tap Play, answer the order on the board, then compare the screen with its baseline.
|
|
108
|
-
* const steps: VisualStep[] = [
|
|
109
|
-
* { tap: { key: "play" } },
|
|
110
|
-
* { answer: { intent: "deliver", payload: { orderId: "o1" } } },
|
|
111
|
-
* { checkpoint: "open" }
|
|
112
|
-
* ];
|
|
113
|
-
* ```
|
|
114
|
-
*/
|
|
115
|
-
type VisualStep = {
|
|
116
|
-
checkpoint: string;
|
|
117
|
-
} | { [K in StepCommand]: { [P in K]: InputOf<Catalogue[K]["input"]> } }[StepCommand];
|
|
118
|
-
/**
|
|
119
|
-
* A visual test as `defineVisualTest` answers it: its name, where it starts, its steps, and
|
|
120
|
-
* `webgl`, true when it also runs in the WebGL leg. Frozen data; nothing runs at definition.
|
|
121
|
-
*
|
|
122
|
-
* @example
|
|
123
|
-
* ```ts
|
|
124
|
-
* const test: VisualTest = defineVisualTest("reward-popup", {
|
|
125
|
-
* start: { player: { coins: 40 }, checkpoint: "home" },
|
|
126
|
-
* steps: [{ tap: { key: "play" } }, { checkpoint: "board" }]
|
|
127
|
-
* });
|
|
128
|
-
* test.name; // "reward-popup"
|
|
129
|
-
* test.webgl; // false
|
|
130
|
-
* Object.isFrozen(test.steps); // true
|
|
131
|
-
* ```
|
|
132
|
-
*/
|
|
133
|
-
type VisualTest = {
|
|
134
|
-
readonly name: string;
|
|
135
|
-
readonly start: Readonly<VisualStart>;
|
|
136
|
-
readonly steps: readonly VisualStep[];
|
|
137
|
-
readonly webgl: boolean;
|
|
138
|
-
};
|
|
139
|
-
/**
|
|
140
|
-
* What the runner needs of an app: the control app, the plugins the base commands act on and
|
|
141
|
-
* the plugins the readers read. Every app composed with the screen set fits.
|
|
142
|
-
*
|
|
143
|
-
* @example
|
|
144
|
-
* ```ts
|
|
145
|
-
* const app: VisualApp = createApp({ plugins: [...screen, boardFeature], pluginConfigs });
|
|
146
|
-
* ```
|
|
147
|
-
*/
|
|
148
|
-
type VisualApp = ControlApp & {
|
|
149
|
-
readonly anim: AnimApi;
|
|
150
|
-
readonly ui: UiApi;
|
|
151
|
-
readonly renderer: Api;
|
|
152
|
-
readonly world: Api$1;
|
|
153
|
-
readonly model: Api$2;
|
|
154
|
-
readonly input: InputApi;
|
|
155
|
-
readonly lifecycle: Api$3;
|
|
156
|
-
};
|
|
157
|
-
/**
|
|
158
|
-
* The dev page the browser leg opens: its URL, the viewport width in CSS px (390 by default), the
|
|
159
|
-
* device scale (2 by default) and which Chrome to launch. The page height follows the inert frame
|
|
160
|
-
* of the renderer: 520 for a 390 wide portrait game at the default 4:3. Without `browser` the leg
|
|
161
|
-
* launches Playwright's own Chromium, and the system Chrome when that one is not installed.
|
|
162
|
-
*
|
|
163
|
-
* @example
|
|
164
|
-
* ```ts
|
|
165
|
-
* // The fixture's dev server, a 390 px wide phone at 2x, the system Chrome.
|
|
166
|
-
* const page: VisualPage = { url: "http://localhost:3000/", browser: { channel: "chrome" } };
|
|
167
|
-
* ```
|
|
168
|
-
*/
|
|
169
|
-
type VisualPage = {
|
|
170
|
-
url: string;
|
|
171
|
-
width?: number;
|
|
172
|
-
deviceScaleFactor?: number;
|
|
173
|
-
browser?: {
|
|
174
|
-
channel?: string;
|
|
175
|
-
executablePath?: string;
|
|
176
|
-
};
|
|
177
|
-
};
|
|
178
|
-
/**
|
|
179
|
-
* How the runner builds a game: a fresh app per test, not started, with the screen set
|
|
180
|
-
* composed; the renderer stays inert in Bun. `page` is the dev page of the pixel leg.
|
|
181
|
-
*
|
|
182
|
-
* @example
|
|
183
|
-
* ```ts
|
|
184
|
-
* // tests/visual/run.ts of a game.
|
|
185
|
-
* const setup: VisualSetup = { app: () => createScreenGame().app, page: { url: "http://localhost:3000/" } };
|
|
186
|
-
* ```
|
|
187
|
-
*/
|
|
188
|
-
type VisualSetup = {
|
|
189
|
-
app: () => VisualApp;
|
|
190
|
-
page?: VisualPage;
|
|
191
|
-
};
|
|
192
|
-
/**
|
|
193
|
-
* How far a pixel may differ: `threshold` is the largest channel delta that still counts as the
|
|
194
|
-
* same pixel, `ratio` the share of differing pixels a checkpoint tolerates.
|
|
195
|
-
*
|
|
196
|
-
* @example
|
|
197
|
-
* ```ts
|
|
198
|
-
* const tolerance: VisualTolerance = { ratio: 0.001, threshold: 24 }; // the defaults
|
|
199
|
-
* ```
|
|
200
|
-
*/
|
|
201
|
-
type VisualTolerance = {
|
|
202
|
-
ratio: number;
|
|
203
|
-
threshold: number;
|
|
204
|
-
};
|
|
205
|
-
/**
|
|
206
|
-
* The backend the browser leg draws with: `"webgpu"`, or `"webgl"` for the WebGL leg.
|
|
207
|
-
*
|
|
208
|
-
* @example
|
|
209
|
-
* ```ts
|
|
210
|
-
* const renderer: VisualRenderer = "webgl"; // what --webgl asks for
|
|
211
|
-
* ```
|
|
212
|
-
*/
|
|
213
|
-
type VisualRenderer = "webgpu" | "webgl";
|
|
214
|
-
/**
|
|
215
|
-
* The options of a run. Each one left out comes from the command line (`argv`), then from its
|
|
216
|
-
* default: `dir` `"tests/visual"`, `update` false, `pixels` true with a `page` on a Mac,
|
|
217
|
-
* `renderer` `"webgpu"`, `settleFrames` 600, `tolerance` `{ ratio: 0.001, threshold: 24 }`,
|
|
218
|
-
* `argv` `process.argv.slice(2)`. An option given here wins over a flag. `renderer: "webgl"`
|
|
219
|
-
* runs only the tests with `webgl: true`, on the page with `?renderer=webgl`, and compares their
|
|
220
|
-
* pictures with `screen.webgl.webp`.
|
|
221
|
-
*
|
|
222
|
-
* @example
|
|
223
|
-
* ```ts
|
|
224
|
-
* // The same as `bun tests/visual/run.ts --only reward-popup --no-pixels`.
|
|
225
|
-
* const options: VisualOptions = { only: ["reward-popup"], pixels: false };
|
|
226
|
-
* // The same as `bun tests/visual/run.ts --webgl`.
|
|
227
|
-
* const webgl: VisualOptions = { renderer: "webgl" };
|
|
228
|
-
* ```
|
|
229
|
-
*/
|
|
230
|
-
type VisualOptions = {
|
|
231
|
-
dir?: string;
|
|
232
|
-
update?: boolean;
|
|
233
|
-
pixels?: boolean;
|
|
234
|
-
renderer?: VisualRenderer;
|
|
235
|
-
only?: readonly string[];
|
|
236
|
-
settleFrames?: number;
|
|
237
|
-
tolerance?: VisualTolerance;
|
|
238
|
-
argv?: readonly string[];
|
|
84
|
+
type Stub<Name extends string = string> = {
|
|
85
|
+
readonly kind: "stub";
|
|
86
|
+
readonly outcome: Name;
|
|
87
|
+
readonly payload?: Json;
|
|
239
88
|
};
|
|
240
89
|
/**
|
|
241
|
-
*
|
|
242
|
-
*
|
|
243
|
-
*
|
|
244
|
-
* @example
|
|
245
|
-
* ```ts
|
|
246
|
-
* const outcome: Outcome = "written"; // the first run of a new checkpoint
|
|
247
|
-
* ```
|
|
90
|
+
* The outcome names of one entry of a node table. Distributes over a union: for a flow widened
|
|
91
|
+
* to `Flow.AnyFlow` an entry is a node, a sub-flow or a slot, and every outcome name is allowed,
|
|
92
|
+
* not only the slot's `done`.
|
|
248
93
|
*/
|
|
249
|
-
type
|
|
94
|
+
type OutcomeNameOf<Entry> = Entry extends {
|
|
95
|
+
readonly outcomes: infer Tags;
|
|
96
|
+
} ? keyof Tags & string : never;
|
|
250
97
|
/**
|
|
251
|
-
*
|
|
252
|
-
*
|
|
253
|
-
*
|
|
98
|
+
* One optional stub per node of the flow. The outcome must be one the replaced entry declares:
|
|
99
|
+
* a wrong one and an unknown key are compile errors, and `isolate()` refuses them at run time too.
|
|
100
|
+
* A flow widened to `Flow.AnyFlow` takes every key and every outcome.
|
|
254
101
|
*
|
|
255
102
|
* @example
|
|
256
103
|
* ```ts
|
|
257
|
-
* //
|
|
258
|
-
* const
|
|
259
|
-
* name: "claimed", state: "different", describe: "same", pixels: "skipped", first: "player.coins"
|
|
260
|
-
* };
|
|
104
|
+
* // boardFlow holds settings: settingsFlow (outcome closed) and energy (outcomes watch, later).
|
|
105
|
+
* const stubs: StubsOf<typeof boardFlow.nodes> = { settings: stub("closed"), energy: stub("later") };
|
|
261
106
|
* ```
|
|
262
107
|
*/
|
|
263
|
-
type
|
|
264
|
-
name: string;
|
|
265
|
-
state: Outcome;
|
|
266
|
-
describe: Outcome;
|
|
267
|
-
pixels: Outcome | "skipped";
|
|
268
|
-
pixelRatio?: number;
|
|
269
|
-
first?: string;
|
|
270
|
-
verdict?: "rendering" | "behaviour";
|
|
271
|
-
};
|
|
108
|
+
type StubsOf<Nodes extends NodeTable> = { readonly [Name in keyof Nodes & string]?: Stub<OutcomeNameOf<Nodes[Name]>> };
|
|
272
109
|
/**
|
|
273
|
-
*
|
|
110
|
+
* What `isolate` composes around one feature.
|
|
274
111
|
*
|
|
275
112
|
* @example
|
|
276
113
|
* ```ts
|
|
277
|
-
* const
|
|
278
|
-
*
|
|
279
|
-
* checkpoints: [{ name: "open", state: "same", describe: "same", pixels: "skipped" }]
|
|
114
|
+
* const options: IsolateOptions<typeof boardFlow.nodes> = {
|
|
115
|
+
* flow: boardFlow, shared: sharedLayer, stubs: { settings: stub("closed") }, player: fresh
|
|
280
116
|
* };
|
|
281
117
|
* ```
|
|
282
118
|
*/
|
|
283
|
-
type
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
119
|
+
type IsolateOptions<Nodes extends NodeTable> = {
|
|
120
|
+
/** The feature's flow. It becomes the one node of the harness main flow, named by `as`. */flow: AnyFlow & {
|
|
121
|
+
readonly nodes: Nodes;
|
|
122
|
+
};
|
|
123
|
+
/**
|
|
124
|
+
* The key the game's main flow holds the flow under. `flow.id` by default.
|
|
125
|
+
*
|
|
126
|
+
* @example
|
|
127
|
+
* ```ts
|
|
128
|
+
* // The mini game's main flow holds info: infoFlow, whose id is "infoPopup".
|
|
129
|
+
* isolate(infoFeature, { flow: infoFlow, as: "info", player: { count: 0 } });
|
|
130
|
+
* // paths read "info/show", as in the game
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
as?: string; /** The shared layer, composed as `logicOnly` before the feature. */
|
|
134
|
+
shared?: FeaturePlugin; /** Node name of `flow` to the stub that replaces it. A node not listed runs for real. */
|
|
135
|
+
stubs?: StubsOf<Nodes>; /** More plugins the feature needs headless, for example a feature-own plugin. */
|
|
136
|
+
plugins?: readonly AnyPluginInstance[]; /** The player a new save starts from. */
|
|
137
|
+
player: Json; /** The session at start. `{}` by default. */
|
|
138
|
+
session?: Json; /** The rng seed. `1` by default. */
|
|
139
|
+
seed?: number;
|
|
287
140
|
};
|
|
288
141
|
/**
|
|
289
|
-
* What
|
|
290
|
-
* `"different"`.
|
|
142
|
+
* What one app of an isolated feature may pin over the options.
|
|
291
143
|
*
|
|
292
144
|
* @example
|
|
293
145
|
* ```ts
|
|
294
|
-
* const
|
|
295
|
-
* process.exitCode = report.ok ? 0 : 1;
|
|
146
|
+
* const game = await createHeadless(boardOnly({ player: { ...fresh, coins: 7 }, clock: fakeClock(1_000_000) }));
|
|
296
147
|
* ```
|
|
297
148
|
*/
|
|
298
|
-
type
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
type VisualFlags = {
|
|
304
|
-
dir?: string;
|
|
305
|
-
update?: boolean;
|
|
306
|
-
pixels?: boolean;
|
|
307
|
-
renderer?: VisualRenderer;
|
|
308
|
-
only?: readonly string[];
|
|
149
|
+
type IsolateSeams = {
|
|
150
|
+
/** The player this app starts from, instead of `options.player`. */player?: Json; /** The session this app starts with, instead of `options.session`. */
|
|
151
|
+
session?: Json; /** The rng seed of this app, instead of `options.seed`. */
|
|
152
|
+
seed?: number; /** The clock source of this app, `fakeClock()` in a test. The system clock by default. */
|
|
153
|
+
clock?: ClockSource;
|
|
309
154
|
};
|
|
310
|
-
//#endregion
|
|
311
|
-
//#region src/visual/define.d.ts
|
|
312
155
|
/**
|
|
313
|
-
*
|
|
314
|
-
*
|
|
315
|
-
* where the runner saves `state.json` and `describe.json` next to the test. `walk` runs fast: a
|
|
316
|
-
* popup its last node opens never mounts, so the step before a checkpoint that shows a node's
|
|
317
|
-
* result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
|
|
318
|
-
* (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
|
|
319
|
-
* The answer is a frozen copy; nothing runs here.
|
|
320
|
-
*
|
|
321
|
-
* @param name - The test name, also its folder: lowercase letters, digits and dashes.
|
|
322
|
-
* @param test - Where it starts, its steps and its WebGL leg.
|
|
323
|
-
* @param test.start - The player, and optionally the session, the rng and the checkpoint.
|
|
324
|
-
* @param test.steps - The commands and the checkpoints, in order.
|
|
325
|
-
* @param test.webgl - True to run the test in the WebGL leg too; false by default.
|
|
326
|
-
* @returns The test, frozen.
|
|
327
|
-
* @throws {Error} When the name or a checkpoint name is not a file name, a checkpoint name is
|
|
328
|
-
* used twice, or a step is not one step command.
|
|
329
|
-
* @example
|
|
330
|
-
* ```ts
|
|
331
|
-
* // tests/visual/home.visual.ts: Home with the glowing Play plank, in both legs.
|
|
332
|
-
* const home = defineVisualTest("home", { start: atHome, steps: [{ checkpoint: "rest" }], webgl: true });
|
|
333
|
-
* home.webgl; // true
|
|
334
|
-
* ```
|
|
156
|
+
* An app of an isolated feature, as `createApp` returns it: not started, the engine's logic
|
|
157
|
+
* plugins on it.
|
|
335
158
|
*/
|
|
336
|
-
|
|
337
|
-
start: VisualStart;
|
|
338
|
-
steps: readonly VisualStep[];
|
|
339
|
-
webgl?: boolean;
|
|
340
|
-
}): VisualTest;
|
|
341
|
-
//#endregion
|
|
342
|
-
//#region src/visual/run.d.ts
|
|
159
|
+
type IsolatedApp = ReturnType<typeof createApp>;
|
|
343
160
|
/**
|
|
344
|
-
*
|
|
345
|
-
*
|
|
346
|
-
*
|
|
161
|
+
* A stub: the replaced node or sub-flow ends at once with this outcome. Written as a sub-flow of
|
|
162
|
+
* one transit node, so a route can still substitute it for one visit:
|
|
163
|
+
* `{ at: "board/energy", result: { outcome: "watch" } }`.
|
|
347
164
|
*
|
|
348
|
-
* @param
|
|
349
|
-
* @
|
|
350
|
-
* @
|
|
165
|
+
* @param outcome - An outcome the replaced entry declares.
|
|
166
|
+
* @param payload - The payload it carries, for an outcome with data.
|
|
167
|
+
* @returns The stub as plain data.
|
|
351
168
|
* @example
|
|
352
169
|
* ```ts
|
|
353
|
-
* //
|
|
354
|
-
*
|
|
355
|
-
* parseVisualArgv(["--no-pixels", "--dir", "shots"]); // { pixels: false, dir: "shots" }
|
|
356
|
-
* parseVisualArgv(["--webgl"]); // { renderer: "webgl" }
|
|
170
|
+
* stub("closed"); // { kind: "stub", outcome: "closed" }
|
|
171
|
+
* stub("orderComplete", { rewardId: "r1" }); // { kind: "stub", outcome: "orderComplete", payload: { rewardId: "r1" } }
|
|
357
172
|
* ```
|
|
358
173
|
*/
|
|
359
|
-
declare function
|
|
174
|
+
declare function stub<const Name extends string>(outcome: Name, payload?: Json): Stub<Name>;
|
|
360
175
|
/**
|
|
361
|
-
*
|
|
362
|
-
*
|
|
363
|
-
*
|
|
364
|
-
*
|
|
365
|
-
*
|
|
366
|
-
*
|
|
367
|
-
* `screen.webp`, lossless: a missing file is written, `--update` rewrites every file of the tests
|
|
368
|
-
* run, a JSON file is compared exactly. The browser leg opens the dev page in Chrome with WebGPU
|
|
369
|
-
* through `playwright-core`, pauses its game, plays each test there on stepped frames of 1000/60
|
|
370
|
-
* ms, so two runs see the same game time, checks the page's state against `state.json` and
|
|
371
|
-
* compares the picture in the page: a pixel differs above `tolerance.threshold`, and the screen
|
|
372
|
-
* differs when more than `tolerance.ratio` of its pixels do; `screen.actual.webp` and
|
|
373
|
-
* `screen.diff.webp` are written beside the baseline then. A pixel difference over the same state
|
|
374
|
-
* and describe is a rendering regression (`verdict: "rendering"`). One line per checkpoint goes
|
|
375
|
-
* to the branded console; the script sets the exit code from `ok`.
|
|
176
|
+
* Composes one feature without the rest of the game: the shared layer and the feature as
|
|
177
|
+
* `logicOnly`, the feature's flow inside a harness main flow `isolated`, and every stubbed node
|
|
178
|
+
* replaced by a sub-flow that ends with the stub's outcome. Every exit of the flow lands on the
|
|
179
|
+
* rest node `exited`, whose intent `again` enters the flow once more. Paths read as in the game:
|
|
180
|
+
* the harness holds the flow under `options.as`, its id by default. A node that is not stubbed
|
|
181
|
+
* runs for real, even when it belongs to a feature that is not composed.
|
|
376
182
|
*
|
|
377
|
-
* @param
|
|
378
|
-
* @param
|
|
379
|
-
* @
|
|
380
|
-
* @
|
|
381
|
-
* @throws {Error} When a test is not well formed, two tests share a name, `only` names no test,
|
|
382
|
-
* a flag misses its value, or the browser leg fails as a whole: no `page`, no
|
|
383
|
-
* `playwright-core`, no Chrome, or a page that does not open, expose `game` and `doors`, start,
|
|
384
|
-
* draw with the requested backend or stop loading. In the WebGL leg, also when `only` names a
|
|
385
|
-
* test without `webgl: true`.
|
|
183
|
+
* @param feature - The feature under test.
|
|
184
|
+
* @param options - Its flow, the shared layer, the stubs, the starting state.
|
|
185
|
+
* @returns A factory of apps, not started; pass one to `createHeadless`.
|
|
186
|
+
* @throws {Error} For a stub key that is not a node of the flow, or an outcome the node does not declare.
|
|
386
187
|
* @example
|
|
387
188
|
* ```ts
|
|
388
|
-
* // tests/
|
|
389
|
-
* const
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* );
|
|
393
|
-
*
|
|
189
|
+
* // tests/fixtures/mini-game: Home alone; its info popup is stubbed to end with ok at once.
|
|
190
|
+
* const homeOnly = isolate(homeFeature, {
|
|
191
|
+
* flow: mainFlow, stubs: { info: stub("ok") }, player: startingPlayer, session: startingSession
|
|
192
|
+
* });
|
|
193
|
+
* const game = await createHeadless(homeOnly());
|
|
194
|
+
* const state = await game.walk([{ at: "main/home", intent: "info" }]);
|
|
195
|
+
* state.path; // "main/home"
|
|
394
196
|
* ```
|
|
395
197
|
*/
|
|
396
|
-
declare function
|
|
198
|
+
declare function isolate<Nodes extends NodeTable>(feature: FeaturePlugin, options: IsolateOptions<Nodes>): (seams?: IsolateSeams) => IsolatedApp;
|
|
397
199
|
//#endregion
|
|
398
|
-
export { type
|
|
200
|
+
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 };
|