The suite waits for the editor now, and the count is 930 (#93)

admin committed 2026-09-16 08:08:14 +02:00
1 parent e6849977eb
commit f05d610b86
2 files changed
+32 -4

No files matched your search

+29 -2
@@ -12,7 +12,7 @@ npm run check # typecheck + lint + format:check + tests, the gate CI runs
# code is lost (tail's is reported), so a red run reads as green # code is lost (tail's is reported), so a red run reads as green
npm run build # writes dist/ and regenerates index.html npm run build # writes dist/ and regenerates index.html
./build.sh # the same under `set -o pipefail`, for callers that pipe the output ./build.sh # the same under `set -o pipefail`, for callers that pipe the output
node tools/seed-e2e.mjs && node tools/e2e.mjs # 910 editor checks, screenshots in /tmp/gm-e2e-*.png node tools/seed-e2e.mjs && node tools/e2e.mjs # 930 editor checks, screenshots in /tmp/gm-e2e-*.png
GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step GM_STEPS=20 node tools/e2e.mjs # or 1-12, or 6,20 — a subset while working on one step
./e2e.sh start [GM_STEPS] # the same run detached, see below ./e2e.sh start [GM_STEPS] # the same run detached, see below
./e2e.sh status # live progress, or the last run's result ./e2e.sh status # live progress, or the last run's result
@@ -69,6 +69,33 @@ scene state and share the CDP driver in `tools/lib/harness.mjs` — `press`, `un
`object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it `object(id)`, `world()`, `mutate(js)` and friends, so a check reads like the interaction it
exercises. exercises.
**The suite waits for the editor, not for the clock (#93).** Every action in the harness — `click`,
`doubleClick`, `wheel`, `drag`, `press`, `typeText`, `select`, `mutate`, `undo`, `redo`, `showTab` —
ends in `settle()`, which waits for two *drawn* frames (~32 ms) through `__gm.afterFrames()` rather
than for a deadline somebody estimated. `afterFrames` wakes the loop itself, because an editor that
draws nothing would never redeem a promise of a frame, and gives up after a timeout instead of
hanging the run; `settle()` falls back to `sleep(150)` when the page has no such hook (an old
build), since silently not waiting would turn every later check into a coin toss. What a frame
cannot answer has its own condition: `waitUntil(expr)`, `play()`/`stop()` waiting on
`__gm.state.mode`, and `saved()` for the autosave.
That replaced about 550 `sleep()` calls and took the suite from 564 s of step time (88% of it pure
waiting) to 243 s (76%). Three kinds of wait were deliberately kept, and removing them breaks
things:
- **Loading takes real time.** The 500/600 ms waits behind texture and font changes in
`12-textures` and `15-aspect` are waiting for bytes, not for a picture. Two frames are no answer
to that.
- **Game time is not frame time.** `setTimeScale`, `shaderTime`, `particleTime`, the pause
measurements in `18-pause`, and the live values on graph nodes in `18` — those are carried by a
running graph, so the run must be given a tick, not a repaint.
- **The scroll measurement in `11-menus`** times a real inertia, with its protocol in the comment.
One consequence of the new speed: a step can now finish before the autosave of the step before it
has fired (it runs 1 s after the last model change and paints the status twice). `33-render-on-change`
therefore waits for `__gm.state.saveStatus` to leave `dirty`/`saving` before it measures idleness —
it must measure *that nothing happens any more*, not *that it waited long enough*.
A step that needs a tab opens it with `openTab(t, 'container')` from `tools/e2e/lib/scaffold.mjs`, A step that needs a tab opens it with `openTab(t, 'container')` from `tools/e2e/lib/scaffold.mjs`,
not by clicking its name: since the tenth tab the row hides some of them at the test window's width, not by clicking its name: since the tenth tab the row hides some of them at the test window's width,
and the helper goes through the `»` menu when it has to. Clicking the label directly took the and the helper goes through the `»` menu when it has to. Clicking the label directly took the
@@ -205,4 +232,4 @@ widths to find click points. The **render device is not in this repo** — `/dev
and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which and a 1 GB `/dev/shm` come from `container.options` in the runner's own `config.yaml`, which
applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets applies to every job on that runner. Without them chromium falls back to llvmpipe, which gets
WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`, WebGL 1.0 only. Measured in the job container: `ANGLE (Intel, Vulkan 1.3.230, HD Graphics 630)`,
WebGL 2.0, and the same 910 checks as here. WebGL 2.0, and the same 930 checks as here.
+3 -2
@@ -32,16 +32,17 @@ fed to a colour port is broadcast to all three channels. A colour cannot be fed
| **Texture size** | out `width`, `height` | The picture in pixels: of the frame, when the sprite wears one. | | **Texture size** | out `width`, `height` | The picture in pixels: of the frame, when the sprite wears one. |
| **Time** | out `value` | Seconds since the editor started; see *Coordinates and time* below. | | **Time** | out `value` | Seconds since the editor started; see *Coordinates and time* below. |
| **Output** | in `color`, `alpha` (number) | What the sprite is drawn with: the colour, clamped to 0-1, and the opacity, clamped too. Each input left unconnected stays the texture's. One per graph. Its *Edge* setting says how the picture continues past its border when sampled outside it: *Clamp* (the default) holds the border pixel, *Mirror* turns the picture back on itself, *Repeat* starts it over for tiles. | | **Output** | in `color`, `alpha` (number) | What the sprite is drawn with: the colour, clamped to 0-1, and the opacity, clamped too. Each input left unconnected stays the texture's. One per graph. Its *Edge* setting says how the picture continues past its border when sampled outside it: *Clamp* (the default) holds the border pixel, *Mirror* turns the picture back on itself, *Repeat* starts it over for tiles. |
| **Property** | out `value` | Declares a number the sprite sets: `Name` is the row below the sprite's Shader dropdown and the uniform in the fragment; `Default`, `Min`, `Max` are the row's range. A name is one word (letters, digits, `_`), unique in the graph. | | **Property** | out `value` | Declares a number the sprite sets: `Name` is the row below the sprite's Shader dropdown and the uniform in the fragment; `Default`, `Min`, `Max` are the row's range. A name is one word (letters, digits, `_`), unique in the graph, and not one of the rows a sprite has anyway — `enabled`, `scale`, `rotation`, `mirrorX`, `mirrorY`, `width`, `height`, `color`, `texture`, `shader`. Both rows would sit on the same key and the uniform would read the sprite's width instead of the declared default, so the node says *"width" is a sprite property* and nothing compiles. |
| **Color** | out `color` | A constant colour, picked in the inspector. | | **Color** | out `color` | A constant colour, picked in the inspector. |
| **Number** | out `value` | A constant number, typed on the node. Handy where the same value feeds more than one input, or where an input has nothing to type on (Luminance, the Output's alpha). | | **Number** | out `value` | A constant number, typed on the node. Handy where the same value feeds more than one input, or where an input has nothing to type on (Luminance, the Output's alpha). |
| **Add**, **Subtract**, **Multiply**, **Divide** | in `a`, `b` (any) · out `result` | Arithmetic, per channel for colours. An unconnected input is the number typed on the node (0, or 1 for the second input of Multiply and Divide). | | **Add**, **Subtract**, **Multiply**, **Divide** | in `a`, `b` (any) · out `result` | Arithmetic, per channel for colours. An unconnected input is the number typed on the node (0, or 1 for the second input of Multiply and Divide). **Dividing by 0 gives 0**, the same answer the `Divide` of a behavior and of a particle graph give: `a / 0.0` is NaN in GLSL, and the Output's `clamp(NaN, 0, 1)` is undefined — the pixel would be whatever the driver does, different on Intel, on Nvidia and on a phone. |
| **Mix** | in `a`, `b` (any), `t` (number) · out `result` | Blends from `a` to `b`: `t` = 0 is `a`, 1 is `b`, beyond that it extrapolates. | | **Mix** | in `a`, `b` (any), `t` (number) · out `result` | Blends from `a` to `b`: `t` = 0 is `a`, 1 is `b`, beyond that it extrapolates. |
| **Luminance** | in `color` · out `value` | How bright a colour is (Rec. 709 weights), as one number. | | **Luminance** | in `color` · out `value` | How bright a colour is (Rec. 709 weights), as one number. |
| **Sin**, **Floor**, **Fract**, **Abs** | in `value` (any) · out `result` | The sine of a number in radians (one wave every 6.283); rounded down to a whole number; the part after the decimal point (2.75 becomes 0.75, a rising value becomes a sawtooth); the distance from 0 with the sign dropped (-0.3 becomes 0.3, a value crossing 0 becomes a V). Per channel for colours. | | **Sin**, **Floor**, **Fract**, **Abs** | in `value` (any) · out `result` | The sine of a number in radians (one wave every 6.283); rounded down to a whole number; the part after the decimal point (2.75 becomes 0.75, a rising value becomes a sawtooth); the distance from 0 with the sign dropped (-0.3 becomes 0.3, a value crossing 0 becomes a V). Per channel for colours. |
| **Step** | in `edge` (number), `value` (any) · out `result` | 0 while `value` is below `edge`, 1 from `edge` on: a hard cut. Per channel for colours. | | **Step** | in `edge` (number), `value` (any) · out `result` | 0 while `value` is below `edge`, 1 from `edge` on: a hard cut. Per channel for colours. |
| **Pow** | in `value` (any), `exponent` (number) · out `result` | `value` to the power of `exponent`: 2 squares it, 0.5 is the square root; an easing curve on `u`, a gamma on a colour. Below 0 the value counts as 0, because GLSL leaves `pow` undefined there (0 to a power of 0 or less is undefined too). Per channel for colours. | | **Pow** | in `value` (any), `exponent` (number) · out `result` | `value` to the power of `exponent`: 2 squares it, 0.5 is the square root; an easing curve on `u`, a gamma on a colour. Below 0 the value counts as 0, because GLSL leaves `pow` undefined there (0 to a power of 0 or less is undefined too). Per channel for colours. |
| **Sqrt** | in `value` (any) · out `result` | The square root: 0.25 becomes 0.5, 4 becomes 2. The same as `Pow` with exponent 0.5, in one node. Below 0 the value counts as 0, because GLSL leaves `sqrt` undefined there. Per channel for colours. | | **Sqrt** | in `value` (any) · out `result` | The square root: 0.25 becomes 0.5, 4 becomes 2. The same as `Pow` with exponent 0.5, in one node. Below 0 the value counts as 0, because GLSL leaves `sqrt` undefined there. Per channel for colours. |
| **Clamp** | in `value` (any), `min`, `max` (number) · out `result` | Holds the value inside `min`..`max`, per channel for colours. The Output clamps the colour it is given anyway; this one holds a value *inside* the graph, before it is added to or multiplied. |
An input with nothing to type on it (the Output's two, Luminance's colour) that is left unconnected An input with nothing to type on it (the Output's two, Luminance's colour) that is left unconnected
reads the texel: its colour for a colour port, its alpha for the Output's `alpha`. That is why the reads the texel: its colour for a colour port, its alpha for the Output's `alpha`. That is why the