@moku-labs/game 0.7.0 → 0.8.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.
Files changed (34) hide show
  1. package/README.md +5 -4
  2. package/dist/assets.d.mts +1 -1
  3. package/dist/{capture-fpd83Mi5.mjs → capture-BX5Gfoty.mjs} +1 -384
  4. package/dist/{commands-mzE9PX0H.d.mts → commands-C1kIfOG5.d.mts} +6 -4
  5. package/dist/control.d.mts +4 -3
  6. package/dist/control.mjs +2 -2
  7. package/dist/{define-BbgXv4cy.d.mts → define-BPpZcpxW.d.mts} +4 -3
  8. package/dist/headless-CvamCUcR.mjs +186 -0
  9. package/dist/headless-q98SwADR.d.mts +181 -0
  10. package/dist/index.d.mts +23 -22
  11. package/dist/index.mjs +17 -6
  12. package/dist/{input-CK4nxBO1.mjs → input-z_35Jwfn.mjs} +2 -2
  13. package/dist/inspect.d.mts +8 -6
  14. package/dist/inspect.mjs +2 -2
  15. package/dist/jsx-dev-runtime.d.mts +1 -1
  16. package/dist/jsx-runtime.d.mts +1 -1
  17. package/dist/project.d.mts +28 -28
  18. package/dist/project.mjs +7 -7
  19. package/dist/registry-DlpRCibU.mjs +384 -0
  20. package/dist/{run-Bcyg32M5.mjs → run-CUPSFM85.mjs} +4 -187
  21. package/dist/{runtime-HTlmRb81.d.mts → runtime-CvDZZg1S.d.mts} +1 -1
  22. package/dist/{sources-DVWJ8nfr.mjs → sources-BBUd15vQ.mjs} +1 -1
  23. package/dist/testing.d.mts +3 -328
  24. package/dist/testing.mjs +2 -2142
  25. package/dist/{types-BMEn0tSC.d.mts → types-BWzCSRM2.d.mts} +12 -2974
  26. package/dist/{types-CYYKjvLn.d.mts → types-CDSKa0vl.d.mts} +5 -182
  27. package/dist/{types-ClF8say0.d.mts → types-DF5kkW-C.d.mts} +5 -4
  28. package/dist/{types-BaUUcIaN.d.mts → types-DgdMLZnV.d.mts} +8 -7
  29. package/dist/{types-CXyipKhE.d.mts → types-DlgsfOmW.d.mts} +10 -9
  30. package/dist/types-ERb2Y9U5.d.mts +2967 -0
  31. package/dist/visual.d.mts +331 -0
  32. package/dist/visual.mjs +2142 -0
  33. package/llms.txt +6 -5
  34. package/package.json +7 -8
@@ -1,190 +1,7 @@
1
- import { a as sheetAlone, u as graphHash } from "./capture-fpd83Mi5.mjs";
1
+ import { a as sheetAlone } from "./capture-BX5Gfoty.mjs";
2
2
  import { n as isDev, r as notInstalled, t as controlRefused } from "./dev-wKLgOw7c.mjs";
3
- import { a as isProjectionTarget, i as recordCheat, o as readTarget, r as isTainted, s as defineCommand, t as inputOrEmpty } from "./input-CK4nxBO1.mjs";
4
- //#region src/plugins/flow/headless.ts
5
- const noPayload = null;
6
- /** Seed of a repro that pins no rng state, the seed `saveOf` writes. */
7
- const defaultSeed = 1;
8
- /**
9
- * Turns whatever `run()` rejected with into an error that can be thrown again.
10
- *
11
- * @param value - The rejection value.
12
- * @returns The value itself when it is an error, a wrapped one otherwise.
13
- * @example
14
- * ```ts
15
- * asError("offline").message; // "[game] The headless run failed.\n offline."
16
- * ```
17
- */
18
- function asError(value) {
19
- if (value instanceof Error) return value;
20
- return /* @__PURE__ */ new Error(`[game] The headless run failed.\n ${String(value)}.`);
21
- }
22
- /**
23
- * Starts the one loop unless the app's own `onStart` already did, and keeps a fatal error until
24
- * someone asks for it. `run()` rejects only on a fatal error, and nobody awaits it here.
25
- *
26
- * @param app - The started app.
27
- * @returns The holder of the fatal error.
28
- */
29
- function startLoop(app) {
30
- const fatal = { error: void 0 };
31
- if (app.flow.state().running) return fatal;
32
- app.flow.run().catch((error) => {
33
- fatal.error = error;
34
- });
35
- return fatal;
36
- }
37
- /**
38
- * Waits until the loop rests for the first time, or ends. An empty route walks nowhere: it only
39
- * waits for the gate the loop opens at the node it enters, so a start that is a chain of transit
40
- * nodes is played out before anything reads the game.
41
- *
42
- * @param app - The started app whose loop is running.
43
- * @param fatal - Holder of the fatal error of `run()`.
44
- * @returns Resolves once the loop rests at its first rest node.
45
- * @throws {Error} When the loop failed fatally instead of reaching a rest node.
46
- */
47
- async function settleAtFirstRest(app, fatal) {
48
- await app.flow.walk([]).catch((error) => {
49
- throw asError(fatal.error ?? error);
50
- });
51
- if (fatal.error !== void 0) throw asError(fatal.error);
52
- }
53
- /**
54
- * Starts an app headless: sets flow mode `"fast"`, awaits `app.start()`, starts `flow.run()`
55
- * unless the app's own `onStart` already did, and waits until the loop rests for the first time.
56
- * `game.state().path` therefore names the first rest node of the graph as soon as the call
57
- * resolves. A fatal error of the loop rejects this call, and a later one is re-thrown by `walk`
58
- * and by `stop`, so a headless test never loses it.
59
- *
60
- * @param app - An app that is not started yet.
61
- * @returns The game: walk it, answer it, read it, stop it.
62
- * @throws {Error} When the loop failed fatally before it reached its first rest node.
63
- * @example
64
- * ```ts
65
- * // A test starts the merge game without a screen. The call resolves at the first rest node.
66
- * const game = await createHeadless(app);
67
- *
68
- * game.state().path; // "home": the transit node "boot" was played out
69
- * game.state().mode; // "fast"
70
- * await game.stop();
71
- * ```
72
- */
73
- async function createHeadless(app) {
74
- app.flow.setMode("fast");
75
- await app.start();
76
- const fatal = startLoop(app);
77
- await settleAtFirstRest(app, fatal);
78
- return {
79
- walk: async (route) => {
80
- const state = await app.flow.walk(route).catch((error) => {
81
- throw asError(fatal.error ?? error);
82
- });
83
- if (fatal.error !== void 0) throw asError(fatal.error);
84
- return state;
85
- },
86
- answer: (answer) => app.flow.gate.answer(answer),
87
- state: () => app.flow.state(),
88
- history: () => app.flow.history(),
89
- stop: async () => {
90
- await app.stop();
91
- if (fatal.error !== void 0) throw asError(fatal.error);
92
- }
93
- };
94
- }
95
- /**
96
- * Reads the rest node a repro starts at: its checkpoint, or the start of the main flow.
97
- *
98
- * @param app - The started app.
99
- * @param repro - Starting state, optional checkpoint and the route.
100
- * @returns The path of the node the repro enters.
101
- * @throws {Error} When the graph has no main flow to start from.
102
- */
103
- function reproPath(app, repro) {
104
- if (repro.checkpoint !== void 0) return repro.checkpoint;
105
- const graph = app.flow.describe();
106
- const start = graph.flows[graph.main]?.start;
107
- if (start === void 0) throw new Error(`[game] runRepro() found no flow "${graph.main}" to start from.\n Name the checkpoint the repro was taken at.`);
108
- return start;
109
- }
110
- /**
111
- * Builds the bookmark a repro is entered with: its state, its checkpoint and the hash of the
112
- * graph that runs now, so a checkpoint is accepted and any other rest node is checked. The route
113
- * is not part of it: the caller walks it after the restore.
114
- *
115
- * @param app - The started app.
116
- * @param repro - Starting state, optional checkpoint and the route.
117
- * @returns The bookmark to restore.
118
- * @throws {Error} When the repro names no checkpoint and the graph has no main flow.
119
- * @example
120
- * ```ts
121
- * // The game.restore command loads a bug report taken at the "home" checkpoint.
122
- * const bookmark = reproBookmark(app, { player: { coins: 7 }, checkpoint: "home", route: [] });
123
- * bookmark.path; // "home"
124
- * bookmark.rng; // { seed: 1, streams: {} }: the repro pinned no rng
125
- * await app.flow.restore(bookmark);
126
- * ```
127
- */
128
- function reproBookmark(app, repro) {
129
- return {
130
- path: reproPath(app, repro),
131
- input: noPayload,
132
- player: repro.player,
133
- session: repro.session ?? {},
134
- rng: repro.rng ?? {
135
- seed: defaultSeed,
136
- streams: {}
137
- },
138
- graph: graphHash(app.flow.describe())
139
- };
140
- }
141
- /**
142
- * Plays a repro: starts the app headless, restores the bookmark built from its state and
143
- * checkpoint, then walks its route. The app keeps running, so the caller can read more than the
144
- * result and stops it itself.
145
- *
146
- * @param app - An app that is not started yet.
147
- * @param repro - Starting state, optional checkpoint and the route.
148
- * @returns The path the run ended at, and the committed state.
149
- * @example
150
- * ```ts
151
- * // A bug report on the dice game: "reset keeps my coins". Replay it from the "home" checkpoint.
152
- * const reset = { at: "home", intent: "reset" };
153
- * const result = await runRepro(app, { player: { coins: 7 }, checkpoint: "home", route: [reset] });
154
- *
155
- * result.player; // { coins: 0 }; result.path is ["home"]
156
- * await app.stop(); // the app keeps running after the run
157
- * ```
158
- */
159
- async function runRepro(app, repro) {
160
- const game = await createHeadless(app);
161
- await app.flow.restore(reproBookmark(app, repro));
162
- const state = await game.walk(repro.route);
163
- const ended = app.flow.bookmark();
164
- return {
165
- path: state.stack.map((frame) => frame.node),
166
- player: ended.player,
167
- session: ended.session
168
- };
169
- }
170
- /**
171
- * Steps the frame loop by hand: `count` calls of `app.time.step(deltaMs)`. A headless game has no
172
- * frame source, so this is the only thing that moves the frame phases.
173
- *
174
- * @param app - A started app.
175
- * @param count - Number of frames.
176
- * @param deltaMs - Milliseconds per frame.
177
- * @example
178
- * ```ts
179
- * // A headless game has no frame source: the test moves the six frame phases by hand.
180
- * stepFrames(app, 2, 16);
181
- * app.time.snapshot(); // frame: 2, delta: 16, elapsed: 32
182
- * ```
183
- */
184
- function stepFrames(app, count, deltaMs) {
185
- for (let frame = 0; frame < count; frame += 1) app.time.step(deltaMs);
186
- }
187
- //#endregion
3
+ import { n as reproBookmark } from "./headless-CvamCUcR.mjs";
4
+ import { a as isProjectionTarget, i as recordCheat, o as readTarget, r as isTainted, s as defineCommand, t as inputOrEmpty } from "./input-z_35Jwfn.mjs";
188
5
  //#region src/plugins/anim/control.ts
189
6
  /**
190
7
  * @file anim plugin — the anim command of the `/control` door: switch reduced motion. Dev builds
@@ -1282,4 +1099,4 @@ async function run(app, command, ...input) {
1282
1099
  };
1283
1100
  }
1284
1101
  //#endregion
1285
- export { stepFrames as a, runRepro as i, commands as n, createHeadless as r, run as t };
1102
+ export { commands as n, run as t };
@@ -1,4 +1,4 @@
1
- import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-BaUUcIaN.mjs";
1
+ import { a as JsxChild, m as UiIntrinsicElements, t as DescriptionNode } from "./types-DgdMLZnV.mjs";
2
2
 
3
3
  //#region src/plugins/ui/jsx/runtime.d.ts
4
4
  /** What the compiler passes as props: the children plus whatever the tag declared. */
@@ -1,6 +1,6 @@
1
1
  import { i as scaleByFits, w as Layer } from "./visual-DUvvPPCt.mjs";
2
2
  import { r as notInstalled } from "./dev-wKLgOw7c.mjs";
3
- import { c as defineSource, n as cheatsOf, o as readTarget, r as isTainted, t as inputOrEmpty } from "./input-CK4nxBO1.mjs";
3
+ import { c as defineSource, n as cheatsOf, o as readTarget, r as isTainted, t as inputOrEmpty } from "./input-z_35Jwfn.mjs";
4
4
  //#region src/plugins/flow/doors/read.ts
5
5
  /**
6
6
  * @file flow/doors — `read` and `watch`, the `/inspect` side. Neither changes the game.
@@ -1,8 +1,5 @@
1
- import { $t as SaveDoc, Kt as Json, Qt as ProviderCall, Ut as Api$2, Zt as PlayerStateProvider, an as Api$3, k as Api$1, m as Api, pn as FakeClock } from "./types-BMEn0tSC.mjs";
2
- import { _ as stepFrames, a as InputOf, d as HeadlessApp, f as HeadlessGame, g as runRepro, h as createHeadless, m as ReproResult, p as Repro, r as ControlApp } from "./types-CYYKjvLn.mjs";
3
- import { H as InputApi } from "./types-BaUUcIaN.mjs";
4
- import { b as AnimApi, v as UiApi } from "./types-CXyipKhE.mjs";
5
- import { t as commands } from "./commands-mzE9PX0H.mjs";
1
+ import { G as SaveDoc, R as Json, U as PlayerStateProvider, W as ProviderCall, at as FakeClock } from "./types-ERb2Y9U5.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-q98SwADR.mjs";
6
3
 
7
4
  //#region src/plugins/clock/fake.d.ts
8
5
  /**
@@ -73,326 +70,4 @@ declare function memory(fixture?: {
73
70
  */
74
71
  declare function saveOf(player: Json, seed?: number): SaveDoc;
75
72
  //#endregion
76
- //#region src/visual/types.d.ts
77
- /** The base commands of the `/control` door, keyed by their short names. */
78
- type Catalogue = typeof commands;
79
- /**
80
- * The commands a step may name: the catalogue of `/control` minus `bookmark` (it only reads),
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.
83
- *
84
- * @example
85
- * ```ts
86
- * const name: StepCommand = "tap"; // also "answer", "walk", "step", "reducedMotion", ...
87
- * ```
88
- */
89
- type StepCommand = Exclude<keyof Catalogue, "bookmark" | "capture" | "debug">;
90
- /**
91
- * Where a visual test starts: what `game.restore` loads, a repro without its route. The player
92
- * is required; the session, the rng and the checkpoint are optional, as in a repro.
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[];
239
- };
240
- /**
241
- * What a comparison with a baseline file found: the same content, a different one, or no
242
- * baseline (or `--update`), so the file was written.
243
- *
244
- * @example
245
- * ```ts
246
- * const outcome: Outcome = "written"; // the first run of a new checkpoint
247
- * ```
248
- */
249
- type Outcome = "same" | "different" | "written";
250
- /**
251
- * The outcome of one checkpoint. `first` is the JSON path of the first difference, the state's
252
- * before the describe's. `pixels` is `"skipped"` when the pixel leg did not run; `pixelRatio`
253
- * and `verdict` come with a pixel difference.
254
- *
255
- * @example
256
- * ```ts
257
- * // The player's coins differ from the baseline; the layout is the same.
258
- * const result: CheckpointResult = {
259
- * name: "claimed", state: "different", describe: "same", pixels: "skipped", first: "player.coins"
260
- * };
261
- * ```
262
- */
263
- type CheckpointResult = {
264
- name: string;
265
- state: Outcome;
266
- describe: Outcome;
267
- pixels: Outcome | "skipped";
268
- pixelRatio?: number;
269
- first?: string;
270
- verdict?: "rendering" | "behaviour";
271
- };
272
- /**
273
- * The outcome of one visual test: every checkpoint it reached, and the error that ended it early.
274
- *
275
- * @example
276
- * ```ts
277
- * const result: VisualTestResult = {
278
- * name: "reward-popup",
279
- * checkpoints: [{ name: "open", state: "same", describe: "same", pixels: "skipped" }]
280
- * };
281
- * ```
282
- */
283
- type VisualTestResult = {
284
- name: string;
285
- checkpoints: readonly CheckpointResult[];
286
- error?: string;
287
- };
288
- /**
289
- * What `runVisualTests` answers: `ok` is true when no test failed and no outcome is
290
- * `"different"`.
291
- *
292
- * @example
293
- * ```ts
294
- * const report: VisualReport = await runVisualTests(setup, [rewardPopup], { argv: [] });
295
- * process.exitCode = report.ok ? 0 : 1;
296
- * ```
297
- */
298
- type VisualReport = {
299
- ok: boolean;
300
- tests: readonly VisualTestResult[];
301
- };
302
- /** What the command line asked for: the flags it carried, nothing else. */
303
- type VisualFlags = {
304
- dir?: string;
305
- update?: boolean;
306
- pixels?: boolean;
307
- renderer?: VisualRenderer;
308
- only?: readonly string[];
309
- };
310
- //#endregion
311
- //#region src/visual/define.d.ts
312
- /**
313
- * Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
314
- * `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
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
- * ```
335
- */
336
- declare function defineVisualTest(name: string, test: {
337
- start: VisualStart;
338
- steps: readonly VisualStep[];
339
- webgl?: boolean;
340
- }): VisualTest;
341
- //#endregion
342
- //#region src/visual/run.d.ts
343
- /**
344
- * Reads the flags of the visual tests from a command line: `--update`, `--no-pixels`, `--webgl`,
345
- * `--only <name>` (repeatable) and `--dir <path>`. An argument it does not know is left to the
346
- * script that got it, so a test runner's own arguments pass through.
347
- *
348
- * @param argv - The arguments after the script name.
349
- * @returns The flags that were given, and nothing else.
350
- * @throws {Error} When `--only` or `--dir` has no value.
351
- * @example
352
- * ```ts
353
- * // `bun tests/visual/run.ts --update --only reward-popup`
354
- * parseVisualArgv(["--update", "--only", "reward-popup"]); // { update: true, only: ["reward-popup"] }
355
- * parseVisualArgv(["--no-pixels", "--dir", "shots"]); // { pixels: false, dir: "shots" }
356
- * parseVisualArgv(["--webgl"]); // { renderer: "webgl" }
357
- * ```
358
- */
359
- declare function parseVisualArgv(argv: readonly string[]): VisualFlags;
360
- /**
361
- * Runs visual tests: the headless leg over every selected test, then the browser leg over the
362
- * same tests when `pixels` is on (by default only with a `page` on a Mac). With `renderer:
363
- * "webgl"` (`--webgl`) only the tests with `webgl: true` run, the page opens with
364
- * `?renderer=webgl` and its pictures go to `screen.webgl.webp`; `state.json` and `describe.json`
365
- * are shared with the WebGPU leg and compared as always. A checkpoint saves
366
- * `state.json` and `describe.json` in `<dir>/<test>/<checkpoint>/`, and the browser leg
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`.
376
- *
377
- * @param setup - How to build a game: a fresh, unstarted app per test, and the dev page.
378
- * @param tests - Every visual test of the game.
379
- * @param options - Overrides of the flags of `argv` and of the defaults.
380
- * @returns The report: `ok` when no test failed and nothing is `"different"`.
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`.
386
- * @example
387
- * ```ts
388
- * // tests/visual/run.ts of a game, `bun tests/visual/run.ts --update` on a Mac
389
- * const report = await runVisualTests(
390
- * { app: () => createScreenGame().app, page: { url: "http://localhost:3000/" } },
391
- * [rewardPopup]
392
- * );
393
- * report.tests[0]?.checkpoints[0]; // { name: "open", state: "written", describe: "written", pixels: "written" }
394
- * ```
395
- */
396
- declare function runVisualTests(setup: VisualSetup, tests: readonly VisualTest[], options?: VisualOptions): Promise<VisualReport>;
397
- //#endregion
398
- export { type CheckpointResult, type HeadlessApp, type HeadlessGame, type Repro, type ReproResult, type VisualApp, type VisualOptions, type VisualPage, type VisualRenderer, type VisualReport, type VisualSetup, type VisualStart, type VisualStep, type VisualTest, type VisualTestResult, type VisualTolerance, createHeadless, defineVisualTest, fakeClock, memory, parseVisualArgv, runRepro, runVisualTests, saveOf, stepFrames };
73
+ export { type HeadlessApp, type HeadlessGame, type Repro, type ReproResult, createHeadless, fakeClock, memory, runRepro, saveOf, stepFrames };