Screenshots and artifacts¶
Examples are in Python
Kotlin has the same methods in camelCase (dump_hierarchy → dumpHierarchy, save_to →
saveTo) and takes Paths. Kotlin + JUnit 5
shows this page's examples in Kotlin.
Screenshots and dumps¶
Each returns a typed value, not a file: keep it in memory, assert on it, attach it to a report
or write it where you like. They share one artifact shape: bytes (the serialized form),
media_type, extension and save(path) (which creates parent directories).
| Call | Returns |
|---|---|
device.screenshot() |
Screenshot: the PNG bytes (checked against the server's SHA-256), its format, width and height |
device.dump_hierarchy() |
Hierarchy: the accessibility tree as xml (a string) and as UTF-8 bytes. Diagnostic only; lookups never use it |
device.driver_log() |
DriverLog: the driver's recent lines for the session, text joined with newlines |
device.info() |
DeviceInfo: API level, manufacturer, model, display size and rotation, the package owning the focused window, and the device conditions; bytes is JSON |
shot = device.screenshot()
print(shot.width, shot.height)
shot.save("build/shots/login.png")
device.capture() takes all four at once, in parallel, each within a timeout (default 30 s),
and returns a Capture: the screenshot, hierarchy, info and driver_log parts,
artifacts (the produced ones by name) and failures (why a missing part is missing). It never
raises for the device, so it is safe in an except. save_to(dir) writes
<prefix>.<part>.<ext> files (the prefix defaults to the serial).
capture = device.capture()
capture.save_to("build/evidence/after-login")
for part, why in capture.failures.items():
print(f"no {part}: {why}")
Recordings¶
device.start_recording() and device.stop_recording() return a Recording artifact
(bytes, media_type, extension, save(path)). Nothing is recorded unless you ask; this is
not a live screen stream or an automatic failure artifact.
device.start_recording(audio_source="output")
app.element(res("play")).tap()
app.wait(text("0:05")).visible()
device.stop_recording().save("build/recordings/playback.mkv")
- Tracks. The default is video only (MP4, up to 30 seconds or 16 MiB; 1024 px, at most
15 fps). Add
audio_source="output"for video and audio in one Matroska file, orvideo=Falsewith an audio source for Opus audio only (up to 60 seconds or 3 MiB).max_secondsends the capture early. - Audio sources.
output(Android 11+) records device playback but redirects it away from the device speakers;playback(Android 13+) keeps local playback, but apps may opt out;micrecords the device microphone. Android 10 and earlier can record video only.outputrecords after the device's media volume, and that route has its own volume (at the default 5 of 15 a test tone comes out about 32 dB quieter);playbackrecords the app's sound before volume. Useplaybackon Android 13+ when levels matter, or set the media volume while anoutputrecording runs. - scrcpy. The server's machine needs scrcpy
(
tap start --scrcpy PATH, defaultscrcpyonPATH). One recording per attached device; detach discards an unfinished one. - Timing. The start call returns while scrcpy is still starting on the device (about half a second), so the first moment can be missing, and a capture the device refuses is reported by the stop call. Do not stop a recording immediately after starting it. A static screen produces no new frames, so a video file can be shorter than the time it was recording.
Failure artifacts¶
When a test fails, the pytest plugin and the JUnit extension call device.capture() for each
device of the test, before detaching it, while the screen still shows the failure, and save
the result:
tap-artifacts/<nodeid>/ (JUnit: build/tap-artifacts/<class>/<method>/)
├── failure.txt the exception and stack trace
├── device-emulator-5554.screenshot.png screenshot
├── device-emulator-5554.hierarchy.xml accessibility hierarchy
├── device-emulator-5554.device-info.json API, model, display, focused package
└── device-emulator-5554.driver-log.txt the driver's log for the session
The file prefix is <role>-<serial>, so a two-device test yields sender-… and receiver-….
Devices are captured in parallel, and so are the four parts of each device, each within 30 s, so
one slow or hung device or part does not cost the others their artifacts. Capture never masks
the original failure: a file that cannot be produced (the device went away, the time ran out) is
simply missing, and the test still fails with its real error. TAP_CAPTURE=off turns this off
(Python + pytest, Kotlin + JUnit 5).
device.capture() is an ordinary call, so a test can take the same evidence whenever it wants:
after a step, or in an except.
Reading the hierarchy dump¶
The XML is UiAutomator's view of every window on screen (each window's root under one
<hierarchy>), which is what selectors search. The attributes that selectors match are
resource-id, text, content-desc, hint, class, package (what device.app(pkg)
checks), plus the boolean state flags.
A node another window (a dialog, the keyboard) covers completely is left out, as Android
reports it not visible; one covered partly is in the dump, and a gesture on it fails with
NOT_INTERACTABLE / OBSCURED when its touch point is under the other window.
If the element you wanted is missing from the dump, it is missing from the accessibility tree:
a Compose node without testTag + testTagsAsResourceId, a View with
importantForAccessibility="no", or content not yet laid out. Fix the app's semantics rather
than reaching for coordinates.