Semantic e2e for iOS, Android, and web

Write the test the way you’d tell a person.

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.

Try an intent — the demo matches it on the live screen
phasedump
intent—
score—
gate—
9:41LTE
Sign in
field · Email
Email
field · Password
Password
button · Sign in
Sign in
dropped Back
live inspect 0 0

Why

Selectors rot. This file does not contain any.

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.

Coupled to the implementation
# locators, IDs, sleeps
click('[data-testid="sign-in"]')
sleep(2000)
Coupled to the screen
# convoy-e2e
- tap: Sign in
- see: the home screen
  • Same file, three platforms. Phrase the step against the screen. iOS, Android, and web share the runtime.
  • Ambiguous is a fail, not a guess. Two controls fit → rephrase. A wrong control is never tapped quietly.
  • Coding agents can write this. Skills ship with the package. Agents describe what a human sees — they do not invent locators.

How it works

Intent in. Control out.

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.

01 dump

Snapshot

idb, uiautomator, or the DOM. The live accessibility tree, every step.

02 normalize

Keep labels

Role, name, bounds. Unlabelled chrome is dropped before matching.

03 match

Score

Presence first, then target. Which control is this phrase talking about?

04 gate

Refuse a guess

Pass, ambiguous, or not found. Silence is not a tap.

05 act

Tap / type / see

Retry until the next screen settles. Default window: 20 seconds.

Authoring

Two dialects. One runtime.

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
  1. Reach the screen

    Drive there, or run until the suite fails on that step.

  2. Inspect if needed

    npx convoy inspect when two controls could fit. Tighten the phrase.

  3. Capture

    npx convoy capture --name login-screen writes a fixture and a PNG.

  4. Re-run

    Ambiguous → rephrase. Not found → the control is gone. Traces: .convoy/runs/.

Gates

A miss is a diagnosis.

Scores, on-screen labels, next action. Not a stack trace about a missing CSS selector.

pass

The control matched. Proceed.

ambiguous

Two or more controls fit. Tighten the phrase.

not found

Missing, unlabelled, or still loading. Often a product bug.

timeout

The window elapsed. The screen never arrived.

Platforms

One runner. Four ways in.

npx convoy doctor prints whatever this machine is missing.

iOS

Simulator, .app, idb. Convoy boots, installs, launches.

Android

adb and a device that is already running. It will not start an AVD.

Web

Playwright Chromium and a base URL. Optionally spawn the app server.

Fixture

Recorded JSON. No device, no key. Offline authoring and unit tests.

Start

Install it in the app repo. Not this one.

terminal
# 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.

init
Config, .env, sample test. Never overwrites an existing .env.
doctor
Node, tools, resolved platform. Missing idb/adb is a warning, not a hard fail.
inspect
Labelled element table for the current screen.
capture
Fixture JSON + PNG under .convoy/captures/.
run
Every *.e2e.yaml / *.e2e.ts, or a file, or a folder.
flags
--platform --tag --headed --headless --shard 1/4 --junit