平台

Android tests

Bind an emulator to the run, then write the same locators, checks, agent steps and invariants as on the web.

本页还没有中文版,下面是英文原文。

An Android test reads like a web test. screen finds elements on the device's screen, expect waits for checks, and an invariant compares your backend with Stripe. danwo boots the emulator, routes its traffic through the proxy, and installs your build.

Coming from Detox? Coming from Detox maps each Detox call to danwo's.

A test on a device

tests/buy-credits.danwo.ts
import type { DeviceFixtures } from "@danwo/mobile"
import { type StripeFixtures, test as base } from "@danwo/sdk"

const test = base.extend<StripeFixtures & DeviceFixtures>()

test("buys 500 credits", async ({ screen, expect, device }) => {
  await screen.getByTestId("buy").tap()

  /* Checkout opens in a Custom Tab; the same locators read the web page. */
  expect((await device.foreground()).browser).toBe(true)
  await screen.getByLabel("Email").fill("[email protected]")
  await screen.getByRole("button", { name: "Pay $5.00" }).tap()

  await expect(screen.getByTestId("credits")).toHaveText("Credits: 500")
})

getByTestId matches React Native's testID. A Custom Tab, the in-app browser a checkout often opens, is part of the screen.

Every tap waits for its element to be enabled, so a button that is greyed out while the app loads is tapped once it turns on. Just before it presses, danwo reads the screen again and finds the element there, so a form the keyboard slid up is tapped where it is now.

Bind the emulator in the config

withAndroidDevice wraps your environment and binds an emulator to it:

danwo.config.ts · the device
return withAndroidDevice(environment, {
  avd: "danwo_spike",
  worker: workerIndex,
  snapshot: "danwo_clean",
  app: { package: "com.danwo.shopmobile", apk: build.apk, scheme: "danwoshop" },
})

The full file, with the backend and Stripe behind the proxy, is in examples/shop-mobile. For each worker, danwo:

  1. boots the AVD headless, or uses one that already runs;
  2. runs adb root and puts the run's certificate in the trust store;
  3. turns off animations, Chrome's first-run screens and the captive-portal probe;
  4. routes the device's traffic through the proxy;
  5. installs the APK.
OptionWhat it does
avdthe AVD to boot
workergives each worker its own emulator (console port 5554 + 2 × worker)
snapshotboots from a saved AVD snapshot: seconds instead of a minute
serialuses a device that already runs, as it is
permissionsruntime permissions to grant before each launch
app.schemethe link scheme app.open(path) uses, as <scheme>://<path>

Pass blocked: ANDROID_BACKGROUND_HOSTS from @danwo/mobile to your services. It blocks the emulator's own background traffic (Chrome sync, Play services), so the matrix never faults it.

Remote devices

A remote device is an Android device that your machine reaches over adb's network protocol instead of an emulator it boots. Use one when the machine can't run an emulator, such as a CI runner without hardware acceleration, or when your team keeps its devices in one place.

Give withAndroidDevice the device's address as adb:

danwo.config.ts · a remote device
return withAndroidDevice(environment, {
  adb: "10.0.4.12:5555",
  worker: workerIndex,
  app: { package: "com.danwo.shopmobile", apk: build.apk, scheme: "danwoshop" },
})

Any service that hands you an adb connect address works:

  • a device cloud that exposes adb for its virtual devices;
  • a device farm's remote access session;
  • your team's own farm, or one machine's emulator shared on the network.

What danwo does with it

For each worker, danwo:

  1. runs adb connect to the address and waits for the device;
  2. checks that the device runs Android 7 (API 24) or newer;
  3. puts the run's certificate in the trust store and turns off animations, as on an emulator;
  4. runs adb reverse, so the device reaches the proxy at 127.0.0.1 on the device itself;
  5. installs the APK.

The device never needs a route to your machine. All of its traffic, and the proxy's answers, travel inside the adb connection. danwo never boots, wipes or snapshots a remote device, and the avd and snapshot options are ignored.

When the run ends, danwo uninstalls the APK and removes its certificate. It puts the device's settings back, and it disconnects the device only when the run made the connection.

Pass the address from CI

DANWO_ADB_TARGET overrides the config's choice. Keep avd for your own machine and let CI point the same config at its devices:

DANWO_ADB_TARGET=10.0.4.12:5555 npx @danwo/cli test --workers 1

For several workers, list one address per worker, separated by commas. Worker n takes the nth.

OptionWhat it doesDefault
adbthe device's host:port, or a list with one per worker
reversemore ports the device reaches on your machine, such as a dev server
connectTimeoutMshow long the device may take to come online30 000

danwo needs adb root to add its certificate. Most device clouds' virtual devices allow it. A retail phone does not, and the run stops with an error that says so.

When it can't connect

Error saysWhat to check
is not reachable over adbthe address, that the cloud session is up, and that its port is open
has not authorized this computeraccept the prompt on the device, or add your adb key to the cloud
needs API 24 (Android 7) or newerpick a newer device or image
cannot run adbd as rootuse a virtual device or a rooted build

What a test can do

FixtureOn Android
screengetByTestId, getByRole, getByLabel, getByText; tap, fill, check, press, selectOption, longPress
expectthe same checks as the web, including toHaveScreenshot
app.openopens a deep link; with no path, restarts the app
deviceforeground(), back(), openLink(url), logs(), swipe(), dismissKeyboard()
agentact, assert, extract, waitFor, with device tools

Every attempt starts the app afresh: its data cleared, its permissions granted.

Swipe and put the keyboard away

device.swipe drags one finger across the screen. A slow swipe turns a carousel's page or pulls a list to refresh:

tests/feed.danwo.ts
import type { DeviceFixtures } from "@danwo/mobile"
import { test as base } from "@danwo/sdk"

const test = base.extend<DeviceFixtures>()

test("turns the carousel and refreshes the feed", async ({ screen, expect, device }) => {
  /* The finger moves left, so the carousel shows its next page. */
  await device.swipe({ direction: "left", durationMs: 600 })
  await expect(screen.getByText("Page 2")).toBeVisible()

  /* Put the keyboard away, then pull the list down to refresh it. */
  await device.dismissKeyboard()
  await device.swipe({ direction: "down", durationMs: 900 })
})
OptionWhat it doesDefault
directionthe way the finger moves: up, down, left or right
withinthe box to swipe across, in screenshot pixelsthe whole screen
from, tothe two points to swipe between, in place of direction
durationMshow long the finger takes300

dismissKeyboard() does nothing when no keyboard shows. To hold an element, call longPress() on its locator, as on the web:

tests/feed.danwo.ts · long press
await screen.getByTestId("post").longPress({ durationMs: 900 })

It waits for the element to be enabled, like tap(), and holds for 800 ms by default. The agent has the same gestures.

The agent on a device

Plain-English steps work too. The agent reads the screen as a list of elements and acts with taps, typing, scrolls, swipes and long presses. It sees which elements are disabled, checked or selected, such as the active tab:

tests/agent-buy.danwo.ts
import { test, z } from "@danwo/sdk"

test("buys 500 credits when asked in plain words", async ({ agent, screen, expect }) => {
  const { credits } = await agent.extract("the credits balance", z.object({ credits: z.number() }))

  await agent.act("Start buying 500 credits")
  await agent.act("Pay for the credits with the email {email}", {
    params: { email: "[email protected]" },
  })
  await expect(screen.getByTestId("credits")).toHaveText(`Credits: ${credits + 500}`)
})

The first run records each step. Later runs, and every matrix trial, replay them with no model call. In the example shop, the recorded run made 6 model calls in about 17 seconds; the replay made none, in about 6.

Next

本页内容

在每个 Pull Request 上运行

每个 Pull Request 一个 GitHub Check 和一条评论,并附上完整的运行记录。

配置 CI