Maestro test reporting
A practical guide to reporting Maestro results: how its built-in JUnit reporter works, CI patterns for Android and iOS, the CI-specific issues that actually trip up Maestro flows, and how to add hosted, historical analysis — AI failure clustering, flaky-flow scoring, and per-launch risk — on top with Qualflare.
Worth saying up front: Qualflare doesn’t run your Maestro flows. It’s a results and observability layer, not a device-execution cloud — it ingests whatever JUnit-XML file your existing CI, local emulator run, or device cloud already produced.
How Maestro flows and JUnit reporting work
Maestro is an open-source, YAML-based UI testing framework for Android, iOS, and web apps, built by
mobile.dev. A “flow” is a plain YAML file — launchApp,
tapOn, inputText,
assertVisible — with no compiled test code, and the same
flow file can drive an Android build and an iOS build of the same app, since Maestro interacts with the
UI/accessibility tree rather than framework-specific code:
# .maestro/login.yaml — a Maestro flow: readable YAML, not code
appId: com.example.app
---
- launchApp
- tapOn: "Log In"
- inputText: "[email protected]"
- tapOn: "Password"
- inputText: "hunter2"
- tapOn: "Submit"
- assertVisible: "Welcome back" Maestro ships its own JUnit reporter out of the box — the same simplicity as Espresso, no conversion step. One flag writes a standard JUnit-XML report:
# Run every flow in a directory, write one JUnit-XML report
maestro test --format junit --output maestro-results.xml .maestro/
Per Maestro’s own docs,
“JUnit is the standard for CI/CD integration and for test reporting.” Point the command at a directory
and every flow inside it lands in the same report — one <testsuite> element,
one <testcase> per flow. HTML and
html-detailed formats exist too, with screenshots
of failed steps, but for CI pipelines and any downstream tool — Qualflare included — JUnit-XML is the
one that matters. Maestro is also actively developed: the project has
15,000+ GitHub stars and
ships releases roughly every one to four weeks, so the CLI surface is worth checking against its own
docs periodically rather than assuming a syntax learned a year ago still holds.
Why maestro test’s own report isn’t enough for CI-wide reporting
The JUnit-XML file maestro test writes is excellent
at describing one run — which flows passed, which failed, and why. Its limits show up the same
way any single-run report’s do: nothing connects today’s report.xml to
yesterday’s. Run the suite again tomorrow and you get a new file, usually written to the same path, so
it overwrites the last one unless you explicitly archive it as a CI artifact. There’s no history across
runs, no way to tell whether a specific flow has been getting flakier over the last two weeks or just
had a one-off environment hiccup, and — for teams running Espresso on Android and XCTest on iOS alongside
Maestro for cross-platform E2E, a common setup — no way to see all three as one release-health picture.
For anything beyond a single CI run, you need a place that collects results over time and analyzes them across runs and frameworks. That’s the gap the rest of this guide fills — first the CI plumbing, then the analysis layer.
CI patterns: GitHub Actions, GitLab CI, and Maestro Cloud
GitHub Actions. Run maestro test against
whatever emulator or simulator the job already has connected, write JUnit-XML, then upload — always,
even on failure, since a failing run is the one you most need reported:
# .github/workflows/mobile-e2e.yml
maestro-e2e:
runs-on: macos-14 # or ubuntu-latest with a connected Android emulator
steps:
- uses: actions/checkout@v4
- name: Run Maestro flows
run: maestro test --format junit --output maestro-results.xml .maestro/
- name: Upload results to Qualflare
if: always() # upload even when flows fail — that's the point
run: qf myapp collect maestro-results.xml --format maestro GitLab CI. GitLab renders JUnit XML natively in the pipeline’s Tests tab, so emit it the same way:
# .gitlab-ci.yml — JUnit output doubles as GitLab's native test report
maestro-e2e:
script:
- maestro test --format junit --output maestro-results.xml .maestro/
artifacts:
when: always
reports:
junit: maestro-results.xml Maestro Cloud. mobile.dev’s own paid product is a different
piece of the puzzle: it’s a place to run flows — parallel execution across managed devices —
not a place to analyze results over time. If you execute flows on Maestro Cloud instead of (or
alongside) self-hosted emulators, the reporting step doesn’t change: point qf collect at
whatever JUnit-XML file that run produced. Qualflare cares about the results file, not where the flow
executed — check Maestro Cloud’s own CI integration docs for exactly how it exposes that file as a
build output or artifact.
Common Maestro reporting problems (and fixes)
None of this is a niche complaint — mobile build instability is measurably rising. The Bitrise Mobile Insights Report 2025 found the share of teams experiencing any test flakiness grew from 10% to 26% between 2022 and 2025. The issues below are the Maestro-flavored version of that trend.
- Flow YAML syntax errors fail silently, not loudly. A typo
in a selector, a wrong key name, or a missing
---document separator often doesn’t throw a clear parse error — it just makes a step never match, so the flow times out ontapOnorassertVisibleinstead of failing with an obvious “invalid flow” message. Before assuming it’s an app bug, re-run the flow with--debug-output ./maestro-debug, which writes step-by-step screenshots and view-hierarchy dumps you can diff against what the flow expected. - Element-matching timing on slower CI emulators.
assertVisibleandtapOnpoll against Maestro’s default ~7-second assertion timeout, which is usually enough on a fast local simulator but not on a cold-booted, hardware-acceleration-less CI emulator. For a genuinely slow step — payment confirmations, cold app starts — reach for an explicitextendedWaitUntil: { visible: "...", timeout: 30000 }rather than inflating the whole suite’s patience. - Sharded runs need real connected devices, not just a flag.
--shard-alland--shard-splitdistribute flows across N connected devices — they don’t provision devices for you. If fewer emulators or simulators are actually booted than the shard count, the run stalls or fails waiting on devices that don’t exist. Confirm the device count matches the shard count before raising either in CI. - The results file location becomes a collision risk. Omit
--outputand Maestro writesreport.xmlto the current working directory by default. Running multiple flows aggregates them into that one file correctly — but two parallel CI jobs (an Android emulator job and an iOS simulator job, say) that both omit--outputwill both write to the same default path and clobber each other. Give every job its own explicit--outputfilename, as in the snippets above. - The upload step never runs on a failing build. The same
ordering mistake as any framework: if the upload step doesn’t run with
if: always()(or the equivalent “run even on failure” setting on your CI platform), the failing run — the one you most need visibility into — never reaches Qualflare at all.
Send Maestro results to Qualflare
It’s the same setup as every other framework — no per-framework configuration. First, have Maestro write JUnit-XML, which you’re probably already doing for GitLab or another CI test-report tab:
# Run every flow in a directory, write one JUnit-XML report
maestro test --format junit --output maestro-results.xml .maestro/
Then upload it with the Qualflare CLI, passing --format maestro so
the launch is labeled correctly:
# Upload the results — label the launch as Maestro explicitly
qf myapp collect maestro-results.xml --format maestro
Worth being precise about what that flag does: it routes through the exact same JUnit-XML-compatible
ingestion path every other JUnit-XML framework uses — Espresso, converted XCTest output, JUnit (Java),
and so on. There’s no bespoke Maestro-specific parsing logic behind it, because there’s nothing
Maestro-specific to parse: a Maestro JUnit-XML file has the same
<testsuite>/<testcase> shape
as everyone else’s. The flag — or a filename hint such as
maestro-results.xml, which the CLI docs note may
suffice on its own — exists to label the launch as Maestro in the dashboard, not to unlock different
parsing behavior. In CI it’s the one extra step shown in the snippets above. Authenticate the CLI once
with your Qualflare access token, stored as a CI secret — see the
CLI docs.
What you get on top of Maestro’s own output
- AI failure clustering. When a backend change breaks 15 Maestro flows across screens, Qualflare groups them by root cause so you triage a handful of clusters instead of 15 stack traces.
- Flaky-flow scoring from history. Maestro’s own JUnit-XML doesn’t carry a retry/flaky flag, so Qualflare scores each flow’s flakiness from its pass/fail pattern across CI runs on the same branch — the signal a single run’s output can’t give you.
- Framework-agnostic aggregation — the actual differentiator. Maestro Cloud is building its own flake detection scoped to Maestro flows specifically. Qualflare’s view spans your whole mobile stack: Espresso, XCTest, Maestro, and everything else land in the same dashboard, so an Android-only Espresso regression and a Maestro E2E flow failing on the same build show up side by side instead of in two unrelated tools.
- Per-launch risk. Every CI run becomes a “launch” with a risk rating, the failing areas, and recommended next steps — a ship / don’t-ship signal that arrives with the results.
- History & trends. Pass rate and flakiness over time across branches and platforms — the aggregation a per-run report file can’t do on its own.
- Incidental reach into React Native and Flutter results. Maestro drives the UI regardless of the underlying app framework, so if your team also uses it to test a React Native or Flutter app, those results land in the same dashboard too — not because Qualflare has native React Native or Flutter support, but because Maestro’s own cross-platform reach carries through the same JUnit-XML pipeline.
Maestro’s own JUnit output vs Qualflare
| Maestro’s own output | Qualflare | |
|---|---|---|
| History across CI runs | — | Yes |
| Aggregates Espresso + XCTest + Maestro in one dashboard | — | Yes |
| AI failure clustering (root cause) | — | Yes |
| Flaky-flow scoring over time | — | Yes |
| Screenshots of failed steps | Yes (HTML format) | Text/stack trace only |
| Local, zero-setup, offline | Yes | — |
They’re complementary: keep Maestro’s HTML report (or --debug-output artifacts)
for screenshots and local debugging, add Qualflare for hosted, historical CI observability across
Maestro, Espresso, and XCTest together.
Get AI analysis on your Maestro runs
Start free — write JUnit-XML, run qf collect, and get your first AI analysis in minutes.
Testing the native layers too? See Espresso test reporting for Android and XCTest test reporting for iOS, and how to get all three into one dashboard. Chasing intermittent flows? Read fixing flaky mobile tests or the mobile testing complete guide. Qualflare supports Maestro alongside 23+ other frameworks — see the full list, or browse all framework reporting guides. Weighing tools? See how it compares to other test management platforms.
Frequently asked questions
Does Maestro produce JUnit-XML natively, or do I need a converter?
Natively — no converter needed. Running maestro test --format junit --output <file> <flowFiles> writes a standard JUnit-XML report directly, the same as Espresso and unlike XCTest’s .xcresult bundles, which need a separate conversion step. Maestro’s own docs describe JUnit as “the standard for CI/CD integration and for test reporting.”
Does Qualflare run my Maestro flows on real devices or emulators?
No. Qualflare is a results-management and observability layer, not a device-execution cloud — it never provisions or touches an emulator, simulator, or real device. It only needs the JUnit-XML file a run already produced, so it works the same whether that run happened on a local emulator, a self-hosted CI runner, or Maestro Cloud.
Does this replace Maestro Cloud?
No — they solve different problems. Maestro Cloud is mobile.dev’s own execution platform: it runs your flows in parallel on managed devices. Qualflare is the results and analysis layer on top: it ingests whatever JUnit-XML a run produced — from Maestro Cloud or anywhere else — and adds history, AI failure clustering, and flaky scoring. Teams using Maestro Cloud for execution still want somewhere to analyze results over time and alongside their other frameworks, which is what Qualflare adds.
Can Qualflare aggregate Maestro results alongside Espresso and XCTest?
Yes — that’s the main reason mobile teams add it. Point the CLI at each framework’s results file (Maestro’s JUnit-XML, Espresso’s native XML, XCTest’s converted JUnit-XML) using the same project name, and they land in one dashboard: one pass rate, one flaky-test list, one set of defects across Android, iOS, and your cross-platform E2E layer, instead of three separate reports.
How do I send Maestro results to Qualflare?
Run maestro test --format junit --output maestro-results.xml <flowFiles>, then upload with the CLI: qf <project> collect maestro-results.xml --format maestro. The --format flag (or a filename hint like maestro-results.xml, per the CLI docs) labels the launch as Maestro; the underlying parsing is the same JUnit-XML path every other framework uses.
Does Qualflare detect flaky Maestro flows?
Yes, from run history. Maestro’s own JUnit-XML doesn’t carry a per-attempt retry/flaky flag the way some frameworks do, so Qualflare scores flakiness the way it does for any JUnit-XML-only framework: by watching a flow’s pass/fail pattern across launches on the same branch over time, rather than reading a flag out of a single file.
Setup reflects the Qualflare CLI (docs.qualflare.com) and Maestro’s CLI docs as of August 2026. Published 14 August 2026. Written by İbrahim Süren, founder of Qualflare.