Licence
MIT
Version
0.1.2
Deps
7
Size
14 kB
Vulns
0
Weekly
0
PlaywrightMobile
Playwright-native Android automation. Zero configuration. Zero mobile expertise required.
Installation
npm install -D @playwrightmobile/test @playwright/test
Environment Check
npx playwrightmobile doctor
✔ Android SDK found
✔ ADB found
✔ Device detected
✔ Environment ready
Your First Test
import { test, expect } from '@playwrightmobile/test';
test('login flow', async ({ mobile }) => {
await mobile.launch({ app: './app.apk' });
await mobile.getByPlaceholder('Email').fill('admin@test.com');
await mobile.getByPlaceholder('Password').fill('password');
await mobile.getByRole('button', { name: 'Login' }).click();
await expect(mobile.getByText('Dashboard')).toBeVisible();
});
npx playwright test
That's it. No Appium. No desired capabilities. No server startup.
Configuration
Create playwright.config.ts:
import { defineMobileConfig } from '@playwrightmobile/test';
export default defineMobileConfig({
mobileDefaults: {
screenshot: 'only-on-failure',
trace: 'retain-on-failure'
},
projects: [
{ name: 'android-emulator' },
{ name: 'android-real-device', use: { deviceId: 'R3CN90XXXXX' } },
{
name: 'browserstack',
use: {
playwrightmobile: {
cloud: {
provider: 'browserstack',
username: process.env.BS_USERNAME,
accessKey: process.env.BS_ACCESS_KEY,
deviceName: 'Google Pixel 9',
platformVersion: '14.0'
}
}
}
}
]
});
Locator API
| Method | Example |
|---|---|
getByText() |
mobile.getByText('Login') |
getByRole() |
mobile.getByRole('button', { name: 'Submit' }) |
getByPlaceholder() |
mobile.getByPlaceholder('Email') |
getByLabel() |
mobile.getByLabel('Username') |
getByTestId() |
mobile.getByTestId('login-btn') |
getByAccessibilityId() |
mobile.getByAccessibilityId('loginButton') |
locator() |
mobile.locator('//android.widget.Button') |
Gestures
// Tap / Click
await mobile.getByText('Submit').click();
await mobile.getByText('Item').dblclick();
await mobile.getByText('Hold').longPress();
// Text input
await mobile.getByPlaceholder('Search').fill('query');
await mobile.getByPlaceholder('Search').clear();
// Gestures
await mobile.swipe({ direction: 'up' });
await mobile.swipe({ direction: 'left', distance: 0.8 });
await mobile.scroll({ direction: 'down' });
// Pinch / Zoom
await mobile.pinch({ scale: 0.5 }); // pinch-in (zoom out)
await mobile.pinch({ scale: 2.0, centerX: 400, centerY: 600 }); // pinch-out (zoom in)
// Keys
await mobile.pressKey('enter');
await mobile.pressKey('back');
Assertions
await expect(mobile.getByText('Dashboard')).toBeVisible();
await expect(mobile.getByText('Loading')).toBeHidden();
await expect(mobile.getByRole('button', { name: 'Submit' })).toBeEnabled();
await expect(mobile.getByRole('checkbox')).toBeChecked();
await expect(mobile.getByRole('checkbox')).toBeChecked({ checked: false }); // unchecked
await expect(mobile.getByText('Hello')).toHaveText('Hello World');
await expect(mobile.getByRole('button')).toHaveCount(3);
await expect(mobile.getByRole('option')).toBeSelected();
await expect(mobile.getByRole('list')).toBeScrollable();
await expect(mobile.getByTestId('avatar')).toHaveAttribute('content-desc', 'User Avatar');
Auto-Wait & Auto-Scroll
Every action automatically:
- Waits for the element to be visible
- Waits for the element to be enabled
- Scrolls to find the element if not in viewport
- Retries intelligently on transient failures
You never need waitForElement(), sleep(), or explicit scrolls.
App Lifecycle
// Launch with APK (auto-installs)
await mobile.launch({ app: './my-app.apk' });
// Launch already-installed app
await mobile.launch({ appPackage: 'com.example.app' });
// Launch with options
await mobile.launch({
appPackage: 'com.example.app',
appActivity: '.MainActivity',
clearData: true,
permissions: ['android.permission.CAMERA']
});
// Deep links
await mobile.openDeepLink('example://dashboard');
// Terminate / relaunch
await mobile.terminate('com.example.app');
await mobile.activate('com.example.app');
Device Features
// Permissions
await mobile.grantPermission('com.example.app', 'android.permission.CAMERA');
await mobile.denyPermission('com.example.app', 'android.permission.LOCATION');
// GPS
await mobile.setGps({ latitude: 37.7749, longitude: -122.4194 });
// Network
await mobile.setNetworkCondition({ offline: true });
await mobile.setNetworkCondition({ offline: false });
await mobile.setNetworkCondition({ latency: 200, downloadSpeed: 1000, uploadSpeed: 500 }); // emulator only
// Biometrics
await mobile.authenticateBiometric(true); // success
await mobile.authenticateBiometric(false); // failure
// Notifications
await mobile.openNotifications();
// Orientation
await mobile.setOrientation('landscape');
await mobile.setOrientation('portrait');
const orientation = await mobile.getOrientation(); // 'portrait' | 'landscape'
// Keyboard
await mobile.hideKeyboard();
const shown = await mobile.isKeyboardShown();
// Clipboard
await mobile.setClipboard('hello world');
const text = await mobile.getClipboard();
// File transfer
await mobile.pushFile('./local/file.txt', '/sdcard/file.txt');
await mobile.pullFile('/sdcard/file.txt', './local/file.txt');
// App state
const state = await mobile.getAppState('com.example.app');
// 'foreground' | 'background' | 'not_running' | 'not_installed'
const installed = await mobile.isAppInstalled('com.example.app');
// WebView / hybrid apps
const contexts = await mobile.getContexts();
// ['NATIVE_APP', 'WEBVIEW_com.example.app']
await mobile.switchContext('WEBVIEW_com.example.app');
await mobile.switchContext('NATIVE_APP');
// Device info
const info = await mobile.deviceInfo();
console.log(info.model, info.platformVersion);
Code Generation
Record interactions and generate test code:
npx playwrightmobile codegen
Interact with your device. Press Ctrl+C to stop and print generated code.
Device Management
npx playwrightmobile devices
📱 Connected Devices
✔ 🖥 Android SDK built for x86_64 (Android 14) [emulator-5554]
✔ 📱 Pixel 9 (Android 14) [R3CN90XXXXX]
Failure Diagnostics
On test failure, PlaywrightMobile automatically collects:
test-results/
login_flow/
screenshot.png
logcat.txt
video.mp4
trace.json.gz
Cloud Providers
The same test code runs on any provider. Only the config changes.
// playwright.config.ts
projects: [
{
name: 'browserstack',
use: { playwrightmobile: { cloud: { provider: 'browserstack', ... } } }
},
{
name: 'perfecto',
use: { playwrightmobile: { cloud: { provider: 'perfecto', ... } } }
},
{
name: 'lambdatest',
use: { playwrightmobile: { cloud: { provider: 'lambdatest', ... } } }
},
{
name: 'saucelabs',
use: {
playwrightmobile: {
cloud: {
provider: 'saucelabs',
username: process.env.SAUCE_USERNAME,
accessKey: process.env.SAUCE_ACCESS_KEY,
deviceName: 'Google Pixel 9 GoogleAPI Emulator',
platformVersion: '14.0',
datacenter: 'us-west-1'
}
}
}
}
]
Architecture
@playwrightmobile/test
├── @playwrightmobile/fixtures ← Playwright fixture integration
├── @playwrightmobile/assertions ← Extended expect matchers
└── @playwrightmobile/sdk ← MobilePage + MobileLocator API
├── @playwrightmobile/android-driver ← ADB + UIAutomator2
├── @playwrightmobile/locator-engine ← Selector translation
├── @playwrightmobile/device-manager ← Device discovery
├── @playwrightmobile/session-manager ← Session lifecycle
├── @playwrightmobile/cloud-adapters ← BrowserStack/Perfecto/LambdaTest
└── @playwrightmobile/trace ← Trace recording
Docker
# Run tests in Docker with USB device passthrough
docker-compose run test-real-device
# Run tests against emulator
docker-compose up emulator
docker-compose run test-emulator