Device
The Device class is the primary interface for interacting with a mobile device. Test functions receive a device instance through the test fixtures.
In addition to the locator methods above, Device provides device-level actions that don’t target a specific element.
device.platform: "android" | "ios"
Section titled “device.platform: "android" | "ios"”Read-only property exposing the platform the device is running. Useful for branching on platform-specific behaviour in tests (e.g. native dialog labels or soft-keyboard handling).
if (device.platform === "android") { await device.pressBack();}device.swipe(direction: string, options?: SwipeOptions): Promise<void>
Section titled “device.swipe(direction: string, options?: SwipeOptions): Promise<void>”Perform a swipe gesture across the screen in the given direction.
On Android, swipe() (like dragXY()) returns once any scrolling it started has come to rest: it waits until no scroll along the swipe’s axis has been reported for 300 ms, at most 3 s. A scroll view that is still flinging takes the next touch as “stop scrolling”, so a tap() made straight after the swipe would otherwise be lost.
await device.swipe("up");await device.swipe("left", { speed: 500, distance: 0.5 });SwipeOptions:
| Option | Type | Description |
|---|---|---|
speed |
number |
Swipe speed in pixels per second |
distance |
number |
Swipe distance as a fraction of screen size (0-1) |
timeoutMs |
number |
Override the default timeout |
device.pressKey(key: string): Promise<void>
Section titled “device.pressKey(key: string): Promise<void>”Press a device key.
await device.pressKey("ENTER");await device.pressKey("HOME");await device.pressKey("VOLUME_UP");device.pressBack(): Promise<void> (Android only)
Section titled “device.pressBack(): Promise<void> (Android only)”Press the Android back button. Convenience method equivalent to device.pressKey("BACK").
await device.pressBack();device.tapXY(x: number, y: number): Promise<void>
Section titled “device.tapXY(x: number, y: number): Promise<void>”Tap at raw screen coordinates in logical points (Android pixels; iOS points). Prefer locator-based tap() in tests; use this for coordinate-driven interaction (e.g., device mirror gestures).
await device.tapXY(120, 340);device.longPressXY(x: number, y: number, options?: { duration?: number }): Promise<void>
Section titled “device.longPressXY(x: number, y: number, options?: { duration?: number }): Promise<void>”Long-press at raw screen coordinates in logical points. options.duration specifies the hold duration in milliseconds (default 1000).
await device.longPressXY(120, 340, { duration: 800 });device.dragXY(from: { x: number; y: number }, to: { x: number; y: number }, options?: { duration?: number }): Promise<void>
Section titled “device.dragXY(from: { x: number; y: number }, to: { x: number; y: number }, options?: { duration?: number }): Promise<void>”Drag/swipe from one point to another in logical points. options.duration specifies the drag duration in milliseconds (default 300).
await device.dragXY({ x: 50, y: 200 }, { x: 50, y: 600 }, { duration: 400 });Platform note:
durationis honored on Android (it scales the gesture’s step count). On iOS the drag is performed via a fixed-duration synthesized gesture, sodurationcurrently has no effect on speed.
device.inputText(text: string): Promise<void>
Section titled “device.inputText(text: string): Promise<void>”Type text into whatever element currently has focus (no locator). Useful for inserting text without first tapping a field.
await device.inputText("hello world");device.takeScreenshot(): Promise<ScreenshotResponse>
Section titled “device.takeScreenshot(): Promise<ScreenshotResponse>”Capture a screenshot of the current device screen. Returns an object with success, data (PNG bytes), and errorMessage fields.
const screenshot = await device.takeScreenshot();device.waitForIdle(timeoutMs?: number): Promise<void>
Section titled “device.waitForIdle(timeoutMs?: number): Promise<void>”Wait until the device UI is idle (no animations, no pending layout passes). Uses the configured default timeout if none is specified.
await device.waitForIdle();await device.waitForIdle(5000);device.installApk(apkPath: string): Promise<void>
Section titled “device.installApk(apkPath: string): Promise<void>”Install an APK on the connected device.
await device.installApk("./app-debug.apk");device.listDevices(): Promise<ListDevicesResponse>
Section titled “device.listDevices(): Promise<ListDevicesResponse>”List the devices the daemon can see. Resolves to { requestId, devices, unusableDevices }, where each
entry is { serial, model, state, isEmulator, platform, osVersion }.
devicesare the devices Tapsmith can target, Android and iOS. A device that was selected and has since gone away stays here withstate: 'Disconnected'.unusableDevicesare Android devices adb lists but cannot use (unauthorized, offline,no permissions (…), …). Theirstateis adb’s whole state string. Tapsmith never selects them, so they cannot be passed tosetDevice;tapsmith list-devicesshows how to fix each one. A serial can appear in both lists when two attached devices share it.
device.setDevice(serial: string): Promise<void>
Section titled “device.setDevice(serial: string): Promise<void>”Target a specific device by its serial number.
Tapsmith’s run paths claim a device before selecting it, and refuse a device that another running Tapsmith session holds
(see Several sessions on one machine). setDevice
itself does not check claims: switching devices mid-test is up to the test.
await device.setDevice("emulator-5554");device.startAgent(targetPackage: string): Promise<void>
Section titled “device.startAgent(targetPackage: string): Promise<void>”Start the Tapsmith on-device agent for the given app package.
await device.startAgent("com.myapp");device.launchApp(packageName: string, options?: LaunchAppOptions): Promise<void>
Section titled “device.launchApp(packageName: string, options?: LaunchAppOptions): Promise<void>”Launch an Android app by package name. This is the mobile equivalent of page.goto(url).
await device.launchApp("com.example.myapp");await device.launchApp("com.example.myapp", { activity: ".MainActivity" });await device.launchApp("com.example.myapp", { clearData: true }); // fresh startawait device.launchApp("com.example.myapp", { waitForIdle: false }); // return immediatelyOptions:
activity?— specific Activity to launch (e.g.,".settings.ProfileActivity")clearData?— clear all app data before launching (default:false)waitForIdle?— wait for the UI to settle after launch (default:true)
device.openDeepLink(uri: string, options?: OpenDeepLinkOptions): Promise<void>
Section titled “device.openDeepLink(uri: string, options?: OpenDeepLinkOptions): Promise<void>”Navigate to a screen via deep link URI.
await device.openDeepLink("myapp://settings/profile");await device.openDeepLink("https://example.com/product/123"); // app linksawait device.openDeepLink("myapp://__reset", { forceColdLaunch: true });Options:
forceColdLaunch?— on iOS simulators, skip the warm in-process delivery attempt and cold-relaunch the app with the URL (default:false). Use when the deep link must start from a fresh process, e.g. a state-clearing reset. No effect on Android or physical iOS, which always deliver warm.
Deep links are delivered to the running app without relaunching it (warm delivery) whenever possible. On iOS simulators, if warm delivery does not reach its destination within a bounded window, Tapsmith automatically falls back to a cold relaunch with the URL.
device.currentPackage(): Promise<string>
Section titled “device.currentPackage(): Promise<string>”Returns the package name of the foreground app.
const pkg = await device.currentPackage(); // "com.example.myapp"device.currentActivity(): Promise<string> (Android only)
Section titled “device.currentActivity(): Promise<string> (Android only)”Returns the current activity name.
const activity = await device.currentActivity(); // ".settings.ProfileActivity"device.terminateApp(packageName?: string): Promise<void>
Section titled “device.terminateApp(packageName?: string): Promise<void>”Force-stop an app.
await device.terminateApp("com.example.myapp");device.getAppState(packageName: string): Promise<AppState>
Section titled “device.getAppState(packageName: string): Promise<AppState>”Check the state of an app. Returns "not_installed", "stopped", "background", or "foreground".
const state = await device.getAppState("com.example.myapp");device.sendToBackground(): Promise<void>
Section titled “device.sendToBackground(): Promise<void>”Press the home button to send the current app to the background.
await device.sendToBackground();device.bringToForeground(packageName: string): Promise<void>
Section titled “device.bringToForeground(packageName: string): Promise<void>”Bring a backgrounded app back to the foreground.
await device.bringToForeground("com.example.myapp");device.resetApp(options?): Promise<AppResetResult>
Section titled “device.resetApp(options?): Promise<AppResetResult>”Bring the app to a known state — the same reset the runner performs for the declared appReset policy, callable from inside a test. The daemon runs a ladder and reports which rung actually ran:
- warm — in-app reset through the app’s reset hook (
@tapsmith/react-nativemarker, orresetAppDeepLink), process kept; - restart — terminate and relaunch, data kept;
- clear — wipe app data and relaunch.
Note that on an app with no in-app reset hook, the default warm request falls back to restart, which keeps app data — pass mode: "clear" when you need an isolation-grade wipe.
Mid-test resets share the daemon’s warm-window state with the runner’s own resets and honour the same appResetColdEvery setting (default 10, 0 = off; a scope’s test.use({ appResetColdEvery }) applies to them too), so a warm resetApp() can come back as a cold relaunch with reason: "cold relaunch: warm-window bound reached (…)".
const result = await device.resetApp() // warm, falling back as neededawait device.resetApp({ mode: "clear", fallback: false }) // exactly a clear, or throwawait device.resetApp({ target: "/settings" }) // warm reset landing on a route| Option | Type | Default | Description |
|---|---|---|---|
mode |
'warm' | 'restart' | 'clear' |
'warm' |
How far to reset |
fallback |
boolean |
true |
Escalate warm → restart → clear when a rung fails |
target |
string |
'/' |
Route to land on after an in-app (warm) reset — it is the reset deep link’s path. A restart/clear rung leaves the app at its launch route; check modeUsed and navigate if needed. |
AppResetResult
| Field | Type | Description |
|---|---|---|
modeRequested / modeUsed |
'warm' | 'restart' | 'clear' |
What was asked for and what actually ran |
fellBack |
boolean |
A lower rung ran because a higher one failed |
coldLaunch |
boolean |
The process was recreated — by a restart/clear rung, a cold-window relaunch, or a warm delivery that had to relaunch the app to land (the reason says which) |
reason |
string? |
Why a fallback or cold relaunch happened, e.g. "cold relaunch: warm-window bound reached (10 resets)" |
durationMs |
number |
Wall time of the whole ladder |
hooksDetected |
boolean |
@tapsmith/react-native hooks were found in the app |
epochBefore / epochAfter |
number? |
In-app reset counter (hooks only) |
steps |
{ name, durationMs, ok, detail? }[] |
Per-rung timings |
The call is recorded in the trace as a resetApp action whose detail line names the rung and reason. Throws when the ladder is exhausted (or fallback: false and the requested rung failed).
device.restartApp(packageName: string, options?: { waitForIdle?: boolean }): Promise<void>
Section titled “device.restartApp(packageName: string, options?: { waitForIdle?: boolean }): Promise<void>”Force-stops and relaunches the app without clearing persistent storage. Resets all in-memory state (React component state, navigation stack) while preserving data on disk (AsyncStorage, SQLite, SharedPreferences).
Use this in beforeEach hooks when tests modify in-memory state and you need isolation, but don’t need a clean persistent state:
beforeEach(async ({ device }) => { await device.restartApp("com.example.myapp") await device.getByDescription("Settings").tap() await expect(device.getByText("Settings", { exact: true })).toBeVisible()})Options:
waitForIdle?— wait for the UI to settle after relaunch (default:true)
device.clearAppData(packageName: string): Promise<void>
Section titled “device.clearAppData(packageName: string): Promise<void>”Clear all app data and cache, providing test isolation similar to Playwright’s fresh browser context.
await device.clearAppData("com.example.myapp");- Android:
pm clear— wipes all persisted state, including the app’s Android Keystore keys. - iOS simulator: clears the app’s data container and the simulator’s keychain, so keychain-backed state (e.g. native Firebase Auth sessions,
expo-secure-storeitems) doesn’t survive a “full” clear. The simulator keychain is device-global, so this clears keychain items for every app on the simulator — fine for test-owned simulators. SetTAPSMITH_NO_KEYCHAIN_STATE=1to skip the keychain wipe. - iOS physical device: uninstalls and reinstalls the app bundle (iOS deletes the app’s keychain entries on uninstall).
device.saveAppState(packageName: string, path: string): Promise<void>
Section titled “device.saveAppState(packageName: string, path: string): Promise<void>”Snapshot the app’s persisted state and save it as a tar.gz archive on the host. The app is stopped before snapshotting to avoid data corruption.
- Android: archives the app’s data directory (
/data/data/<package>/) — SharedPreferences, databases, and internal files. Requires root (emulators) or a debuggable app (run-asfallback on physical devices). Keystore-backed values (EncryptedSharedPreferences,expo-secure-store, and many native auth SDKs) store only their ciphertext in the data directory; the decryption key lives in the Android Keystore, which is device-bound and not captured in the archive.restoreAppStatepreserves the Keystore (it clears the data directory in place rather than runningpm clear, which would wipe the app’s Keystore keys), so these values do round-trip across a save/restore on the same device. Limitation — cross-device / CI: an archive is not portable across devices. The Keystore key only exists on the device that saved the archive, so the restored ciphertext can’t be decrypted on a different device or CI runner, leaving the app unauthenticated. This applies even to auth SDKs that look like plain SharedPreferences: the Firebase Android SDK (used by@react-native-firebase/auth) persists its session token encrypted with a Tink keyset wrapped by an Android Keystore key. For cross-device/CI, mint auth state per device (run the login flow as a setup project on each device) or use API-driven auth (e.g. custom-token sign-in). - iOS simulator: archives the app’s data container plus the simulator’s keychain database (as a reserved
.tapsmith-keychainarchive member), so keychain-backed auth state (e.g. native Firebase Auth,expo-secure-store) round-trips correctly. SetTAPSMITH_NO_KEYCHAIN_STATE=1to disable keychain capture. Best-effort across iOS runtimes: a save/restore on the same simulator is fully supported; restoring an archive captured on a different iOS version is not guaranteed. - iOS physical device: archives the app’s data container only. The keychain is not host-accessible on physical devices, so keychain-backed state (e.g. native auth SDK credentials) is not captured — a warning is logged. Prefer API-driven auth (e.g. custom-token sign-in) for test setup on physical devices.
// Save authenticated state after loginawait device.saveAppState("com.example.myapp", "./auth-state.tar.gz");device.restoreAppState(packageName: string, path: string): Promise<void>
Section titled “device.restoreAppState(packageName: string, path: string): Promise<void>”Restore a previously saved app state archive. Clears the app’s data first, then extracts the archive.
- Android: clears the app’s data directory in place (rather than
pm clear) so the app’s Android Keystore keys survive — this is what lets Keystore-backed values (e.g. native auth tokens) decrypt after a restore on the same device. Then extracts the archive, fixing file ownership and SELinux contexts when running as root. SeesaveAppStatefor the cross-device limitation. - iOS simulator: clears and re-extracts the data container; if the archive contains keychain state, the simulator’s keychain is swapped to match and
securitydis restarted so the change takes effect. The simulator keychain is device-global, so restoring swaps keychain state for every app on the simulator. Archives saved by older Tapsmith versions (without keychain state) restore exactly as before. SetTAPSMITH_NO_KEYCHAIN_STATE=1to skip the keychain swap. - iOS physical device: reinstalls the app and pushes the archived container contents (keychain state is not restored — see
saveAppState).
// Restore state instead of logging in againawait device.restoreAppState("com.example.myapp", "./auth-state.tar.gz");device.grantPermission(packageName: string, permission: string): Promise<void>
Section titled “device.grantPermission(packageName: string, permission: string): Promise<void>”Programmatically grant a permission. On Android, permission is a runtime permission name; on an iOS simulator, it is a service that xcrun simctl privacy accepts, such as photos, location, contacts, calendar or microphone (xcrun simctl help privacy lists them, and camera is accepted too; notifications are not). Some changes make iOS terminate the app if it is running. Not supported on physical iOS devices.
await device.grantPermission("com.example.myapp", "android.permission.CAMERA");await device.grantPermission("com.example.myapp", "android.permission.ACCESS_FINE_LOCATION");await device.grantPermission("com.example.myapp", "photos"); // iOS simulatordevice.revokePermission(packageName: string, permission: string): Promise<void>
Section titled “device.revokePermission(packageName: string, permission: string): Promise<void>”Revoke a previously granted permission. Takes the same permission values as grantPermission(), with the same platform support.
await device.revokePermission("com.example.myapp", "android.permission.CAMERA");device.setClipboard(text: string): Promise<void>
Section titled “device.setClipboard(text: string): Promise<void>”Set the device clipboard content.
await device.setClipboard("Hello, world!");device.getClipboard(): Promise<string>
Section titled “device.getClipboard(): Promise<string>”Read the current device clipboard content.
const text = await device.getClipboard();device.setOrientation(orientation: Orientation): Promise<void>
Section titled “device.setOrientation(orientation: Orientation): Promise<void>”Set the device orientation. Accepts "portrait" or "landscape".
await device.setOrientation("landscape");await device.setOrientation("portrait");device.getOrientation(): Promise<Orientation>
Section titled “device.getOrientation(): Promise<Orientation>”Get the current device orientation.
const orientation = await device.getOrientation(); // "portrait" | "landscape"device.isKeyboardShown(): Promise<boolean>
Section titled “device.isKeyboardShown(): Promise<boolean>”Check if the soft keyboard is currently visible.
if (await device.isKeyboardShown()) { await device.hideKeyboard();}device.hideKeyboard(): Promise<void>
Section titled “device.hideKeyboard(): Promise<void>”Hide the soft keyboard if it is visible. Resolves at once when no keyboard is shown, or when the app is not in the foreground. On iOS it throws if the app is in front but its screen cannot be read, since it cannot tell whether a keyboard is up.
await device.hideKeyboard();On Android this presses BACK, which normally dismisses the soft keyboard.
iOS has no API that puts the keyboard away, so hideKeyboard() does what a user would. It tries these in order and stops as soon as the keyboard is gone:
- Drag the scroll view the focused field is in a few points, if it shows above the keyboard. The drag only starts from a spot clear of the scroll view’s controls. A scroll view beside the field (a list under a search bar) is not dragged, and neither is a web view.
- Tap the keyboard’s own dismiss key, where it has one (iPad).
- Tap a blank spot beside the focused field, above the keyboard, where no control, text, or image is drawn. The spot is inside the field’s own container, so a sheet’s backdrop is not tapped. This works on screens that dismiss the keyboard when you tap outside the field.
- Press the return key, but only in a single-line field whose return key reads “return” or “done”. This also submits the field, as it would for a user (React Native’s
onSubmitEditingfires). An action key such as “go”, “send”, “search” or “next” is never pressed, and neither is return in a multi-line field, where it would type a new line.
If the keyboard is still up after all of them, hideKeyboard() throws instead of reporting success. The error lists what it tried and why each step failed. Dismiss the keyboard the way your screen allows: tap its own Done or close button, or call device.pressKey("enter") if submitting the field is fine.
device.wake(): Promise<void>
Section titled “device.wake(): Promise<void>”Wake the device screen if it is off.
await device.wake();device.unlock(): Promise<void>
Section titled “device.unlock(): Promise<void>”Wake the screen and dismiss the lock screen. Works with non-secure lock screens (no PIN/pattern). Useful for CI and emulator setups. On Android the lock screen is only dismissed when it is actually showing, so calling this on an unlocked device never sends gestures to the foreground app.
await device.unlock();device.pressHome(): Promise<void>
Section titled “device.pressHome(): Promise<void>”Press the home button. Convenience method equivalent to device.pressKey("HOME").
await device.pressHome();device.openNotifications(): Promise<void> (Android only)
Section titled “device.openNotifications(): Promise<void> (Android only)”Pull down the notification shade.
await device.openNotifications();device.openQuickSettings(): Promise<void> (Android only)
Section titled “device.openQuickSettings(): Promise<void> (Android only)”Pull down the quick settings panel.
await device.openQuickSettings();device.pressRecentApps(): Promise<void> (Android only)
Section titled “device.pressRecentApps(): Promise<void> (Android only)”Open the recent apps screen. Convenience method equivalent to device.pressKey("APP_SWITCH").
await device.pressRecentApps();device.setColorScheme(scheme: ColorScheme): Promise<void> (Android only)
Section titled “device.setColorScheme(scheme: ColorScheme): Promise<void> (Android only)”Set the system UI mode. Accepts "dark" or "light".
await device.setColorScheme("dark");await device.setColorScheme("light");device.getColorScheme(): Promise<ColorScheme>
Section titled “device.getColorScheme(): Promise<ColorScheme>”Get the current system color scheme.
const scheme = await device.getColorScheme(); // "dark" | "light"device.close(): void
Section titled “device.close(): void”Close the gRPC connection to the daemon.

