---
name: agentic-browser-testing
description: >-
Goal-driven E2E testing where a browser agent (Playwright MCP / computer-use) reads a
natural-language goal and explores the app via the accessibility tree to assert outcomes —
no pre-written script. Covers when intent-driven beats scripted, making agent runs
deterministic (pinned model, temperature 0, seeded data, bounded steps, explicit success
assertion, snapshot-not-pixel), cost/latency control, the accessibility-tree-first
interaction model, CI gating, and graduating a stable run into a scripted Playwright test.
Use when: "agentic browser test," "goal-driven browser test," "let an agent explore the app,"
"natural-language E2E," "browser agent smoke test," "Playwright MCP test."
Not for: Writing/maintaining deterministic scripted Playwright tests — that is
playwright-automation. Testing your product's OWN LLM features — that is ai-system-testing.
Related: playwright-automation, ai-system-testing, exploratory-testing, test-reliability, qa-project-context.
license: MIT
metadata:
author: kindlmann
version: "1.0"
category: ai-qa
---
A scripted Playwright test breaks the moment a button moves or a class renames; writing one
for a dashboard that changes weekly is a maintenance treadmill. This skill stands up a
goal-driven browser agent instead: it reads a natural-language goal, explores the app via the
accessibility tree (Playwright MCP `browser_snapshot`), and asserts the outcome against an
explicit oracle. The failure mode it prevents is the one that makes teams distrust agents — an
agent that reports "success" while stuck on the login page because nothing forced it to prove
where it landed. You leave with a deterministic, CI-gated agent run and a graduation path to a
durable scripted test once the flow stabilizes.
## Quick Route
| Situation | Go to |
|-----------|-------|
| Stand up a goal-driven run from scratch | Discovery + `references/setup.md` |
| Decide agentic vs scripted for a given flow | Fit: Intent-Driven vs Scripted |
| Agent passes one run, fails the next | Determinism |
| "How does it click without screenshots?" | Interaction Model |
| Runs are slow / burning tokens | Cost and Latency |
| Agent reports false success | Success Assertion (the Oracle) |
| Flow is stable — make it permanent | Graduation → `references/graduation-and-ci.md` |
| Block a merge on the goal | CI Gating → `references/graduation-and-ci.md` |
| Canvas / no accessibility tree | Canvas Fallback → `references/graduation-and-ci.md` |
## Discovery Questions
First, check `.agents/qa-project-context.md` in the project root and skip anything it already
answers (stack, environments, seed/reset tooling, model access).
1. **Which flow, and how often does its UI change?** Fast-changing/experimental UI favors
intent-driven; a stable critical path (login) favors scripted. This decides the whole approach.
2. **Is there a seeded fixture and a way to reset state?** Determinism is impossible without
seeded data and a per-run reset. If neither exists, that is step zero.
3. **Can you deep-link past auth to a seeded entry point?** Re-driving login every run is the
biggest avoidable cost; a seeded entry URL scopes the goal and cuts steps.
4. **What is the unambiguous success oracle?** Specific account text, a `/dashboard` URL, an
order number — plus a forbidden state. "No error" is not an oracle.
5. **Does the target render to canvas / WebGL?** No accessibility tree means snapshot-first
won't work; plan the vision fallback or instrument the canvas with ARIA.
6. **Which model and budget?** Pin a model id and a step budget up front; tier cheap steps to
Haiku 4.5 / Sonnet 4.6 and reserve Opus 4.8 for genuinely ambiguous flows.
---
## Core Principles
1. **Intent, not instructions — but only where churn earns it.** The agent reads a goal and
finds its own path through the accessibility tree, so it survives a moved button or renamed
class that would break a selector. That resilience costs 2-5x the time and money of a
scripted run, so spend it on fast-changing UI and hard-to-locate flows, not on stable
critical paths.
2. **An agent run is untrustworthy until it is deterministic.** Same goal, same seeded app
must produce the same verdict. That requires temperature 0, a pinned model id, seeded data
with a reset, a bounded step budget, and an explicit pass/fail assertion. Without these you
have a coin flip, not a test.
3. **The oracle lives outside the agent.** Never let the LLM self-grade "looks good." Success
is a checkable assertion against the final `browser_snapshot` — specific expected text, a
URL, AND a forbidden-state negative check — evaluated by your harness, not the model.
4. **Accessibility tree first, pixels last.** `browser_snapshot` returns roles, refs, and
accessible names (~200-400 tokens) and is deterministic and cheap. Screenshots, pixel
coordinates, vision, and OCR are a scoped last resort for canvas only, never the default.
5. **Graduation is the goal, not perpetual agent runs.** Once a flow is stable, promote the
run to a durable scripted `tests/*.spec.ts` with role-based locators. An agent that has
been green for two weeks should become a fast, free regression test — keep the agent for
exploration, not for guarding a settled path.
---
## Fit: Intent-Driven vs Scripted
The decision is per-flow, not per-project. Run `risk-based-testing` first if you need the
risk map; this table is the routing rule once you have it.
| Flow characteristic | Use | Why |
|---------------------|-----|-----|
| Stable, high-frequency critical path (login, payment) | **Scripted + pinned** (`playwright-automation`) | Runs every PR; must be fast, free, and deterministic. No upside to re-exploring it. |
| Fast-changing / experimental UI (a dashboard that churns weekly, a redesign in flight) | **Agentic / intent-driven** | Selectors would break constantly; a goal survives layout churn. |
| Hard-to-locate flow you can't reliably select | **Agentic** | The agent finds the control by role/name instead of you reverse-engineering a selector. |
| Exploratory smoke / "does the happy path still work at all" | **Agentic** | One NL goal covers a lot of ground without a maintained script. |
| Anything in CI that must never falsely pass | Scripted, OR agentic **with a hard oracle** | Non-determinism is a false-pass risk you must actively cap. |
**The rule, stated plainly:** keep stable critical paths scripted and pinned; point
intent-driven agents at fast-changing UI and exploratory smoke. Do not move everything to the
agent — it is slower, costlier, and non-deterministic, and not every test should be agentic.
---
## The Interaction Model (accessibility-tree-first)
Playwright MCP is **not** computer-use with screenshots and pixel coordinates. It is
accessibility-tree-first:
1. `browser_navigate` to the seeded entry URL.
2. `browser_snapshot` returns the **accessibility tree** — each interactive element as a
`role`, a stable `ref`, and its `accessible name` (from ARIA/labels). ~200-400 tokens.
3. The agent picks an element by `ref` and calls `browser_click` or `browser_type`.
4. `browser_wait_for` waits on text appearing/disappearing — never a fixed sleep.
5. Re-`browser_snapshot` after the DOM changes; assert against that tree.
Why not screenshots: the snapshot is **token-efficient** (thousands of tokens cheaper than an
image), **deterministic** (text refs, not fuzzy pixel matching), and needs no vision model or
OCR. Feeding screenshots as the primary input makes the run slower, pricier, and flakier.
`browser_take_screenshot` is for human evidence only, never as the assertion input.
See `references/setup.md` for the MCP registration, the full tool table, and the goal prompt.
---
## Determinism: making a run trustworthy in CI
A run that passes once and fails the next with no app change is not yet a test. The fix is
never "just retry" or bumping temperature for "smarter" exploration — that adds variance. Pin
the variables instead:
| Lever | Setting |
|-------|---------|
| Model | **Pinned model id** (e.g. `claude-haiku-4-5-20251001`), never `latest` |
| Sampling | **temperature 0** — no creative wandering in CI |
| Data | **Seeded fixture + reset/seed the database** before every run |
| Scope | **Bounded step budget** (`maxSteps`), e.g. 18 — exceeding it FAILS, never auto-retries |
| Oracle | **Explicit pass/fail verdict** asserted against the snapshot |
| Evidence | Assert on the **accessibility tree**, never a screenshot diff |
Avoid: `temperature: 0.7` or `1` for exploration, retry-until-pass loops,
`waitForTimeout` sleeps, and screenshot-based assertions. Each one hides flakiness rather than
removing it. Full harness config in `references/setup.md`.
---
## Success Assertion: the Oracle (where agents fail silently)
This is the sharpest failure mode: the agent reports success while stuck on the login page,
because "page loaded / no error / looks good" was accepted as success and the LLM was allowed
to self-grade. Force an explicit oracle the harness checks — never the agent.
For the goal *"sign in as an existing user and confirm the dashboard shows the right account name"*:
```text
SUCCESS (all must hold — assert against the final browser_snapshot):
- URL matches /dashboard
- Snapshot contains the specific expected account name text, e.g. "Acme Corp — Jane R."
NEGATIVE / forbidden state (fail fast if any is true):
- Still on a URL matching /login → FAIL
- Snapshot contains role="alert" with "invalid credentials" → FAIL
VERDICT: harness emits {"passed": true|false}; the LLM does not decide.
```
The positive checks (specific account name + `/dashboard` URL) prove where it landed; the
**negative check** (must NOT be on the login page) is what kills the false pass. "No error,"
"didn't crash," "screenshot looks correct," and "trust the agent" are not success criteria.
---
## Cost and Latency
Agent runs are 2-5x slower and pricier than scripted tests — a step is an LLM round-trip, the
dominant cost. Cut spend without losing coverage by going *smaller*, not *bigger*:
- **Step budget** — keep `maxSteps` low and enforced; fewer round-trips, less drift.
- **Model tiering** — Haiku 4.5 / Sonnet 4.6 for cheap navigation steps; reserve Opus 4.8 for
genuinely ambiguous exploration. Don't run the biggest model on every step.
- **Prompt caching** — cache the static system prompt, tool schemas, and goal; they repeat
every run.
- **Scope via a seeded entry point** — one narrow goal per run, deep-linked past login instead
of re-driving it each time.
- **Snapshot over screenshots** — the a11y snapshot is ~200-400 tokens; a full-page screenshot
is thousands. Default to snapshot.
Backwards moves to reject: "use a bigger model / Opus 4.8 for every step," "raise the step
limit," "screenshot every step," and running with no budget at all. See `references/setup.md`.
---
## Graduation and CI Gating
Promote a stabilized goal into a durable scripted test, and gate merges on the verdict. Both
are detailed in `references/graduation-and-ci.md`; the essentials:
- **Graduate** with **Playwright Test Agents** (planner / generator / healer, shipped in
Playwright **v1.56.0**). `npx playwright init-agents --loop=claude`. The **planner** writes a
Markdown test plan to `specs/.md`; the **generator** turns it into `tests/.spec.ts`
with **role-based locators** (`getByRole`, `getByLabel`, `getByText`) verified against the live
DOM; the **healer** repairs broken locators. This is the promotion path — not "keep running it
as an agent," not recorded clicks, not `page.locator('xpath=...')`, not data-testid-only.
- **Gate CI** so a failed goal exits **non-zero** and emits a **machine-readable** verdict
(`{"passed": true|false}` in `result.json`); the GitHub Actions job parses the boolean and
`exit 1`s on false. State is seeded/ephemeral and reset per run, with a step budget and a
timeout cap. Never `continue-on-error: true`, never "always exit 0," never a prose verdict a
human reads.
- **Canvas with no accessibility tree:** prefer instrumenting the canvas with ARIA; as a scoped
last resort enable `--caps=vision` to unlock `browser_mouse_click_xy` for that flow only.
`browser_snapshot` will not work on a raw canvas, but don't make coordinates the default and
don't abandon agentic testing.
---
## Migrating a brittle script to a goal (honest tradeoffs)
Converting an 80-line script that re-types login and walks 6 hardcoded steps into a single NL
**goal** with an **explicit success assertion** is a real win for a churning flow — but state
the downsides honestly:
- **Non-determinism / false-pass risk** — the run could pass falsely; that's why the hard
oracle and the negative check are non-negotiable.
- **Cost/latency** — 2-5x slower; bound it with a **step budget** and a seeded entry point.
- **Not every test should be agentic** — keep stable paths scripted, and plan to **graduate**
this one back to a scripted test once it stabilizes.
Reject the over-promise: it is **not** "strictly better with no downsides," do **not** "migrate
everything," and never drop the assertions to make it pass.
---
## Anti-Patterns
### 1. Reflexively writing a scripted Playwright test
"Browser test" pattern-matches to codegen, so the default is `page.goto` / `page.locator` /
`await expect(page...)` / hunting `data-testid`. That misses the entire point. A goal-driven
agent reads NL intent and explores via `browser_snapshot` — no pre-written selectors.
### 2. "Use the agent for everything"
Over-selling the new toy. The agent is 2-5x slower and non-deterministic. Stable critical paths
(login) stay scripted and pinned; intent-driven wins on fast-changing UI. Never "always use the
agent" or "agents replace all scripted tests."
### 3. Fixing flakiness with retries or higher temperature
"Just retry" and bumping temperature for "smarter" exploration both add variance. The real
levers are temperature 0, a pinned model, seeded data, a bounded step budget, and an explicit
verdict.
### 4. Assuming computer-use = screenshots + pixel coordinates
Playwright MCP is accessibility-tree-first. Defaulting to vision, screenshots, OCR, or
`mouse_click_xy` is slower, costlier, and flakier. Pixels are a canvas-only last resort.
### 5. "No error = success" (the false pass)
Accepting "page loaded / didn't crash / looks good" and letting the LLM self-grade is exactly
why the agent reports success while stuck on login. Require specific expected text, a URL, and
a forbidden-state negative check, evaluated by the harness.
### 6. Bigger model / more steps to go faster
Backwards. Opus 4.8 on every step and raising `maxSteps` raise cost and latency without buying
reliability. Smaller models, tighter budgets, caching, and tighter scope are the fix.
### 7. Running an agent forever instead of graduating
A goal green for two weeks on a now-stable flow should become a scripted `tests/*.spec.ts` via
Playwright Test Agents. "Keep running it as an agent," recording clicks, and xpath locators are
all wrong promotions.
### 8. Prose verdict in CI
Returning a paragraph for a human to read, or `continue-on-error: true`, lets a failed goal
merge. The run must exit non-zero on failure with a machine-readable boolean.
---
## Done When
- Goal prompt exists as natural-language intent (no `page.locator` / `page.goto` /
`data-testid` in the goal) with a START seeded entry URL.
- An explicit success oracle is defined: specific expected text AND a URL check AND a
forbidden-state negative check, asserted against `browser_snapshot` — not a screenshot.
- Run config pins a model id, sets `temperature: 0`, a `maxSteps` budget, and a seed; no
`waitForTimeout`, no retry-until-pass.
- Interaction is snapshot-first: `browser_navigate` / `browser_snapshot` /
`browser_click` / `browser_type` / `browser_wait_for`; screenshots used only for evidence.
- The runner emits `result.json` with `{"passed": true|false}` and `exit 1`s on false; the CI
job gates the merge on the boolean (no `continue-on-error`, no `always exit 0`).
- CI seeds/resets ephemeral state per run and enforces a step budget and a timeout cap.
- A graduation trigger is recorded (e.g. "green for 2 weeks → run `init-agents`, generate
`tests/.spec.ts` with `getByRole` locators").
- If any target is canvas/WebGL, the vision fallback (`--caps=vision` +
`browser_mouse_click_xy`) is scoped to that flow only, or the canvas is instrumented with ARIA.
---
## Related Skills
- **playwright-automation** — Writing and maintaining deterministic scripted Playwright tests
and Page Objects. Go there to author the durable test; this skill graduates an agent run into one.
- **ai-system-testing** — Testing your product's OWN LLM/AI features (prompt regression, model
output quality). This skill tests any app *using* an agent; it does not test your AI feature.
- **exploratory-testing** — Human SBTM exploration and bug hunting. The agentic smoke goal is
the automated cousin; use exploratory-testing for charter-driven manual sessions.
- **test-reliability** — Self-healing locators and quarantine for *scripted* flaky tests at
runtime. Complements the determinism levers here once a test has graduated.
- **qa-project-context** — The universal dependency; supplies stack, environments, seed/reset
tooling, and model access that every question above depends on.
## Reference Files (in `references/`)
- **setup.md** — Playwright MCP registration (`.mcp.json`), the snapshot tool table, the
natural-language goal prompt with success/negative assertions, the determinism harness
config (pinned model, temperature 0, maxSteps, seed, prompt cache), and cost/latency levers.
- **graduation-and-ci.md** — Playwright Test Agents promotion pipeline (planner → `specs/*.md`,
generator → `tests/*.spec.ts` with role-based locators, healer), the GitHub Actions gating
workflow with a machine-readable boolean verdict, and the canvas `--caps=vision` fallback.