Integrate your Maestro test suite with App Percy
Run App Percy visual tests on your Maestro mobile flows using the @percy/maestro-app SDK on BrowserStack or a self-hosted runtime.
Use the @percy/maestro-app SDK to add App Percy visual testing to your Maestro flows. You add a screenshot step to a flow, then run that flow with App Percy, which captures the screen and uploads it to your App Percy project.
The SDK is the same on both runtimes. The difference is where the tests run and who manages the infrastructure. Choose the tab that matches where you run your Maestro tests:
- BrowserStack SDK: Tests run on BrowserStack infrastructure. Your Maestro flow runs on a BrowserStack App Automate session, and BrowserStack provides the device, manages the runtime, and injects the device metadata for you.
-
Self-hosted: Tests run on your own infrastructure, such as a local machine, a CI runner, or your own device lab. You run
maestro test, wrap the run withpercy app:exec, and supply the device metadata yourself.
With the BrowserStack SDK, your Maestro flow runs on a BrowserStack App Automate session. To set it up, you install the SDK, add a screenshot step to your flow, and run a build on App Automate with your App Percy token.
Prerequisites
Before you begin, ensure you have the following:
- A Percy account with an App Percy project.
- Node.js and npm installed.
- A Maestro flow that runs on BrowserStack App Automate Maestro.
- Your BrowserStack username and access key.
Install the SDK
Install the SDK and the Percy CLI as development dependencies:
The SDK ships a percy/ directory that contains the flow and script files your Maestro flow references at runtime. Reference this directory in either of these ways before you zip your test suite for upload to BrowserStack:
- Reference under
node_modules: Keep the SDK innode_modulesand include thenode_modules/@percy/maestro-app/percydirectory when you zip your test suite. - Copy into your workspace (recommended): Copy the SDK’s
percy/directory into your workspace to produce a smaller zip.
To copy the directory into your workspace, run the following command:
Your workspace then has the following layout:
Capture a screenshot
To capture a screenshot at any point in your flow, add a runFlow step that references the SDK’s percy-screenshot.yaml and sets the SCREENSHOT_NAME environment variable:
The first screenshot step initializes App Percy, so you don’t need to configure it separately.
SCREENSHOT_NAME must match the pattern ^[a-zA-Z0-9_-]+$. Names with spaces, dots, or slashes are rejected with an HTTP 400 response.
Run your tests
Run your flow through the Percy CLI:
App Percy starts, creates a build, captures each screenshot in the flow, and uploads them to your project.
Configuration
Set the following environment variables to control how App Percy captures each screenshot. On BrowserStack Maestro sessions, the device variables are injected for you, so you only set the ones you want to override.
| Variable | Required | Default | Purpose |
|---|---|---|---|
SCREENSHOT_NAME |
Yes | N/A | Name for the screenshot. Must match ^[a-zA-Z0-9_-]+$. |
PERCY_DEVICE_NAME |
No | Injected on BrowserStack | Device identifier, such as Pixel 7. BrowserStack sets this from the session device. |
PERCY_OS_VERSION |
No | Injected on BrowserStack | OS version, such as 13 on Android or 17 on iOS. BrowserStack sets this from the session device. |
PERCY_ORIENTATION |
No | portrait |
Screen orientation, either portrait or landscape. |
PERCY_REGIONS |
No | N/A | JSON array of region masks. |
PERCY_SYNC |
No | false |
Set to true to wait for comparison results. |
PERCY_FULLSCREEN |
No | false |
Set to true when the screen has no system chrome. |
PERCY_STATUS_BAR_HEIGHT |
No | Android 120, iOS 100
|
Height in image pixels to mask at the top of the screen. |
PERCY_NAV_BAR_HEIGHT |
No | Android 100, iOS 80
|
Height in image pixels to mask at the bottom of the screen. |
Status bar and navigation bar masking
App Percy masks the device status bar and navigation bar so that the clock, battery, and other dynamic system elements don’t trigger false visual differences. The defaults differ by platform:
| Variable | Android default | iOS default |
|---|---|---|
PERCY_STATUS_BAR_HEIGHT |
120 |
100 |
PERCY_NAV_BAR_HEIGHT |
100 |
80 |
Device pixel densities vary, so the default heights don’t fit every device. Override PERCY_STATUS_BAR_HEIGHT and PERCY_NAV_BAR_HEIGHT per device when the mask doesn’t cover the full bar. For example, newer iPhone models have a taller status bar area at the top of the screen, so they need a taller status bar mask than the iOS default.
Regions
Use the PERCY_REGIONS environment variable to mark areas of a screenshot for a specific comparison algorithm. Set it to a JSON array, where each object defines a region and the algorithm to apply.
The following algorithms are available:
-
ignore: Excludes the region from comparison. Any change inside the region is ignored. -
standard: Compares the region with the standard sensitivity. -
intelliignore: Uses AI to ignore dynamic content such as advertisements and carousels. -
layout: Checks structural layout with a pixel-level tolerance.
Coordinate-based regions
Define a region by its pixel boundaries:
Element-based regions
Define a region by a UI element. The supported locators differ by platform.
On Android, use resource-id, text, content-desc, or class:
On iOS, use id, the accessibility identifier, or class:
Run on BrowserStack
When you trigger a Maestro build through the BrowserStack App Automate API, pass your App Percy token in the appPercy object of the build payload. This field is the same on both Android and iOS.
Use the appPercy field to pass your Percy token. The percyOptions field is silently dropped, so screenshots won’t reach your project if you use it.
The following request triggers an Android build:
The following request triggers an iOS build and passes an extra Percy variable through the env sub-object:
Pass any PERCY_* variable through the appPercy.env sub-object.
Unsupported features
The following features aren’t supported in version 1.0.0 of the SDK:
- Full-page or scrollable capture.
- Animation freezing and Percy-specific CSS.
- XPath region selectors on Android.
- Automatic device-metadata detection on iOS.
The following features are planned for a later release:
- The
PERCY_IGNORE_ERRORSandPERCY_ENABLEDswitches. - The
textandxpathelement selectors on iOS. - Rendering of
PERCY_LABELSon the dashboard.
On the self-hosted runtime, you run maestro test and wrap it with percy app:exec. This command starts the Percy CLI on your machine, creates the App Percy build, runs your test, and finalizes the build when the test exits. App Percy captures full-screen and region-masked screenshots on Android or iOS, on real devices and on simulators or emulators, and uploads them to your project.
Prerequisites
Before you begin, ensure you have the following:
- A Percy account with an App Percy project, and its project token. The token starts with
app_. - Node.js and npm installed. Node.js 14 or higher.
- The Percy CLI, version
1.32.3-beta.3or later. Self-hosted Maestro support, including screenshot directory auto-resolution, isn’t in the stable@percy/cliyet, so install this version explicitly.@percy/cli@betaalso resolves to it. -
Maestro 2.0 or higher, installed and on your
PATH. - A connected Android or iOS device, emulator, or simulator that
maestro testcan drive. - For Android element regions,
adbon yourPATH.
Install and copy the SDK files
Install the SDK and the Percy CLI as development dependencies, then copy the SDK’s percy/ directory into your workspace:
The copy step keeps your runFlow paths short. Alternatively, reference the SDK’s percy/ directory under node_modules. Your workspace then has the following layout:
Capture a screenshot
To capture a screenshot at any point in your flow, add a runFlow step that references the SDK’s percy-screenshot.yaml and sets the SCREENSHOT_NAME environment variable:
SCREENSHOT_NAME is the only required input per snapshot. The first screenshot step initializes App Percy, so you don’t need to configure it separately.
SCREENSHOT_NAME must match the pattern ^[a-zA-Z0-9_-]+$. Names with spaces, dots, or slashes are rejected with an HTTP 400 response.
Run your tests
Set your App Percy token as an environment variable:
export PERCY_TOKEN=<your-percy-token>
$Env:PERCY_TOKEN="<your-percy-token>"
set PERCY_TOKEN=<your-percy-token>
Run your flow through percy app:exec and pass the device metadata as Maestro -e flags:
On the self-hosted runtime, you supply the device tags yourself, unlike on BrowserStack sessions where they’re injected for you. Because they can’t be derived from an arbitrary device, PERCY_DEVICE_NAME and PERCY_OS_VERSION are required.
The command resolves the screenshot output directory for you, points both Maestro and the Percy CLI at the same path, and creates it if needed. The following table describes how it resolves the directory:
| Your setup | What the command does |
|---|---|
| You set nothing | Uses ./.percy-out and creates it. If that path isn’t writable, the command falls back to a temporary directory and logs a warning. |
You export PERCY_MAESTRO_SCREENSHOT_DIR
|
Uses your value and injects a matching --test-output-dir. |
You pass --test-output-dir to maestro test
|
Uses your flag and mirrors it to PERCY_MAESTRO_SCREENSHOT_DIR for the CLI. |
| You set both | Uses your paths and changes nothing. |
Your explicit value always takes precedence. You only need to set the directory to pin a specific location, for example to keep artifacts or give each device its own directory when running on multiple devices.
Where to set each environment variable
Maestro and the Percy CLI read environment variables from different places:
- Device tags, such as
PERCY_DEVICE_NAME,PERCY_OS_VERSION, and the per-snapshot variables, are read by the SDK inside the Maestro flow. Pass them as Maestro-eflags. - Device addressing for element-region resolution, such as
PERCY_IOS_DRIVER_HOST_PORT,PERCY_IOS_DEVICE_UDID, andANDROID_SERIAL, is read by the Percy CLI. Export these in the shell beforepercy app:exec. A Maestro-eflag only reaches Maestro, not the CLI.
Configuration
Set the following environment variables to control how App Percy captures each screenshot:
| Variable | Required | Where you set it | Purpose |
|---|---|---|---|
PERCY_TOKEN |
Yes | Shell export | Your App Percy project token. Starts with app_. Source it from your CI secret store and never commit it. |
PERCY_DEVICE_NAME |
Yes | Maestro -e flag |
Device name for the Percy tag, such as Pixel 7 or iPhone 15 Pro. |
PERCY_OS_VERSION |
Yes | Maestro -e flag |
OS version, such as 13 on Android or 17 on iOS. |
SCREENSHOT_NAME |
Yes, per snapshot | Per-runFlow env |
Screenshot name. Use only letters, digits, hyphens, and underscores (^[a-zA-Z0-9_-]+$). Names with spaces or other characters, such as Home Screen or login@1, aren’t supported. Use Home_Screen or login-1 instead. |
PERCY_MAESTRO_SCREENSHOT_DIR |
No | Shell export | Directory the Percy CLI reads screenshots from. Auto-resolved to ./.percy-out. Set it only to pin a specific location. |
PERCY_SERVER |
No | Maestro -e flag |
Percy CLI address. If snapshots log not reachable, set -e PERCY_SERVER=http://localhost:5338 explicitly. |
PERCY_ORIENTATION |
No | Flow env
|
Screen orientation, either portrait or landscape. Defaults to portrait. |
PERCY_REGIONS |
No | Per-runFlow env |
JSON array of region masks. |
PERCY_TEST_CASE |
No | Per-runFlow env |
Logical test name shown alongside the snapshot. |
PERCY_LABELS |
No | Per-runFlow env |
Comma-separated labels for dashboard filtering. |
Device addressing
Export the following variables in the shell. They apply only to element regions:
| Variable | Platform | When you need it |
|---|---|---|
PERCY_IOS_DRIVER_HOST_PORT |
iOS | Real devices, sharded runs, or Maestro versions that don’t bind 7001. Match your --driver-host-port. |
PERCY_IOS_DEVICE_UDID |
iOS | Optional. A single connected device auto-selects. |
ANDROID_SERIAL |
Android | When more than one Android device or emulator is connected. |
Status bar and navigation bar masking
App Percy masks the device status bar and navigation bar so that the clock, battery, and other dynamic system elements don’t trigger false visual differences. The defaults differ by platform:
| Variable | Android default | iOS default |
|---|---|---|
PERCY_STATUS_BAR_HEIGHT |
120 |
100 |
PERCY_NAV_BAR_HEIGHT |
100 |
80 |
Device pixel densities vary, so the default heights don’t fit every device. Override PERCY_STATUS_BAR_HEIGHT and PERCY_NAV_BAR_HEIGHT per device when the mask doesn’t cover the full bar.
Regions
Use the PERCY_REGIONS environment variable to mask areas of a screenshot so that dynamic content, such as timestamps, animations, and A/B-tested layouts, doesn’t trigger false visual differences. Set it to a JSON array, where each object defines a region and the algorithm to apply.
The following algorithms are available:
-
ignore: Excludes the region from comparison. Any change inside the region is ignored. -
standard: Compares the region with the standard sensitivity. -
intelliignore: Uses AI to ignore dynamic content such as advertisements and carousels. -
layout: Checks structural layout with a pixel-level tolerance.
Coordinate-based regions
Define a region by its pixel boundaries. Coordinates are native PNG pixels, where 0,0 is the top-left corner:
The SDK auto-masks the device status bar and the bottom navigation bar, so you usually only need manual coordinate regions for in-content masking.
Element-based regions
Define a region by a UI element. The Percy CLI resolves the element’s bounding box at snapshot time by running maestro hierarchy. The supported locators differ by platform.
On Android, use resource-id, text, content-desc, class, or id:
On iOS, use only the id selector:
Each platform resolves element regions differently on the self-hosted runtime:
- On Android, element regions work when
adbis on yourPATHand exactly one device is connected, or whenANDROID_SERIALis exported. The CLI runsmaestro hierarchyto resolve selectors. - On iOS, the CLI auto-discovers the Maestro driver port on current Maestro versions, up to 2.4.0. For real iOS devices, sharded runs, or Maestro versions on an ephemeral port, export
PERCY_IOS_DRIVER_HOST_PORTto match your--driver-host-portin the shell beforepercy app:exec.
If no driver is reachable, App Percy drops the element regions with a warning and still uploads the snapshot with any coordinate regions intact. If a selector matches nothing in the live hierarchy, App Percy skips that one region and uploads the snapshot.
Run on multiple devices
Each device is one percy app:exec invocation, on its own port and its own output directory. Because the default output directory is per working directory, concurrent runs in the same directory collide. Give each invocation its own --test-output-dir and matching PERCY_MAESTRO_SCREENSHOT_DIR. To merge concurrent sessions into a single App Percy build, set a shared parallel nonce:
Each invocation needs its own --port, its own --test-output-dir, and its own device. Omit the nonce to ship each session as a separate build.
Troubleshooting
The following table lists common issues and their fixes:
| Symptom | Likely cause | Fix |
|---|---|---|
| A snapshot logs that the Percy CLI is not reachable | The SDK didn’t pick up the Percy CLI address from the environment. | Add -e PERCY_SERVER=http://localhost:5338 to maestro test. |
HTTP 400 Invalid screenshot name
|
SCREENSHOT_NAME has a space or punctuation. |
Use letters, numbers, _, and - only, such as Home_screen. |
| A warning that screenshots fell back to a temporary directory |
./.percy-out isn’t writable. |
Export PERCY_MAESTRO_SCREENSHOT_DIR to a writable path. |
HTTP 400 Missing required env: PERCY_MAESTRO_SCREENSHOT_DIR
|
Your @percy/cli predates self-hosted support. |
Install @percy/cli@1.32.3-beta.3 or later. |
HTTP 404 Screenshot not found
|
A custom --test-output-dir doesn’t match PERCY_MAESTRO_SCREENSHOT_DIR. |
Set both to the same path, or set neither and let the command resolve it. |
The Percy build finalizes with Snapshot command was not called
|
The flow never called runFlow: percy/flows/percy-screenshot.yaml. |
Confirm your flow invokes the Percy sub-flow. |
iOS element regions skipped with no Maestro driver found
|
You’re running a real device or a sharded setup. | Export PERCY_IOS_DRIVER_HOST_PORT to match your --driver-host-port. |
Android element regions skipped with an adb warning |
adb isn’t on the PATH, or zero or multiple devices are connected. |
Install Android Platform Tools. Connect one device or export ANDROID_SERIAL. |
An element region logs not found - skipping
|
The selector doesn’t match the live hierarchy. | Run maestro hierarchy to verify. iOS supports only id. Android supports resource-id, text, content-desc, class, and id. |
Related topics
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
We're sorry to hear that. Please share your feedback so we can do better
Contact our Support team for immediate help while we work on improving our docs.
We're continuously improving our docs. We'd love to know what you liked
Thank you for your valuable feedback!