All projects
Project

Astur

Playwright-style mobile testing for real Android and iOS apps. Native agents (UIAutomator, XCUITest), no Appium server. Native, React Native, Flutter and WebView apps, with a live inspector and codegen.

androidappiumappium-alternativeautomation-testinge2e-testingexpoflutteriosmobile-automationmobile-testingplaywrightplaywright-testqareact-nativetest-automationtesting-toolstypescriptuiautomatorwebviewxcuitest

Playwright-style tests for real Android and iOS apps

Astur drives your app through native agents, UIAutomator on Android and XCUITest on iOS, with no Appium server in between. You write tests with the Playwright Test API you already know, and run them on emulators, simulators and real devices.

import { test, expect } from '@astur-mobile/test';

test('sign in', async ({ device }) => {
  await device.getByLabel('Email').fill('qa@example.com');
  await device.getByRole('button', { name: 'Sign in' }).tap();
  await expect(device.getByText('Welcome')).toBeVisible();
});

It is built on Playwright Test, so retries, projects, reporters, the HTML report and the VS Code play button all work the way they already do.

Why Astur

  • No server in the middle. Tests talk to a native agent on the device: UIAutomator on Android, XCUITest on iOS. No Appium server, no WebDriver bridge.
  • The API you know. getByRole, getByLabel, expect(...).toBeVisible(), fixtures, projects, retries: Playwright Test, pointed at a phone.
  • Whatever your app is built in. Native SDKs, React Native and Expo, Flutter, Jetpack Compose, SwiftUI, .NET MAUI, plus real DOM control of in-app WebViews and mobile Chrome and Safari.
  • See before you write. The inspector mirrors the device, shows the live tree, ranks locators, and records taps into a spec you keep.
  • Results you can trust. Assertions wait for the condition and fail when the device cannot be read. An action whose response is lost fails instead of running twice.
  • Secure by default. Every agent and the inspector require a per-session token.

Quick start

npm create astur@latest

The scaffolder asks what you target and writes a working config and a first test. Then:

npx astur-mobile doctor    # check your environment
npx astur-mobile test      # run the suite

Adding Astur to an existing project instead:

npm install -D @astur-mobile/test astur-mobile @playwright/test

Requirements: Node.js 18 or newer. For Android, the SDK platform-tools (adb) and an emulator or a device with USB debugging. For iOS, macOS with Xcode; real devices also need an Apple signing team for the XCUITest runner. npx astur-mobile doctor checks all of it and says what is missing.

Writing a test

import { expect, test } from '@astur-mobile/test';

test.use({
  astur: {
    platform: 'android',
    device: { kind: 'emulator' },
    app: './apps/demo.apk'
  }
});

test('login', async ({ device }) => {
  await device.app.launch();

  await device.getByLabel('Email').fill('qa@example.com');
  await device.getByLabel('Password').fill('secret');
  await device.getByRole('button', { name: 'Sign in' }).tap();

  await expect(device.getByText('Welcome')).toBeVisible();
});

Finding elements

device.getByTestId('login-submit');
device.getByLabel('Email');
device.getByText('Welcome');
device.getByRole('button', { name: 'Sign in' });
device.getByPlaceholder('Search…');
device.getByType('android.widget.Button');

A screen of repeated rows needs more than one selector, so locators compose:

const row = device.getByType('Cell').filter({ hasText: 'Rye' });
await row.getByRole('button', { name: 'Add' }).tap();

device.getByRole('listitem').filter({ hasNotText: 'Sold out' }).first();
device.getByText('Retry').or(device.getByText('Try again'));

Scope to a parent, filter({ hasText, hasNotText, has, hasNot }), combine with and() / or(), and pick with first() / last() / nth(i). See Locators.

For the rare element none of that can pin down, by.native({ ios, android }) sends a platform query straight to the agent: an NSPredicate on iOS, a BySelector chain on Android.

Asserting

Assertions retry until they pass or time out, and every action auto-waits.

await expect(device.getByText('Welcome')).toBeVisible();
await expect(device.getByLabel('Email')).toHaveValue('qa@example.com');
await expect(device.getByRole('checkbox')).toBeChecked();
await expect(device.getByRole('menuitem')).toHaveCount(3);
await expect(device.getByTestId('hero')).toHaveScreenshot('hero.png');

toBeVisible, toBeHidden, toExist, toBeEnabled, toBeDisabled, toBeChecked, toBeSelected, toBeFocused, toBeEmpty, toHaveText, toContainText, toHaveValue, toHaveLabel, toHaveType, toHaveBounds, toHaveCount, toHaveScreenshot.

Gestures, state and the device

await device.getByTestId('card').dragTo(device.getByTestId('dropzone'));
await device.getByText('Footer link').scrollIntoView();
await device.getByText('Item').longPress();

await device.getByLabel('Email').inputValue();
await device.app.reset({ launch: true });
await device.orientation.landscape();
await device.screenshot({ path: 'test-results/home.png' });

What it can do

Any native-rendering frameworkNative SDK, React Native, Expo, Jetpack Compose, SwiftUI, .NET MAUI, NativeScript, Capacitor. Full table
FlutterThe live widget tree over the Dart VM service on Android; the accessibility tree on iOS
In-app WebViewsReal DOM automation with device.webContext(). WebViews
Mobile webDrive a site in Chrome or Safari on the device itself. Mobile Web
Network observationAssert on the HTTP calls an app makes, not just what it renders. Network
Visual comparisontoHaveScreenshot() with per-device baselines and masking. Visual Comparison
Inspector and codegenLive element inspection and recorded specs. Inspector

CLI

npx astur-mobile doctor        # check the environment
npx astur-mobile devices       # list devices, emulators, simulators
npx astur-mobile init          # scaffold config and a first test
npx astur-mobile codegen       # live inspector and spec recorder (alias: inspect)
npx astur-mobile screenshot    # capture a device screen to a PNG
npx astur-mobile test          # run the suite

Documentation

astur-mobile.github.io/Astur, also in Arabic and Spanish.

Getting Started · Configuration · Locators · Android · iOS · Platform Limits · Troubleshooting

Examples

Runnable Android and iOS suites live in examples/, and as a clonable starter in astur-boilerplate. Both run against the Astur demo app, which ships with them.

About the name

Astur is named after the astrolabe, the instrument that folded an observatory into something you could hold: turn the rete, sight a star along the alidade, read the answer off the limb. In tenth-century Aleppo, Mariam al-Asturlabiya was known for building them. Astur is the same idea for mobile testing: a lean native runtime in place of a heavy server stack.

Where a platform cannot do something, Astur says so instead of pretending. See Platform Limits.

Contributing and security

Contributions are welcome; see CONTRIBUTING.md. For vulnerabilities, follow SECURITY.md rather than opening a public issue.

If Astur is useful to you, a ⭐ on the repository helps other mobile teams find it.

Sponsors

Astur is built in the open and has no sponsors yet — be the first.

If Astur saves your team time, consider supporting development:

Sponsor on GitHub   Support on Ko-fi

License

Apache 2.0. Copyright 2026 Amr Salem and Astur contributors.

"Astur" is a trademark; see the trademark policy.