e2e replaces Maestro’s YAML flows with TypeScript tests. Each command becomes an awaited call, usually on a locator, or one agent goal covers several. Both runners can use the same simulator or emulator while you move one flow at a time.
Migrate this project's Maestro flows to e2e, one flow at a time. Docs: https://e2e.tester.army/docs/migrate/maestro.md and https://e2e.tester.army/docs/mobile.md
- Read the migration guide above. It maps every Maestro config option and command to e2e and marks what e2e does not have yet. The mobile guide covers devices and app builds.
- e2e tests are TypeScript. If the project has no
package.json, as in a native iOS or Android project, create the test project in ane2e/folder beside the app. - If the test project has no e2e config (
e2e.config.ts,.mts,.js, or.mjs), runnpx e2e init --yes --engine mobile(or the pnpm or bun equivalent) there. It writes a mobile config with one target for this machine's platform and an example test against the Settings app, and installs nothing yet. Rewritetests/example.e2e.tsfor your app or delete it. - Read the e2e skill (
npx e2e guideprints it,npx e2e guide <topic>prints a topic) and follow it for every step below. - If you ran init, ask me which model to use for agent steps: a ChatGPT, GitHub Copilot, OpenCode Console, or SuperGrok subscription, an API key, or a local model. Configure it with its provider package. If it needs a sign-in, give me the
npx e2e logincommand and wait until I confirm. - Port the flows'
appIdand.maestro/config.yamlinto the e2e config: one target per platform withmobile({ platform }),app: { bundleId }on each target, and tags as{ tags }on tests. Move passwords and tokens fromenvintocredentialsorsecrets. - Install dependencies, then run
npx agent-device doctorand fix what it reports. - List the flows and ask me which one to move first. Suggest one whose
optionalsteps andrunFlow: { when }branches often need updating. - Rewrite that flow as a
.e2e.tstest undertests/(e2e runstests/**/*.e2e.tsby default), with one awaited call per command. Start it withapp.open(), since nothing launches on its own. Follow the guide's "Rewrite selectors" section: e2e matches the whole string and fails on several matches. Where the guide says "Not yet" or "No equivalent", use the workaround it gives, or leave that flow in Maestro and tell me why. - Keep the launch and the final assertions deterministic, with
app.open()and locator assertions. Where a run of scripted taps breaks often, propose anagent.actgoal for it instead. - Build and install the app, run the new file with
npx e2e run <file>on each platform target, and fix it until it passes. Keep the Maestro flow until I confirm it can go. - Summarize which flows moved, which stayed in Maestro, and why.
For a migration that spans several sessions, the migrate-to-e2e skill records what moved in a progress file and keeps the old suite running until you retire each test.
Port the config
The reference pages hold the details: screen, expect, test, and the mobile engine.
Rewrite one flow
Start with a flow whose optional steps and runFlow: { when } branches
often need updating.
Map each command
Calls on device run only on a device, so a test that uses it declares
requires: ['device'] and other targets skip it. Calls on screen, app,
agent, and expect(locator) run on every target. “Not yet” marks a
command e2e doesn’t have today, and “No equivalent” one it leaves out on
purpose.
App lifecycle
App lifecycle
Taps and input
Taps and input
Gestures
Gestures
Assertions and waits
Assertions and waits
Device
Device
AI commands and flow control
AI commands and flow control
Rewrite selectors
Four differences from Maestro’s selectors cause most edits.
- Maestro reads text and id as case-insensitive regular expressions, so
tapOn: ".*Save.*"matches any text with Save in it, and"Mute Account"matchesMute account.getByTextandgetByTestIdmatch the whole string, case included. WritegetByText(/^mute account$/i)orgetByTestId(/^row-/)for a pattern, or{ exact: false }for a case-insensitive substring of text. - Maestro taps the first of several matches. e2e fails with
LOCATOR_AMBIGUOUS, so write.first()when the first match is what you mean. isVisible(), the check that replacesoptionalandrunFlow: { when }, isfalseon zero matches andLOCATOR_AMBIGUOUSon several. Add{ visible: true }to the query or.first()when a hidden twin may exist.below,above,leftOf,rightOf, andcontainsDescendantshave no port. e2e hasfilter({ has, hasText })and chaining for structure, and nothing spatial. UsegetByLabel, a test id, or a goal:agent.act('enter {email} in the email field', { params })finds the field.
What differs on purpose
- Tests get no agent-device client. Every call goes through
screen,app,device, or the agent, so e2e records and redacts it. - A device has no
test.setupor{ session }, see what differs from the web.