@moku-labs/game 0.15.31 → 0.15.33
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/dist/app/page.d.mts +1 -1
- package/dist/app/page.mjs +17 -2
- package/dist/app/system.d.mts +2 -2
- package/dist/app.d.mts +2 -2
- package/dist/app.mjs +2 -2
- package/dist/{commands-C8N_rpwt.d.mts → commands-C1gTg70a.d.mts} +8 -8
- package/dist/control.d.mts +1 -1
- package/dist/control.mjs +1 -1
- package/dist/{fake-6mazmHK3.mjs → fake-hnJcJeYf.mjs} +7 -1
- package/dist/{index-DMeqKGSu.d.mts → index-ChuOBsZB.d.mts} +2 -2
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +1 -1
- package/dist/inspect.d.mts +4 -4
- package/dist/inspect.mjs +1 -1
- package/dist/jsx-dev-runtime.d.mts +1 -1
- package/dist/jsx-runtime.d.mts +1 -1
- package/dist/{run-BnriyKUu.mjs → run-G4m1APMv.mjs} +3 -12
- package/dist/{runtime-HwTBjkIL.d.mts → runtime-CgJuBIYZ.d.mts} +1 -1
- package/dist/{sources-DM5Zcl4o.mjs → sources-MfRpM8sV.mjs} +2 -5
- package/dist/{src-5qdNjLKN.mjs → src-D5Nyvdhu.mjs} +388 -36
- package/dist/testing.d.mts +2 -2
- package/dist/testing.mjs +2 -2
- package/dist/{types-DHamyZq1.d.mts → types-BqfwZ_lM.d.mts} +1 -1
- package/dist/{types-DmU_qlvA.d.mts → types-CF6CJ55K.d.mts} +3 -3
- package/dist/{types-BAp9HdR4.d.mts → types-DGHVGP5i.d.mts} +33 -25
- package/dist/{visual-HGn_TF2x.d.mts → visual-C2HHHs46.d.mts} +15 -8
- package/dist/visual.d.mts +1 -1
- package/dist/visual.mjs +92 -31
- package/llms.txt +2 -2
- package/package.json +1 -1
|
@@ -2502,9 +2502,12 @@ type ElementChange<Value> = (view: ViewHandle<unknown>, previous: Value, next: V
|
|
|
2502
2502
|
/**
|
|
2503
2503
|
* The motion hooks of one element: the hook triple of `world.projection`, which `defineMotion`
|
|
2504
2504
|
* of `anim` returns. `ui` plays the `change` hooks of `Box` (the rects), of `Transform` (the rest
|
|
2505
|
-
* poses)
|
|
2506
|
-
*
|
|
2507
|
-
*
|
|
2505
|
+
* poses), of the visual the element is drawn with (`Shape`, `Sprite`, `NineSlice` or `Text`: its
|
|
2506
|
+
* values before and after, when a render or a restyle moves a number of it) and of every extra
|
|
2507
|
+
* component of the `components` prop (its values before and after). A hook for any other
|
|
2508
|
+
* component name is accepted, so every `defineMotion` result fits; `ui` never plays it, and a dev
|
|
2509
|
+
* build names it once (`ui:change-never-plays`). Every hook gets the hint routed to the element's
|
|
2510
|
+
* key in that frame step.
|
|
2508
2511
|
*
|
|
2509
2512
|
* @example
|
|
2510
2513
|
* ```ts
|
|
@@ -2648,27 +2651,29 @@ type StyleBasis = {
|
|
|
2648
2651
|
};
|
|
2649
2652
|
/**
|
|
2650
2653
|
* One live element: the entity, the Yoga node, the resolved style and the place in the tree.
|
|
2651
|
-
* `rect` is natural: under a `fit` ancestor it is the rect before that ancestor's scale. `fit`
|
|
2652
|
-
*
|
|
2653
|
-
*
|
|
2654
|
-
*
|
|
2655
|
-
*
|
|
2656
|
-
* `change` hook of each of them returned; `warnedOwned` the names of the
|
|
2657
|
-
* element owns that were already logged. A windowed scroll carries its
|
|
2658
|
-
* warnings of its props in `warned`; `scrolledIn` marks an element a
|
|
2659
|
-
* scroll created: it plays no `enter` hook. A scroll carries its
|
|
2660
|
-
* `
|
|
2661
|
-
*
|
|
2662
|
-
*
|
|
2663
|
-
*
|
|
2664
|
-
*
|
|
2665
|
-
*
|
|
2666
|
-
*
|
|
2667
|
-
*
|
|
2668
|
-
* the
|
|
2669
|
-
*
|
|
2670
|
-
*
|
|
2671
|
-
*
|
|
2654
|
+
* `rect` is natural: under a `fit` ancestor it is the rect before that ancestor's scale. `fit` is
|
|
2655
|
+
* the element's own fit scale (1 without `fit: "contain"`), and `rest` the rest `Transform` last
|
|
2656
|
+
* written for it. `loop` is the motion of the running `loop` hook, kept apart from `handles`: it
|
|
2657
|
+
* never ends, so the exit sweep must not wait for it. `extras` holds the values of the
|
|
2658
|
+
* `components` prop added last time, by component name; `extraHandles` the motion the last
|
|
2659
|
+
* `change` hook of each of them and of the visual returned; `warnedOwned` the names of the
|
|
2660
|
+
* `components` values the element owns that were already logged. A windowed scroll carries its
|
|
2661
|
+
* `window` and the one-time warnings of its props in `warned`; `scrolledIn` marks an element a
|
|
2662
|
+
* range change of a windowed scroll created: it plays no `enter` hook. A scroll carries its
|
|
2663
|
+
* `scroll` motion from its enter. A text or an icon that sizes itself keeps in `measuredSize` what
|
|
2664
|
+
* its measure function last answered, before Yoga's clamp: a bound text whose new string measures
|
|
2665
|
+
* the same asks for no solve. `measuredWidth` is the width Yoga offered that call when the text
|
|
2666
|
+
* wrapped at it (only a `"box"` style does), `undefined` otherwise: such a text is measured again
|
|
2667
|
+
* at its laid-out width and gets a `Text.box`, any other gets none. `basis` is what `style` was
|
|
2668
|
+
* resolved on, set once the style is on the layout node and the components are written; an
|
|
2669
|
+
* element without one is patched in full at its next patch. `index` is its place among the
|
|
2670
|
+
* element children of its parent at the last diff (0 for a root element): with its `zIndex` it
|
|
2671
|
+
* gives the child its `Order`. `cause` is why the element appeared: `"mount"` when it came with
|
|
2672
|
+
* the first build of its root and the root came by a mount, `"change"` otherwise; its `enter` hook
|
|
2673
|
+
* is told. `rebuilds` counts the patches in a row that brought a loop of another reference that is
|
|
2674
|
+
* not really another loop: a dev build names the element once it reaches three. `endless` holds
|
|
2675
|
+
* the motions a hook returned after it started a track that repeats forever: they never end, so
|
|
2676
|
+
* no exit waits for them (F42).
|
|
2672
2677
|
*/
|
|
2673
2678
|
type Element = {
|
|
2674
2679
|
entity: Entity;
|
|
@@ -2724,7 +2729,10 @@ type Element = {
|
|
|
2724
2729
|
blended?: boolean;
|
|
2725
2730
|
basis?: StyleBasis; /** The motions of its hooks that started a `repeat: "forever"` track; made with the first one. */
|
|
2726
2731
|
endless?: Set<MotionHandle>; /** What its enter hook did at the `Box`, until `ui.enter` keeps the play or plays it again (F49). */
|
|
2727
|
-
posed?: PosedEnter | undefined;
|
|
2732
|
+
posed?: PosedEnter | undefined; /** The value of its visual last written: what a `change` hook of the visual gets as the value before (F85). */
|
|
2733
|
+
look?: AnyComponentValue | undefined; /** The write pass in which a `change` hook of its visual last returned a motion. */
|
|
2734
|
+
lookPass?: number | undefined; /** The value of its visual before that pass: a second write in the pass plays the hook from it. */
|
|
2735
|
+
lookFrom?: object | undefined;
|
|
2728
2736
|
};
|
|
2729
2737
|
/**
|
|
2730
2738
|
* One `set` of an enter hook: the fields it wrote, and the values they had before. A hook may write any component of
|
|
@@ -1,9 +1,9 @@
|
|
|
1
|
-
import { Vr as Api$3, Y as Api, nn as Api$1, wr as Api$2 } from "./types-D27I4Q7b.mjs";
|
|
2
|
-
import { P as AnimApi, r as UiApi } from "./types-
|
|
3
|
-
import { N as InputApi } from "./types-
|
|
1
|
+
import { Vr as Api$3, Y as Api, nn as Api$1, vr as Api$4, wr as Api$2 } from "./types-D27I4Q7b.mjs";
|
|
2
|
+
import { P as AnimApi, r as UiApi } from "./types-BqfwZ_lM.mjs";
|
|
3
|
+
import { N as InputApi } from "./types-DGHVGP5i.mjs";
|
|
4
4
|
import { r as Repro } from "./headless-f31cEZ7f.mjs";
|
|
5
5
|
import { a as InputOf, r as ControlApp } from "./types-CgE5L_7f.mjs";
|
|
6
|
-
import { t as commands } from "./commands-
|
|
6
|
+
import { t as commands } from "./commands-C1gTg70a.mjs";
|
|
7
7
|
|
|
8
8
|
//#region src/visual/types.d.ts
|
|
9
9
|
/** The base commands of the `/control` door, keyed by their short names. */
|
|
@@ -32,7 +32,9 @@ type StepCommand = Exclude<keyof Catalogue, "bookmark" | "capture" | "debug">;
|
|
|
32
32
|
type VisualStart = Omit<Repro, "route">;
|
|
33
33
|
/**
|
|
34
34
|
* One step of a visual test: a `/control` command by its short name with that command's input,
|
|
35
|
-
* typed from the catalogue, or a checkpoint by its name.
|
|
35
|
+
* typed from the catalogue, or a checkpoint by its name. A checkpoint lands every motion
|
|
36
|
+
* (`anim.finishAll()`) before its snapshot; with `finish: false` it does not, and the snapshot
|
|
37
|
+
* shows the motions where they stand after the same frames in both legs (F87).
|
|
36
38
|
*
|
|
37
39
|
* @example
|
|
38
40
|
* ```ts
|
|
@@ -40,12 +42,15 @@ type VisualStart = Omit<Repro, "route">;
|
|
|
40
42
|
* const steps: VisualStep[] = [
|
|
41
43
|
* { tap: { key: "play" } },
|
|
42
44
|
* { answer: { intent: "deliver", payload: { orderId: "o1" } } },
|
|
43
|
-
* { checkpoint: "open" }
|
|
45
|
+
* { checkpoint: "open" },
|
|
46
|
+
* // The hint that fades out at the end of its animation, caught while it still shows.
|
|
47
|
+
* { checkpoint: "hint", finish: false }
|
|
44
48
|
* ];
|
|
45
49
|
* ```
|
|
46
50
|
*/
|
|
47
51
|
type VisualStep = {
|
|
48
52
|
checkpoint: string;
|
|
53
|
+
finish?: boolean;
|
|
49
54
|
} | { [K in StepCommand]: { [P in K]: InputOf<Catalogue[K]["input"]> } }[StepCommand];
|
|
50
55
|
/**
|
|
51
56
|
* A visual test as `defineVisualTest` answers it: its name, where it starts, its steps, and
|
|
@@ -84,7 +89,8 @@ type VisualApp = ControlApp & {
|
|
|
84
89
|
readonly world: Api$1;
|
|
85
90
|
readonly model: Api$2;
|
|
86
91
|
readonly input: InputApi;
|
|
87
|
-
readonly lifecycle: Api$3;
|
|
92
|
+
readonly lifecycle: Api$3; /** The clock: the pixel leg puts its moment on the page, so both legs see one `clock.now()`. */
|
|
93
|
+
readonly clock: Api$4;
|
|
88
94
|
};
|
|
89
95
|
/**
|
|
90
96
|
* The dev page the browser leg opens: its URL, the viewport width in CSS px (390 by default), the
|
|
@@ -254,7 +260,8 @@ type VisualFlags = {
|
|
|
254
260
|
/**
|
|
255
261
|
* Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
|
|
256
262
|
* `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
|
|
257
|
-
* where the runner saves `state.json` and `describe.json` next to the test
|
|
263
|
+
* where the runner lands every motion and saves `state.json` and `describe.json` next to the test;
|
|
264
|
+
* `{ checkpoint: name, finish: false }` keeps the motions running for the snapshot. `walk` runs fast: a
|
|
258
265
|
* popup its last node opens never mounts, so the step before a checkpoint that shows a node's
|
|
259
266
|
* result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
|
|
260
267
|
* (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
|
package/dist/visual.d.mts
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import { a as VisualApp, c as VisualRenderer, d as VisualStart, f as VisualStep, h as VisualTolerance, i as CheckpointResult, l as VisualReport, m as VisualTestResult, n as runVisualTests, o as VisualOptions, p as VisualTest, r as defineVisualTest, s as VisualPage, t as parseVisualArgv, u as VisualSetup } from "./visual-
|
|
1
|
+
import { a as VisualApp, c as VisualRenderer, d as VisualStart, f as VisualStep, h as VisualTolerance, i as CheckpointResult, l as VisualReport, m as VisualTestResult, n as runVisualTests, o as VisualOptions, p as VisualTest, r as defineVisualTest, s as VisualPage, t as parseVisualArgv, u as VisualSetup } from "./visual-C2HHHs46.mjs";
|
|
2
2
|
export { type CheckpointResult, type VisualApp, type VisualOptions, type VisualPage, type VisualRenderer, type VisualReport, type VisualSetup, type VisualStart, type VisualStep, type VisualTest, type VisualTestResult, type VisualTolerance, defineVisualTest, parseVisualArgv, runVisualTests };
|
package/dist/visual.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { n as
|
|
1
|
+
import { t as VISUAL_CLOCK } from "./fake-hnJcJeYf.mjs";
|
|
2
|
+
import { n as read, t as sources } from "./sources-MfRpM8sV.mjs";
|
|
3
|
+
import { n as commands, t as run } from "./run-G4m1APMv.mjs";
|
|
3
4
|
import path from "node:path";
|
|
4
5
|
import { createBrandConsole } from "@moku-labs/common/cli";
|
|
5
6
|
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
@@ -62,15 +63,41 @@ function stepNames() {
|
|
|
62
63
|
return Object.keys(commands).filter((name) => isStepCommand(name)).join(", ");
|
|
63
64
|
}
|
|
64
65
|
/**
|
|
65
|
-
* Takes
|
|
66
|
-
*
|
|
66
|
+
* Takes a checkpoint step apart: its name, a string, and `finish`, a boolean when it is there; no
|
|
67
|
+
* other key.
|
|
68
|
+
*
|
|
69
|
+
* @param at - The test and the step, for the message.
|
|
70
|
+
* @param fields - The step.
|
|
71
|
+
* @returns The checkpoint.
|
|
72
|
+
* @throws {Error} When the name is not a string, `finish` not a boolean, or another key is there.
|
|
73
|
+
* @example
|
|
74
|
+
* ```ts
|
|
75
|
+
* checkpointEntry('[game] Visual test "fade", step 2', { checkpoint: "glow", finish: false }); // { kind: "checkpoint", name: "glow", finish: false }
|
|
76
|
+
* ```
|
|
77
|
+
*/
|
|
78
|
+
function checkpointEntry(at, fields) {
|
|
79
|
+
const { checkpoint: name, finish = true, ...others } = fields;
|
|
80
|
+
if (Object.keys(others).length > 0) throw new Error(`${at} is not one command.\n A checkpoint takes its name and finish, like { checkpoint: "name", finish: false }.`);
|
|
81
|
+
if (typeof name !== "string") throw new TypeError(`${at}: the checkpoint name is not a string.\n Write the checkpoint as { checkpoint: "name" }.`);
|
|
82
|
+
if (typeof finish !== "boolean") throw new TypeError(`${at}: finish of the checkpoint "${name}" is not a boolean.\n Write finish: false to keep the motions running, or leave it out.`);
|
|
83
|
+
return {
|
|
84
|
+
kind: "checkpoint",
|
|
85
|
+
name,
|
|
86
|
+
finish
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Takes one step apart and checks its shape: a checkpoint (its name, and `finish` when it is
|
|
91
|
+
* there), or exactly one step command with an input object. The checkpoint name is checked by the
|
|
92
|
+
* caller.
|
|
67
93
|
*
|
|
68
94
|
* @param test - The test name, for the message.
|
|
69
95
|
* @param step - The step.
|
|
70
96
|
* @param index - Its index in the steps, from 0.
|
|
71
97
|
* @returns The checkpoint or the command with its input.
|
|
72
98
|
* @throws {Error} When the step holds no key or more than one, names something that is not a
|
|
73
|
-
* step, gives a command no input object, or a checkpoint no string
|
|
99
|
+
* step, gives a command no input object, or a checkpoint no string name or a `finish` that is
|
|
100
|
+
* not a boolean.
|
|
74
101
|
* @example
|
|
75
102
|
* ```ts
|
|
76
103
|
* stepEntry("reward-popup", { tap: { key: "play" } }, 0); // { kind: "command", name: "tap", input: { key: "play" } }
|
|
@@ -80,15 +107,9 @@ function stepEntry(test, step, index) {
|
|
|
80
107
|
const fields = step;
|
|
81
108
|
const [name, ...others] = Object.keys(fields);
|
|
82
109
|
const at = `[game] Visual test "${test}", step ${index + 1}`;
|
|
110
|
+
if (Object.hasOwn(fields, "checkpoint")) return checkpointEntry(at, fields);
|
|
83
111
|
if (name === void 0 || others.length > 0) throw new Error(`${at} is not one command.\n Write one command by its short name, or { checkpoint: "name" }.`);
|
|
84
112
|
const value = fields[name];
|
|
85
|
-
if (name === "checkpoint") {
|
|
86
|
-
if (typeof value === "string") return {
|
|
87
|
-
kind: "checkpoint",
|
|
88
|
-
name: value
|
|
89
|
-
};
|
|
90
|
-
throw new Error(`${at}: the checkpoint name is not a string.\n Write the checkpoint as { checkpoint: "name" }.`);
|
|
91
|
-
}
|
|
92
113
|
if (!isStepCommand(name)) throw new Error(`${at} names "${name}", which is not a step.\n Use a checkpoint or one of: ${stepNames()}.`);
|
|
93
114
|
if (!isInputObject(value)) throw new Error(`${at} (${name}) has no input object.\n Pass the input of the command, like { tap: { key: "play" } }.`);
|
|
94
115
|
return {
|
|
@@ -173,7 +194,8 @@ function checkVisualTest(name, steps) {
|
|
|
173
194
|
/**
|
|
174
195
|
* Defines a visual test: where it starts, what `game.restore` loads, and its steps. A step is a
|
|
175
196
|
* `/control` command by its short name with that command's input, or `{ checkpoint: name }`,
|
|
176
|
-
* where the runner saves `state.json` and `describe.json` next to the test
|
|
197
|
+
* where the runner lands every motion and saves `state.json` and `describe.json` next to the test;
|
|
198
|
+
* `{ checkpoint: name, finish: false }` keeps the motions running for the snapshot. `walk` runs fast: a
|
|
177
199
|
* popup its last node opens never mounts, so the step before a checkpoint that shows a node's
|
|
178
200
|
* result is `answer` or `tap`. With `webgl: true` the test also runs in the WebGL leg
|
|
179
201
|
* (`--webgl`), whose pictures go to `screen.webgl.webp`: set it on a screen with a custom filter.
|
|
@@ -773,17 +795,19 @@ async function settleAt(play, where) {
|
|
|
773
795
|
if (play.loop.error !== void 0) throw play.loop.error;
|
|
774
796
|
}
|
|
775
797
|
/**
|
|
776
|
-
* Plays a checkpoint: settle, land every track and loop
|
|
798
|
+
* Plays a checkpoint: settle, land every track and loop unless it says `finish: false`, two
|
|
799
|
+
* frames, then read and compare.
|
|
777
800
|
*
|
|
778
801
|
* @param play - The test being played.
|
|
779
|
-
* @param
|
|
802
|
+
* @param entry - The checkpoint: its name, and whether it lands the motions.
|
|
780
803
|
* @param where - The step, for the message.
|
|
781
804
|
* @returns The outcome of the checkpoint; `pixels` is skipped in this leg.
|
|
782
805
|
*/
|
|
783
|
-
async function checkpoint$1(play,
|
|
806
|
+
async function checkpoint$1(play, entry, where) {
|
|
784
807
|
const { app, test, run: options } = play;
|
|
808
|
+
const { name } = entry;
|
|
785
809
|
await settleAt(play, where);
|
|
786
|
-
app.anim.finishAll();
|
|
810
|
+
if (entry.finish) app.anim.finishAll();
|
|
787
811
|
await frames(app, REST_FRAMES);
|
|
788
812
|
const file = (kind) => baselineFile(options.dir, test.name, name, kind);
|
|
789
813
|
const state = await checkBaseline(file("state.json"), stableJson(stateOf(app)), options.update);
|
|
@@ -819,7 +843,7 @@ async function playSteps$1(play, checkpoints) {
|
|
|
819
843
|
const entry = stepEntry(test.name, step, index);
|
|
820
844
|
const where = `step ${index + 1} (${entry.kind === "checkpoint" ? "checkpoint" : entry.name})`;
|
|
821
845
|
if (entry.kind === "checkpoint") {
|
|
822
|
-
checkpoints.push(await checkpoint$1(play, entry
|
|
846
|
+
checkpoints.push(await checkpoint$1(play, entry, where));
|
|
823
847
|
continue;
|
|
824
848
|
}
|
|
825
849
|
await failing(test.name, where, () => runStep(app, entry.name, entry.input));
|
|
@@ -1234,13 +1258,16 @@ async function pageStep(argument) {
|
|
|
1234
1258
|
}
|
|
1235
1259
|
}
|
|
1236
1260
|
/**
|
|
1237
|
-
* Plays a checkpoint in the page: settle, land every track and loop
|
|
1261
|
+
* Plays a checkpoint in the page: settle, land every track and loop unless the step says
|
|
1262
|
+
* `finish: false` (F87), two stepped frames, then the
|
|
1238
1263
|
* picture of the whole canvas as lossless WebP and the state through the doors. The game is
|
|
1239
1264
|
* paused, so the picture is the stage after the last stepped frame. With the frame check on, a
|
|
1240
1265
|
* missed frame or mark logged in the meantime fails the checkpoint.
|
|
1241
1266
|
*
|
|
1242
|
-
* @param argument - The test and the step,
|
|
1267
|
+
* @param argument - The test and the step, the most frames to settle, and whether to land the
|
|
1268
|
+
* motions.
|
|
1243
1269
|
* @param argument.settleFrames - The most frames to settle.
|
|
1270
|
+
* @param argument.finish - True to land every motion before the picture.
|
|
1244
1271
|
* @returns The state and the picture, or why the checkpoint failed.
|
|
1245
1272
|
*/
|
|
1246
1273
|
async function pageCheckpoint(argument) {
|
|
@@ -1252,7 +1279,7 @@ async function pageCheckpoint(argument) {
|
|
|
1252
1279
|
ok: false,
|
|
1253
1280
|
error: unsettled
|
|
1254
1281
|
};
|
|
1255
|
-
game.anim.finishAll();
|
|
1282
|
+
if (argument.finish) game.anim.finishAll();
|
|
1256
1283
|
await pageTick(handles);
|
|
1257
1284
|
await pageTick(handles);
|
|
1258
1285
|
const captured = await game.renderer.capture();
|
|
@@ -1497,6 +1524,17 @@ function pageScript() {
|
|
|
1497
1524
|
}
|
|
1498
1525
|
//#endregion
|
|
1499
1526
|
//#region src/visual/leg-browser.ts
|
|
1527
|
+
/**
|
|
1528
|
+
* @file visual — the browser leg: Chrome with WebGPU on a Mac, driven by Playwright. It loads
|
|
1529
|
+
* `playwright-core` with a dynamic import only here, opens the dev page once per test, waits until
|
|
1530
|
+
* the page has loaded what it asked for, turns on the renderer's check of skipped frames
|
|
1531
|
+
* (`debug.verifyFrames`), pauses the game and steps it to the start time, plays the
|
|
1532
|
+
* test inside the page through `globalThis.game` and `globalThis.doors` on stepped frames, and at
|
|
1533
|
+
* each checkpoint checks the page's state against `state.json` and its picture against
|
|
1534
|
+
* `screen.webp`. The WebGL leg opens the page with `?renderer=webgl` and keeps its pictures in
|
|
1535
|
+
* `screen.webgl.webp`. The pixels are compared in the page (`page-script.ts`); the files are written by
|
|
1536
|
+
* `files.ts`. A failure of the page itself fails the leg; a failure of a step ends its test only.
|
|
1537
|
+
*/
|
|
1500
1538
|
/** The Chrome flags of `.planning/e2e/run.mjs`: WebGPU on Metal, on any GPU. */
|
|
1501
1539
|
const WEBGPU_FLAGS = [
|
|
1502
1540
|
"--enable-unsafe-webgpu",
|
|
@@ -1636,6 +1674,20 @@ async function launchChrome(chromium, browser) {
|
|
|
1636
1674
|
throw new Error(`[game] No Chrome launched: ${own.reason}.\n Install Playwright's Chromium (bunx playwright-core install chromium), set page.browser, or run with --no-pixels.`);
|
|
1637
1675
|
}
|
|
1638
1676
|
/**
|
|
1677
|
+
* The init script that puts the moment of the headless leg's clock on the page before it loads
|
|
1678
|
+
* (F86): a dev page then runs its app on a fake clock standing there, as the headless leg does.
|
|
1679
|
+
*
|
|
1680
|
+
* @param moment - The moment, epoch milliseconds.
|
|
1681
|
+
* @returns The script.
|
|
1682
|
+
* @example
|
|
1683
|
+
* ```ts
|
|
1684
|
+
* clockScript(1_000_000); // 'globalThis["__mokuVisualClock"] = 1000000;'
|
|
1685
|
+
* ```
|
|
1686
|
+
*/
|
|
1687
|
+
function clockScript(moment) {
|
|
1688
|
+
return `globalThis[${JSON.stringify(VISUAL_CLOCK)}] = ${JSON.stringify(moment)};`;
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1639
1691
|
* Sizes the page: `page.width` CSS px wide (390), as tall as the inert frame of the game is at
|
|
1640
1692
|
* that width, so the reference frame of the page equals the one of the headless leg; a phone
|
|
1641
1693
|
* with touch at `page.deviceScaleFactor` (2). Builds one app of the setup, never started, to read
|
|
@@ -1762,14 +1814,16 @@ async function startPage(page, url, run) {
|
|
|
1762
1814
|
* @param context - The browser context.
|
|
1763
1815
|
* @param url - The URL of the page the leg opens.
|
|
1764
1816
|
* @param run - The run options, for `settleFrames` and `renderer`.
|
|
1817
|
+
* @param moment - The moment the headless leg's clock stands at, for the page's clock (F86).
|
|
1765
1818
|
* @returns The open page.
|
|
1766
1819
|
* @throws {Error} When the page does not open, never exposes `game` and `doors`, does not start,
|
|
1767
1820
|
* draws with another backend or never stops loading. The page is closed then.
|
|
1768
1821
|
*/
|
|
1769
|
-
async function openPage(context, url, run) {
|
|
1822
|
+
async function openPage(context, url, run, moment) {
|
|
1770
1823
|
const page = await context.newPage();
|
|
1771
1824
|
try {
|
|
1772
1825
|
await page.addInitScript({ content: pageScript() });
|
|
1826
|
+
await page.addInitScript({ content: clockScript(moment) });
|
|
1773
1827
|
await page.goto(url).catch((error) => {
|
|
1774
1828
|
throw new Error(`[game] The page at ${url} did not open: ${firstLine(error)}.\n Serve the dev page (moku-game dev), or run with --no-pixels.`);
|
|
1775
1829
|
});
|
|
@@ -1825,17 +1879,19 @@ async function checkScreen(play, file, where, screen) {
|
|
|
1825
1879
|
* headless leg wrote, the picture against the baseline of the leg's backend.
|
|
1826
1880
|
*
|
|
1827
1881
|
* @param play - The test being played.
|
|
1828
|
-
* @param
|
|
1882
|
+
* @param entry - The checkpoint: its name, and whether it lands the motions.
|
|
1829
1883
|
* @param where - The step, for the message.
|
|
1830
1884
|
* @returns What the browser leg found there.
|
|
1831
1885
|
* @throws {Error} When the page did not settle, gave no picture, or broke.
|
|
1832
1886
|
*/
|
|
1833
|
-
async function checkpoint(play,
|
|
1887
|
+
async function checkpoint(play, entry, where) {
|
|
1834
1888
|
const { test, run } = play;
|
|
1889
|
+
const { name } = entry;
|
|
1835
1890
|
const shot = await callPage(play.page, pageCheckpoint, {
|
|
1836
1891
|
test: test.name,
|
|
1837
1892
|
where,
|
|
1838
|
-
settleFrames: run.settleFrames
|
|
1893
|
+
settleFrames: run.settleFrames,
|
|
1894
|
+
finish: entry.finish
|
|
1839
1895
|
});
|
|
1840
1896
|
if (shot.screen === void 0) throw new Error(`[game] Visual test "${test.name}", ${where}: the page gave no picture.\n Serve a dev build with ${BACKEND[run.renderer]}: renderer.capture() answers only there.`);
|
|
1841
1897
|
const file = (kind) => baselineFile(run.dir, test.name, name, kind);
|
|
@@ -1882,7 +1938,7 @@ async function playSteps(play, found) {
|
|
|
1882
1938
|
const entry = stepEntry(test.name, step, index);
|
|
1883
1939
|
const where = `step ${index + 1} (${entry.kind === "checkpoint" ? "checkpoint" : entry.name})`;
|
|
1884
1940
|
if (entry.kind === "checkpoint") {
|
|
1885
|
-
found.set(entry.name, await checkpoint(play, entry
|
|
1941
|
+
found.set(entry.name, await checkpoint(play, entry, where));
|
|
1886
1942
|
continue;
|
|
1887
1943
|
}
|
|
1888
1944
|
await callPage(play.page, pageStep, {
|
|
@@ -1901,13 +1957,15 @@ async function playSteps(play, found) {
|
|
|
1901
1957
|
* @param play - The test and the run options; the page is opened here.
|
|
1902
1958
|
* @param play.test - The test.
|
|
1903
1959
|
* @param play.run - The run options.
|
|
1904
|
-
* @param
|
|
1960
|
+
* @param target - The page the leg opens: its URL, and the moment of the headless leg's clock.
|
|
1961
|
+
* @param target.url - The URL.
|
|
1962
|
+
* @param target.moment - The moment, epoch milliseconds.
|
|
1905
1963
|
* @param headless - The result of the test in the headless leg.
|
|
1906
1964
|
* @returns The result of the test with its pixel fields.
|
|
1907
1965
|
* @throws {Error} When the page fails to open: that fails the leg.
|
|
1908
1966
|
*/
|
|
1909
|
-
async function browserTest(context, play,
|
|
1910
|
-
const page = await openPage(context, url, play.run);
|
|
1967
|
+
async function browserTest(context, play, target, headless) {
|
|
1968
|
+
const page = await openPage(context, target.url, play.run, target.moment);
|
|
1911
1969
|
const found = /* @__PURE__ */ new Map();
|
|
1912
1970
|
let failed;
|
|
1913
1971
|
try {
|
|
@@ -1954,7 +2012,10 @@ async function runBrowserLeg(setup, tests, run, headless, load = loadChromium) {
|
|
|
1954
2012
|
if (page === void 0) throw new Error("[game] The pixel leg needs the dev page.\n Pass page: { url } in the setup, or run with --no-pixels.");
|
|
1955
2013
|
const chromium = await load();
|
|
1956
2014
|
const options = pageSize(setup, page);
|
|
1957
|
-
const
|
|
2015
|
+
const target = {
|
|
2016
|
+
url: rendererUrl(page.url, run.renderer),
|
|
2017
|
+
moment: setup.app().clock.now()
|
|
2018
|
+
};
|
|
1958
2019
|
const browser = await launchChrome(chromium, page.browser);
|
|
1959
2020
|
try {
|
|
1960
2021
|
const context = await browser.newContext(options);
|
|
@@ -1967,7 +2028,7 @@ async function runBrowserLeg(setup, tests, run, headless, load = loadChromium) {
|
|
|
1967
2028
|
results.push(before.error === void 0 ? await browserTest(context, {
|
|
1968
2029
|
test,
|
|
1969
2030
|
run
|
|
1970
|
-
},
|
|
2031
|
+
}, target, before) : before);
|
|
1971
2032
|
}
|
|
1972
2033
|
return results;
|
|
1973
2034
|
} finally {
|
package/llms.txt
CHANGED
|
@@ -12,7 +12,7 @@ Facts: Bun only. ESM only. `"sideEffects"` lists the root entry `./dist/index.mj
|
|
|
12
12
|
- `@moku-labs/game/app/system` (the browser, the native shell): `systemShellOf`, `fromSystem`, `storeSave`, `createSystemApp`, the type `SystemModules`, over the optional peer `@moku-labs/system`. The one entry that names it, in types only: the page hands it the loaders.
|
|
13
13
|
- `@moku-labs/game/cli` (node and bun only): `runCli`, the bin `moku-game`, `preparePage`, the editor's dev page, `watchKeys`, the watch that keeps `generated/` fresh next to it, and `watchEngine`, the watch that tells when the installed engine changed.
|
|
14
14
|
- `@moku-labs/game/testing`: headless play, repro runs, isolated feature tests (`isolate`, `stub`), fake clock, the `fx` of a node for a unit test (`fakeFx`), in-memory save provider. No `node:` import.
|
|
15
|
-
- `@moku-labs/game/visual` (node and bun only): the visual tests `defineVisualTest`, `runVisualTests`, `parseVisualArgv`. The bin runs them: `moku-game visual`.
|
|
15
|
+
- `@moku-labs/game/visual` (node and bun only): the visual tests `defineVisualTest`, `runVisualTests`, `parseVisualArgv`. The bin runs them: `moku-game visual`. A step is a `/control` command or `{ checkpoint: name, finish?: boolean }`: a checkpoint lands every motion before its snapshot, `finish: false` keeps them running (same frames in both legs). Both legs run on one clock: the pixel leg's dev page runs on a fake clock at the headless app's `clock.now()` (`startMoment`), standing still like the headless one.
|
|
16
16
|
- `@moku-labs/game/assets` (node and bun only): the asset key scanner, the production packer and the string compiler. The package bin `moku-game-assets` runs it.
|
|
17
17
|
- `@moku-labs/game/inspect`: the read door of the editor, safe in a production build. See "Doors for the editor".
|
|
18
18
|
- `@moku-labs/game/control`: the write door of the editor, dev builds only. See "Doors for the editor".
|
|
@@ -550,7 +550,7 @@ The graph. Modules `runner`, `gate`, `inbox`, `fx`, `features`. Runner methods s
|
|
|
550
550
|
|
|
551
551
|
A screen is a projection whose `view` returns JSX. The game's `tsconfig.json` sets `"jsx": "react-jsx"` and `"jsxImportSource": "@moku-labs/game"`; screens are `.tsx` files. There is no DOM and no React.
|
|
552
552
|
|
|
553
|
-
- Tags: `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel` (swallows taps), `image` (`texture`, `fit?: "contain" | "cover" | "fill"`), `icon` (`name`, `fit?`), `text` (`content: string | Message`, or `bind={bind(Counter, "value")}` with `components={[Counter({ value })]}`; formats `int`, `mm:ss`, `h:mm:ss`, `duration`; `Countdown({ until })` with `bind(Countdown, "left", { format: "mm:ss" })` counts down to a `clock` moment; `style` is a text style key), `button` (`intent` + `payload`, or `local`, never both), `scroll` (`axis?: "x" | "y"` (`"y"`), fixed at spawn, another one later warns `ui:scroll-axis-fixed`; children for a short list, laid out as a column on `y` and a row on `x`, or the windowed form `rows`, `rowHeight`, `overscan?` (5), `row: index => node`, on `y` only: `axis: "x"` with `rows` throws `[game] A windowed scroll moves on y only.`; row state belongs in the model, a row that leaves the window loses its local state; `snap?: number[]` stops in offsets, kept inside `0..max`: a release glides to the stop nearest `offset + v × 260` (v in units a ms) on `1 − (1 − p)³` over `min(620, 260 + distance × 1.1)` ms; without `snap` a release flings, `v ×= 0.998` a ms until under 0.02, 0 when the finger rested over 50 ms; `offset?: number` (0) where it stands from the start, drawn there in its first frame, a new value glides there, the same value does nothing; hard edges, nothing passes on to a parent scroll; a press on a child that leaves the tap slop along the scroll's axis goes to the scroll and the child answers nothing, a finger on a moving scroll only stops it; every number in `time.delta` ms, so a time scale applies; the public component `ScrollState { offset, max, moving }` on its entity, whole units `0..max`, written when it changes and once with `moving: false` when it stops, read by a system and by `game.explain`; `app.ui.scroll(key, to)` and the visual step `{ scroll: { key, to } }` set it at once), `input` (a text field; its text lives in the component's `local`, see the ui README). Common props: `key`, `style`, `state: { active?, disabled?, selected? }`, `motion`, `components` (extra component values for the element's entity, such as a filter: `components={[Glow({ strength: 2 })]}`, or `DebugTrigger()`: five taps within 3000 ms of wall time on that view call `app.debug.overlay.open()` on a page with the agent flag, nothing elsewhere; put it on a `button` naming nothing); containers take `hosts?: string[]` (projections drawn inside the element: world views hang under it from the slot's first frame; a JSX projection is laid out in the slot's content box). Children draw and are hit in markup order at equal `zIndex`; the roots of a scene draw in the order of its `projections` (the `ui` layer sorts by `Order`; `ui: { sort: "none" }` keeps insertion order). `defineMotion({ when: "change" })` plays its `enter` and `exit` only for a commit or a popup, never when its screen mounts or unmounts; declare a motion at module level (`ui:motion-rebuilt` names a loop rebuilt in a view). Style adds `nineSlice`, `tint`, `offsetX`, `offsetY`, `scale`, `origin`, `fit: "contain"`; state variants `is: { hover, pressed, disabled, active, selected, covered }`. `popup(Component, props, { over })` stacks a popup over a kept one.
|
|
553
|
+
- Tags: `screen`, `layer`, `row`, `column`, `stack`, `spacer`, `panel` (swallows taps), `image` (`texture`, `fit?: "contain" | "cover" | "fill"`), `icon` (`name`, `fit?`), `text` (`content: string | Message`, or `bind={bind(Counter, "value")}` with `components={[Counter({ value })]}`; formats `int`, `mm:ss`, `h:mm:ss`, `duration`; `Countdown({ until })` with `bind(Countdown, "left", { format: "mm:ss" })` counts down to a `clock` moment; `style` is a text style key), `button` (`intent` + `payload`, or `local`, never both), `scroll` (`axis?: "x" | "y"` (`"y"`), fixed at spawn, another one later warns `ui:scroll-axis-fixed`; children for a short list, laid out as a column on `y` and a row on `x`, or the windowed form `rows`, `rowHeight`, `overscan?` (5), `row: index => node`, on `y` only: `axis: "x"` with `rows` throws `[game] A windowed scroll moves on y only.`; row state belongs in the model, a row that leaves the window loses its local state; `snap?: number[]` stops in offsets, kept inside `0..max`: a release glides to the stop nearest `offset + v × 260` (v in units a ms) on `1 − (1 − p)³` over `min(620, 260 + distance × 1.1)` ms; without `snap` a release flings, `v ×= 0.998` a ms until under 0.02, 0 when the finger rested over 50 ms; `offset?: number` (0) where it stands from the start, drawn there in its first frame, a new value glides there, the same value does nothing; hard edges, nothing passes on to a parent scroll; a press on a child that leaves the tap slop along the scroll's axis goes to the scroll and the child answers nothing, a finger on a moving scroll only stops it; every number in `time.delta` ms, so a time scale applies; the public component `ScrollState { offset, max, moving }` on its entity, whole units `0..max`, written when it changes and once with `moving: false` when it stops, read by a system and by `game.explain`; `app.ui.scroll(key, to)` and the visual step `{ scroll: { key, to } }` set it at once), `input` (a text field; its text lives in the component's `local`, see the ui README). Common props: `key`, `style`, `state: { active?, disabled?, selected? }`, `motion`, `components` (extra component values for the element's entity, such as a filter: `components={[Glow({ strength: 2 })]}`, or `DebugTrigger()`: five taps within 3000 ms of wall time on that view call `app.debug.overlay.open()` on a page with the agent flag, nothing elsewhere; put it on a `button` naming nothing); containers take `hosts?: string[]` (projections drawn inside the element: world views hang under it from the slot's first frame; a JSX projection is laid out in the slot's content box). Children draw and are hit in markup order at equal `zIndex`; the roots of a scene draw in the order of its `projections` (the `ui` layer sorts by `Order`; `ui: { sort: "none" }` keeps insertion order). `defineMotion({ when: "change" })` plays its `enter` and `exit` only for a commit or a popup, never when its screen mounts or unmounts; declare a motion at module level (`ui:motion-rebuilt` names a loop rebuilt in a view). `motion.change` plays `Box` (the rects), `Transform` (the rest poses), the visual the element is drawn with (`Shape`, `Sprite`, `NineSlice`, `Text`, when a render or a restyle moves a number of it: `sweep`, `alpha`, `w`; a returned motion brings the numbers home from where they stand and the other fields land at once; the last motion of that name is cancelled first) and each extra of `components`, each as `(view, previous, next, hint)`; a dev build names any other `change` hook once, `ui:change-never-plays` `{ key, name }`. An element a render replaces (a root whose view returns another key or tag, a child of another key or tag) plays its exit with `"change"`, everything under it plays its own, and it leaves `find` at once, so the new element takes the keys they share. Style adds `nineSlice`, `tint`, `offsetX`, `offsetY`, `scale`, `origin`, `fit: "contain"`; state variants `is: { hover, pressed, disabled, active, selected, covered }`. `popup(Component, props, { over })` stacks a popup over a kept one.
|
|
554
554
|
- Style vocabulary (`defineStyle`, plain data): flow `direction`, `wrap`, `justify`, `align`, `alignSelf`, `gap`, `grow`, `shrink`; box `padding`, `margin`, `width`, `height`, `minWidth`, `minHeight`, `maxWidth`, `maxHeight`, `aspect`, `overflow`; position `position`, `left`, `top`, `right`, `bottom`, `reason`; visual `fill`, `stroke`, `strokeWidth`, `radius`, `alpha`, `shape: "rect" | "triangle" | "arc"` (`"rect"`; a triangle fills the box pointing right; an arc is only a `stroke` of `strokeWidth` on the circle inscribed in the box, `min(width, height) − strokeWidth` across, from 12 o'clock clockwise over `sweep`), `dash` (dash length of the stroke, gaps of half a dash; not on an arc), `sweep` (0..1, default 1, clamped; 0 draws nothing, 1 a closed ring), `cap: "round" | "butt"` (`"round"`; the ends of an open arc); variants `is: { pressed?, disabled?, active?, selected? }` and `when: { portrait?, landscape?, tall?, wide? }`, each a partial style merged in when the flag holds. Lengths are reference units or `"safeArea.top" | "safeArea.right" | "safeArea.bottom" | "safeArea.left"`. No font size here: it comes from the text style.
|
|
555
555
|
- `defineComponent(name, { local?, outcomes?, view(props, local) })`. A component with `outcomes` is a popup: `await fx(popup(Component, props))` opens the gate for its outcomes and resolves with `{ intent, payload }` of the pressed button. A button with `local` writes into the nearest component instance; it never reaches the graph.
|
|
556
556
|
- `tr(key, params?)` returns a frozen `Message` `{ key, params }`; no locale is read at the call site. Strings live in `strings/<locale>.json` of any feature or sub-feature, as ICU MessageFormat 1 (`{n, plural, one {# order} other {# orders}}`, `select`, `selectordinal`, `number`, `date`, `time`; no skeletons, no `currency`). `bun run assets:keys` compiles them. The keys of the top-level features and the layers go into `generated/strings.<locale>.ts`, which one feature registers; a sub-feature's keys start with `<name>.` (`ep01.title`) and go into `generated/strings.<name>.<locale>.ts`, which the sub-feature registers lazily, `strings: { en: () => import("@generated/strings.ep01.en"), ru: () => import("@generated/strings.ep01.ru") }`. With `assets` composed those modules load and unload with the bundle named like the sub-feature: every way the bundle comes (a node entry, a scene's `bundles`, `load`, a restore, the `safeNode`) brings its strings, a locale switch brings them in the new locale, and while the bundle is not loaded a key of it gives `⟦key⟧` and `has(key)` is false. A compiled module calls the root helpers `messageArgument(value, intl)` (an element stays an element part, a list goes through `Intl.ListFormat`, a number through `Intl.NumberFormat`, else text) and `messageDuration(ms)` (`95_000` gives `{ minutes: 1, seconds: 35 }`). `app.i18n`: `locale()`, `setLocale(locale)` (async, emits `i18n:locale-changed`), `format(message, locale?)` returns `Part[]`, `plain(message)` returns a string, `has(key)`, `isKey(value)`, `locales()`, `replace(locale, messages)` (dev hot swap of a generated strings file). Config: `locale: "en"`, `fallback: "en"`, `locales: {}`.
|