Android
Astur’s Android driver uses public Android tools for lifecycle and artifacts:
adb devices -lfor discoveryadb install -rfor installationmonkeyoram startfor launchuiautomator dumpfor legacy UI snapshots and diagnosticsinput tap,input text,input swipe, andinput keyeventonly for the legacy fallback pathscreencapfor screenshotsaapt dump badgingfor APK package/activity inference when available
No Appium server is required.
Astur defaults to the Kotlin UIAutomator native-agent path for element lookup, waits, actions, and gestures. The published Android package includes the agent APKs, so normal npm installs do not require a separate agent build step. Use automation.engine: 'auto' only while migrating if you need fallback to the old ADB/XML path.
Install Android SDK
Section titled “Install Android SDK”Install Android Studio or the command line Android SDK. Ensure platform tools are available:
adb versionIf adb is not found, add platform tools to PATH.
macOS example:
export ANDROID_HOME="$HOME/Library/Android/sdk"export PATH="$ANDROID_HOME/platform-tools:$ANDROID_HOME/emulator:$PATH"Start An Emulator
Section titled “Start An Emulator”Use Android Studio Device Manager or the command line:
emulator -list-avdsemulator -avd Pixel_8_API_35Then verify:
adb devices -lnpx astur-mobile devices --androidReal Android Device
Section titled “Real Android Device”On the device:
- enable Developer Options
- enable USB debugging
- connect over USB
- approve the debugging prompt
Verify:
adb devices -lThe state must be device. If it is unauthorized, unlock the device and approve the prompt.
Android Configuration
Section titled “Android Configuration”import { defineConfig } from '@astur-mobile/test';
export default defineConfig({ testDir: './tests', use: { astur: { platform: 'android', device: { kind: 'emulator', avd: 'Pixel_9_API_35', autoBoot: true, headless: true, bootTimeout: 120_000 }, app: { path: './apps/demo.apk' } } }});Optional Native-Agent Endpoint
Section titled “Optional Native-Agent Endpoint”agent: { mode: 'auto', endpoint: 'tcp:127.0.0.1:8787', launchTimeout: 15_000, commandTimeout: 10_000}Environment override:
export ASTUR_ANDROID_AGENT_ENDPOINT=tcp:127.0.0.1:8787Use agent.mode: 'required' in CI once the Android native-agent command set is stable for your test suite.
By default, Astur starts one Android native-agent session per Playwright worker. It does not reinstall the agent before every spec; the bundled agent APKs are installed only when the agent app or test package is missing. Set ASTUR_ANDROID_AGENT_FORCE_INSTALL=1 when developing the agent and you intentionally need to refresh the APKs on the device.
With device.avd, Astur starts the emulator when no matching online emulator is found. With app.path, Astur installs the APK before the test starts. When aapt is available from the Android SDK, Astur also infers packageName and launch activity.
You can still make everything explicit:
device: { id: 'emulator-5554'},app: { path: './apps/demo.apk', packageName: 'com.example', activity: '.MainActivity'}Use a specific device.id for parallel runs. Loose selectors like { kind: 'emulator' } are convenient locally but are not safe for parallel device allocation yet.
Android Device Options
Section titled “Android Device Options”device: { kind: 'emulator', avd: 'Pixel_9_API_35', autoBoot: true, headless: true, wipeData: false, bootTimeout: 120_000, emulatorArgs: ['-no-snapshot-save']}Fields:
avd: Android Virtual Device name fromemulator -list-avdsautoBoot: starts the AVD when no matching online emulator is found; defaults to true whenavdis setheadless: adds-no-window; defaults to truewipeData: adds-wipe-data; useful for clean CI runs, destructive to emulator statebootTimeout: max wait time forsys.boot_completedemulatorArgs: extra emulator arguments
APK Metadata Inference
Section titled “APK Metadata Inference”If the config only provides:
app: { path: './apps/demo.apk'}Astur tries:
aapt dump badging ./apps/demo.apkIt fills:
app.packageNameapp.activity
If aapt is not available, set ASTUR_AAPT or provide package metadata manually.
Astur supports three Android app modes:
// Install a local APK.app: { path: './apps/demo.apk' }
// Download the APK during the run, then install it.app: { url: 'https://example.com/apps/demo.apk' }
// Launch an app that is already installed on the device.app: { packageName: 'com.example', activity: '.MainActivity' }use.astur.timeout defines the default timeout for element actions and mobile assertions. Per-action overrides still work when needed.
Supported Android Actions
Section titled “Supported Android Actions”| Area | API | Notes |
|---|---|---|
| App lifecycle | device.app.install(), launch(), terminate(), reset(), uninstall() | Uses the configured APK/package by default. |
| App storage | device.app.clearData(), device.app.clearCache() | Uses Android package-manager commands. |
| Permissions | device.permissions.grant('camera'), revoke('camera') | Accepts Android permission names or Astur shorthand where available. |
| Orientation | device.setOrientation('landscape'), device.orientation.portrait() | Uses Android display/orientation control. |
| Lock state | device.lock(), device.unlock(), device.isLocked() | Uses Android shell/device state APIs. |
| Native locators | getByText(), getByLabel(), getByTestId(), getByRole() | Runs through the UIAutomator native-agent path by default. |
| Coordinates | device.tap(), device.longPress(), device.swipe(), device.drag() | Useful for gesture surfaces and inspector-generated fallback steps. |
| Scroll into view | locator.scrollIntoView({ direction, maxScrolls }) | Cross-platform. Swipes the surrounding scroll view until the element is visible, then resolves with its snapshot. Replaces hand-written “swipe in a loop until visible” page-object helpers. |
Element and gesture examples:
await device.getByText('Continue').tap();await device.getByLabel('Email').fill('qa@example.com');await device.getByRole('button', { name: 'Submit' }).longPress({ durationMs: 800 });
// Scroll a long form until the target is on screen, then act on it.await device.getByText('Submit').scrollIntoView();await device.getByLabel('Biometric login').scrollIntoView({ direction: 'up', maxScrolls: 6 });
await device.tap({ x: 120, y: 780 });await device.longPress({ x: 360, y: 900 }, { durationMs: 900 });await device.swipe({ start: { x: 500, y: 1200 }, end: { x: 500, y: 300 }, durationMs: 300});await device.drag({ start: { x: 120, y: 1100 }, end: { x: 360, y: 420 }, durationMs: 800});await device.setOrientation('landscape');await device.orientation.portrait();
await device.back();await device.home();await device.recentApps();await device.pressKey('ENTER');await device.lock();await device.unlock();await device.system.isLocked();
await device.screenshot();When native-agent endpoint mode is enabled and healthy, element/gesture commands can run through the device-side Kotlin agent transport. If endpoint mode is unavailable in auto, Astur falls back to current ADB/UIAutomator behavior.
device.gestures also exposes tap, longPress, pressAndHold, swipe, and drag as a grouped API. device.navigation exposes back, home, and recentApps.
Scrolling To Off-Screen Elements
Section titled “Scrolling To Off-Screen Elements”locator.scrollIntoView() is the built-in way to bring an element that is below or above the fold into view before acting on it. It is cross-platform (Android and iOS) and lives on every locator, so you no longer need to hand-write “swipe in a loop until visible” helpers in page objects. If the element is already visible it returns immediately without scrolling.
// Default: scroll down within the viewport, up to 10 swipes.await device.getByText('Save changes').scrollIntoView();
// Reveal something above the current position.await device.getByLabel('Biometric login').scrollIntoView({ direction: 'up' });
// Scroll inside a specific scrollable container instead of the whole screen.await device.getById('product-42').scrollIntoView({ container: device.getById('catalog-list'), maxScrolls: 15});| Option | Default | Purpose |
|---|---|---|
direction | 'down' | Direction to scroll the content toward the target: 'down', 'up', 'left', or 'right'. |
maxScrolls | 10 | Maximum number of scroll gestures before giving up. |
durationMs | 400 | Duration of each scroll gesture. |
container | viewport | Scrollable element to swipe within. Defaults to the device viewport, which Astur resolves per platform (iOS viewport vs Android tree bounds). |
timeout / interval | session defaults | Forwarded to the final visibility wait. |
If the element never appears, scrollIntoView() throws a timeout error naming the selector and the scroll direction it tried.
App management maps to ADB package manager commands:
device.app.install(path?)->adb install -rdevice.app.uninstall(packageName?)->adb uninstalldevice.app.clearData(packageName?)->pm cleardevice.app.clearCache(packageName?)->pm clear --cache-onlydevice.app.reset({ reinstall, launch })-> force-stop plus eitherpm clearor uninstall/installdevice.permissions.grant(permission, packageName?)->pm grantdevice.permissions.revoke(permission, packageName?)->pm revoke
Short permission names such as camera are normalized to Android permission constants like android.permission.CAMERA; pass the full permission string when you need exact control.
Device state helpers map to Android keyguard and power commands:
device.lock()/device.system.lock()->KEYCODE_SLEEPdevice.unlock()/device.system.unlock()->KEYCODE_WAKEUPpluswm dismiss-keyguarddevice.isLocked()/device.system.isLocked()-> parseddumpsys windowstate
Android system keys accept friendly names such as BACK, HOME, ENTER, MENU, APP_SWITCH, RECENTS, VOLUME_UP, and VOLUME_DOWN. Raw Android key codes such as KEYCODE_BACK or numeric values still work.
The Android example suite is split by functionality under examples/specs: login.test.ts, forms.test.ts, forms-slider.test.ts, media-upload.test.ts, tap-laboratory.test.ts, swipe.test.ts, drag-and-drop.test.ts, and webview.test.ts. They share the fixtures.ts app fixture and the single page-object file at pages/astur-demo-app.page.ts.
Locator Mapping
Section titled “Locator Mapping”| Astur locator | Android source |
|---|---|
getByLabel() / by.label() | content-desc or resource id |
getByTestId() / getById() / by.id() | resource-id |
getByText() / by.text() | text or content-desc |
getByRole() / by.role() | normalized Android widget class plus accessible name |
getByType() / by.type() | Android class |
Prefer accessibility labels and stable resource ids.