# Worked example — authoring over MCP

> For the complete documentation index, see [llms.txt](/llms.txt)

This is the orchestration an agent runs to author a test end-to-end — the literal
tool-call trace, plus the exact input schema for the recording tools.

## The loop

```
explore_start  →  goto  →  get_page_snapshot  →  explore_step×N (with refs)
               →  save_exploration_as_test  →  run_test  →  (step / resume / inspect_runtime on pause)
```

## Trace: record a sign-in

```jsonc
// 1. Start a recording session.
explore_start({
  scenario_name: "auth/login",
  title: "Sign in",
  description: "Demo user signs in"
})
// → { explorationId, availableVariables, availableFlows }
// If availableFlows already has "signin", DON'T re-record it — call explore_run_flow.

// 2. Navigate. goto needs no element ref.
explore_step({
  explorationId, action: "goto", url: "/login",
  section: "Sign in", description: "Open the login page"
})

// 3. Snapshot to get element ref handles ([ref=eN]).
get_page_snapshot()
// → outline containing [ref=e3] email input, [ref=e5] password input, [ref=e8] submit button

// 4. Record actions, addressing elements by ref.
explore_step({
  explorationId, action: "fill",
  locator: { kind: "locator", steps: [{ kind: "ref", ref: "e3" }] },
  value: "demo@example.com",
  section: "Sign in", description: "Type the email"
})
explore_step({
  explorationId, action: "fill",
  locator: { kind: "locator", steps: [{ kind: "ref", ref: "e5" }] },
  value: "hunter2",
  section: "Sign in", description: "Type the password"
})
explore_step({
  explorationId, action: "click",
  locator: { kind: "locator", steps: [{ kind: "ref", ref: "e8" }] },
  section: "Sign in", description: "Submit the form"
})

// 5. Save to disk. flow:-marked steps are extracted into _helpers/<flow>.js.
save_exploration_as_test({ explorationId, scenarioName: "auth/login" })
// → writes unotest/e2e/auth/login.js

// 6. Verify it runs green.
run_test({ scenario: "auth/login" })
// → { runtimeId }. If it pauses (breakpoint/failure): inspect_runtime → patch → resume.
```

:::note[Assertions aren't recorded actions]
The recordable actions are interactions and waits — not `assert*`. Add assertions
(`assertVisible`, `assertText`, …) to the generated `.js`, or use `wait_for_text`
as an in-flight checkpoint while recording. Credentials should be
[variables](/concepts/variables/), not literals like above.
:::

## explore_step / explore_record — input schema

`explore_step` runs one action. With an `explorationId` it **records** the step;
without one it executes **ad-hoc** (no recording). `explore_record` is the same
envelope but `section` + `description` are **required** and locators **must** be
ref-form.

| Field | Type | Notes |
| --- | --- | --- |
| `action` | string | **required** — one of the actions below |
| `explorationId` | string | present → record · absent → ad-hoc run |
| `locator` | ref locator | element to act on (see below) |
| `url` | string | for `goto` |
| `pattern` | string | for `wait_for_url` (substring match) |
| `value` | string \| string[] | for `fill`, `select_option` |
| `key` | string | for `press` (e.g. `"Enter"`) |
| `text` | string | for `wait_for_text` |
| `options` | object | action options (e.g. `{ force: true }`, nav options) |
| `section` | string | group label · **required when recording** |
| `description` | string | step intent · **required when recording** |
| `flow` | string | mark step as part of a reusable `flow_<name>` |
| `allowNoRef` | boolean | allow a non-ref locator (last resort) |

### Actions and their fields

| Action | Fields |
| --- | --- |
| `goto` | `url` |
| `reload`, `go_back`, `go_forward` | — |
| `click`, `double_click`, `hover`, `check`, `uncheck`, `scroll_into_view`, `wait_for` | `locator` |
| `fill` | `locator`, `value` (string) |
| `press` | `locator`, `key` |
| `select_option` | `locator`, `value` (string \| string[]) |
| `wait_for_text` | `text` |
| `wait_for_url` | `pattern` |
| `enter_frame` | `locator` |
| `exit_frame`, `set_page` | tab/frame switch — see the live tool description |

## Ref locators

While recording, address elements by the `[ref=eN]` handles from
`get_page_snapshot` (or `get_aria_snapshot`):

```json
{ "kind": "locator", "steps": [{ "kind": "ref", "ref": "e8" }] }
```

Recording **rejects non-ref locators** (so saved tests get stable
`getByRole`/`getByTestId` selectors, not brittle ones). Pass `allowNoRef: true`
only as a last resort for a hand-written locator. On save, refs are resolved to
the stable selector form per the [selector priority](/concepts/selectors/).

## After saving

The generated `unotest/e2e/auth/login.js` is plain `.js` with `step("…")`
blocks. Add assertions, run `run_test`, then `npx @unotest/web lint`. On failure,
read the [failure bundle](/concepts/failure-bundles/) and propose a **diff** — the
human approves it ([self-healing is never silent](/concepts/debugging/)).
