Writing flows
The flow file: goal, expectations, data, context and setup.
A flow is a JSON file. Three fields are required: name, app.bundleId and goal. Everything else sharpens what "worked" means or sets the app up first.
{
"name": "Shop: check out with a saved card",
"app": { "bundleId": "com.example.shop", "platform": "ios" },
"goal": "Add the first product to the cart, open the cart, and pay with the saved card",
"context": "The cart is the bag icon at the top right. Paying opens a confirmation sheet.",
"expect": "An order confirmation is shown with an order number",
"expectText": ["Thank you"],
"checkpoints": ["The cart shows one item"],
"data": { "promo": "SAVE10" },
"maxSteps": 15
}Fields
| Field | What it does |
|---|---|
name | Shown everywhere. A Prefix: rest name puts the flow in a project named by the prefix. |
app.bundleId | The app's bundle id (iOS) or package name (Android). |
app.platform | ios (default) or android. |
app.env, app.args | Environment variables and launch arguments for the app. |
app.url | A deep link opened right after launch, so the flow starts on the screen it is about. See deep links. |
goal | What to accomplish, in plain language. This is what the model reads at every step. |
context | Standing facts about the app, for every step: how a screen is laid out, which unlabelled control is which. The goal says what to do this time; context says what is always true. A project can set context for every flow in Settings. |
expect | A plain-language success condition, judged by the model on the final screen. |
expectText | Text that must be visible on the final screen. The most reliable check: use it when you can. |
seenText | Text that must have appeared at some point during the run, even if it is gone by the end. |
checkpoints | Intermediate conditions, checked as the run goes; a run that skips one fails. |
data | Named values the flow may type. Refer to them in the goal by name ("type the promo code"). |
tags | Labels such as ["smoke", "checkout"]. Suites select flows by them; testedok run --tag smoke runs one tag. |
maxSteps | Stops a lost run. Default 20. |
setup | State to establish before launch. See below. |
requires | Flows that must have passed first. See required flows. |
Placeholders
Anywhere in a flow's strings:
{{run}}: a short random tag, different each run, so data does not collide.{{ vars.name }}: a value from the selected environment.{{ secrets.name }}: a secret, typed into the app and seen by nothing else.{{ inbox.new }},{{ inbox.code }}: a fresh email address and the code that arrives at it. See inboxes.{{ account.role.user }},{{ account.role.password }}: a leased test account.
Expectations
A run passes when its expectations hold on the final screen. Prefer expectText: it is exact and costs no model call. expect is for outcomes that are hard to pin to a string ("a confirmation is shown"). Text you typed into a field does not count as seen: the check is for what the app shows back.
Setup
setup runs before the app launches, on the device, so a flow starts from a known state:
"setup": {
"clearData": true,
"permissions": { "grant": ["camera"], "revoke": ["location"] },
"appearance": "dark"
}| Key | Effect |
|---|---|
clearData | Wipes the app's data container and defaults: a fresh-install state. |
reinstall | Uninstalls and reinstalls from a cached copy; also resets permissions. |
permissions | grant, revoke or reset lists of permission names (camera, location, notifications, photos, …). |
appearance | light or dark. |
shell | Commands to run first ({adb} expands to the adb binary on Android). |
Routes and replay
A passing run saves a route under routes/. The next run of the same flow replays it step by step while each screen still matches what was recorded, and starts deciding again at the first difference. --no-replay ignores routes. Delete a route file to forget it.
Where flows live
Anywhere; tests/ is the convention. testedok run <folder> finds every *.json in it. Commit them: they are versioned with the app they test.