Recording tests
Public guide snapshot. View source. For exact flags in your installation, use olef commands <path> --json.
Record a flow once, in the device viewer or by driving the app with
olef commands, and save it as a test folder: a Bash script of one
olef command per step, each naming its element and checking its own
result, plus a screenshot of every step. The test then replays without AI;
see test runs for running and repairing it.
Every session keeps a journal of what is done in it, so there is nothing to switch on: an agent explores, then saves.
The sample app has a folder saved this way from a viewer recording of its
Mission flow, unedited:
samples/shadowdroid-test-app/tests/olef/mission_recorded/ in the
development repository (its hand-written twin is mission_recovery).
TOKEN="$(mktemp -d)/session.token"olef session open --agent explorer --token-out "$TOKEN" \ --app io.github.andriyo.shadowdroid.sample --clear-data --profile automation \ --launch .MainActivity --expect-rid status_textexport OLEF_SESSION_FILE="$TOKEN"olef record step open-missionolef ui tap --rid nav_mission --expect-rid mission_claim_button… # explore; dead ends and typos are fineolef record save mission_recovery # clean up, compile, replay, write the folderolef test run tests/olef/mission_recovery --repeat 3olef session closeIn the viewer
Section titled “In the viewer”Click Record steps (or press Cmd+T) and use the app. Recording starts
from the screen as it is. Shift-click asks to clear the app’s data and
relaunch the screen in front first, so the test starts from a fresh install’s
state. Clearing erases whatever the app keeps on the device (a signed-in
account, a debug switch such as “use the local backend”), so it is never the
default; tests that need it can run the app’s own setup script before
session open. The
Steps tab shows each step as the line the test will run, with its
screenshot, how the element is found (id, label, text or coordinates) and
the recorder’s confidence. Remove a step or a generated check with its ✕.
Check turns the next click on the screen into a check of what that
element shows (it is not tapped).
Click the button again to stop. The tab then lists what was left out and
why, and offers Save as test (the replay below, then the folder),
Check it (test run --repeat 3) and Send to the agent with the
folder’s path. The MCP recording tool returns the same steps and the saved
folder.
The viewer runs in the session of the agent that opened it, so a recording can mix the person’s input and the agent’s commands.
| Command | Does |
|---|---|
record start [--app P] [--clear-data] [--launch [ACTIVITY]] [--expect-rid R | --expect-text T] |
Begins a recording, optionally from a fresh start. A bare --launch relaunches the app’s screen in front. --clear-data refuses to clear anything but an installed app of yours. |
record stop |
Ends it and keeps the screen it ended on (the last step’s checks are read from it). |
record step "<name>" |
Names the commands that follow: step pay-with-the-saved-card. Commands with OLEF_STEP set keep that name. |
record mark <name> |
Names a point, for record save --from/--to. |
record check <selector> [--has-text S | --checked [B] | --selected [B] | --enabled [B] | --gone] |
Adds a check of your own; it must hold now. Written as ui wait … under # checked by you. |
record show |
The recording as it would be saved: each step’s lines, what clean-up removes and why, and the script. Changes nothing. |
record status |
Whether a recording is on, and how much it holds. |
record save <name> [-o DIR] [--from MARK] [--to MARK] [--last N] [--no-verify] [--force] [--journal DIR] |
Cleans up, compiles, replays and writes the folder (default tests/olef/<name>/, or tests.dir in .olef/config.json). |
record discard |
Starts over from here; the journal itself is kept. |
Without record start, record save takes the whole session (after the
newest record discard). --journal DIR reads a session folder whose
session is closed, such as one a script left.
What saving removes
Section titled “What saving removes”Every removal is listed with its reason, in the save result, in record show, in the folder’s README and in recording.json. Nothing is dropped
silently.
- Failed commands whose input never reached the app: a misspelt id, an ambiguous selector, a disabled or covered target, a refused guard. A failed action whose input was delivered stays, without the expectation that failed; one whose outcome is unknown (a timeout) stays and is left to the replay check.
- Reads (
ui dump,ui find, screenshots,log,why,net log) and checks that failed. Waits and checks that passed stay. - Olef tooling:
net start/stop,video,connect, the debuggers and the runtime. A test that started the proxy would leave the device proxied;net rulemocks stay, they are part of the flow. - Dead ends: a run of actions after which the screen is back where it was, with nothing lasting in between. “Where it was” compares what can be acted on (ids, labels, places) plus switches, selections, slider values and what input fields hold, ignoring labels that changed while recording (a status line). Lasting: typing, a switch, checkbox or chip, a slider, a request that writes data (POST, PUT, PATCH, DELETE, when the proxy is capturing), a change to app or device state. A run is kept when a check in it saw something it caused that lasted, or a check right after it passed that did not hold before it.
- A single action counts as a no-op only when nothing visible changed at all: a tap whose only effect is a new line of text (a request’s result) is kept. The recording’s last action is never a dead end.
- Scrolling (viewer swipes,
ui scroll-to, coordinate swipes) before an action becomes--revealon that action. - Viewer taps that only focused a field before typing;
ui textfocuses it. --if-interaction/--if-screen(bound to one recorded screen) are stripped from what stays.
How a step is written
Section titled “How a step is written”- The element: every way to name it is scored under the CLI’s own
matching rules on the screen as it was when the step acted:
--rid(view id or Compose testTag) 100,--desc70,--text60, a label inside a clickable container 55/50; it must resolve to that element alone; digits, long text and generated-looking ids cost points; typed text and labels that changed while recording are never used. The best goes in the script, the next two inrecording.jsonfortest heal. An agent’s own selector is kept unless one is clearly more stable (30 points). Ids from a dump (--id,--handle) and coordinates are always replaced; coordinates stay only when nothing names the element, marked# fragile. - Viewer input is resolved against the screen at touch-down (requested
the moment the finger goes down; a late one falls back to the last
settled screen). A tap is
ui tap, ≥ 500 msui long-tap --duration-ms, two quick taps on one placeui double-tap, a drag on a sliderui set-progress --value(whole numbers on a wide range), Backui back, rotationdevice orientation. Typing (Backspace included) is oneui text --rid … --clear "$PARAM", into the field tapped before it, with the value read from the field afterwards; password fields get--value-env. A tap on an element with no click action gets--via touch. - Parameters: each typed value becomes a variable named after the
field’s label (
RUN_CODEfrom “Run code”) or id, with the typed value as its default, and checks that show the value use the variable:--expect-text "Incident claimed by $OPERATOR_CALLSIGN". - Checks, for a step that has none of its own, from what it changed
between the screen before it, the settled screen after it and the screen
the next step started on: a switch, checkbox or chip it tapped gets
--expect-checked; else text it changed in place (a status line, its stable prefix: “json-post completed status=200” of “… bytes=171 elapsed=93ms”), else a landmark it brought on screen (--expect-rid), else new short text. A dialog button that went away, with its label, getsui wait --rid … --gone. Typing and sliders read their own value back.--timeout-msis added only where the recording showed a step needed longer than the session’s limit. - Confidence per step:
high,medium(found by label or text, or the screen moved under the finger) orlow(coordinates, no check), written as# low confidence: …above the step and listed bytest lint.
An agent that checks its own steps (--expect-*, ui wait) keeps those
checks. Generated checks need the effect to be on a recorded screen: an
agent that fires the next command before the app reacts leaves less to
read than a person in the viewer does.
How the test starts
Section titled “How the test starts”The test’s session open line puts the app’s runtime permissions back the
way the recording found them, so what one test answered cannot break the
next:
- The recording reads the app’s runtime permissions as it starts (
record start,record discard, orsession open --app), after each step that answers a permission dialog, and atrecord stop. - A permission that was granted gets
--grant; one denied with “don’t ask again” gets--deny, so its dialog never shows. Both go back to what they were when the test’s session closes. - A permission whose dialog the recording answered gets
--reset-permission, so the dialog shows again for the test’sperm dialogstep. Which permission it was comes from what the answer changed. When that cannot be told (a journal from an older version), the test resets every permission first (--reset-permissions) andrecord savewarns. - A permission left undecided (never answered, or refused once so Android
still asks) is not set. A test that depends on one can add
--reset-permissionor--revokeitself.
olef session open --token-out "$TOKEN" --agent test:livd_brand_search --app "$APP" \ --profile automation \ --deny android.permission.POST_NOTIFICATIONS \ --launch com.livdapp.client.MainActivity --expect-rid home_screenThe replay check
Section titled “The replay check”record save runs the cleaned script once from a fresh start, in a session
nested under yours, with the olef that saved it:
- It passes: the folder is written.
- It fails at a step whose element came from a removed step: the nearest removed dead end before it goes back, and it replays again. A generated check that did not hold on the replay is dropped instead (it read a transient screen). At most four replays.
- It fails for another reason (the device, the environment, the start):
nothing is written. The error (
record_replay_failed) names the step, cause and the replay’s report;--no-verifywrites the script as compiled.
The save result lists verify.replays (each with its run folder),
restored and dropped_checks.
The test folder
Section titled “The test folder”tests/olef/mission_recovery/ test.sh the script (executable); see test runs for the format screens/ one half-size JPEG per step (<step>.jpg), committed with the test README.md the steps in plain English, parameters, how to run it, what saving removed recording.json steps as compiled, alternative selectors, removals, the replay, provenance responses/ the kept steps' server responses (net export fixtures), when the proxy captured expect/ request payloads the kept steps sent (POST/PUT/PATCH JSON, volatile values as {{any}}/{{number}}/{{iso-date}})record save --force replaces only these files; mocks/, data/ and
anything else in the folder are yours and stay. Without --force an
existing test.sh is never overwritten (the error says when it was edited
since it was generated).
Network fixtures are written for flows inside each kept step’s window, a
timing heuristic. Using them (strict offline replay) and checking the
expect/ payloads (net wait) come with the determinism kit.