Skip to content

Commit 0724de6

Browse files
committed
Improve control-driven E2E sessions and editor state
1 parent 62bc788 commit 0724de6

174 files changed

Lines changed: 4028 additions & 2390 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/design/e2e.md

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -5,28 +5,33 @@ description: How the SBC E2E harness drives a real editor session and records re
55

66
# Graphical end-to-end testing
77

8-
The E2E harness tests the editor as a user sees it. It launches an isolated
9-
Spring process, finds its window, drives mouse and keyboard input through X11,
10-
and records the observable result. It complements the headless Smoke suite:
8+
The E2E harness tests the editor as a user sees it. It launches Spring, finds
9+
its window, drives mouse and keyboard input through X11, and records the
10+
observable result. Grouped runs reuse one resettable process per compatible
11+
launch environment by default; scenarios marked `@scenario(isolated=True)` get
12+
their own process. It
13+
complements the headless Smoke suite:
1114
Smoke proves native integration and in-engine tests; E2E proves UI ownership,
1215
input routing, and rendered behaviour.
1316

14-
Scenarios that are about a feature rather than about a widget belong in
15-
`scenarios/control.py`, driven through the control channel instead of X11 — no
16-
coordinates, no sleeps, and several times faster. See
17+
Scenarios that are about a feature rather than about a widget belong in the
18+
domain modules under `tools/e2e/scenarios/`, driven through the control channel
19+
instead of X11 — no coordinates, no sleeps, and several times faster. See
1720
[programmatic-control.md](programmatic-control.md).
1821

1922
## Run a scenario
2023

2124
```bash
22-
just test-e2e map-export
23-
just test-e2e map-export --tag ui:rust
25+
just test-e2e map-workflows
26+
just test-e2e map-workflows --tag ui
2427
```
2528

26-
The runner writes one directory under `artifacts/ui-e2e/` per case. It contains
27-
the scenario report, input/event trace, command trace, engine log, and the
28-
individual screenshots taken at meaningful checkpoints. Review images at their
29-
native resolution.
29+
The runner writes the scenario report, input/event trace, command trace, engine
30+
log, and individual screenshots under `artifacts/ui-e2e/`. A focused invocation
31+
uses one run directory and writes `suite-report.md` beside it. A grouped
32+
invocation puts its scenario directories and one aggregate `suite-report.md`
33+
under a timestamped suite directory; every run report links back to that
34+
summary. Review images at their native resolution.
3035

3136
## What a scenario may assert
3237

@@ -74,7 +79,9 @@ tool is required.
7479

7580
## Lifecycle
7681

77-
Every run owns a temporary Spring write directory. It is removed after logs
78-
and screenshots have been copied to the artifact directory. `--keep-open`
79-
preserves the process and write directory for diagnosis; it is not normal test
80-
behaviour.
82+
Each process owns a temporary Spring write directory. Shared cases with the
83+
same launch environment reuse it after an undo/reload reset; isolated cases
84+
get a fresh one. The directory is removed after logs and screenshots have been
85+
copied to the artifact directory.
86+
`--keep-open` preserves the process and write directory for diagnosis; it is
87+
not normal test behaviour.

docs/design/programmatic-control.md

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,8 @@ A control channel lets a script drive a running SpringBoard the way a user
99
drives it — open an editor, set its fields, run a command, place the camera,
1010
capture the result — without synthesising X11 input.
1111

12-
Status: **design, not built.**
12+
Status: **implemented for editor fields, registered commands, camera state, and
13+
ordered capture; domain handles are the next layer.**
1314

1415
## Why
1516

@@ -41,6 +42,11 @@ requests cross to the main thread on a queue drained once per `update()`;
4142
replies return the same way. One request per line, no JSON-RPC batches —
4243
requests on a connection apply in order and reply in order.
4344

45+
Project reloads replace the native module and therefore close existing sockets.
46+
The Python client detects that replacement, waits for the new `instance_id`,
47+
refreshes the schema, and reconnects subsequent calls; a caller does not need
48+
to rebuild its editor or camera handles.
49+
4450
## Three surfaces, all first-class
4551

4652
**Editors**`ui.open(tab, editor)`, `ui.set(editor, field, value)`,
@@ -64,7 +70,13 @@ is what makes binding fail fast: a client resolves its handles at connect time,
6470
so an unknown editor or field raises immediately with a list of what exists,
6571
rather than silently doing nothing three minutes into a run.
6672

67-
Alongside these: `camera.set/get` (position, target, fov) and `capture(path)`.
73+
Alongside these: `camera.set/get` (rendered position, direction, fov, and
74+
controller distance/height), `camera.trace_screen_ray(x, y)`,
75+
`camera.zoom(factor)`, `capture(path)`,
76+
`runtime.barrier()` (two input-idle native updates, for ordering external input), and
77+
`runtime.reload_native_modules()` for native-module lifecycle testing. The
78+
reload deliberately preserves engine world/project state; it is not a project
79+
reset.
6880

6981
## The Python client
7082

@@ -89,7 +101,7 @@ with control.connect(write_dir) as sb:
89101
lighting.groundDiffuseColor = (0.9, 0.45, 0.2, 1.0) # and here, listing the fields
90102
lighting.shadowMode = "Full"
91103

92-
sb.camera.set(position=(2048, 900, 2048), target=(2048, 0, 2048))
104+
sb.camera.set(position=(2048, 900, 2048), direction=(0, -1, 0), height=900)
93105
sun(dirX=0.5)
94106
sb.capture(out / "lit.png")
95107
```
@@ -125,18 +137,20 @@ other:
125137

126138
A reply is sent once its effect has landed, not once the request was accepted:
127139
`ui.open` answers when the editor is on screen, `capture` when the image is on
128-
disk. Requests on a connection apply and answer in order, and `capture` is
129-
queued at `draw_screen_post`, so an image already contains every call before it.
130-
No scenario needs a sleep to make a screenshot honest.
140+
disk, and `runtime.barrier` after two input-idle native updates. Requests on a
141+
connection apply and answer in order, and `capture` is queued at
142+
`draw_screen_post`, so an image already contains every call before it. No
143+
scenario needs a sleep to make a screenshot honest.
131144

132145
Unknown method, unknown editor, unknown field, out-of-range value, and failed
133146
deserialisation are all structured errors returned before anything is applied.
134147
Nothing in this channel may fail by doing nothing.
135148

136149
## Scope
137150

138-
**Built**: `describe`, `ui.open/set/get`, `command.execute`, `camera.set/get`,
139-
`capture`. `SBC_CONTROL_FILE` names the discovery file and turns the channel on;
151+
**Built**: `describe`, `ui.open/set/get`, `command.execute`,
152+
`camera.set/get/trace_screen_ray/zoom`,
153+
`capture`, `runtime.barrier`, `runtime.reload_native_modules`. `SBC_CONTROL_FILE` names the discovery file and turns the channel on;
140154
the E2E harness sets it per run and exposes the connection as
141155
`run_state.control`.
142156

@@ -153,7 +167,7 @@ widget needs a real display server, and stays with the X11 harness.
153167
## Relationship to the X11 harness
154168

155169
The two coexist. Feature scenarios — set things up, run the feature, look at the
156-
result — move to `tools/e2e/scenarios/control.py` and get faster and
157-
layout-independent: `lighting` there covers what `env.lighting_panel` covers, in
170+
result — move to the relevant domain module under `tools/e2e/scenarios/` and get
171+
faster and layout-independent: `lighting` there covers the environment lighting domain, in
158172
2s of scenario time against 14s. Scenarios genuinely about input routing, focus
159173
and hit-testing keep clicking. See [e2e.md](e2e.md).

docs/porting/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ one small, reviewed domain at a time into `rust-stable`.
3838
Native UI scenarios run against the real editor window:
3939

4040
```bash
41-
just test-e2e <target> --tag ui:rust
41+
just test-e2e <target> --tag ui
4242
```
4343

4444
Every changed golden must be visually inspected before it is recorded as

docs/porting/e2e-control-port.md

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
# E2E control-channel port
2+
3+
Status: in progress — Environment, Scenario Info, Map editor fields, and the
4+
non-gesture setup in brush/state scenarios are now control-driven.
5+
6+
The control channel should replace X11 in every domain-editor scenario that is
7+
setting editor state, running an editor feature, asserting emitted commands, or
8+
positioning the camera for a capture. Domain-editor scenarios do not test their
9+
own widget interaction. X11 remains the authority for the separate UI suite:
10+
input routing, hit testing, hover, text editing, drag/stroke behaviour, and
11+
dialog/widget layout.
12+
13+
Scenarios share a resettable Spring process with compatible launch environments
14+
by default. Scenarios that change the project or require a fresh filesystem
15+
opt into `@scenario(isolated=True)` and get their own process.
16+
17+
## Current API
18+
19+
Available now: typed editor `open`/`get`/`set`, typed registered-command
20+
execution, camera `get`/`set`/`zoom`, ordered captures, and a reset boundary that
21+
undoes native history before reloading native modules. This already removes
22+
coordinates and waits from semantic editor tests.
23+
24+
Asset fields currently travel as schema text; a semantic, validated asset value
25+
is not available yet. Editor action buttons, object create/select/place
26+
operations, project/file-dialog operations, and synthetic in-engine input are
27+
also not available. Those boundaries determine the partial and blocked rows
28+
below.
29+
30+
## Proposed migration order
31+
32+
| Status | Scenarios | Plan |
33+
| --- | --- | --- |
34+
| Done | `lighting`, `sky`, `water`, `info-panel`, `map-editors` | Domain fields and emitted commands use control only. |
35+
| Partial | `heightmap`, `texture-paint`, `metal-paint`, `grass-paint` | Scalar/editor setup uses control; the actual stroke, asset-grid choice, action selection, or project dialog remains X11. |
36+
| UI / blocked | `teams-panel`, `settings-panel` | Teams needs typed add/select/edit handles before it can become a domain test. Settings owns the shading-dialog interaction contract. |
37+
| Second batch | `props-panel`, `collision`, `units-panel`, `feature-placement-actions`, `brush-size`, `project-workflows`, `map-workflows` | Convert the domain state/command portion once object/project helpers exist. Move the remaining input setup out to focused UI coverage. |
38+
| Camera-only cleanup | `pattern-preview`, `texture-paint`, `metal-paint`, `grass-paint`, `cursortip`, `deselect`, `rotation`, `selection-drag`, `object-actions`, `selection`, `clipboard-actions` | Done: deterministic framing uses `camera.zoom`; no domain scenario uses mouse-wheel camera zoom. A dedicated camera-wheel test is intentionally out of scope for now. |
39+
| Keep X11 | `terrain-stationary-hold`, `heightmap`, all `chonsole-*`, `module-reload`, `developer-console`, `developer-console-copy`, `gallery`, `gallery-pickers`, `gallery-tooltips`, `gallery-dialogs`, `main-panel`, `hide-interface`, `all-editors`, `panel-tabs-are-choices`, `import-action`, `dialogs`, `project-status-bar`, `notifications`, `export-warning`, `ui-sweep`, `def-grid`, `feature-grid-tooltip-after-cursortip` | These explicitly test keyboard focus, pointer routing, held strokes, hover, clicking a control, modal layout, or rendering. Replacing their core interaction would stop testing what they exist to test. |
40+
41+
The former water UI scenario's terrain-basin stroke and `/water 4` input belong in focused
42+
stroke/console tests, not in the water domain test. `map_export` and
43+
`map_roundtrip` remain blocked until a project API is deliberately designed.
44+
45+
## Focused UI suite
46+
47+
The retained X11 scenarios should become a compact, cross-cutting UI suite:
48+
one test per widget/interaction contract, not one copy per domain editor. The
49+
existing gallery, picker, tooltip, dialog, Chonsole, input-state, toolbar, and
50+
visual-sweep scenarios are its nucleus. When a domain scenario gives up a
51+
click/drag/picker assertion, move that coverage here only if no existing UI
52+
scenario already covers the same generic contract. `field_modal_handoff` is
53+
the first such extraction: it covers modal-binding cleanup across editors
54+
without being presented as Scenario Info coverage.
55+
56+
## API additions worth designing before the second batch
57+
58+
1. Typed asset fields: an `AssetPath`/asset-reference value accepted by
59+
`Editor.set`, validated against the live field schema.
60+
2. Typed editor actions: invoke a named registered editor action through the
61+
same behaviour path as a button, with schema discovery and no raw strings.
62+
3. Object domain handles: create, select, inspect, and mutate objects without
63+
pretending those operations are generic editor fields.
64+
4. Project domain handles: create/save/load/export by typed request. Do not
65+
expose file-dialog clicks as an API.
66+
67+
Do not add `input.*` merely to avoid X11 in the retained scenarios. Its job is
68+
to test the native input state machine and belongs only where pointer/keyboard
69+
semantics are themselves under test.
70+
71+
## Progress
72+
73+
| Batch | Status | Notes |
74+
| --- | --- | --- |
75+
| Environment | done | `lighting`, `sky`, and `water` now cover their editor domain state. |
76+
| Scenario Info | done | `info-panel` now sets and reads metadata through control. |
77+
| Map editor fields | done | `map-editors` covers terrain, texture, metal, grass, and rendering fields through control. |
78+
| Brush/state setup | partial | Scalar setup is control-driven; pointer gestures and picker/action contracts remain X11. |
79+
| Focused UI suite | existing, to consolidate | Own generic editor/widget interaction coverage. |
80+
| Team domain | blocked | Needs typed add/select/edit API; keep its current test explicitly UI until then. |
81+
| API additions | proposed | Design only when the second batch is reached. |
82+
| Second batch | blocked | Depends on typed object/project/action support. |
83+
| Camera cleanup | done | Domain/object setup uses the typed camera surface; the only remaining wheel event is Chonsole suggestion-list scrolling. |

docs/porting/native-e2e-review.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ None.
2020
| `selection` | Reviewed and recaptured for the versioned 1371px window geometry. A subsequent regression holds left during a box drag, right-clicks to cancel it, checks that the transient outline is gone, then completes a fresh box selection. |
2121
| `feature-placement-actions` | Its panel-local visual comparison was using full-window coordinates; corrected crop coordinates now assert the Add → Brush change. |
2222
| `gallery`, `gallery-dialogs`, `gallery-pickers`, `gallery-tooltips` | Reviewed the current fields, drag, dialogs, pickers, and tooltips; stale references were recaptured. |
23-
| `native-dev-console` | Reviewed the current console/status presentation, including the live line count; stale references were recaptured. |
23+
| `developer-console` | Reviewed the current console/status presentation, including the live line count; stale references were recaptured. |
2424
| `props-panel` | Form references were updated for full-width controls. The outside-drag assertion now unambiguously marks the releasing drag and tolerates only the documented live numeric glyph variation. |
2525
| `units-panel` | Reviewed panel and world output; stale references were recaptured. |
2626

docs/porting/verification.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Rules:
3030
Run a native target with:
3131

3232
```bash
33-
just test-e2e <target> --tag ui:rust
33+
just test-e2e <target> --tag ui
3434
```
3535

3636
## Editors
@@ -46,9 +46,9 @@ just test-e2e <target> --tag ui:rust
4646
| 7 | Map → Metal | VERIFIED | Pattern, size, rotation, amount, and painting. E2E `map-editors`, `metal-paint`. |
4747
| 8 | Map → Grass | VERIFIED | Pattern, detail, size, rotation, and painting. E2E `map-editors`, `grass-paint`. |
4848
| 9 | Map → Settings | VERIFIED | Rendering flags, splat fields, detail texture, and New/Existing texture paths. E2E `map-editors`, `settings-panel`. |
49-
| 10 | Env → Lighting | VERIFIED | Shadow mode, direction, six colours, and densities. E2E `lighting-panel`. |
50-
| 11 | Env → Sky | VERIFIED | Atmosphere colours, fog bounds, and skybox picker. E2E `sky-panel`. |
51-
| 12 | Env → Water | VERIFIED | Scalars, booleans, colours, Normal/Foam/base textures, and visible water. E2E `water-panel`. |
49+
| 10 | Env → Lighting | VERIFIED | Shadow mode, direction, six colours, and densities through typed control. E2E `lighting`. |
50+
| 11 | Env → Sky | VERIFIED | Atmosphere colours and fog bounds through typed control. E2E `sky`; skybox picker remains UI coverage. |
51+
| 12 | Env → Water | VERIFIED | Scalars, booleans, colours, and texture fields through typed control. E2E `water`; visible water and pickers remain UI coverage. |
5252
| 13 | Misc → Info | VERIFIED | Name, description, version, and author commit a complete scenario record. E2E `info-panel`. |
5353
| 14 | Misc → Teams | VERIFIED | Roster, add/remove, edit modal, resources, colour, position, side, and row refresh. E2E `teams-panel`. |
5454

@@ -73,7 +73,7 @@ just test-e2e <target> --tag ui:rust
7373
| 36b | Cursor tooltip | VERIFIED | Empty-ground suppression, feature/unit content, and map hover. E2E `cursortip`. |
7474
| 37 | Pattern brush preview | VERIFIED | Fresh E2E `pattern-preview` (2026-07-18): a Terrain/Add state without a pattern had no footprint; choosing the pattern produced the inspected textured ground projection beneath the same cursor. |
7575
| 38 | Ray-trace agreement | TODO | Existing selection tests do not prove click, drag, and preview resolve the same point. |
76-
| 39 | Developer console | VERIFIED | Fresh E2E `native-dev-console` and `native-dev-console-copy` (2026-07-18): reviewed cleared/visible/hidden/Problems screens, live status controls, multiline selection, Ctrl+C, and Ctrl+A copy ownership. |
76+
| 39 | Developer console | VERIFIED | Fresh E2E `developer-console` and `developer-console-copy` (2026-07-18): reviewed cleared/visible/hidden/Problems screens, live status controls, multiline selection, Ctrl+C, and Ctrl+A copy ownership. |
7777
| 40 | Chonsole | DONE | Dedicated scenarios cover editing, completion cycling, mouse hover/click, scroll, texture/rule completion, persistence, and native reload. Re-run and inspect them as one current verification pass. |
7878
| 41 | Status strip | DONE | Metrics, command journal, undo/redo/clear controls, and a status golden are implemented. Re-run with developer console and inspect layout/current metrics. |
7979

justfile

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -144,18 +144,28 @@ dev-panel config="config/ui-rust.json": build-native
144144
run config="config/ui-rust.json": build-native
145145
PYTHONPATH="{{tool_pythonpath}}" uv run --locked sbc-smoke manual --config "{{config}}"
146146

147-
# Run black-box UI E2E tests. Does not rebuild native code; run `just build`
148-
# first when testing Rust UI changes.
147+
# Run black-box UI E2E tests. The native plugin is rebuilt first. Multi-case
148+
# invocations store all scenario artifacts and the aggregate suite report under
149+
# one timestamped suite folder.
149150
[group('test')]
150-
test-e2e target *args:
151+
test-e2e target *args: build-native
151152
PYTHONPATH="{{tool_pythonpath}}" uv run --locked sbc-e2e run "{{target}}" {{args}}
152153

154+
# `just test-e2e all` shares compatible launch environments by default and
155+
# isolates only scenarios explicitly marked `@scenario(isolated=True)`.
156+
157+
# Summarise existing E2E artifacts without launching Spring. For a bounded run:
158+
# `just e2e-report --after 20260730-115416 --before 20260730-121013`.
159+
[group('test')]
160+
e2e-report *args:
161+
PYTHONPATH="{{tool_pythonpath}}" uv run --locked sbc-e2e report {{args}}
162+
153163
# List every reference image and whether it is approved or still ai-reviewed.
154164
[group('test')]
155165
goldens-status:
156166
@PYTHONPATH="{{tool_pythonpath}}" uv run --locked sbc-e2e goldens-status
157167

158-
# Approve a case's reference images (the human OK): `just goldens-approve rotation-rust`.
168+
# Approve a case's reference images (the human OK): `just goldens-approve rotation`.
159169
# Optionally name individual shots. Only a human runs this.
160170
[group('test')]
161171
goldens-approve case *shots:

native/src/sbc/chonsole/ui/controller.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@ impl ChonsoleController {
4141
if self.view.process_suggestion_clicks(core) {
4242
self.view.refresh(interface, core)?;
4343
}
44-
self.view.process_suggestion_hovers();
44+
self.view.process_suggestion_hovers()?;
4545
self.view.update(interface)
4646
}
4747
pub fn draw_screen(&mut self, interface: &NativeInterfaceRef) -> Result<(), Error> {

0 commit comments

Comments
 (0)