---
title: Mobilewright Testing in the Cloud | TestingBot
description: Run Mobilewright mobile tests on real iPhones, iPads and Android devices
  in the TestingBot cloud, plus iOS simulators and Android emulators.
source_url:
  html: https://testingbot.com/support/app-automate/mobilewright
  md: https://testingbot.com/support/app-automate/mobilewright.md
---

# Mobilewright Testing

[Mobilewright](https://github.com/mobile-next/mobilewright) is an open source mobile UI test framework with a Playwright-style API. You write tests in TypeScript or JavaScript against `device`, `screen` and `expect` fixtures, and Mobilewright drives your iOS or Android app.

TestingBot maintains [`@testingbot/mobilewright-driver`](https://github.com/testingbot/mobilewright-testingbot), which runs those same tests on our device cloud instead of a device attached to your laptop: real iPhones, iPads and Android phones and tablets in our European datacenter, plus iOS simulators and Android emulators.

- No local Appium server, Xcode or Android SDK required.
- Your app is uploaded and pre-installed on the device before the first test runs.
- Every device slot becomes a TestingBot session with video, command logs and device logs.
- Pass and fail verdicts, build names and git metadata are reported for you.

The driver requires **Node.js 18 or newer** and **Mobilewright 0.0.53 or newer** , the first release that accepts driver instances. Its peer dependency is `@mobilewright/protocol >=0.0.53 <0.1.0`, which Mobilewright installs for you.

## Quickstart

The fastest way to start is the scaffolder. It asks a few questions and writes a `mobilewright.config.ts` plus a sample test into your project. Add `--yes` to accept the defaults.

```bash
npx @testingbot/mobilewright-driver init
```

To set things up by hand instead, install the packages as dev dependencies:

```bash
npm install --save-dev mobilewright @mobilewright/test @testingbot/mobilewright-driver
```

### Credentials

The driver reads your TestingBot key and secret from the environment. You can find them on your [member edit page](https://testingbot.com/members/user/edit).

```bash
export TESTINGBOT_KEY=your_key
export TESTINGBOT_SECRET=your_secret
```

`TB_KEY` and `TB_SECRET` are accepted as aliases. If you would rather not use environment variables, pass `key` and `secret` to the driver directly. Do not commit them to your repository.

## Configuration

Point Mobilewright at TestingBot by passing a `TestingBotDriver` instance in your `mobilewright.config.ts`. Because TestingBot sessions start with the app already installed, the app under test has to be declared up front in the `apps` option.

```typescript
import { defineConfig } from 'mobilewright';
import { TestingBotDriver } from '@testingbot/mobilewright-driver';

export default defineConfig({
  testDir: '.',
  bundleId: 'com.example.MyApp',
  driver: new TestingBotDriver({
    apps: {
      android: './build/app.apk',
      'ios-simulator': './build/app-sim.zip',
      'ios-real': './build/app.ipa', // must be test-signed for real devices
    },
  }),
  projects: [
    { name: 'android', use: { platform: 'android', deviceType: 'emulator' } },
    { name: 'ios', use: { platform: 'ios', deviceType: 'real', osVersion: '>=17' } },
  ],
});
```

A few things worth knowing about `apps`:

- Local `.apk`, `.ipa` and simulator `.zip` paths are uploaded to [TestingBot Storage](https://testingbot.com/support/api/storage#upload) automatically. Already-uploaded builds can be referenced with their `tb://` URL.
- The keys are `android`, `android-emulator`, `android-real`, `ios`, `ios-simulator` and `ios-real`. The most specific one wins: `ios-real` overrides `ios`.
- A value can be an array. The first entry is the app under test, the rest are installed alongside it, which is how you ship a mock server or a test harness with your build.

A Mobilewright test looks the same whether it runs locally or on TestingBot:

```typescript
import { test, expect } from '@mobilewright/test';

test('shows the welcome screen', async ({ device, screen, bundleId }) => {
  await device.launchApp(bundleId);
  await expect(screen.getByText('Welcome')).toBeVisible();
});
```

## Selecting devices

Device selection comes from the Mobilewright project config, in the `use` block. Regular expressions and version ranges work on both real and virtual devices.

| Key | Example | Notes |
| --- | --- | --- |
| `platform` | `'ios'` or `'android'` | Required. |
| `deviceType` | `'real'`, `'simulator'`, `'emulator'` | Defaults to a virtual device. |
| `deviceName` | `/iPhone 1[45]/`, `/pixel/i` | Regex, matched against the TestingBot device catalogs. |
| `osVersion` | `'17'`, `'17.4'`, `'>=16 <18'` | Exact version, prefix or range expression. |
| `deviceId` | `'2241'` | Pin one specific physical device. The id comes from [GET /v1/devices](https://testingbot.com/support/api/devices). The Mobilewright docs mark `deviceId` as local-driver-only; this driver resolves it against the TestingBot catalog, so it works here too. |

Real devices resolve against the live physical catalog, where idle devices are preferred. If the device you asked for is busy, TestingBot queues your session until it frees up rather than failing. Virtual devices resolve against the simulator and emulator catalog, where the newest matching OS version wins. See the [supported device list](https://testingbot.com/support/app-automate/devices) for what is currently available.

## Running tests

```bash
npx mobilewright test
```

Each device slot maps to a TestingBot session on your [dashboard](https://testingbot.com/members), with video and a pass or fail verdict reported automatically. Prefix the command with `DEBUG=testingbot:*` to print every allocation, hub command, upload and result the driver performs.

```bash
DEBUG=testingbot:* npx mobilewright test
```

## Driver options

Everything the driver accepts. Only `apps` is required; the values below are examples, and the comments call out the real defaults. Anything left unset is simply not sent, in which case the TestingBot server-side default applies.

```typescript
new TestingBotDriver({
  apps: { android: './build/app.apk' }, // app under test, most specific key wins
                                        // ('ios-real' beats 'ios'); tb:// URLs allowed.
                                        // Array = app under test first, helper apps after:
                                        // android: ['./app.apk', './mock-server.apk']
  key: '...', // default: TESTINGBOT_KEY (or TB_KEY) env
  secret: '...', // default: TESTINGBOT_SECRET (or TB_SECRET) env
  sessionPerTest: false, // true = fresh session (and video) per test
  name: 'checkout flow', // session name (default 'mobilewright')
  hubUrl: '...', // default: https://hub.testingbot.com/wd/hub
  apiUrl: '...', // default: https://api.testingbot.com/v1
  build: 'ci-1234', // default: auto-detected from CI env / TESTINGBOT_BUILD
  testResults: 'on', // 'off' disables pass/fail reporting

  allocationTimeout: 395_000, // ms to wait for a device (default 395_000)
  commandTimeout: 60_000, // ms per WebDriver command (default 60_000)
  idleTimeout: 230, // seconds before TestingBot reaps an idle session (default 230)
  maxDuration: 1800, // max session length in seconds (unset by default)
  appiumVersion: '2.11.2', // pin the Appium version (unset = TestingBot's default)

  timeZone: 'Europe/Brussels', // device timezone (tz database name)
  geoCountryCode: 'DE', // route device traffic via a proxy in this country
  throttleNetwork: '3G', // or { downloadSpeed, uploadSpeed, latency, loss }
  autoGrantPermissions: true, // Android: auto-grant app permission dialogs
  autoAcceptAlerts: true, // iOS: auto-accept system permission alerts
  screenshots: true, // screenshot at every step (default false)
  video: true, // session video (default true)
  recordLogs: 'strip-parameters', // command logs: true | false | 'strip-parameters'
  public: false, // make results publicly accessible
  extra: 'commit=abc123', // custom metadata on the test detail page
  tabletOnly: false, // restrict allocation to tablets
  phoneOnly: false, // ...or to phones

  capabilities: {}, // extra Appium capabilities (escape hatch)
  tbOptions: {}, // extra tb:options entries (escape hatch)
});
```

`capabilities` and `tbOptions` are the escape hatches for anything the driver does not expose directly. See the [Appium capabilities reference](https://testingbot.com/support/app-automate/appium/capabilities) for what you can put in them.

## TestingBot commands inside tests

Some TestingBot features are runtime commands rather than session capabilities. Import the `testingbot` helper and call it from a test body; it targets the device session Mobilewright connected for that test.

```typescript
import { test, expect } from '@mobilewright/test';
import { testingbot } from '@testingbot/mobilewright-driver';

test('checkout survives a slow network', async ({ device, screen, bundleId }) => {
  await device.launchApp(bundleId);

  await testingbot.annotate('starting checkout'); // shows in the session timeline
  await testingbot.throttle('3G');
  await expect(screen.getByText('Order placed')).toBeVisible();

  console.log(testingbot.dashboardUrl());
});
```

| Command | TestingBot command | What it does |
| --- | --- | --- |
| `throttle(conditions)` | `tb:throttle` | Network conditions mid-test: `'Edge'`, `'3G'`, `'4G'`, `'airplane'`, `'disable'`, or `{ downloadSpeed, uploadSpeed, latency, loss }` in kb/s and ms. |
| `annotate(text)` | `tb:test-context` | Logs a step into the session command list so the timeline shows what the test was doing. |
| `setName(name)` | `tb:test-name` | Names the session on the dashboard. |
| `setBuild(build)` | `tb:test-build` | Groups the session under a build. |
| `setTags([...])` | `tb:test-tags` | Tags the session. |
| `setResult(passed)` | `tb:test-result` | Overrides the verdict, as a boolean or `'passed'` / `'failed'`. The driver reports it automatically, so use this only for outcomes Mobilewright cannot see. |
| `updateInfo({...})` | `tb:test-info` | Bulk update of `name`, `build`, `public`, `statusMessage` and `extra`. |
| `breakpoint()` | `tb:break` | Pauses the session for manual inspection with a live view. Debugging only, never leave it in CI. |
| `shell(command, args)` | `mobile: shell` | ADB shell on Android. A whitelisted subset is available on physical devices. |
| `execute(script, ...args)` | any | Escape hatch for any Appium or TestingBot execute-script command. |
| `sessionId()` / `dashboardUrl()` | - | This test's TestingBot session id and dashboard link. |

Throttling is reset automatically when the test's device session is released, so slow-network conditions never leak into the next test that reuses the device. Calling a command outside a test body raises an explanatory error rather than a null reference.

### Recipe: name every session after its test

```typescript
test.beforeEach(async ({ device }, testInfo) => {
  await testingbot.setName(testInfo.title);
  await testingbot.setTags([testInfo.project.name]);
});
```

Requesting the `device` fixture is what makes the session exist before the hook runs. With `sessionPerTest: true` each session gets exactly one name; in pooled mode the last test to run on a session wins.

## Sessions, naming and reporting

- By default a device slot's session is reused across tests, so you pay device startup once. A session that hosted exactly one test is renamed after that test and carries its own verdict. A session that hosted several keeps its configured name (`mobilewright` unless you set one) and gets a run summary in its **status message** instead, such as `12/12 tests passed`, or `1 of 3 tests failed. ...` with the individual failure messages appended. 
- Set `sessionPerTest: true` for one session, one video and one named verdict per test. That is the clearest dashboard, at the cost of device startup on every test. 
- Every session is tagged `mobilewright`, so you can filter the dashboard down to these runs. Git metadata is reported on top of that when Mobilewright captures it: the commit hash, branch, author and subject land in the session's **extra** field, alongside your own `extra` rather than replacing it, and the branch is added as a second tag. Enable it with `captureGitInfo: { commit: true }` in `mobilewright.config.ts`. 
- Failure messages are reported to the session's status message. `build` groups sessions on the dashboard and is auto-detected from GitHub Actions, GitLab, CircleCI, Buildkite, Bitrise, Travis, Azure DevOps, Jenkins and TeamCity, or read from a `TESTINGBOT_BUILD` variable. 

## Testing localhost and staging servers

If your app talks to a backend that is not reachable from the internet, use the [TestingBot Tunnel](https://testingbot.com/support/tunnel). Either run the tunnel yourself and hand the driver its identifier:

```typescript
driver: new TestingBotDriver({ tunnelIdentifier: 'my-tunnel' })
```

Or let the driver manage the tunnel around the run. This requires Java and `npm install --save-dev testingbot-tunnel-launcher`:

```typescript
driver: new TestingBotDriver({ tunnel: true }) // anonymous tunnel
driver: new TestingBotDriver({ tunnel: { identifier: 'ci-42' } }) // named tunnel
```

The driver starts the tunnel before any device is allocated and closes it after the run.

## Running in CI/CD

Store your credentials as secrets and run Mobilewright like any other test command. The build name is auto-detected, so no extra configuration is needed.

```yaml
jobs:
  mobile-tests:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with: { node-version: 20, cache: npm }
      - run: npm ci
      - run: npx mobilewright test
        env:
          TESTINGBOT_KEY: ${{ secrets.TESTINGBOT_KEY }}
          TESTINGBOT_SECRET: ${{ secrets.TESTINGBOT_SECRET }}
          # build name auto-detects as "owner/repo #run" - no config needed
```

The same pattern works on the other CI systems listed under [CI/CD integrations](https://testingbot.com/support/integrations/ci-cd).

## How Mobilewright maps to TestingBot

| Mobilewright concept | TestingBot implementation |
| --- | --- |
| Device allocation | An Appium session on `hub.testingbot.com`. The session UUID is the device id, and busy devices queue server side. |
| App install | Uploaded to TestingBot Storage at allocation time and passed as `appium:app`, helper apps as `appium:otherApps`. Real iOS builds are re-signed automatically. There are no mid-session installs. |
| Taps, swipes and gestures | W3C pointer actions. |
| View hierarchy | Appium page source, mapped to Mobilewright's `ViewNode` tree. |
| Webviews | Appium contexts, through `webViewBridge`. |
| Pass and fail reporting | `PUT /v1/tests/:session` from the `TestObserver` hooks. |
| Video | Recorded by TestingBot. `startRecording({ output })` downloads the MP4 locally at run end. |

## Troubleshooting

| Symptom | Cause and fix |
| --- | --- |
| `TestingBot credentials are missing` | Set `TESTINGBOT_KEY` and `TESTINGBOT_SECRET`, or pass `{ key, secret }` to the driver. |
| `TestingBot sessions must start with an app` | Declare the app in the driver's `apps` option, per platform and device type. |
| `No TestingBot real device matches ...` | Your criteria matched nothing in the catalog. Check the [available devices](https://testingbot.com/support/app-automate/devices) and loosen `osVersion` or `deviceName`. |
| Allocation waits for minutes | The matching device is busy. TestingBot queues the session until it frees up, bounded by `allocationTimeout`. |
| `Test timeout ... while setting up "device"` | More `workers` than your plan's parallel limit. TestingBot queues up to twice the limit server side and the driver waits out the rest. The driver logs your plan's number: set `workers` to at most that, or raise the Mobilewright `timeout`. |
| `... is a simulator-only build and cannot be installed on a real device` | Build a test-signed `.ipa` for `deviceType: 'real'` projects. |
| `"..." is not installed on this device` | The config's `bundleId` is not the package id of the build in `apps`. Check it with `aapt2 dump packagename app.apk`, and look for a stale `bundleId` or environment override. |
| `cannot install "..." mid-session` | TestingBot has no mid-session installs. Every app in `installApps` must also be listed in the driver's `apps` option. |
| Sessions show a run summary instead of a test name | The session hosted several tests, so the summary goes to the status message and the name is left alone. Use `sessionPerTest: true` to get one named session per test. |

## Current limitations

- One TestingBot session may host several Mobilewright tests unless `sessionPerTest: true`. Multi-test sessions report the run level verdict.
- Modifier key chords such as `pressKeys(['ctrl+a'])` work on Android only, because XCUITest cannot hold modifier keys.
- `pressButton` on iOS supports `HOME`, `VOLUME_UP` and `VOLUME_DOWN`. `listApps()` reports the foreground app only.
- Screenshots are always PNG. `applyDeviceSettings` turns Android animations off on a best-effort basis and is a no-op on iOS.
- Real iOS devices need a test-signed `.ipa`. Simulator builds are rejected on real devices with a clear error.
- While attached to a webview, the session's single Appium context is the web layer.

## Frequently asked questions

What is Mobilewright?

Mobilewright is an open source mobile UI test framework from the mobile-next project. You write tests in TypeScript or JavaScript with a Playwright-style API (`device`, `screen`, `expect`) and run them against iOS and Android apps. TestingBot publishes `@testingbot/mobilewright-driver`, a driver that points those same tests at our device cloud.

How do I run Mobilewright tests on TestingBot?

Run `npx @testingbot/mobilewright-driver init` in your test project to scaffold a config and a sample test, export `TESTINGBOT_KEY` and `TESTINGBOT_SECRET`, then run `npx mobilewright test`. See the [quickstart](https://testingbot.com#quickstart) for the manual setup.

Can I run Mobilewright on real iOS and Android devices?

Yes. Set `deviceType: 'real'` in a project's `use` block to allocate physical iPhones, iPads and Android phones or tablets from our EU datacenter. Leave it out and you get an iOS simulator or Android emulator instead. Real iOS devices need a test-signed `.ipa`.

Do I have to upload my app before running?

No. Declare your builds in the driver's `apps` option and local `.apk`, `.ipa` and simulator `.zip` files are uploaded to [TestingBot Storage](https://testingbot.com/support/api/storage#upload) for you. If you already uploaded a build you can reference its `tb://` URL instead.

Why do I get "Test timeout while setting up device"?

You are running more Mobilewright `workers` than your plan's parallel limit. TestingBot queues up to twice the limit server side and the driver waits out the rest, so the test-scoped device fixture eventually times out. The driver logs your plan's parallel limit: set `workers` to at most that number, or raise the Mobilewright `timeout`.

Can I test a staging or localhost backend?

Yes. Pass `tunnelIdentifier` to reuse a [TestingBot Tunnel](https://testingbot.com/support/tunnel) you started yourself, or let the driver open and close one around the run with `tunnel: true`. See [Testing localhost and staging](https://testingbot.com#tunnel).

Why does one session contain several tests?

By default a device slot's session is reused across tests so you only pay device startup once, and such a session reports a run level verdict in its status message, such as "12/12 tests passed". Set `sessionPerTest: true` for one session, one video and one named verdict per test.

How do I debug a failing Mobilewright run?

Prefix the run with `DEBUG=testingbot:*` to log every allocation, hub command, upload and result the driver performs. Every session also has video, command logs and device logs on its test detail page in the [dashboard](https://testingbot.com/members).

## Next steps

- [ **Driver on GitHub**  
Source, issues and releases](https://github.com/testingbot/mobilewright-testingbot)
- [ **Supported devices**  
Real iPhones, iPads and Android devices on the grid](https://testingbot.com/support/app-automate/devices)
- [ **Real device testing**  
How the real-hardware cloud works](https://testingbot.com/real-device-testing)
- [ **TestingBot Tunnel**  
Test against internal or staging APIs](https://testingbot.com/support/tunnel)
- [ **Appium guide**  
The protocol the driver speaks underneath](https://testingbot.com/support/app-automate/appium)
- [ **Pricing**  
Real-device plans and concurrency](https://testingbot.com/pricing)

### Looking for more help?

Have questions or need more information? Reach out via email or Slack.

[Email us](https://testingbot.com/contact/new) [Join our Slack](https://join.slack.com/t/testingb0t/shared_invite/zt-3bcw9xch-jk19~6XPs_xBrsAgAedkCw)
