Package-level declarations

Client, connections, devices, elements, waits, the selector DSL, errors and the value types the server returns.

Types

Link copied to clipboard
class App

One app on a device: its elements, its lifecycle and the waits on it. Obtain with Device.app, which performs no I/O.

Link copied to clipboard
class AppLifecycleException(message: String, cause: Throwable? = null) : TapException

An AUT lifecycle postcondition did not hold (process still alive, window never appeared...).

Link copied to clipboard
data class AppProcess(val pid: Int, val startToken: String)

A process of an app as the host sees it: PID plus the /proc start token that tells reused PIDs apart.

Link copied to clipboard
interface Artifact

Something the device produced that a test may keep: a Screenshot, a Hierarchy, a DeviceInfo or a DriverLog. Every artifact is plain data with its serialized bytes and mediaType, so it can go to a file (save), a report, an HTTP upload or an assertion without touching the device again.

Link copied to clipboard
data class Bounds(val left: Int, val top: Int, val right: Int, val bottom: Int)

A node's bounds in screen pixels.

Link copied to clipboard
class Capture

The diagnostics of one device at one moment (Device.capture): a Screenshot, the Hierarchy, the DeviceInfo and the DriverLog. A part is null when it could not be produced; failures says why, by part name.

Link copied to clipboard
class CommandException(val code: ErrorCode, val detail: String?, val driverMessage: String?, val operation: String, val serial: String, val selector: String?, val requestId: Long, val generation: Long, val durationMs: Long) : TapException

A driver command's outcome was error. code is the protocol error code; detail refines it (e.g. NOT_FOUND/END_REACHED); TRANSPORT_LOST/INDETERMINATE carry the transmission state in detail. Never retry a mutation on INDETERMINATE. requestId and generation identify the command in the driver and daemon logs.

Link copied to clipboard

Finds a running server; never starts one (see TapDaemonProcess).

Link copied to clipboard
data class DaemonEndpoint(val address: String, val token: String?)

A server address and the bearer token its RPCs carry (null: none).

Link copied to clipboard
class Device

One attached device backed by one live driver session, proxied by the host server. All calls are suspend. The client does not serialize calls on one device: issue them sequentially (the normal test style), and use coroutineScope + async for concurrency across devices (sibling failure cancels the other's in-flight RPC). Nothing here caches UI state: App.element and Screen.element return a lazy selector that every action resolves again, and mutations fail with AMBIGUOUS/NOT_FOUND before any input when the selector does not match exactly one node.

Link copied to clipboard
class DeviceBusyException(message: String, cause: Throwable? = null) : TapException

Another session — in this process or any other — holds the device, and DeviceOptions.waitForDevice was zero or ran out. The device is fine; try later or pick another serial.

Link copied to clipboard
data class DeviceEntry(val serial: String, val state: DeviceState, val clientConnectionId: String?, val quarantineReason: String?)

One device ADB lists (TapClient.devices).

Link copied to clipboard
data class DeviceInfo(val apiLevel: Int, val manufacturer: String, val model: String, val product: String, val displayWidth: Int, val displayHeight: Int, val displayRotation: DisplayRotation, val currentPackage: String?, val screenOn: Boolean, val keyguardLocked: Boolean, val keyguardSecure: Boolean, val keyboardShown: Boolean, val autoRotate: Boolean, val animationsEnabled: Boolean, val darkMode: Boolean, val fontScale: Float, val densityDpi: Int, val airplaneMode: Boolean, val wifiEnabled: Boolean, val mobileDataEnabled: Boolean, val systemLocales: List<String> = emptyList(), val stayAwake: Boolean = false, val highContrastText: Boolean? = null, val colorInversion: Boolean? = null, val boldText: Boolean = false) : Artifact

Facts about the device at the moment of Device.info: identity, display geometry and rotation, screen, keyguard and keyboard state, the package owning the focused window and the device conditions (Device.setAnimations and the rest). Serialized as JSON.

Link copied to clipboard
data class DeviceOptions(val skipDriverInstall: Boolean = false, val waitForDevice: Duration = Duration.ZERO)

Device-attachment options. The driver is always the daemon's (bundled, or the APKs given to tap serve --driver-apk X --driver-test-apk Y); skipDriverInstall only skips reinstalling it.

Link copied to clipboard
class DeviceQuarantinedException(val serial: String, message: String, cause: Throwable? = null) : TapException

The device's session journal keeps it out of service (a mutation whose outcome could not be proven, a corrupt journal, an uncertain ADB cleanup). It stays unusable until an explicit reset; retrying or waiting does not help.

Link copied to clipboard

A device's state in TapClient.devices. Only FREE and LEASED devices can be attached.

Link copied to clipboard

Gesture direction: where the finger moves for Element.swipe, the content edge moved towards for Element.scroll and Element.fling (DOWN reveals content below).

Link copied to clipboard

Clockwise display rotation relative to the device's natural orientation (portrait on phones, often landscape on tablets): Surface.ROTATION_0 / _90 / _180 / _270.

Link copied to clipboard
class DriverLog(val lines: List<String>) : Artifact

The driver instrumentation's recent output (Device.driverLog), one entry per line.

Link copied to clipboard
class Element

A selector bound to a device. Every method resolves the selector again on the device, so nothing goes stale between calls; mutations require exactly one match.

Link copied to clipboard
data class ElementSnapshot(val className: String?, val packageName: String?, val resourceName: String?, val text: String?, val contentDescription: String?, val hint: String?, val bounds: Bounds, val checkable: Boolean, val checked: Boolean, val clickable: Boolean, val enabled: Boolean, val focusable: Boolean, val focused: Boolean, val longClickable: Boolean, val scrollable: Boolean, val selected: Boolean, val childCount: Int, val showingHint: Boolean, val actions: List<StandardAction> = emptyList(), val customActions: List<String> = emptyList(), val range: Range? = null)

The state of one node at one instant (Element.snapshot). Text properties are null when the node has none. text is the raw accessibility text: on API 26+ an empty field reports its hint there and showingHint is true.

Link copied to clipboard

Wait builder returned by App.await / Screen.await / Element.await. visible, one and gone poll on the device in a single RPC; property waits poll snapshots from the host with delay-based polling, so test-root cancellation and sibling failure cancel them promptly.

Link copied to clipboard

Why a driver command failed (CommandException.code). UNKNOWN also stands for a code this client version does not know.

Link copied to clipboard
@RequiresOptIn(message = "Experimental Tap API: it may change in any release.", level = RequiresOptIn.Level.WARNING)
annotation class ExperimentalTapApi

Marks API that is not covered by the compatibility promise: it may change or go away in any release, including a patch. Today that is app synchronization (App.awaitIdle and the tap-sync-sdk contract behind it), which only works with apps the driver can see (see the app-lifecycle guide). Opt in with @OptIn(ExperimentalTapApi::class).

Link copied to clipboard

The server's structured reason for a failed call (ServerException.reason). UNSPECIFIED means the failure did not come from the daemon (a transport failure, a proxy, a cancelled call) or carried a reason this client version does not know.

Link copied to clipboard
data class ForegroundActivity(val packageName: String, val className: String)

The activity on top of the screen (Device.foregroundActivity): its app's packageName and its fully qualified className.

Link copied to clipboard
class Hierarchy(val xml: String) : Artifact

The accessibility hierarchy as XML (Device.dumpHierarchy). Diagnostic only: selectors never use it, so keep it out of assertions.

Link copied to clipboard

The encoding of a Screenshot's bytes.

Link copied to clipboard

The Precise / Approximate choice of the location permission dialog (Android 12+).

Link copied to clipboard

How a text property is compared with a selector's value.

Link copied to clipboard
data class Notification(val packageName: String, val title: String?, val text: String?, val actions: List<String>, val clearable: Boolean, val postedAt: Instant)

An active notification (Device.notifications, Device.awaitNotification): the packageName that posted it, its title and text (the android.title / android.text extras, null when it has none), the titles of its action buttons (actions, what Device.openNotification's action names), whether a swipe dismisses it (clearable: false for an ongoing one, such as a foreground service's), and when it was posted.

Link copied to clipboard

Display geometry: taller than wide, or wider than tall (Device.setOrientation, DeviceInfo.orientation).

Link copied to clipboard

A button of the runtime-permission dialog, by what it grants (Device.awaitPermissionPrompt, Device.choosePermission). Which ones a dialog offers depends on the permission, the Android version and the device's permission controller.

Link copied to clipboard
data class PermissionPrompt(val packageName: String, val choices: List<PermissionChoice>, val accuracies: List<LocationAccuracy> = emptyList())

The runtime-permission dialog on screen: the packageName of its window (device.app(packageName) reaches its other elements), the choices it offers, in PermissionChoice order, and the location accuracies it lets the user pick (empty unless it asks for precise location).

Link copied to clipboard
data class Range(val type: RangeType, val min: Float, val max: Float, val current: Float)

A range node's (SeekBar, Slider, RatingBar, ProgressBar) value and bounds, in its own units.

Link copied to clipboard

How a range node counts its value.

Link copied to clipboard
class Recording(val bytes: ByteArray, val extension: String) : Artifact

A bounded device recording (Device.stopRecording): MP4, Matroska, or Opus.

Link copied to clipboard
class Screen

Whatever is visible on a device: every window, of any app or the system UI.

Link copied to clipboard
class Screenshot(val bytes: ByteArray, val format: ImageFormat, val width: Int, val height: Int) : Artifact

A screen capture (Device.screenshot). bytes are the encoded image in format; width and height are its size in pixels.

Link copied to clipboard
class Selector

An immutable description of one accessibility node.

Link copied to clipboard
data class ServerDefaults(val action: Duration, val wait: Duration, val lifecycle: Duration, val idleStable: Duration, val acquire: Duration)

The daemon's default timeouts (ServerInfo.defaults).

Link copied to clipboard
class ServerException(val status: String, val details: String, cause: Throwable? = null, val reason: FailureReason = FailureReason.UNSPECIFIED) : TapException

The host server rejected or failed a call (unknown connection or device, bad argument, driver start failure, ...). status is the gRPC status code name; reason is the server's structured failure reason, FailureReason.UNSPECIFIED when the failure did not come from the daemon.

Link copied to clipboard
data class ServerInfo(val daemonVersion: String, val hostBuildId: String, val protocolVersion: String, val adbExecutable: String, val stateDir: String, val driverAvailable: Boolean, val pid: Long, val defaults: ServerDefaults)

What the server reports about itself (TapClient.info).

Link copied to clipboard

What App.awaitScreenStable watches for changes.

Link copied to clipboard

A standard accessibility action that takes no arguments, as a screen reader performs it (Element.performAction). Click and long click are not here: Element.tap and Element.longTap touch the screen.

Link copied to clipboard
class TapClient(val address: String, val channel: ManagedChannel, token: String? = null)

A channel to a Tap host server. Discovery order: address; the tap.server system property / TAP_SERVER (host:port); a live daemon.json in the state dir (TAP_STATE_DIR, default ~/.tap). The client never starts the daemon: run tap start (or TapDaemonProcess.start) first. A started daemon stays up like the ADB server until tap stop.

Link copied to clipboard

This process's identity at the server: every Device it attaches belongs to it and is detached with it — explicitly by close, or by the server when the process goes away. One per process is the norm; see TapClient.connect.

Link copied to clipboard

Ownership marker for suspending device calls. Installed by tapScope (scripts) and by the JUnit 5 tapTest bridge (which uses the same element with a junit:<method> owner); every suspending Device/App/Element/ElementWait call requires it. TapClient, TapConnection, DaemonDiscovery and TapDaemonProcess never require it: they set up the channel, the connection and the daemon process outside any test scope.

Link copied to clipboard

Explicit daemon lifecycle from a test process: tap start and tap stop through the tap executable (DaemonDiscovery.findBinary). The executable is the only thing that spawns a server; it picks the port, detaches the process and waits for Info to answer.

Link copied to clipboard
open class TapException(message: String, cause: Throwable? = null) : RuntimeException

Base of everything the client raises deliberately.

Link copied to clipboard
class TapUsageException(message: String, cause: Throwable? = null) : TapException

A suspending framework call was made outside its owning scope: tapTest { ... } without an active @TapTest binding, a nested tapTest/tapScope, or a Device/App/Element call from a coroutine that is not a child of that scope (for example GlobalScope). Scripts must wrap device use in tapScope { ... }; JUnit tests must use tapTest { ... }.

Link copied to clipboard
data class Timeouts(val action: Duration = 10.seconds, val wait: Duration = 10.seconds, val lifecycle: Duration = 30.seconds, val pollInterval: Duration = 100.milliseconds)

Per-device defaults. Every call also accepts an explicit timeout.

Link copied to clipboard
data class Toast(val text: String, val packageName: String)

A toast Device.awaitToast saw: its text and the packageName that showed it (on Android 11+ a text toast is drawn by SystemUI but still reported under the app that posted it).

Link copied to clipboard

Why a device-side wait timed out (WaitTimeoutException.reason).

Link copied to clipboard
class WaitTimeoutException(val description: String, val serial: String, val elapsedMs: Long, val polls: Int = 0, val lastObservation: String? = null, cause: Throwable? = null, val reason: WaitReason? = null, val matchCount: Int? = null) : TapException

A wait ran out of time. Carries what was observed so the failure is diagnosable without a rerun: reason and matchCount for the waits the device runs (visible, one, gone, App.awaitVisible, App.awaitScreenStable), lastObservation and polls for the ones the client polls.

Properties

Link copied to clipboard
Link copied to clipboard
const val KEYCODE_BACK: Int = 4
Link copied to clipboard
const val KEYCODE_HOME: Int = 3
Link copied to clipboard
const val KEYCODE_SLEEP: Int = 223
Link copied to clipboard
const val KEYCODE_WAKEUP: Int = 224
Link copied to clipboard
const val MAX_FILE_BYTES: Long

Largest file Device.pushFile, Device.pullFile and Device.addMedia move (512 MiB).

Functions

Link copied to clipboard
fun allOf(first: Selector, vararg rest: Selector): Selector

All of the selectors must hold on one node. Match choice comes from first; the others follow the operand rules of Selector.and.

Link copied to clipboard
fun anyOf(first: Selector, vararg rest: Selector): Selector

Any of the selectors may match: anyOf(text("OK"), text("Allow"), desc("Accept")). Match choice comes from first; the others follow the operand rules of Selector.or.

Link copied to clipboard
suspend fun Device.capture(timeout: Duration = 30.seconds): Capture

Takes a screenshot, the hierarchy, the device info and the driver log at once, each within timeout, and returns them as one Capture. Device failures never escape: a part that fails or runs out of time is null in the result, with its cause in Capture.failures. Only the caller's own cancellation propagates. Use it wherever a test wants evidence (after a step, in a catch); the JUnit extension and the pytest plugin call it for every failed test.

Link copied to clipboard
fun className(value: String, mode: MatchMode = MatchMode.EXACT): Selector

Widget class name, e.g. android.widget.EditText.

Link copied to clipboard

Any node with the given boolean property; combine with the and* builders.

Link copied to clipboard
fun desc(value: String, mode: MatchMode = MatchMode.EXACT): Selector

contentDescription (View) / contentDescription semantics (Compose).

Link copied to clipboard
suspend fun ensureTapBound(operation: String)

Requires an owning TapContext: tapScope { ... } in scripts, tapTest { ... } in JUnit. A coroutine that does not inherit the scope (for example GlobalScope.async) fails here instead of running uncancelled beside the test.

Link copied to clipboard
fun hint(value: String, mode: MatchMode = MatchMode.EXACT): Selector

Hint of an empty text field (an empty EditText shows its hint as its text).

Link copied to clipboard
fun res(name: String): Selector

Resource id name in any package (<package>:id/name), or a Compose testTag name under testTagsAsResourceId. Under App.element only that app's nodes match; under Screen.element any visible package's. Use resId to name the id's package explicitly.

Link copied to clipboard
fun resId(packageName: String, name: String): Selector

Exactly the Android View resource id packageName:id/name.

Link copied to clipboard

Any scrollable node; combine with the and* builders.

Link copied to clipboard
suspend fun <T> tapScope(owner: String = "script", block: suspend CoroutineScope.() -> T): T

Runs block with a TapContext owner, for scripts and other non-JUnit callers. JUnit tests must use tapTest { ... } instead (same marker, plus the extension binding). Nesting (tapScope inside tapScope/tapTest) fails immediately.

Link copied to clipboard
fun text(value: String, mode: MatchMode = MatchMode.EXACT): Selector

Visible text. Compose Text and View TextView/Button both expose this.

Link copied to clipboard

Text containing value.

Link copied to clipboard

Text fully matching the RE2 regular expression re2 (linear time; no backreferences or lookaround).

Link copied to clipboard

Text starting with value.