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

mobile-test

v0.0.2

Published

TypeScript-first mobile testing framework focused on screenshot testing

Readme

mobile-test

TypeScript-first end-to-end tests for iOS and Android apps, with screenshot comparison built in. Tests run in Vitest. Devices are driven through agent-device, so there is no native code to build and no SDK to add to your app.

import { describe, it, expect } from "vitest";
import { device, element, by } from "mobile-test";

describe("Login", () => {
  it("logs in", async () => {
    await device.launch({ path: "/login" });

    await element(by.id("email")).type("[email protected]");
    await element(by.id("password")).type("secret");
    await element(by.id("login-button")).tap();

    await expect(element(by.text("Welcome back"))).toBeVisible();
    await expect(device).toMatchScreenshot("home");
  });
});

Requirements

  • Node 18+ and Bun (or npm/pnpm)
  • macOS with Xcode for iOS simulators; Android SDK with adb on your PATH for emulators
  • A booted simulator or emulator with your app installed. The app needs no test build; only testIDs on the elements you want to find.

Setup

bun add -d mobile-test vitest

vitest.config.mts:

/// <reference types="vitest/config" />
import { defineConfig } from "vitest/config";
import { defineConfig as defineMobileTestConfig, mobileTestProjects } from "mobile-test";

const mobileTest = defineMobileTestConfig({
  app: {
    ios: { bundleId: "com.example.app", scheme: "example" },
    android: { appId: "com.example.app", scheme: "example" },
  },
  projects: [
    { name: "ios-simulator", platform: "ios" },        // first booted simulator
    { name: "android-emulator", platform: "android" }, // first booted emulator
  ],
  screenshots: { dir: "./.screenshots" },
});

export default defineConfig({
  test: {
    projects: mobileTestProjects(mobileTest, {
      include: ["e2e/**/*.test.ts"],
      testTimeout: 60_000,
      hookTimeout: 60_000,
      fileParallelism: false,
    }),
  },
});

Add the matcher types to your tsconfig.json:

{ "compilerOptions": { "types": ["mobile-test/matchers"] } }

Run:

bunx vitest run --project ios-simulator
bunx vitest run --project android-emulator
UPDATE_SCREENSHOTS=true bunx vitest run --project ios-simulator   # accept new baselines

The first run on a fresh simulator builds agent-device's iOS runner with Xcode, which takes a few minutes. Later runs reuse the cached runner. In CI, run agent-device prepare ios-runner once after booting the simulator.

Any installed build of your app works: a release build with the bundle embedded, or a debug build served by Metro while you iterate. Screenshot baselines are shared between the two; the builds render a few pixels differently, so set screenshots.maxDiffPercentage to a small value (the example app uses 0.01) if you switch between them.

API

Device

await device.launch()                       // configured app, fresh start
await device.launch({ path: "/settings" })  // scheme://settings
await device.launch({ path: "/settings", relaunch: false })  // keep the app running (see below)
await device.launch({ url: "other://x", bundleId: "com.other.app" })
await device.openUrl({ path: "/settings" }) // onto the running app
await device.terminate("com.example.app")
await device.install("./build/app.apk")     // for the configured app id
await device.waitForAnimationToEnd()        // screenshot-diff based
await device.hideKeyboard()
await device.pressHome()
await device.setLocation(37.7749, -122.4194)

Elements

element(by.id("submit"))            // testID / accessibilityIdentifier / resource-id
element(by.text("Sign in"))         // label, value or title; string or RegExp
element(by.label(/item \d+/i))
element(by.role("button"))          // lowercase XCUIElementType name on iOS, class name on Android
element(by.id("row")).atIndex(2)
element(by.id("cell").withAncestor(by.id("list")))

await el.tap(); await el.doubleTap(); await el.longPress(1.5)
await el.type("text"); await el.replaceText("text"); await el.clear()
await el.swipe("up"); await el.scrollTo(target); await el.scrollToEnd("down")
await el.isVisible(); await el.exists(); await el.getText()

device.launch terminates and restarts the app by default so each test starts clean. Pass relaunch: false for tests that only navigate: the app stays running and the deep link is opened onto it. On Android that is a few hundred milliseconds instead of a relaunch. On iOS it saves little: opening a URL onto a running app shows the system "Open in app?" alert, which the framework accepts, and the route transition still has to settle.

Assertions (auto-retry until actionTimeout)

await expect(el).toBeVisible()
await expect(el).not.toBeVisible()
await expect(el).toHaveText("3")
await expect(el).toHaveValue("Alice")
await expect(el).toBeEnabled()

await expect(device).toMatchScreenshot("home")
await expect(device).toMatchScreenshot("home", {
  threshold: 0.1,            // per-pixel color sensitivity
  maxDiffPercentage: 0.5,    // fail above this
  antialiasing: true,
  mask: [element(by.id("clock"))],
})
await expect(element(by.id("avatar"))).toMatchScreenshot("avatar")

Baselines live in <screenshots.dir>/baseline/<device name>/, the latest run in latest/, and diff images in diff/. A missing baseline is created on first run.

Configuration

| Option | Default | Notes | |---|---|---| | app.ios | | { bundleId, scheme } or a bundle id string | | app.android | | { appId, scheme } or a package name string | | projects[] | auto | { name, platform, device? }; device is a simulator/emulator name or id. Omit it to use the first booted one. | | screenshots.dir | ./screenshots | | | screenshots.threshold | 0.1 | | | screenshots.maxDiffPercentage | 0 | | | screenshots.antialiasing | true | | | screenshots.pixelDensity | auto | iOS simulator scale (2 for iPad/SE, 3 for iPhone); detected when omitted | | actionTimeout | 5000 | ms to wait for an element or assertion | | logLevel | info | debug prints per-call timings |

Troubleshooting

From a checkout of this repo, packages/mobile-test has a quick health check that lists devices, opens your app through agent-device, and times a snapshot and a screenshot:

bun run check-device -- --platform ios --app com.example.app
bun run check-device -- --platform android --app com.example.app --device emulator-5554

How it works

  • Each Vitest project opens one agent-device session, claims the selected device and warms the runner before tests start.
  • Elements are resolved from agent-device's raw accessibility snapshot; locator matching happens in TypeScript, so RegExp, atIndex and withAncestor work the same on both platforms.
  • Coordinates are logical points on iOS and device pixels on Android, matching each platform's snapshot.
  • Screenshots are compared with odiff.

Known limitations

  • Android reports the placeholder of an empty TextInput as its value, so toHaveValue("") does not pass on Android yet (agent-device does not expose the hint flag).
  • element.clear() sends delete keys; on Android that goes through adb.
  • Keyboard dismissal on iOS falls back to pressing return, then swiping and tapping, because the simulator keyboard has no dismiss key.
  • Only one Vitest worker may drive a device at a time (fileParallelism: false).