Loading...
Loading...
Mobile device automation, testing, and UI verification for iOS and Android. Use before writing mobilewright tests, automation scripts, or verifying mobile UI implementations.
npx skill4agent add mobile-next/mobilewright-skill mobilewrightPRODUCT_BUNDLE_IDENTIFIER*.pbxprojbundleIdentifierapp.jsonapp.config.jsapplicationIdbuild.gradlebuild.gradle.ktspackageAndroidManifest.xmlapp.jsonexpo.ios.bundleIdentifierexpo.android.packagemobilewright.config.tsbundleIdmobilewright.config.tsnpx mobilewright initmobilewright.config.tsexample.test.tsnpx mobilewright doctornpx mobilewright init # scaffold config + example test
npx mobilewright doctor # verify environment setup
npx mobilewright devices # list connected devices/simulators
npx mobilewright test # run tests
npx mobilewright screenshot # take a screenshot of a connected device
npx mobilewright screenshot -o login.png # save to specific file
npx mobilewright screenshot -d <device-id> # target a specific deviceimport { defineConfig } from 'mobilewright';
export default defineConfig({
platform: 'ios', // 'ios' or 'android'
bundleId: 'com.example.myapp', // app bundle ID
deviceName: 'iPhone 16', // regex to match device name (optional)
timeout: 10_000, // global timeout in ms (optional)
});screen.viewTree()const tree = await screen.viewTree();
console.log(JSON.stringify(tree, null, 2));testIdlabeltypetextvalue| Priority | Locator | Why |
|---|---|---|
| 1 | | Most stable. Set by developers, won't change with copy or i18n. |
| 2 | | Semantic and cross-platform. Maps to native types on both iOS and Android. |
| 3 | | Accessibility label. Stable if accessibility is maintained. |
| 4 | | Good for text fields. Only available on Locator, not on Screen. |
| 5 | | Fragile. Changes with copy updates, i18n, or minor rewording. |
| 6 | | Platform-specific. Ties your code to iOS or Android element types. |
@mobilewright/testimport { test, expect } from '@mobilewright/test';
test.use({ bundleId: 'com.example.myapp', video: 'on' });
test.beforeEach(async ({ device, bundleId }) => {
// Fresh app state for every test
await device.terminateApp(bundleId).catch(() => {});
await device.launchApp(bundleId);
});
test.describe('login flow', () => {
test('user can sign in with valid credentials', async ({ screen }) => {
await screen.getByLabel('Email').fill('user@example.com');
await screen.getByLabel('Password').fill('password123');
await screen.getByRole('button', { name: 'Sign In' }).tap();
await expect(screen.getByText('Welcome back')).toBeVisible();
});
});npx mobilewright test
npx mobilewright test login.test.ts # specific file
npx mobilewright test --grep "sign in" # run a single test by name
npx mobilewright test --reporter list # line-by-line output, good for CI/agents
npx mobilewright test --reporter json # machine-readable JSON output| Fixture | Scope | Description |
|---|---|---|
| test | The device screen — use this for all element interactions |
| worker | Device connection — use for app lifecycle, orientation, URLs |
| test | The configured bundle ID string |
test.use()test.use({
bundleId: 'com.example.app', // override app bundle ID
platform: 'android', // override platform
deviceId: '5A5FCFCA-...', // target specific device
video: 'on', // record video ('on' | 'retain-on-failure')
});import { ios, android } from 'mobilewright';
const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;
// Do your work
const tree = await screen.viewTree();
const screenshot = await screen.screenshot();
await screen.getByRole('button', { name: 'Start' }).tap();
// Always close when done
await device.close();// Auto-discover first booted simulator
const device = await ios.launch();
// Launch a specific app
const device = await ios.launch({ bundleId: 'com.example.app' });
// Match device by name (regex)
const device = await ios.launch({ deviceName: 'My.*iPhone' });
// Target exact device
const device = await ios.launch({ deviceId: '5A5FCFCA-...' });
// List available devices
const devices = await ios.devices();
const devices = await android.devices();import { ios } from 'mobilewright';
const device = await ios.launch({ bundleId: 'com.example.myapp' });
const { screen } = device;
// Take a screenshot to verify the UI looks correct
const screenshot = await screen.screenshot();
// Dump the view tree to verify elements exist and have correct properties
const tree = await screen.viewTree();
// Verify specific elements are present and have expected content
const title = await screen.getByRole('text', { name: 'Welcome' }).getText();
const isButtonEnabled = await screen.getByTestId('submit-btn').isEnabled();
await device.close();screen.getByLabel('Email') // accessibility label
screen.getByTestId('login-button') // accessibility identifier
screen.getByText('Welcome') // exact text match
screen.getByText(/welcome/i) // regex match
screen.getByText('welcome', { exact: false }) // substring match
screen.getByType('TextField') // native element type
screen.getByRole('button', { name: 'Sign In' }) // semantic role + nameawait screen.screenshot() // PNG buffer
await screen.screenshot({ format: 'jpeg', quality: 80 })
await screen.swipe('up') // scroll down
await screen.swipe('down', { distance: 300, duration: 500 })
await screen.pressButton('HOME')
await screen.goBack() // press BACK (Android only, no-op on iOS)
await screen.viewTree() // full view hierarchy as JSONawait locator.tap()
await locator.doubleTap()
await locator.longPress({ duration: 1000 })
await locator.fill('hello@example.com') // tap to focus + typeawait locator.getText() // visible text / label
await locator.getValue() // input value
await locator.isVisible() // boolean
await locator.isEnabled() // boolean
await locator.isSelected() // boolean
await locator.isFocused() // boolean
await locator.isChecked() // booleanawait locator.count() // number of matching elements
await locator.all() // array of Locator, one per match
locator.first() // first matching element
locator.last() // last matching element
locator.nth(2) // element at index (0-based, negative counts from end)await screen.getByType('Cell').nth(2).tap();const count = await screen.getByType('Cell').count();
expect(count).toBe(5);const row = screen.getByType('Cell');
await row.getByRole('button', { name: 'Delete' }).tap();
await row.getByPlaceholder('Enter name').fill('Arthur');getByPlaceholderawait locator.waitFor({ state: 'visible' })
await locator.waitFor({ state: 'hidden' })
await locator.waitFor({ state: 'enabled' })
await locator.waitFor({ state: 'disabled', timeout: 10_000 }).notawait expect(locator).toBeVisible();
await expect(locator).not.toBeVisible();
await expect(locator).toBeEnabled();
await expect(locator).toBeDisabled();
await expect(locator).toHaveText('Welcome back!');
await expect(locator).toHaveText(/welcome/i);
await expect(locator).toContainText('back');
await expect(locator).toHaveValue('user@example.com');
await expect(locator).toBeHidden();
await expect(locator).toBeChecked();
await expect(locator).toBeSelected();
await expect(locator).toBeFocused();
await expect(locator).toBeVisible({ timeout: 10_000 });// App lifecycle
await device.launchApp('com.example.app');
await device.launchApp('com.example.app', { locale: 'fr_FR' });
await device.terminateApp('com.example.app');
await device.installApp('/path/to/app.ipa');
await device.uninstallApp('com.example.app');
const apps = await device.listApps();
const foreground = await device.getForegroundApp();
// Navigation / deep links
await device.goto('myapp://settings');
await device.openUrl('https://example.com');
// Orientation
await device.setOrientation('landscape');
const orientation = await device.getOrientation();
// Recording
await device.startRecording({ output: 'recording.mp4' });
await device.stopRecording();
// Cleanup
await device.close();getByRole| Role | iOS | Android |
|---|---|---|
| Button, ImageButton | Button, ImageButton, ReactViewGroup* |
| TextField, SecureTextField, SearchField | EditText, ReactEditText |
| StaticText | TextView, Text, ReactTextView |
| Image | ImageView, ReactImageView |
| Switch | Switch, Toggle |
| -- | Checkbox |
| Slider | SeekBar |
| Table, CollectionView, ScrollView | ListView, RecyclerView, ReactScrollView |
| NavigationBar | Toolbar |
| Link | Link |
buttonclickable="true"accessible="true"test.beforeEach(async ({ device, bundleId }) => {
await device.terminateApp(bundleId).catch(() => {});
await device.launchApp(bundleId);
});@mobilewright/testtest.afterEach(async ({ screen }, testInfo) => {
const screenshot = await screen.screenshot();
await testInfo.attach('screenshot', { body: screenshot, contentType: 'image/png' });
});await screen.swipe('up'); // scroll down to reveal content below
await expect(screen.getByText('Footer Content')).toBeVisible();async function login(screen: any, email: string, password: string) {
await screen.getByLabel('Email').fill(email);
await screen.getByLabel('Password').fill(password);
await screen.getByRole('button', { name: 'Sign In' }).tap();
}
test('logged in user sees dashboard', async ({ screen }) => {
await login(screen, 'user@test.com', 'pass123');
await expect(screen.getByText('Dashboard')).toBeVisible();
});screen.viewTree()await expect(locator).toBeVisible()await locator.isVisible()terminateApplaunchApp{ exact: false }screen.tap(x, y)await sleep(2000)waitFortap()fill()await locator.waitFor({ state: 'visible' }); await locator.tap();await locator.tap()isVisible()expect(locator).toBeVisible()screen.viewTree()getByRole('button')getByType('UIButton')