Wires Appium for cross-platform mobile UI automation - uses the WebDriver protocol, picks a driver per platform (XCUITest for iOS, UiAutomator2 / Espresso for Android, Mac2 for macOS, Windows for desktop), authors tests in JS / Python / Java / Ruby / .NET, configures `desiredCapabilities`, runs against simulators / emulators / device farms. Use when a single test suite must cover both iOS and Android, or when the team's stack is multi-platform (iOS + Android + Mac + Windows).
80
100%
Does it follow best practices?
Run evals on this skill
Adds up to 20 points to the overall score
View guide
Passed
No findings from the security scan
The driver model is the load-bearing concept: Appium itself is the WebDriver protocol; per-platform automation is a driver (XCUITest driver talks to iOS, UiAutomator2 / Espresso to Android, Mac2 to macOS, Windows to Windows desktop). Client libraries exist for JavaScript, Python, Java, Ruby, and .NET (per appium-docs).
If the app is iOS-only, xcuitest-suite
is lighter (no external server). For React Native specifically,
detox-testing is faster.
npm install -g appium
# Install per-platform drivers:
appium driver install xcuitest # iOS
appium driver install uiautomator2 # Android
appium driver install espresso # Android (alternative; gray-box)
appium driver install mac2 # macOS desktop
appium driver install windows # Windows desktop
# Verify:
appium driver listappium server -p 4723The default port is 4723; client tests connect to
http://localhost:4723 (no /wd/hub suffix in Appium 2.x).
import { remote } from 'webdriverio';
const capabilities = {
platformName: 'Android',
'appium:automationName': 'UiAutomator2',
'appium:deviceName': 'Android Emulator',
'appium:app': '/path/to/app.apk',
};
describe('Cart flow', () => {
let driver;
before(async () => {
driver = await remote({
hostname: 'localhost',
port: 4723,
capabilities,
});
});
after(async () => {
await driver.deleteSession();
});
it('adds an item to cart', async () => {
const addButton = await driver.$('id=add_to_cart_button');
await addButton.click();
const cartCount = await driver.$('id=cart_count');
const text = await cartCount.getText();
expect(text).toBe('1');
});
});The automationName capability picks the driver:
UiAutomator2 (Android), XCUITest (iOS), Espresso (Android
gray-box), Mac2, Windows.
| Capability | Use |
|---|---|
platformName | iOS / Android / Mac / Windows |
appium:automationName | Driver name (XCUITest / UiAutomator2 / Espresso etc.) |
appium:deviceName | Logical device name (Simulator / Emulator / specific) |
appium:platformVersion | OS version (e.g. 14.0) |
appium:app | Path to .apk / .ipa |
appium:bundleId / appium:appPackage | Pre-installed app |
appium:noReset | Don't reinstall app between sessions |
appium:newCommandTimeout | Server idle timeout (default 60s) |
For device-farm runs, capabilities also include the farm's authentication tokens / device-pool selection.
// By accessibility ID - the most cross-platform
await driver.$('~login-button').click(); // ~ prefix in WebdriverIO
// By id - works for both platforms with platform-prefixed IDs
await driver.$('id=add_to_cart_button'); // Android
await driver.$('id=add-to-cart-button'); // iOS uses accessibility ID
// XPath - works everywhere; brittle (selector-quality anti-pattern)
await driver.$('//XCUIElementTypeButton[@name="Submit"]');
// Image-based locator (Appium-specific) - fallback when no IDs
await driver.findElementByImage('./assets/login-button.png');The cross-platform strategy: production code sets the same
accessibility ID on both iOS (accessibilityIdentifier) and
Android (contentDescription or resource-id). Tests use one
selector for both.
# Local with one platform
WDIO_OS=android wdio run wdio.conf.js
# Multi-platform: WebdriverIO multi-capability config
# wdio.conf.js
exports.config = {
capabilities: [
{ platformName: 'iOS', 'appium:platformVersion': '17.4', ...iosCaps },
{ platformName: 'Android', 'appium:platformVersion': '14', ...androidCaps },
],
runner: 'local',
services: ['appium'],
};The services: ['appium'] line auto-starts the Appium server
inside the test runner - no separate server step.
# Multi-platform matrix
jobs:
ui-tests:
strategy:
matrix:
platform: [iOS, Android]
runs-on: ${{ matrix.platform == 'iOS' && 'macos-15' || 'ubuntu-latest' }}
steps:
- uses: actions/checkout@v5
- if: matrix.platform == 'Android'
uses: reactivecircus/android-emulator-runner@v2
with:
api-level: 34
script: WDIO_OS=android npx wdio run wdio.conf.js
- if: matrix.platform == 'iOS'
run: |
xcrun simctl boot 'iPhone 15'
WDIO_OS=ios npx wdio run wdio.conf.js| Anti-pattern | Why it fails | Fix |
|---|---|---|
| Per-platform separate test code | Defeats Appium's reuse value. | One test, multi-capability config (Step 6). |
| XPath selectors as default | Slowest selector strategy; brittle to UI tree changes. | Accessibility IDs first (Step 5). |
Hard-coded device name ('iPhone 15') | Test fails when sim doesn't exist. | Use genericDevice: true or generated capabilities. |
Ignoring noReset between tests | App state leaks; flaky. | noReset: false (default) reinstalls per session. |
One mega-test using before for the whole suite | Suite-long flake; one tiny issue restarts everything. | Per-test beforeEach reset; small focused tests. |
| Mixing Appium 1.x and 2.x docs | Server URL / capability format differ; failures cryptic. | Pin Appium 2.x; use Appium 2.x docs only. |
xcuitest-suite,
espresso-suite - per-platform
native alternatives.detox-testing,
maestro-flows - cross-platform
alternatives with different trade-offs.mobile-device-matrix-toolkit - orchestrates Appium-driven matrix runs across simulators /
emulators / farms.