Skip to content

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.

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.

await device.swipe("up");
await device.swipe("left", { speed: 500, distance: 0.5 });

SwipeOptions:

OptionTypeDescription
speednumberSwipe speed in pixels per second
distancenumberSwipe distance as a fraction of screen size (0-1)
timeoutMsnumberOverride 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 selector-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: duration is honored on Android (it scales the gesture’s step count). On iOS the drag is performed via a fixed-duration synthesized gesture, so duration currently 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 selector). 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<DeviceInfo[]>

Section titled “device.listDevices(): Promise<DeviceInfo[]>”

List all connected Android devices and emulators.

device.setDevice(serial: string): Promise<void>

Section titled “device.setDevice(serial: string): Promise<void>”

Target a specific device by its serial number.

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 start
await device.launchApp("com.example.myapp", { waitForIdle: false }); // return immediately

Options:

  • 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): Promise<void>

Section titled “device.openDeepLink(uri: string): 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 links

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> (Android only)

Section titled “device.sendToBackground(): Promise<void> (Android only)”

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.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-store items) 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. Set TAPSMITH_NO_KEYCHAIN_STATE=1 to 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-as fallback 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. restoreAppState preserves the Keystore (it clears the data directory in place rather than running pm 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-keychain archive member), so keychain-backed auth state (e.g. native Firebase Auth, expo-secure-store) round-trips correctly. Set TAPSMITH_NO_KEYCHAIN_STATE=1 to 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 login
await 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. See saveAppState for 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 securityd is 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. Set TAPSMITH_NO_KEYCHAIN_STATE=1 to 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 again
await device.restoreAppState("com.example.myapp", "./auth-state.tar.gz");

device.grantPermission(packageName: string, permission: string): Promise<void> (Android only)

Section titled “device.grantPermission(packageName: string, permission: string): Promise<void> (Android only)”

Programmatically grant an Android runtime permission.

await device.grantPermission("com.example.myapp", "android.permission.CAMERA");
await device.grantPermission("com.example.myapp", "android.permission.ACCESS_FINE_LOCATION");

device.revokePermission(packageName: string, permission: string): Promise<void> (Android only)

Section titled “device.revokePermission(packageName: string, permission: string): Promise<void> (Android only)”

Revoke a previously granted runtime permission.

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!");

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();
}

Hide the soft keyboard if it is visible.

await device.hideKeyboard();

Wake the device screen if it is off.

await device.wake();

Wake the screen and dismiss the lock screen. Works with non-secure lock screens (no PIN/pattern). Useful for CI and emulator setups.

await device.unlock();

device.pressHome(): Promise<void> (Android only)

Section titled “device.pressHome(): Promise<void> (Android only)”

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"

Close the gRPC connection to the daemon.