# YAML test flow format

A flow is **two YAML documents** in one file: a config map, a `---` separator, then an ordered
list of steps. Validate any flow without a device using the `validate_flow` tool.

## Config (document 1)

| Field   | Required | Meaning                                    |
| ------- | -------- | ------------------------------------------ |
| `appId` | yes      | package to launch                          |
| `name`  | no       | human label                                |
| `tags`  | no       | list of labels                             |
| `env`   | no       | variables, referenced in steps as `${VAR}` |

## Steps (document 2)

Each step is a one-key map `command: args` (or a bare command name for `back`/`scroll`/`launchApp`).

| Command              | Args                                    | Example                                                               |
| -------------------- | --------------------------------------- | --------------------------------------------------------------------- |
| `launchApp`          | `{ clearState?, permissions?, appId? }` | `- launchApp: { clearState: true }`                                   |
| `tapOn`              | selector                                | `- tapOn: { id: login_button }`                                       |
| `inputText`          | `{ into?: selector, text }`             | `- inputText: { into: { id: email }, text: "${USER}" }`               |
| `assertVisible`      | selector                                | `- assertVisible: "Welcome"`                                          |
| `assertNotVisible`   | selector                                | `- assertNotVisible: { id: spinner }`                                 |
| `scroll`             | `{ direction? }`                        | `- scroll: { direction: DOWN }`                                       |
| `scrollUntilVisible` | `{ element, direction?, timeoutMs? }`   | `- scrollUntilVisible: { element: { text: Terms }, direction: DOWN }` |
| `pressKey`           | key name                                | `- pressKey: ENTER`                                                   |
| `takeScreenshot`     | label                                   | `- takeScreenshot: home`                                              |
| `runFlow`            | `{ file, env? }` or `{ commands }`      | `- runFlow: { file: subflows/logout.yaml }`                           |
| `repeat`             | `{ times, commands }`                   | `- repeat: { times: 3, commands: [...] }`                             |
| `back`               | —                                       | `- back`                                                              |
| `hideKeyboard`       | —                                       | `- hideKeyboard`                                                      |
| `switchContext`      | context string                          | `- switchContext: "WEBVIEW_com.your.app"`                             |

## Selectors

`{ id, text, css, index, enabled, checked, focused, selected, optional }`. A bare string is shorthand
for `{ text }`. `id`/`text` are regex full-match. Prefer `id` — a `text`/`index`-only selector is
flagged as fragile by `validate_flow`. For Compose, expose ids via `testTagsAsResourceId` (see
`docs/compose-testability.md`).

`css` is a **WebView-only** selector (a CSS selector like `#email` or `[data-testid="go"]`) — it is
ignored on the native path and only used inside a WEBVIEW context. `id`/`text`/`css` all interpolate
`${VAR}`.

`optional: true` on a selector makes the step **conditional**: if the element is absent (concluded
after a short lookup — ~2s for `tapOn`/`inputText`/`assertVisible`; `scrollUntilVisible` uses its own
scroll timeout), the step is **skipped** and the flow continues instead of failing. Use it to dismiss
a dialog, permission prompt, or promo that may not always appear.

## WebView contexts (`switchContext`)

By default a flow runs against the **native** UI (`NATIVE_APP`). `switchContext: "WEBVIEW_<package>"`
hops into that app's debuggable WebView; from there `tapOn`/`inputText`/`assertVisible`/
`assertNotVisible` address elements by their `css` selector and are JS-driven over the DevTools
protocol (no self-heal on this path). `switchContext: NATIVE_APP` (or the `NATIVE` alias) switches
back and closes the WebView session; the runner also closes it automatically when the flow ends.

For an app with several WebView pages (e.g. a chat widget plus the main app), append `@<url-match>` to
select one by URL: `switchContext: "WEBVIEW_com.your.app@yourdomain\.com"` (the match is a regex; a domain
substring works). The whole context string interpolates `${VAR}`. The `observe_webview` tool takes the same
selector as a `match` parameter.

In a WEBVIEW context, `inputText` **requires** an `into` selector, and a selector needs `css` or
`text` (resource-`id`s don't exist in the DOM) — `validate_flow` warns otherwise. See
[webview-testing.md](webview-testing.md) for the prerequisites (a debuggable WebView) and how to
discover selectors.

## Variables

Reference config `env` values as `${VAR}` in any string. A `${VAR}` not defined in `env:` is a
validation warning (it may still be supplied at runtime in M3b). Variable references are resolved in step arguments only; config fields (appId, name, tags) are not interpolated.

## Validation

`validate_flow` checks the schema, lints fragile selectors and undefined variables, and resolves
`runFlow` subflow files (erroring on a missing file or an include cycle). It also **warns** when a `text`/`id` selector contains an unescaped regex metacharacter (e.g. a literal `?` or `*`) — escape it (`\\?`) or use `.*`. See `authoring-tests.md`. It runs entirely
offline; running a flow on a device is the M3b runner.
