Skip to content

Commit ad7ec61

Browse files
authored
Merge pull request #2059 from virtualcell/feat/ui-macro-recorder
Record UI interactions as replayable scripts, for demos and help pages
2 parents 8c64e1e + 910ce1d commit ad7ec61

16 files changed

Lines changed: 2327 additions & 19 deletions

File tree

.claude/skills/swing-debug/SKILL.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,37 @@ pre-redirect lines. `bridge.sh log 200` serves the tail.
7979
fire menu items from the models, opening no popup at all.
8080
- `JTree` rows report the text the **renderer** draws, so VCell's database trees
8181
read as "Lee 2026 Systems-level…" rather than `PublicationInfo@415f5f4f`.
82+
- **Not everything in a menu bar is a menu.** VCell puts icon-only controls straight into
83+
it — the detach toggle is a `JMenuItem` whose entire label is a tooltip — so `/menu`,
84+
which matches items by visible text, cannot address them. Click those by `name=`.
85+
86+
## Recording a flow instead of writing one
87+
88+
`bridge.sh record start` … drive the UI … `bridge.sh record stop <file.json>` captures what
89+
you did as an editable script, replayed with `bridge.sh replay <file.json>`. Good for
90+
tutorials and demos, and for turning a manual repro into a scenario.
91+
92+
Three things decide whether it works, and all three have bitten:
93+
94+
- **Both routes are recorded**: real input via the AWT listener, and the bridge's own
95+
model-based endpoints (which post no event) by reporting themselves. Robot helpers do not
96+
double-report. The flip side is that **scripted setup done while recording lands in the
97+
script** — do it before `record start`, or pass `--no-bridge-actions`.
98+
- **A modal dialog blocks input to everything behind it**, so clicks aimed at the main
99+
window produce no events at all. The version-mismatch warning on a source build appears a
100+
moment *after* the menus do — `wait` for it, don't look once. `record status` distinguishes
101+
the two failures: `rawEvents: 0` means nothing was seen, not that nothing was captured.
102+
- **Replay is patient where a scenario has to be.** It retries a step until it takes, so the
103+
menu item that sits disabled for a moment after a modal dialog closes needs no retry loop
104+
of your own.
105+
106+
Keep assertions in the scenario, not the recording — replay `--from N --to N` a step at a
107+
time so state is checked *between* clicks. `scenarios/detach-window-recorded.sh` is the
108+
worked example, and passes the same 15 checks as the hand-written original.
109+
110+
For a filmed replay use `--driver robot`: the cursor glides to each target and clicks for
111+
real. The default `semantic` driver moves no cursor, which is right for CI and wrong for a
112+
video. Same recording either way.
82113

83114
## Reusable recipes
84115

tools/debug-bridge/README.md

Lines changed: 74 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,10 @@ so getting a live, drivable client takes one command instead of a rediscovered r
77
|---|---|
88
| `launch-client.sh` | Launch the client **from `target/classes`** (no packaging) with the bridge on, using the bundled install4j JRE and a cached dependency classpath. Backgrounds itself and waits for `/health`. |
99
| `bridge.sh` | CLI over the bridge's HTTP endpoints: `tree`, `find`, `menu`, `wait`, `assert`, `shot`, `log`, … URL-encodes arguments, pretty-prints via `jq` when available; `wait`/`assert` exit non-zero on failure so scenarios are plain shell. |
10-
| `scenarios/` | Reusable end-to-end scripts built on `bridge.sh``smoke.sh` checks bridge, main frame, menus, EDT and screenshots using only semantic selectors. |
10+
| `replay.py` | Replay a recorded script (`bridge.sh replay`). Issues each step through the endpoint that already implements that verb, retries a step until it takes, and waits for a window a step opened rather than sleeping. `--from`/`--to` play a slice; `--speed`/`--max-delay` re-time it. |
11+
| `scenarios/` | Reusable end-to-end scripts built on `bridge.sh``smoke.sh` checks bridge, main frame, menus, EDT and screenshots using only semantic selectors; `detach-window.sh` and its recorded twin `detach-window-recorded.sh` drive the modeless-child-window round trip. |
12+
| `doc-scaffold.py` | Turn a recording plus its captures into a VCell help-page skeleton (`<vcelldoc>` XML for `UserDocumentation/originalXML`). Writes the mechanical parts — ordered steps, image references — and leaves the prose to a person. |
13+
| `scenarios/recordings/` | Captured UI scripts. JSON, hand-editable, replayed by `replay.py`. |
1114

1215
## Quick start
1316

@@ -24,3 +27,73 @@ tools/debug-bridge/scenarios/smoke.sh # end-to-end sanity
2427
local install at `~/Applications/VCell_Alpha` for the JRE/native libs; override with
2528
`VCELL_API_HOST` / `VCELL_INSTALL_DIR`. The client's real log is
2629
`~/.vcell/logs/vcellrun_<site>.log` — or just `bridge.sh log 100`.
30+
31+
## Recording and replaying
32+
33+
```bash
34+
tools/debug-bridge/bridge.sh record start # or: record start <file>
35+
# ... drive the UI by hand: click, type, pick menu items ...
36+
tools/debug-bridge/bridge.sh record stop scenarios/recordings/my-flow.json
37+
38+
tools/debug-bridge/bridge.sh replay scenarios/recordings/my-flow.json --dry-run
39+
tools/debug-bridge/bridge.sh replay scenarios/recordings/my-flow.json
40+
DRIVER=robot ... # or --driver robot: the cursor visibly moves
41+
```
42+
43+
**Both routes are recorded.** Real mouse and keyboard input reaches the AWT event queue and
44+
is captured by the listener. The bridge's own model-based endpoints (`click` on a button,
45+
`menu`, `settext`, `tab`, `row`, `trow`, `expand`) post no event at all, so they report the
46+
step themselves — a session scripted entirely with `bridge.sh` records correctly. Robot-driven
47+
helpers (`rbclick`, `drow`, `dtrow`, …) do not report themselves, because the listener already
48+
sees their real events.
49+
50+
Two consequences worth knowing: **scripted setup performed while recording ends up in the
51+
script** — start with `--no-bridge-actions`, or do setup before `record start`. And if a
52+
recording still comes out empty, `bridge.sh record status` reports `rawEvents`, which
53+
separates "saw nothing" (a modal dialog blocking input will do that) from "captured nothing".
54+
55+
**The file is written as you go.** `record start` creates it immediately and rewrites it
56+
after every step, so a client that crashes — or that you kill — costs at most the step in
57+
progress, not the take. `record start <file>` chooses where that happens; `record stop
58+
<file>` names the final destination and moves the auto-named working file there. Every
59+
reply, `status` included, tells you the current path.
60+
61+
**One recording, two replay drivers.** `semantic` (the default) is fast and moves no
62+
cursor — right for CI. `robot` glides the pointer to each target and clicks for real —
63+
the only mode worth filming, since a click with no pointer near it reads as broken. The
64+
recording is identical; only playback differs.
65+
66+
Replay handles the timing traps a fixed `sleep` cannot: it **retries a step until it
67+
takes** (a menu item sits disabled for a moment after a modal dialog is dismissed) and
68+
**waits for a window a step opened** before moving on.
69+
70+
The recording holds the navigation; assertions stay in the scenario. `detach-window-recorded.sh`
71+
shows the split — it replays one step at a time with `--from`/`--to` so it can check state
72+
*between* clicks, which a single end-to-end playback could not.
73+
74+
## Capturing a feature for the help system
75+
76+
```bash
77+
bridge.sh record start scenarios/recordings/my-feature.json
78+
# ... drive the feature by hand ...
79+
bridge.sh record stop
80+
81+
bridge.sh replay scenarios/recordings/my-feature.json --shots /tmp/shots --shot-scale 0.5
82+
doc-scaffold.py scenarios/recordings/my-feature.json --shots /tmp/shots \
83+
--target MyFeature --title "My Feature" --out page.xml
84+
```
85+
86+
Then a person copies the captures worth keeping into
87+
`vcell-client/UserDocumentation/originalXML/topics/image/` under names that mean something,
88+
updates the `imgReference` targets, writes the prose, and adds a `<tocitem>` to `TOC.xml`.
89+
90+
Three things this pipeline learned the hard way:
91+
92+
- **`--shot-delay` matters.** `/idle` drains the EDT, but VCell fills many panels from
93+
background tasks, so an idle EDT does not mean the pixels are final. Captured too early,
94+
two different steps produce byte-identical images and the page documents the wrong screen.
95+
- **Scale the captures.** `DocumentCompiler` rejects any image over 500,000 bytes. At
96+
`--shot-scale 0.5` a full window lands around 30KB; at full size it will not.
97+
- **The help build is skipped when it looks done.** The `build-documentation` profile
98+
activates only when `target/classes/vcellDoc` is *missing*, so a plain rebuild silently
99+
leaves doc changes untested. Remove that directory, or run `DocumentCompiler` directly.

tools/debug-bridge/bridge.sh

Lines changed: 51 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,11 @@
1010
# health | windows | tree [maxDepth] | menus
1111
# find [--type T] [--name N] [--text T] [--contains S] [--limit N]
1212
# props <selector> listeners <selector>
13-
# shot [window] log [lines]
13+
# findrow <selector> <text> [--exact] row number by displayed text (searches the WHOLE
14+
# model, unlike tree's 25-row/100-row dump cap)
15+
# findrow <selector> --apptype SPRINGSALAD find an application by its type, not its
16+
# name or position - both of which vary per model
17+
# shot [window] log [lines] (shot takes ?scale/name/dir via replay)
1418
# Act:
1519
# click <selector> rclick <selector>
1620
# settext <selector> <text> [--enter]
@@ -19,8 +23,16 @@
1923
# trow <selector> <row> [col] dtrow <selector> <row> [col] rtrow <selector> <row> [col]
2024
# menu "<Menu>Item[>Sub]" [window]
2125
# highlight <selector> [ms]
26+
# glide <selector> [ms] move the real cursor there (for a watched replay)
27+
# rbclick <selector> [glideMs] [row] native press/release; unlike click it IS recordable
2228
# iconify <selector> [true|false] minimize/restore a window; reports what the OS did
2329
# wbounds <selector> x y w h move/resize a window
30+
# Record / replay:
31+
# record start [file] [--no-bridge-actions] | stop [file] | status
32+
# (flushed to disk each step; bridge-driven actions are recorded too unless
33+
# --no-bridge-actions, so do scripted SETUP before starting the recording)
34+
# replay <script.json> [--driver semantic|robot] [--speed N] [--max-delay MS]
35+
# [--from N] [--to N] [--shots DIR] [--shot-scale F]
2436
# Synchronize / assert (exit 0 on success, 1 on failure):
2537
# wait [find opts] [--state showing|enabled|gone] [--timeout MS] [--interval MS]
2638
# assert [find opts] [--gone]
@@ -145,14 +157,51 @@ case "$cmd" in
145157
${3:+--data-urlencode "column=$3"} | pretty ;;
146158
rrow) get rightClickTreeRow --data-urlencode "path=$1" --data-urlencode "row=$2" | pretty ;;
147159
menu) get menu --data-urlencode "path=$1" ${2:+--data-urlencode "window=$2"} | pretty ;;
160+
findrow)
161+
case "${2:-}" in
162+
--apptype) get findRow --data-urlencode "path=$1" --data-urlencode "appType=$3" | pretty ;;
163+
*) get findRow --data-urlencode "path=$1" \
164+
$([ "${3:-}" = "--exact" ] && echo "--data-urlencode text=$2" || echo "--data-urlencode contains=$2") | pretty ;;
165+
esac
166+
;;
148167
props) get props --data-urlencode "path=$1" | pretty ;;
149168
listeners) get listeners --data-urlencode "path=$1" | pretty ;;
150169
highlight) get highlight --data-urlencode "path=$1" --data-urlencode "ms=${2:-2000}" | pretty ;;
170+
glide) get glide --data-urlencode "path=$1" --data-urlencode "ms=${2:-600}" | pretty ;;
171+
rbclick) get robotClick --data-urlencode "path=$1" --data-urlencode "glideMs=${2:-0}" \
172+
${3:+--data-urlencode "row=$3"} | pretty ;;
173+
174+
record)
175+
action="${1:-status}"
176+
case "$action" in
177+
status) get record --data-urlencode "action=status" | pretty ;;
178+
start)
179+
# record start [file] [--no-bridge-actions]
180+
cb=true
181+
for a in "$@"; do [ "$a" = "--no-bridge-actions" ] && cb=false; done
182+
f="$2"; [ "$f" = "--no-bridge-actions" ] && f=""
183+
get record --data-urlencode "action=start" --data-urlencode "captureBridgeActions=$cb" \
184+
${f:+--data-urlencode "file=$f"} | pretty ;;
185+
stop) get record --data-urlencode "action=stop" ${2:+--data-urlencode "file=$2"} | pretty ;;
186+
*) echo "record: expected start, stop or status" >&2; exit 2 ;;
187+
esac
188+
;;
189+
190+
replay)
191+
script="$1"
192+
shift || true
193+
PY=""
194+
for candidate in python3 python py; do
195+
if command -v "$candidate" >/dev/null 2>&1; then PY="$candidate"; break; fi
196+
done
197+
[ -n "$PY" ] || { echo "replay: need python3 on PATH" >&2; exit 2; }
198+
"$PY" "$(dirname "$0")/replay.py" "$script" --port "$PORT" "$@"
199+
;;
151200
iconify) get iconify --data-urlencode "path=$1" --data-urlencode "iconified=${2:-true}" | pretty ;;
152201
wbounds) get windowBounds --data-urlencode "path=$1" --data-urlencode "x=$2" --data-urlencode "y=$3" --data-urlencode "w=$4" --data-urlencode "h=$5" | pretty ;;
153202

154203
help|*)
155-
sed -n '2,30p' "$0" | sed 's/^# \{0,1\}//'
204+
sed -n '2,36p' "$0" | sed 's/^# \{0,1\}//'
156205
[ "$cmd" = help ] || exit 2
157206
;;
158207
esac

0 commit comments

Comments
 (0)