Test real apps
the way real users
touch them.
No Appium. No Java server. No code changes. air-qa drives Android, iOS, and web UI straight through the OS accessibility layer, orchestrated by plain YAML flows.
It reads your app.
It doesn’t live inside it.
Appium and WebDriver-style tools sit inside your app — agents, bridges, a Java server, wired up before a single tap fires. air-qa reads the accessibility tree every Android and iOS device already broadcasts, and drives real taps, input, and scrolls straight against it.
No SDK to add. No test target to maintain. No instrumentation that drifts out of sync the next time your app ships.
Everything you need
to ship with confidence.
Zero code changes
Taps and inputs run against real native accessibility bounds. React Native testID maps through on both iOS and Android.
Native speed
No Java server to boot, no WebDriver handshake — the runner talks to the device directly.
YAML flows
Declarative, Maestro-style syntax — readable steps, not a page-object framework to maintain.
AI video → test
Hand it a screen recording. Gemini watches the video and writes back an executable YAML script.
AI severity grading
Every failure is graded CRITICAL → LOW with a one-line blast-radius reason.
Mobile + web, one flow
Complete a transaction on the app, switch into a real browser, confirm it landed on the portal — one run, one report.
Device matrix
Run the same suite across multiple devices and emulators in parallel, with auto-detection.
Live dashboard
A polling browser dashboard shows pending / running / passed / failed for every file, live.
Graceful stop
Ctrl+C mid-suite finishes the current step, closes the app and browser cleanly, and still reports what ran.
One YAML shape.
Two ways to run it.
appId: com.customerportal.dev
webBaseUrl: https://portal.customerbank.dev
webHeadless: true
---
# mobile side
- launchApp
- tapOn: "Send Money"
- tapOn: { testID: "amount-input" }
- inputText: "500"
- tapOn: "Confirm Transfer"
- assertVisible: "Transaction Successful"
- saveText:
from: { testID: "txn-reference" }
as: txnRef
# same run, real browser
- web:
- launchApp: "/login"
- tapOn: { testID: "portal-username" }
- inputText: "admin@customerbank.dev"
- tapOn: "Sign In"
- tapOn: "Transactions"
- assertVisible: "{{txnRef}}"
Runs the transfer on the real app.
Native accessibility taps, no mock.Captures the reference number into {{txnRef}}.
Switches into a real Chromium session.
Same run, same report — browser opens lazily.Confirms the same transaction on the portal.
One assertion closes the loop.platform: web
webBaseUrl: https://portal.customerbank.dev
webHeadless: true
---
- launchApp: "/authentication/login"
- tapOn: { selector: 'input[placeholder="User Number"]' }
- inputText: "${PORTAL_USER}"
- tapOn: { selector: 'input[placeholder="Password"]' }
- inputText: "${PORTAL_PASSWORD}"
- tapOn: { selector: "a.btn.btn-primary" }
- assertVisible: "Dashboard"
No mobile app at all.
platform: web skips straight to the browser — no web: wrapper needed.Navigates directly.
A relative path resolves againstwebBaseUrl.Raw CSS when there’s no testID.
{ selector: "..." } is the escape hatch for markup keyed on placeholders or classes.Same runner, same report.
Retries, screenshots, and the HTML report work exactly as they do on mobile.Two jobs. Not a chatbot.
Screen recording → runnable test
Record yourself using the app once. air-qa hands the video to Gemini, which watches it and writes back a complete YAML script — taps, text input, PIN pads, assertions — ready to run.
Every failure, triaged automatically
When a step fails, the full flow and the failing step are graded for severity — with a one-line reason — so a delivery team sees what needs attention first.
A report that tells you why.
One YAML. Three surfaces.
Android
uiautomator UI-tree dumps over adb. Animations disabled automatically so taps land on the frame you expect.
adb · uiautomatoriOS
Drives the simulator through idb — the same accessibility bridge Xcode’s own tooling relies on.
idb-companionWeb
For the portal riding alongside your app, or a standalone platform: web file — headless or headed Chromium via Playwright.
Set once, at the top of the file.
appId
The Android package / iOS bundle id to launch and close. Omit it entirely for a pure platform: web file.
platform
android · ios · web. Defaults to the --platform flag (itself defaulting to android) — a file’s own value always wins.
webBaseUrl / webHeadless
Base URL and headless flag for a web: block, or a pure platform: web file.
closeApp: false
Leaves the app open after this file finishes, instead of the default auto-close between files.
expectFail: true
Marks a negative test case — a failing run reports PASS (XFAIL); an unexpected pass reports FAIL (XPASS).
continueOnFailure: true
Don’t halt the suite if this file’s steps fail — the runner moves on to the next one.
The full command reference.
Nineteen step types cover everything from a single tap to a full mobile-to-browser handoff.
launchAppLaunches the app — or, inside web:, navigates (a relative path resolves against webBaseUrl).tapOn: "text"Matches visible text, testID, id, or content-desc.tapOn: { text, index }Disambiguates when more than one element matches.tapOnPoint: "50%, 50%"Or absolute coordinates, e.g. "500, 1000".inputText: "text"Types into the focused native field — falls back to tapping an on-screen keyboard key by key if none is focused.inputText: { text, onScreen }Forces on-screen key-by-key typing, e.g. for a custom PIN pad.eraseText: nOr clearText: true to clear the whole field.hideKeyboardtapOn/assertVisible already do this automatically — use it to dismiss at another point, e.g. before a scroll.assertVisible: "text"Fails the step if the element isn’t on screen.extendedWaitUntil: { visible, timeout }Waits up to timeoutms for something to eventually appear.wait: msA fixed pause.scroll: { direction, times, toEnd }Scrolls a fixed amount, or all the way to the end.scrollUntilVisible: { element, direction }Scrolls until a target element appears (Android only).runFlow: "path.yaml"Imports a sub-flow inline — also takes { file, continueOnFailure }.waitForUser: "message"Or pause. Pauses and types what you enter into the active field.if: { visible|notVisible } then: [...] else: [...]Branches on current state — a one-shot check, not a wait.randomNumber: { min, max, as }Saves a random integer as a runtime {{var}}.saveText: { from, as }Captures an element’s visible text as a runtime {{var}}, reusable in any later step.web: [...]Runs a nested step list against a browser instead of the device — see “Pick your surface” above.Two steps. You’re testing.
Install
git clone git@github.com:ayush0710/air-qa.git
cd air-qa && npm install && npm link
cp .env.example .envPrivate repo — invite-only, you’ll need collaborator access. Testing a web portal too? Also run npx playwright install chromium once.
Run
air-qa tests/checkout-flow.yaml --platform androidAdd --devices a,b for parallel devices, or swap in --platform web --dashboard for a live browser view.