Snapshot
idb, uiautomator, or the DOM. The live accessibility tree, every step.
Semantic e2e for iOS, Android, and web
Type into Email. Tap Sign in. See the home screen. Convoy reads the live screen, matches the phrase, and acts. The file never contains a selector, a test ID, or a wait.
Why
A restyle, a renamed test ID, a new wrapper — and the suite is red for a reason that has nothing to do with the product. Convoy never looks those up. It matches what a person can see.
# locators, IDs, sleeps click('[data-testid="sign-in"]') sleep(2000)
# convoy-e2e - tap: Sign in - see: the home screen
How it works
Convoy does not look up a test ID. It snapshots the screen, keeps the labelled controls, and scores the phrase against what is actually there.
idb, uiautomator, or the DOM. The live accessibility tree, every step.
Role, name, bounds. Unlabelled chrome is dropped before matching.
Presence first, then target. Which control is this phrase talking about?
Pass, ambiguous, or not found. Silence is not a tap.
Retry until the next screen settles. Default window: 20 seconds.
Authoring
YAML handles flows and branches with which. TypeScript is
for loops, platform splits, and extra setup. If two controls could
fit, tighten the phrase from convoy inspect.
name: user has logged in platforms: [ios] tags: [smoke] steps: - type: ${EMAIL} into: Email - type: ${PASSWORD} into: Password - tap: Sign in - which: enable notifications: - tap: Not now the home screen: [] - see: the home screen
import { e2e } from "convoy-e2e"; e2e.serial("user has logged in", { platforms: ["ios"] }, (step) => { step("signs in", async (t) => { await t.type(process.env.EMAIL, { into: "Email" }); await t.type(process.env.PASSWORD, { into: "Password" }); await t.tap("Sign in"); const screen = await t.which([ "enable notifications", "the home screen", ]); if (screen === "enable notifications") { await t.tap("Not now"); } await t.see("the home screen"); }); });
Drive there, or run until the suite fails on that step.
npx convoy inspect when two controls could fit. Tighten the phrase.
npx convoy capture --name login-screen writes a fixture and a PNG.
Ambiguous → rephrase. Not found → the control is gone. Traces: .convoy/runs/.
Gates
Scores, on-screen labels, next action. Not a stack trace about a missing CSS selector.
The control matched. Proceed.
Two or more controls fit. Tighten the phrase.
Missing, unlabelled, or still loading. Often a product bug.
The window elapsed. The screen never arrived.
Platforms
npx convoy doctor prints whatever this machine is missing.
Simulator, .app, idb. Convoy boots, installs, launches.
adb and a device that is already running. It will not start an AVD.
Playwright Chromium and a base URL. Optionally spawn the app server.
Recorded JSON. No device, no key. Offline authoring and unit tests.
Start
# your application repository
npm install -D convoy-e2e
npx convoy init --platform ios --yes
npx convoy doctor
npx convoy run
TYPESAFE_API_KEY and app secrets live in .env.
Commit convoy.config.json. Never a UDID or a password in JSON.
Matching stays host-side via Jev. The API key never enters an iOS, Android, or web binary. No key → local heuristic, fine for fixtures, not for a real app.
Reference:
README.
Agents: agent/ skills.
.env, sample test. Never overwrites an existing .env..convoy/captures/.*.e2e.yaml / *.e2e.ts, or a file, or a folder.--platform --tag --headed --headless --shard 1/4 --junit