Docs

Using qa-capture

Install it once, run it from your app's repository on the branch of the pull request, drive the app by hand, and put the result on the pull request. Everything stays on your machine except the comment you choose to post.

Install

npm install -g qa-capture     # or: npx qa-capture <command>

You also need three things on the machine that is plugged into the device:

  • adb. Found automatically inside a standard Android Studio install. Set ANDROID_HOME if yours lives somewhere unusual.
  • ffmpeg and ffprobe, 5.1 or newer. On PATH, or point QA_FFMPEG_DIR at a folder containing both. An older ffprobe reports no frame timestamps, which would turn every tap into a no-op, so the tool refuses it.
  • gh, the GitHub CLI, 2.99 or newer (recommended). It finds the pull request for your branch and posts the comment with the screenshots attached. Install it, then gh auth login. Nothing here logs in for you. Without it you can still record: pass --pr <n> and post the saved comment yourself.

ANTHROPIC_API_KEY is optional and only used by the two commands that send data to a model, checklist generate --send and coverage --send. Recording works without it.

Check the setup

qa-capture doctor

It checks each of those and says what to do about the ones that are missing. Run it first: a dependency discovered halfway through a recording costs the whole session, and you cannot ask someone to tap through it again. With a device attached it also lists the device it will record.

Record a session

  1. Check out the branch of the pull request in your app's repository. The tool reads the diff from there and finds the pull request through gh.
  2. Open the app on the emulator or phone, on the screen you want to start from.
  3. Start recording, drive the app by hand, and press Ctrl-C when you are done.
qa-capture start --highlight --notify

While you drive, the terminal prints a line the moment you reach a screen the pull request touched and takes a lossless screenshot of it. With --highlight the element whose id or string the diff changed is outlined on that screenshot. With --notify a small status-bar notification confirms it on the device.

The first two seconds are the tool's own: it opens and closes the notification shade once as a sync marker, which pins the video to the touch log's clock. Start driving after that.

One accessibility client at a time on Android. Maestro, Appium or an agent cannot drive the same device during a recording.

On a physical phone

An emulator or a phone over USB both work; verified on a Galaxy A25 running Android 16. Turn on USB debugging in Developer options, plug the phone in, unlock it and accept the prompt, then check adb devices says device. On Samsung, also turn on USB debugging (Security settings). With more than one device attached, pass --device <serial>.

Add --buzz and the phone vibrates briefly at each changed-screen capture, so you know it was taken without looking at the terminal. Keep the phone unlocked during the recording: an unlock swipe at the start is recorded as a step of its own.

You don't need to do anything about the phone's clocks. A phone that has slept keeps touches on a different clock from the video, and the tool measures and corrects that at the start of every session. It then fine-tunes the video timing from your own taps, since a screen always reacts just after the finger lands.

Flags for start

FlagWhat it does
--pr <n>Name the pull request when it cannot be resolved from the branch, or gh is not installed. Saved into the session, so later commands find it on their own.
--app <package>The app under test, when it is not the one in the foreground.
--device <serial>Which device, when more than one is attached. Falls back to ANDROID_SERIAL, then the first device adb lists.
--duration <sec>Record unattended for a fixed time instead of until Ctrl-C.
--highlightRead the accessibility tree at each match and outline the changed element on the screenshot. Pauses the live stream a few seconds per shot, so it is opt-in.
--notify, --buzzAn on-device cue at each capture: a status-bar notification, a short haptic. Neither draws pixels into the shot.
--no-captureRecord without watching for changed screens.
--tree-sample, --no-tree-sampleRead the whole accessibility tree after each change on screen. On by default for a Flutter app, whose event stream carries only the title; off for the rest. Either flag overrides.
--platform ios, --udid <id>Record the iOS Simulator instead, on a Mac. See Frameworks.

Read the session

qa-capture view

Builds a self-contained HTML page for the latest session and prints where it is. Each step has a before and an after frame, a ring where the finger landed, the screen it was on, and a flag when a control did nothing. Ask one question of it: do these steps describe what you did? Everything downstream rests on that.

A session is a folder under sessions/ in your repo: session.json with the steps, the coverage and the screens the change touched, plus the frames it references. Keep sessions/ and checklists/ out of git.

The checklist

A checklist belongs to a pull request. Its items are what a person should try by hand; each is tied to the screen its words name in the diff, and is marked reached when a session was on that screen.

qa-capture checklist add "Open an order and pick a 15% tip" --pr 42
qa-capture checklist list --pr 42
qa-capture checklist screens --pr 42        # the screens and strings the diff names
qa-capture checklist generate --pr 42       # prints what a model would be sent
qa-capture checklist generate --pr 42 --send
qa-capture checklist pass chk_01 --pr 42    # a person's verdict, by name

generate is the one place the diff leaves your machine, and only with --send. The suggestions come back filtered for filler and for mentioning nothing the diff touched. A verdict of pass is always typed by a person.

Coverage

qa-capture coverage            # prints what it would send, sends nothing
qa-capture coverage --send     # sends a sample of the session's screenshots

Screen matching already marks items reached without any model. coverage --send goes one step further: it sends sampled screenshots and gets back, per item, covered, partial or no evidence. Every failure resolves to no evidence. There is no code path that writes a pass.

Post to the pull request

qa-capture comment            # prints it, saves sessions/<id>/comment.md
qa-capture comment --post     # puts it on the pull request

The comment carries the checklist as a task list a person ticks, the screens the change touched and which were reached, a strip of screenshots with the changed screens first, and the count of taps that produced no visible change. Screenshots are attached to the comment through your own gh login; on a private repository only people who can read the repository can see them.

A second run updates the existing comment rather than piling up. --screenshots <n> attaches more than the default six (up to 24), --no-screenshots posts the summary alone, --new forces a fresh comment. If the pull request was not resolved when you recorded, pass --pr <n> once.

The tool never handles a GitHub token. Posting is a call to gh, which is logged in as you.

Frameworks

FrameworkWhat matches
Views and FragmentsActivity and Fragment class names, view ids and strings. The strongest case: the change usually names a class.
Jetpack ComposeStrings and view ids. Navigation routes are invisible from outside, so screens match on what they show.
FlutterBody strings and Semantics(identifier:) values, read from the accessibility tree after each change on screen, since Flutter's event stream carries only the app-bar title. Switched on by itself when the package ships libflutter.so.
iOS SimulatorStrings and accessibilityIdentifiers, polled from the tree about once a second. Steps come from the video and carry no taps, so there is no dead-control detection on iOS. Needs Xcode, a booted simulator and AXe on PATH. Physical iPhones are not supported.

What leaves the machine

  • Only what you post. The comment body and the attached frames, through gh pr comment. With checklist generate --send: the diff. With coverage --send: a sample of the session's screenshots. Nothing else, ever. No telemetry, no account, no server.
  • Typed text is never recorded. A burst of typing is one step with the coordinates dropped, because on a soft keyboard the coordinates are the characters. Text-changed accessibility events are filtered on the device before a line reaches the host, and a tree read never takes the text of an editable field.
  • On-screen strings are matched, never stored. Only a matched string or a class name ends up in the session. Password fields are ignored.
  • Nothing is installed on the device. It runs shell commands against what the platform already exposes; a temporary log file is removed at the end.

Troubleshooting

Every tap reads as a no-op

Almost always the video clock. An ffprobe older than 5.0 reports no frame timestamps; doctor and start refuse it. Check QA_FFMPEG_DIR points at a folder with both ffmpeg and ffprobe.

No pull request linked

Install gh and log in, or pass --pr <n>. Without a pull request there is no checklist and nowhere to post; the session is still recorded.

The changed screens are never reached

Run qa-capture checklist screens to see what the diff names. If it prints nothing, the diff touches no screen the tool can recognise (a pure logic change, or a release build with obfuscated class names), and the session still records every step.

Another tool is driving the device

Stop it. Android allows one accessibility client at a time, and the recording holds it.

Something else

Run qa-capture doctor again and read it top to bottom. It is written to say what to do about each row.