maestro-runner

devicelab-devApache-2.0活跃维护

用 Go 写的移动端 UI 自动化测试工具,覆盖 Android、iOS、React Native、Flutter 和 Expo,支持真机、模拟器与云设备,可替代 Maestro 且功能全部免费。

Fast mobile UI test automation for Android, iOS, React Native, Flutter & Expo. Open-source Maestro alternative — 100% free, no features behind a paywall. Supports real iOS devices, simulators, emulators, and cloud providers.

v1.1.29发布于 2026-10-15 07:28 UTC527star62fork同步于 2026-11-10 06:01 UTC

怎么上手

从 README 代码块原样提取,可能是其中一种安装方式
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash
curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.28
npx maestro-runner test flows/          # no install step
npm install --save-dev maestro-runner   # or pin it in the project
硬件要求

README 没有说明硬件要求。

下载:源码 zip·GitHub Releases(安装包)本站不镜像安装包

项目说明

这个项目的说明还在生成中,先参考下面「最近发版」和 README。

最近发版

5 个版本v1.1.29
  1. v1.1.29最新正式版2026-10-15 · 2026-10-15 07:28 UTC

    Release 1.1.29

    This release adds --retries, which runs flaky flows again in the same run and marks them as flaky in JUnit and HTML. It also runs Maestro's test command line unchanged, so CI scripts written for Maestro work as they are. Most of the rest closes gaps with Maestro found by running real suites side by side (DuckDuckGo, RNTester, React Navigation, Expo, pager-view, react-native-enriched). Contributors sent a large share of it, as 17 pull requests and a stack of precise bug reports, credited at the end. The DeviceLab Android agent taps faster and recovers from a dropped connection, and the DeviceLab iOS agent reads screens deeper than 62 levels. test now sends one anonymous usage event per run; what it holds and how to turn it off are below and on the telemetry page.

    ⚠️ Behaviour changes

    inputText types at the cursor into a field that already holds text, as Maestro does (DeviceLab Android). It used to replace the field's whole value, so a rich-text editor typed "1", Enter, "2" kept only "2". An empty field is still set in one call. To replace a field's text, use eraseText first, as with Maestro (#189).

    runScript runs every script as written. ${...} in a script is a JavaScript template literal, as in Maestro, and flow variables reach the script as plain names. The runner used to expand ${...} as flow variables first, so Expo's ${SERVER_URL}/process became /process.

    An undefined ${name} expands to undefined, and null to null, as in Maestro. Both used to expand to an empty string.

    travel reads speed in metres per second (default 4), as Maestro does, and walks each leg in 50 steps. It read km/h and paused 3600/speed whole seconds at each point whatever the distance: 72 s a point by default, and none at all above 3600. Leg length is computed as Maestro computes it, so flows keep the timing they were tuned for.

    DeviceLab Android hideKeyboard presses Back when ESC does not close the keyboard. Samsung's keyboard and the AOSP keyboard ignore ESC, so the step passed with the keyboard still open. One Back is sent only while the keyboard is confirmed up, which the keyboard consumes; with no keyboard up nothing is sent, so the step still cannot navigate away (#42). This is what the UIAutomator2 driver does. It is still not Maestro's unconditional Back: a flow that relied on hideKeyboard to leave a screen should use back.

    test sends one anonymous usage event at the end of each run. Counts and versions only: runner version, platform, driver, device kind, flow and step counts, failure kinds and duration. No machine or user id, no flow or file names, selectors, app ids, URLs, environment values or error messages, and nothing is written on your machine; projects are told apart by a one-way hash of the list of flow files run. A local run prints a one-line notice. Turn it off with MAESTRO_RUNNER_NO_TELEMETRY=1 or DO_NOT_TRACK=1; MAESTRO_RUNNER_TELEMETRY_DEBUG=1 prints the event instead of sending it. The update check is separate and unchanged.

    Added

    • --retries N runs failing flows again in the same run, round by round, up to N more times, with the driver and the on-device agent kept up between rounds; works with --parallel. Each earlier attempt is kept as flows/<id>.attempt-<n>.json with its assets; report.json fills attempts and attemptHistory; the console says "passed on attempt 2" or "failed all 3 attempts". JUnit gets an attempts property and Surefire's <flakyFailure> / <rerunFailure>, which CI report viewers show as flaky, and the HTML report badges the flow.
    • Maestro's test command line runs unchanged. Options may follow the flow paths, and global options may follow test. --format junit|html writes that report to `--outpu
  2. v1.1.282026-10-01 · 2026-10-01 09:34 UTC
    查看更新说明

    Release 1.1.28

    This release is about running a Maestro suite and getting Maestro's answer. Most of it came from running real Maestro suites (DuckDuckGo Android and iOS, React Native's RNTester, React Navigation) side by side with Maestro and fixing each place the two disagreed. The largest of those is selector matching: text and id selectors now match the whole value, as Maestro does, which is a behaviour change — see below. DeviceLab becomes the default driver on both platforms: on iOS it runs a new agent, on simulators and on real iPhones. Every flow gets DEVICE_UDID, -e stops splitting values at commas, and the Appium driver closes a long list of gaps with the native drivers.

    ⚠️ Behaviour changes

    DeviceLab is the default driver on Android and on iOS. Running without --driver now uses the on-device DeviceLab agent on both platforms: on Android instead of UIAutomator2, on iOS instead of WebDriverAgent, on simulators and real iPhones (a real iPhone still needs --team-id, as WDA did). Nothing changes for a run that names its driver. To keep the previous defaults, pass --driver uiautomator2 on Android or --driver wda on iOS; both drivers are unchanged. See Changed below for the numbers behind the switch.

    Text and id selectors match the whole value, ignoring case, as in Maestro. A plain text selector used to match any element whose text, content description or hint contained it, so tapOn: Open could land on a "Talk · Open" row and assertVisible: English passed on "Language: English". Ids matched as substrings too: id: login found com.app:id/login_button. Maestro compiles every text and id selector as a case-insensitive regex that must match the whole value (an id may match the whole resource-id or the part after the package prefix), falls back to the literal text when the pattern does not compile, and also accepts a value equal to the pattern. That rule now applies on DeviceLab, UIAutomator2 and Appium on Android, on --driver devicelab and --driver devicelab-legacy on iOS, and on Appium for iOS — both in the queries sent to the device and in the page-source matchers. A flow that relied on substring matching must say so with a regex. The WDA driver keeps its existing predicate matching: it prefers an element whose whole text matches and falls back to a substring match when there is none (#188, #161).

    - tapOn: "Open"               # only an element whose whole text is "Open" (any case)
    - tapOn: ".*Open.*"           # any element whose text contains "Open"
    - tapOn: { id: "login" }      # "login" or "com.app:id/login", not "login_button"
    - tapOn: { id: ".*login.*" }  # the old substring behaviour, spelled out
    

    Regex selectors ignore case again. 1.1.26 made regex text selectors case-sensitive on the premise that Maestro does; it does not — every text and id selector is compiled with IGNORE_CASE, and suites depend on it ((let's get started!|Let's do it!) against "Let's get started!"). Regexes now ignore case. The problem 1.1.26 fixed is kept solved as a preference: when several elements match, those matching in the pattern's own case come first, so ^SIGN OUT$ still taps the "SIGN OUT" button ahead of a "Sign out" row (#151).

    An index: past the number of matches finds no element. It used to fall back to the first match, so assertNotVisible: {id: row, index: 1} ("there is no second row") failed with one row on screen, and a tap with an out-of-range index hit the first row. Now it names no element, on every mobile driver (#188).

    -e keeps commas in values. -e REPLY="Thanks, going with Vue" reached the flow as REPLY=Thanks. Each -e is now one variable, as in Maestro, so -e A=1,B=2 sets A to 1,B=2;

  3. v1.1.272026-09-11 · 2026-09-11 10:46 UTC
    查看更新说明

    Mostly about flows that already work on Maestro working the same way here.

    1.1.26 made selectors stricter, and one of those fixes woke up a parsing bug that had been dormant for months: any inputText that named an id: stopped finding its field, and because that step usually lives in a shared login sub-flow, whole suites went red on their first command. That is fixed. So are three places where a Maestro flow was read differently here without a word — speed: on scrolls, the value: spelling of airplane and dark mode, and an element-relative point: on swipes.

    Two things are new: addMedia takes documents, so a flow can seed a PDF and pick it in the system file picker; and a password passed through a variable no longer ends up in the report.

    No behaviour changes this release — nothing that passes on 1.1.26 should start failing.

    Added

    • addMedia accepts documents — PDF, Word, Excel, PowerPoint, txt, csv, rtf, json and zip. Android places them in Download/, where the system file picker lists them immediately; the iOS simulator places them in "On My iPhone", which every app's document picker browses. A physical iPhone refuses with an explanation — Apple exposes no path into Files storage (#167, thanks @georgetarazi-swipejobs).
      - addMedia:
          - assets/contract.pdf
      - tapOn: "Attach a file"
      - tapOn: "contract.pdf"
      
    • Text selectors match a field's validation error on Android — a Compose field whose only visible content is its error message can be found by that text, as in Maestro 2.9 (UIAutomator2 and Appium drivers).

    Fixed

    • inputText with a selector failed on every native driver since 1.1.26 — the value to type was silently reused as a text predicate on the target field (#166, diagnosed to the line by @laiskajoonas).
    • A password passed through a variable was written into the report — steps inside a runFlow were described after expansion, so ${PASSWORD} appeared in report.html, the flow JSON and the console as the password itself. The per-request client.log carried it too; text-entry request bodies are now redacted on every driver.
    • speed: on scroll and scrollUntilVisible was ignored by every driver — now honoured with Maestro's own formula. An absent speed: keeps the existing default so no current flow scrolls a different distance, and very high speeds are floored at a 50ms swipe, because Android reads a 1ms swipe as a tap (#165, thanks @ismailhaiberhub).
    • Tapping a file in the system file picker did nothing on the DeviceLab driver — the picker ignores injected taps; those now go through input tap (#87, thanks @LandonPatmore).
    • Maestro's value: spelling of setAirplaneMode and setDarkMode was read as disabled.
    • scrollUntilVisible on Appium accepted a container-clipped sliver as fully visible — the 1.1.26 fix now covers the third Android driver (#164).
    • setOrientation returned before the display had turned, so the next step could read a half-rotated screen.
    • An element-relative point: on swipe.from was ignored by the Appium and WDA drivers.
    • stopRecording could return an unplayable file — it now waits for screenrecord to finish writing.
    • WDA on a physical iPhone pins the destination architecture, avoiding the arm64/arm64e ambiguity that can stall startup; MAESTRO_WDA_DEST_ARCH overrides it.

    Changed

    • Formatting is gated in CI.
    • The Go toolchain is pinned and the standard-library CVEs are cleared.

    Validation

    Android on a Pixel 4a, against the released binary: auth 4/4 on each of uiautomator2, devicelab and appium, plus the release's own checks on both native drivers — inputText with an id:, speed: 1 and 100, orientation, a PDF seeded with addMedia and picked in the system file picker, and a login run whose output directory was searched for the password (not found). Browser 50/64, unchanged from baseline on all five suites. iOS simulator (iPhone 16 Pro, iOS 18.6): auth 9/9

  4. v1.1.262026-09-02 · 2026-09-02 16:39 UTC
    查看更新说明

    Mostly about taps and selectors landing where the flow says they should.

    A tap used to be injected first and validated afterwards, so a "rejected" tap had already hit whatever sat under the fold — usually a bottom tab, which navigated away and left every later step running on the wrong screen. scrollUntilVisible would stop on a 9-pixel sliver of a button because the hierarchy handed us a rect it had already clipped. Two selector semantics move closer to Maestro's, and both change what an existing flow matches.

    Reaching the runner got easier too: it is on npm, it compiles on Windows, and it reads flow files with Windows line endings.

    ⚠️ Two behaviour changes

    Both make selectors stricter. A flow that relied on the looser behaviour will start failing — and that is the point, because the looser behaviour was matching things the flow never asked for.

    Regex selectors are case-sensitive. Every regex was previously compiled with (?i), so an anchored pattern could not distinguish what it was written to distinguish: ^SIGN OUT$ matched a "Sign out" row as readily as the "SIGN OUT" button, and page-source order decided which one was tapped. Plain text is not a regex and stays case-insensitive (#151).

    - tapOn: "^SIGN OUT$"          # now matches only the uppercase button
    - tapOn: "(?i)^sign out$"      # opt back in to insensitivity
    - tapOn: "sign out"            # plain text — unchanged
    

    A selector naming both id: and text: must match one element carrying both. The two were emitted as separate queries, so the finder returned on whichever hit first — an AND written by the author behaving as an OR (#157, #158).

    Added

    • npm — npx maestro-runner test flows/, or pin it beside a React Native or Expo project. No postinstall and nothing downloaded at install time, so it works offline, behind a proxy, and in CI that blocks postinstall network access.
    • --window-size <WxH> — set the browser viewport, so one suite covers phone, tablet and desktop breakpoints.
    • --new-command-timeout <seconds> — sets appium:newCommandTimeout; matters on cloud --parallel runs where early sessions are reaped while waiting (#124, thanks @JSap0914 and @devrchoi).
    • Windows builds — the runner compiles for Windows (#159).

    Fixed

    • A tap was injected before it was validated — the off-screen guard ran on a tap that had already landed. It now runs on the device, on the bounds about to be clicked, and declines to click. Five more coordinate paths that never reached that guard validate too, as does the lazy-retry path, which had no check at all (#162).
    • A tap is refused when a window above the target covers the point — a keyboard, a system or permission dialog — and the error names it.
    • Chained UiSelector predicates were first-match-wins, so .resourceId(…).textContains(…) matched on text alone (#160).
    • scrollUntilVisible accepted a container-clipped sliver as fully visible (#164).
    • "Driver not installed" was reported for any adb failure, sending people to reinstall something already there (#163).
    • A CSS selector tapped the first DOM match even when hidden, then timed out with context deadline exceeded.
    • setPermissions was missing on most drivers; iOS now honours location's own vocabulary, and an unusable value is reported instead of silently resetting the permission (#147).
    • A nil-pointer panic in the WebView manager killed the whole run (#149).
    • A step's label: is used in the HTML report instead of being overridden by its selector (#150).
    • Flow files with Windows line endings failed to parse (#159).
    • checked: reads the checked state on the native Android drivers too (#153, #154).
    • Directional relative selectors pick the nearest element, not the deepest (#16).
    • ${VAR} is expanded in every step's env: block.

    Changed

    • Flutter widget trees are no longer re-serialised on every poll (#152).
    • Binaries are built with
  5. v1.1.252026-08-25 · 2026-08-25 05:14 UTC
    查看更新说明

    Release 1.1.25

    This release is about the first five minutes and the oldest complaints: a doctor that says exactly what your machine is missing, a devices listing, a screenshot command, runShell for the one adb call every suite eventually needs, and an inputText that checks what actually landed in the field. It is also about gestures that behave like a user's finger: a real dragAndDrop on every driver — press, hold until the item lifts, move slowly, settle, release — and a scrollUntilVisible that stops when the element is actually visible instead of one pixel in. Flutter apps go from barely drivable to ahead of WDA on the DeviceLab iOS driver, --step-delay slows any flow down for demos and animation-heavy apps, and JUnit reports carry test-tracking properties and failure artifacts into CI.

    Added

    • doctor — checks the toolchain and says what to do about each gap, with --json for CI. A missing platform is a warning rather than an error, so it gates a Linux runner without failing it for having no Xcode; only a broken install or a team ID Xcode does not have exits non-zero. Three checks earn their place: Command-Line-Tools-instead-of-full-Xcode, which otherwise surfaces far from its cause; an AVD naming a system image that is not installed, which nothing reports at run time and which shows up only as a driver polling for a device that will never boot; and --team-id checked against the signing certificates in the login keychain, which is the entire content of error: No Account for Team "..." at WDA build time.
      ✗ Signing identity for team abcd123456 — no certificate belongs to team abcd123456;
        this Mac has: A3RCAA2YAX
          Pass one of the team IDs listed above as --team-id, or add that team's account in Xcode.
      
    • devices — Android devices and emulators, iOS simulators and connected phones in one listing, with --json and --all for the shut-down simulators. A phone that answers usbmux but refuses the lockdown handshake is listed as not-ready with the reason, instead of looking identical to a working one.
    • screenshot — capture the current screen with the same device and driver flags a run takes. Writes a file, or - for stdout.
    • runShell — run a host command from a flow, for the adb, simctl or xcrun call every suite eventually needs.
      - runShell:
          command: adb -s $MAESTRO_DEVICE_ID shell getprop ro.build.version.sdk
          output: SDK
      
      output: binds the command's trimmed output to a flow variable for later steps. The environment carries the flow's variables, the step's own env:, and MAESTRO_DEVICE_ID / MAESTRO_PLATFORM / MAESTRO_APP_ID — the device id being the one that matters, since a bare adb shell fails outright when --parallel has two devices attached. Bounded at 30s by default (timeout: to change it), output capped at 64KB keeping the tail, and a non-zero exit fails the step unless it is optional: true.
    • scrollUntilVisible can scroll inside a container — from: names the scrollable element, for a screen whose scrolling part is an inner list or a horizontal carousel rather than the screen itself. The gesture is centred in the container and inset at each end, so a swipe starting on its edge is not claimed by the parent list instead. UIAutomator2 for now; the other drivers refuse from: by name rather than silently scrolling the screen.
      - scrollUntilVisible:
          element:
            id: "product-item-Playwright"
          from:
            id: "products-list"
      
    • --video never|always|on-failure — keep screen recordings only for the runs worth watching. --record still means always. A recording cannot be started retroactively, so on-failure records every flow and discards the passing ones at the end. The vocabulary matches --artifacts rather than inventing a second spelling for the same idea.
    • lint --json — {checked, failed, results[]} on stdout, for editors, CI and anything

用户点评

登录用户的真实使用记录,可按版本和硬件筛选
共 0 条

    还没有人点评。第一个说出真实体验的人最有价值。

    README 原文

    未翻译

    maestro-runner


    Fast UI test automation for Android, iOS, Web, React Native, Flutter & Expo Open-source Maestro alternative — single binary, no JVM. 100% free, no features behind a paywall. Supports real iOS devices, simulators, emulators, desktop browsers, and cloud providers.

    ~5x faster vs Maestro CLI 14x less memory

    npm installs/mo GitHub downloads CI runs

    license by

    npm CI codecov last release platforms docs

    Documentation | Get Started | CLI Reference | Flow Commands | Contributing


    • Runs Maestro YAML flows on real devices, emulators, simulators, and desktop browsers
    • Supports Android (UIAutomator2), iOS (WebDriverAgent), Web (Chrome CDP), and cloud (Appium)
    • Built-in parallel execution, HTML/JUnit/Allure reports, and JavaScript scripting
    • Addresses 78% of the top 100 most-discussed open issues on Maestro's GitHub

    AI context: llms.txt — structured summary for AI assistants answering questions about this project.

    Install

    Shell — macOS and Linux:

    curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash
    
    # A specific version
    curl -fsSL https://open.devicelab.dev/install/maestro-runner | bash -s -- --version 1.1.28
    

    npm — for React Native, Expo, or any project that already has a package.json:

    npx maestro-runner test flows/          # no install step
    npm install --save-dev maestro-runner   # or pin it in the project
    

    Pinning it means every machine and every CI job runs the same version with no separate bootstrap. There is no postinstall and nothing is downloaded at install time: the binary for your platform arrives as an ordinary optional dependency npm selects by os and cpu, so installs work offline, behind a proxy, and in CI that blocks postinstall network access.

    Both give you the same binary. macOS and Linux, arm64 and x64; on Windows, use WSL.

    First run

    maestro-runner doctor     # check the toolchain and hear exactly what is missing
    maestro-runner devices    # what this machine can drive right now
    

    Run Tests

    maestro-runner test flow.yaml                                           # Android (default)
    maestro-runner --platform ios test flow.yaml                            # iOS
    maestro-runner --platform web test flow.yaml                            # Desktop browser (Chrome)
    maestro-runner --app-file app.apk test flows/                           # Install app and run
    maestro-runner --driver appium --appium-url <server-url> test flow.yaml # Appium
    maestro-runner test --parallel 3 flows/                                 # Parallel on 3 devices
    

    AI coding agents

    maestro-runner skills install    # writes ~/.claude/skills/maestro-runner/SKILL.md
    

    Installs a skill that tells Claude Code (and other agents that read SKILL.md) how to write Maestro flows, run them with maestro-runner, inspect the screen and read the reports. --dir writes it elsewhere (.claude/skills/maestro-runner in a repository), --print prints it. The file carries the binary's version and is replaced when you install from a newer release.

    Key Features

    • Zero migration — Runs your existing Maestro YAML flows as-is, no changes needed
    • Real iOS device testing — Supports physical iOS devices, not just simulators Guide →
    • Cloud testing — BrowserStack, Sauce Labs, LambdaTest, TestingBot via Appium driver Guide →
    • Desktop browser testing — Run Maestro flows on Chrome/Chromium via CDP. Supports css, xpath, id, and text selectors with --platform web Guide →
    • React Native & Flutter — Smart element finding for RN testIDs and Flutter semantics Guide →
    • DeviceLab driver — The default driver on Android and iOS. On Android an on-device agent over WebSocket, ~2x faster than UIAutomator2 and ~5x faster than Maestro CLI; on iOS a prebuilt XCUITest agent, on simulators and real iPhones
    • Parallel execution — Dynamic work distribution across devices, not static sharding. Faster devices pick up more tests automatically, so no device sits idle
    • App install built-in — --app-file app.apk installs the app before testing, so you always test the right build
    • Wide OS compatibility — Android 5.0+ (API 21+) and iOS 15.0+, no version restrictions
    • Reports — HTML, JUnit XML, and Allure-compatible reports out of the box
    • Clear error messages — element not found: text="Login" instead of io.grpc.StatusRuntimeException: UNKNOWN
    • Pre-flight validation — Catches flow errors, circular dependencies, and missing files before execution starts
    • Fast element finding — Native selectors, clickable parent traversal, regex matching, smarter visibility
    • Reliable text input — Direct ADB input with Unicode support, no dropped characters
    • scrollUntilVisible — Native scroll implementation that reliably finds off-screen elements
    • Relative selectors — Find elements by position: below, above, leftOf, rightOf, childOf
    • JavaScript scripting — Embedded JS runtime with HTTP client for dynamic test logic, no external dependencies
    • Configurable timeouts — Per-command and per-flow timeouts, --find-timeout 3000 to make a wrong selector fail in 3 s instead of 17 s, --wait-for-idle-timeout 0 to disable
    • Lightweight — Single binary, no JVM required

    Supported Platforms & Drivers

    DriverPlatformDescription
    DeviceLabAndroidDefault since v1.1.28. On-device WebSocket driver, ~2x faster than UIAutomator2.
    UIAutomator2Android--driver uiautomator2. Direct connection to the device, no external server needed.
    DeviceLabiOSDefault since v1.1.28. The DeviceLab iOS agent, on simulators and real iPhones (real iPhones need --team-id). --driver devicelab-legacy keeps the previous runner.
    WDA (WebDriverAgent)iOS--driver wda. Supports simulators and physical devices. --reuse-wda leaves WebDriverAgent running between runs and reuses it.
    Browser (CDP)Web--platform web. Chrome/Chromium automation via Chrome DevTools Protocol.
    AppiumAndroid & iOS--driver appium. For cloud testing providers and existing Appium infrastructure.

    DeviceLab Driver (Android)

    The DeviceLab driver is the default Android driver (since v1.1.28). It runs automation directly on the device via WebSocket, skipping the UIAutomator2 HTTP layer, for ~2x faster test execution than UIAutomator2 — and ~5x faster than Maestro CLI. --driver uiautomator2 selects the previous default.

    Benchmark: 9 flows, 163 steps on Pixel 4a (Android 13)
    
      DeviceLab:     1m 12s
      UIAutomator2:  2m 24s
      Maestro CLI:   4m 22s
    
    maestro-runner --platform android test flows/          # DeviceLab
    maestro-runner --driver uiautomator2 test flows/        # UIAutomator2
    

    All existing Maestro YAML flows work as-is — no changes needed. The driver also includes bounds stabilization for animated elements and improved special character handling in text selectors.

    CI/CD Integration

    maestro-runner is built for CI/CD pipelines — single binary, no JVM startup, low memory footprint. Works with GitHub Actions, GitLab CI, Jenkins, CircleCI, and any CI system that supports Android emulators or iOS simulators.

    # CI example: auto-start emulator, run tests, shutdown after
    maestro-runner --auto-start-emulator --parallel 2 flows/
    

    Flow Config

    maestro-runner extends the standard Maestro flow YAML with additional fields:

    commandTimeout: 10000       # Default per-command timeout (ms)
    waitForIdleTimeout: 3000    # Device idle wait (ms), 0 to disable
    ---
    - launchApp: com.example.app
    - tapOn: "Login"
    - assertVisible: "Welcome"
    

    Visual Regression (assertScreenshot)

    assertScreenshot compares the current screen (optionally cropped with cropOn) against a reference PNG.

    • First run: if the reference file is missing, maestro-runner writes the captured screenshot as the new baseline and passes.
    • Re-baseline: overwrite existing baselines with --update-screenshots (or MAESTRO_UPDATE_SCREENSHOTS=true).
    • On mismatch, a {name}_diff.png overlay is written next to the reference.
    • Pixel comparison is device-, resolution-, and OS-specific — pin your device config and set thresholdPercentage deliberately (default 95).
    maestro-runner test flows/visual.yaml                  # seeds missing baselines
    maestro-runner test --update-screenshots flows/visual.yaml
    

    Requirements

    • Android testing: adb (Android SDK Platform-Tools)
    • iOS testing: Xcode command-line tools (xcrun)
    • Web/Browser testing: Chrome or Chromium
    • Cloud & Appium testing: Appium 2.x or 3.x — works with local Appium servers and cloud providers (BrowserStack, Sauce Labs, LambdaTest, TestingBot)

    Cloud Providers

    maestro-runner runs Maestro YAML flows on cloud device grids via the Appium driver. Pass the provider's hub URL and a capabilities JSON file:

    maestro-runner --driver appium --appium-url <HUB_URL> --caps caps.json test flows/
    
    • TestingBot — Setup guide for running on TestingBot's real device cloud
    • Sauce Labs — Setup guide for running on Sauce Labs Appium cloud

    Telemetry

    maestro-runner sends one anonymous usage event at the end of each test run: counts and versions only, such as runner version, platform, driver, device kind, flow and step counts, failure kinds and duration. It holds no machine or user id, no flow or file names, selectors, app ids, URLs, environment values or error messages, and nothing is written on your machine. Projects are told apart by a one-way hash of the list of flow files you run; the names themselves are never sent.

    Turn it off with MAESTRO_RUNNER_NO_TELEMETRY=1 or DO_NOT_TRACK=1. Set MAESTRO_RUNNER_TELEMETRY_DEBUG=1 to print the event instead of sending it. Telemetry lists every field.

    The update check is separate: MAESTRO_RUNNER_NO_UPDATE_CHECK=1 turns it off, and on CI the notice is not printed.

    Contributing

    See CONTRIBUTING.md for guidelines.

    License

    maestro-runner is licensed under the Apache License 2.0 — see LICENSE.

    DeviceLab driver binaries

    The prebuilt on-device agents behind --driver devicelab (the Android agent APKs and the iOS agent in drivers/ios/devicelab-ios-agent/) are free to use with maestro-runner, including commercially, but they are not open source. You may redistribute them unmodified as part of maestro-runner (forks, Docker images, CI caches); you may not modify them, ship them separately, or reverse-engineer them, except as permitted by applicable law. See drivers/LICENSE-BINARIES.md for the full terms. Everything else in this repository, including the other drivers, keeps its open-source license.