npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@pwtap/plugin-appium

v1.2.0

Published

Mobile testing for Playwright via Appium — a raw WebdriverIO session (app: WebdriverIO.Browser) over Android + iOS simulator, macOS-first

Readme

@pwtap/plugin-appium

Mobile testing for the Playwright Test Automation Platform via Appium — Android (UiAutomator2) + iOS simulator (XCUITest), macOS-first. One app fixture: a raw WebdriverIO session, no curated facade on top.

npm

Install

Into a @pwtap project (wires the fixture, an env-gated appium project, env keys, and examples):

npx create-pwtap add appium

The app fixture

import { test, expect } from '@fixtures';
import { devices } from '@pwtap/plugin-appium';

test.use({ appium: devices.android }); // or { platform: 'android', device: 'Pixel_API_35' }

test('sign in', async ({ app }) => {
  await app('~Login').click();
  await app('~Username').setValue('John Doe');
  const dashboard = app('~Dashboard');
  await expect.poll(() => dashboard.isDisplayed()).toBe(true);
});

app is callable as a selector shorthand — app('~Login') is app.$('~Login') — and still the full WebdriverIO Browser otherwise: every protocol command and element method is available directly on it ($$, execute('mobile: ...'), saveScreenshot, …). Unlike @pwtap/plugin-maestro, there is no per-command step reporting — wrapping raw WebdriverIO calls one-by-one isn't worth the maintenance cost of proxying its full API, so wrap your own test.step() around a logical action if you want that in the report. WebdriverIO elements don't auto-wait like Playwright locators either — expect.poll(...) gives you the same retry-until-true behavior.

Installing an app under test: pass app (a local path or http(s) URL to an APK / iOS .app/.zip) in the appium option, or set APPIUM_APP_ANDROID/APPIUM_APP_IOS — it becomes the appium:app capability, so the driver installs it during session creation (no separate adb/simctl step). For a built-in app (e.g. the Settings/Preferences example), skip app and target it directly via the capabilities escape hatch (appium:appPackage/appium:appActivity on Android, appium:bundleId on iOS) — see the scaffolded tests/appium/settings.appium.ts.

Appium API cookbook

test('appium api cookbook', async ({ app, device }) => {
  await app('~Login').click();
  await app('~Username').setValue('John Doe');
  await app('~Password').setValue('secret');
  await app('~Submit').click();

  const row = app('~Order row');
  await row.waitForDisplayed({ timeout: 10_000 });
  await expect.poll(() => row.isDisplayed()).toBe(true);

  const items = await app.$$('android=new UiSelector().className("android.widget.TextView")');
  await expect(items.length).toBeGreaterThan(0);

  if (device.platform === 'android') {
    await app.execute('mobile: scrollGesture', {
      left: 0,
      top: 0,
      width: 300,
      height: 700,
      direction: 'down',
      percent: 0.7,
    });
  } else {
    await app.execute('mobile: scroll', { direction: 'down' });
  }
});

Locators

Priority order (fastest/most stable first): accessibility ID (~loginButton, works on both platforms), Android UiAutomator (android=new UiSelector().text("Login")), iOS Predicate String (-ios predicate string:label == "Login") or Class Chain, then XPath as a last resort — it's the slowest strategy and the most brittle across app updates:

const submit =
  device.platform === 'android'
    ? app('android=new UiSelector().text("Submit")')
    : app('-ios predicate string:label == "Submit"');
await submit.waitForDisplayed({ timeout: 10_000 });
await submit.click();

See docs/APPIUM_TESTING.md (scaffolded into your project) for the full locator reference, waiting patterns, and gesture commands (execute('mobile: scrollGesture' | 'mobile: swipe', {...})).

Locator strategy cheat sheet

| Priority | Strategy | Best use case | Notes | | -------- | --------------------------- | -------------------------------------------- | ----------------------------------------------------- | | 1 | ~accessibilityId | Cross-platform, stable UI controls | Fastest + most maintainable; ask devs for stable IDs. | | 2 | id=... / resource-id | Android elements with stable unique IDs | Good fallback when accessibility IDs are missing. | | 3 | android=UiSelector(...) | Android-only, rich attribute filtering | Prefer short selectors over long chained trees. | | 4 | -ios predicate string:... | iOS-only attribute queries | Usually faster and cleaner than XPath. | | 5 | -ios class chain:... | iOS hierarchical matching with better perf | Use when predicate is not enough. | | 6 | XPath | Last resort for hard-to-reach legacy screens | Most brittle/slow; avoid absolute deep paths. |

Recommended flow: start with ~accessibilityId, then platform-specific selectors, use XPath only if you have no stable alternative.

Running

npm run test:appium          # runs Appium tests + auto-generates test-results/appium-report/index.html
npm run report:appium        # regenerates only the Appium HTML report from existing results.json

report:appium is Appium-focused (separate from Playwright HTML): it summarizes Appium-only tests, diagnostics attachments, and server-command/error counts.

A bare npm test stays UI + API — the appium project is gated behind APPIUM=1.

The Appium server

By default the fixture spawns the appium CLI itself, one server per Playwright worker (4723 + workerIndex), and waits for GET /status to report ready. Point at a server you manage yourself instead with APPIUM_SERVER_URL (its lifecycle is then your responsibility). Override the binary with APPIUM_BIN if appium isn't the right name on PATH.

Parallel (the device pool)

The appium project is fullyParallel, and each test reserves its device with a cross-process lock (<platform>:<device>). That pairing is the device pool: tests on the same device serialize (they wait, not skip); tests on different devices or platforms run concurrently:

APPIUM=1 npx playwright test --project=appium --workers=3

Devices

Select with test.use({ appium }): a named device (Android AVD / iOS simulator name or UDID) auto-boots if not running; omit it to use any booted device. When no matching device is available the test skips (never fails). Create a device via Android Studio's AVD Manager / Xcode's Simulator app, or run:

npm run mobile:create-device

The script appends the created device alias into both plugin devices catalogs, so the same alias can be referenced from Appium and Maestro test blocks.

Devices the framework auto-booted are shut down automatically after the run by the appium-teardown project (headed or headless) — set APPIUM_KEEP_DEVICES=1 to keep them for faster reruns. Devices you booted yourself are left running. This registry is shared with @pwtap/plugin-maestro — running both plugins in one project still shuts every auto-booted device down exactly once.

Evidence — video, screenshot, device log

Screen recording and screenshots aren't mobile-specific settings — this fixture reads Playwright's own built-in video/screenshot options (use.video/use.screenshot in playwright.config.ts, or a project/describe override), so one central setting controls both for chromium and appium alike: all seven video modes (off / on / retain-on-failure / on-first-retry / on-all-retries / retain-on-first-failure / retain-on-failure-and-retries), and all four screenshot modes (off / on / only-on-failure / on-first-failure) — captured once at test end (Appium has no per-command concept like Maestro's step screenshots):

// playwright.config.ts
use: { video: 'retain-on-failure', screenshot: 'only-on-failure' }, // now applies to appium too

APPIUM_DEVICE_LOG=1 attaches the device's own system log for the whole test (Android logcat, iOS the unified system log) — off by default.

AI judge example (visual assertion)

If @pwtap/plugin-ai-judge is installed, you can assert a mobile screen with rubric scoring:

import { test, expect } from '@fixtures';

test('home screen passes ai rubric', async ({ app }, testInfo) => {
  const shot = testInfo.outputPath('home.png');
  await app.saveScreenshot(shot);

  await expect({
    image: shot,
    rubric: 'Home screen shows "Welcome", primary CTA is visible, and no error banner is present.',
  }).toPassRubric({ minScore: 80 });
});

Reference-image comparison is also supported:

await expect({ image: shot }).toMatchImage('testData/baselines/home.png', { minScore: 90 });

Environment variables (quick reference)

| Key | One-line description | | --------------------------------------- | ---------------------------------------------------------------------- | | APPIUM_PLATFORM | Default platform for tests that do not set test.use({ appium }). | | APPIUM_DEVICE | Default device alias/UDID when test-level device is not set. | | APPIUM_HEADLESS | Controls emulator/simulator visibility (true hidden, false shown). | | APPIUM_APP_ANDROID / APPIUM_APP_IOS | Default app artifact path/URL applied to appium:app. | | APPIUM_SERVER_URL | Reuses an externally managed Appium server instead of spawning one. | | APPIUM_DEVICE_LOG | Attaches device OS log (logcat / log show) for each test. | | APPIUM_DIAGNOSTICS | Controls diagnostics bundle (off, fail, always). | | APPIUM_KEEP_DEVICES | Keeps framework-booted devices alive after the run. | | APPIUM_BIN | Custom Appium CLI binary path/name instead of default appium. |

Requirements

  • Appium CLI (npm install -g appium) + the uiautomator2/xcuitest drivers (appium driver install uiautomator2 / xcuitest).
  • Android: Android SDK (ANDROID_HOME) + an emulator. iOS: Xcode + a simulator (simulator-only; real iOS devices are not yet supported).
  • Node ≥ 22.23. create-pwtap add appium runs an advisory host check for these.

License

MIT