Modern QA2026Mapping ReAct to CLI Browser Commands
Join

Course03 Agentic Testing Architectures

Cutting-edge · Chapter 03

Mapping ReAct to CLI Browser Commands

Updated Jul 2026

Bridging Theory and Practice

The ReAct pattern is abstract: Observe, Think, Act, Evaluate. To make it concrete, we need to map each phase to actual commands that an AI agent can execute. The Playwright CLI (playwright-cli) — the 2026 default for agent-driven browser control — provides exactly these commands, making it a practical implementation of the ReAct loop for browser testing. (Vibium's vibe-check CLI, which earlier editions of this chapter used, implements the same mapping with a selector-based surface; the loop is identical.)

The Phase-to-Command Mapping

ReAct Phase Testing Activity Playwright CLI Commands
Observe Read the current page state playwright-cli snapshot (YAML with element refs, saved to disk), playwright-cli screenshot, playwright-cli eval "<js>"
Think Decide what to test based on observation Agent reasons internally (no command — LLM inference)
Act Execute the test action playwright-cli fill <ref> "<text>", playwright-cli click <ref>, playwright-cli goto "<url>", playwright-cli press <key>
Evaluate Check the result against expectations Fresh snapshot + read it, eval for dynamic state

Observation Commands in Detail

The observation phase is about gathering state. The CLI's key design choice: state lands on disk, and the agent reads only what it needs:

# Save a structured page snapshot (YAML accessibility summary with refs)
playwright-cli snapshot
# → .playwright-cli/snapshot-....yml — the agent Reads this file on demand

# Capture visual state
playwright-cli screenshot

# Query dynamic state directly
playwright-cli eval "document.querySelectorAll('.error').length"
playwright-cli eval "location.href"

A snapshot looks like:

- textbox "Email" [ref=e8]
- textbox "Password" [ref=e9]
- button "Sign In" [ref=e11]
- link "Forgot Password?" [ref=e12]

Element refs (e8, e11) are how the agent addresses elements in the Act phase — no CSS selector authoring.

Action Commands in Detail

# Navigation
playwright-cli open "https://app.example.com/login"
playwright-cli goto "https://app.example.com/settings"

# Form interaction (refs come from the latest snapshot)
playwright-cli fill e8 "user@test.com"
playwright-cli fill e9 "secret123"
playwright-cli select e14 "United States"

# Clicking
playwright-cli click e11
playwright-cli dblclick e20

# Keyboard
playwright-cli press Enter
playwright-cli press Control+a

# Other input
playwright-cli hover e17
playwright-cli check e21
playwright-cli upload e23 ./fixture.pdf

Auto-waiting is built into the engine — every action waits for the element to be actionable, so the agent doesn't hand-roll waits.

A Complete ReAct Test Using the CLI

Here is how an agent thinks through a login test using the ReAct loop:

TEST OBJECTIVE: Verify that submitting the login form with empty fields
shows appropriate validation errors.

STEP 1 — OBSERVE:
  $ playwright-cli open "https://app.example.com/login"
  $ playwright-cli snapshot
  → reads snapshot: textbox "Email" [ref=e8], textbox "Password" [ref=e9],
    button "Sign In" [ref=e11]

STEP 2 — THINK:
  "I see a login form with email and password fields and a Sign In button.
   My objective is to test empty field submission. I should click Sign In
   without filling in any fields."

STEP 3 — ACT:
  $ playwright-cli click e11

STEP 4 — OBSERVE:
  $ playwright-cli snapshot
  → reads snapshot: alert "Email is required" [ref=e15],
    alert "Password is required" [ref=e16], textbox "Email" [ref=e8] ...

STEP 5 — THINK:
  "Two validation errors appeared: one for email and one for password.
   Both say the field is required. This matches expected behavior.
   Let me also verify we stayed on the login page (no navigation)."

STEP 6 — OBSERVE:
  $ playwright-cli eval "location.href"
  → "https://app.example.com/login"

STEP 7 — THINK:
  "URL has not changed. We are still on the login page. Both required
   field errors are shown. Test objective is met."

STEP 8 — EVALUATE:
  DONE pass "Empty form submission correctly shows validation errors
  for both email ('Email is required') and password ('Password is required').
  Page did not navigate away from /login."

Implementing the Agent in Python

import subprocess
import json
from pathlib import Path

class CliReActAgent:
    """A ReAct test agent driving the browser via playwright-cli."""

    def __init__(self, llm, max_steps=15):
        self.llm = llm
        self.max_steps = max_steps
        self.history = []

    def cli(self, command: str) -> str:
        """Execute a playwright-cli command and return its (minimal) output."""
        result = subprocess.run(
            f"playwright-cli {command}",
            shell=True, capture_output=True, text=True, timeout=60
        )
        if result.returncode != 0:
            return f"ERROR: {result.stderr.strip()}"
        return result.stdout.strip()

    def observe(self) -> dict:
        """Gather current page state. Snapshot goes to disk; read it back."""
        snapshot_path = self.cli("snapshot")          # returns a file path
        snapshot = ""
        if snapshot_path and not snapshot_path.startswith("ERROR"):
            snapshot = Path(snapshot_path.split()[-1]).read_text()[:2000]
        return {
            "url": self.cli('eval "location.href"'),
            "snapshot": snapshot,
            "errors": self.cli('eval "document.querySelectorAll(\'.error\').length"'),
        }

    def run(self, objective: str) -> dict:
        """Execute a test using the ReAct loop."""
        for step in range(self.max_steps):
            # OBSERVE
            state = self.observe()

            # THINK + decide
            prompt = f"""
            You are a QA test agent. Your objective: {objective}

            Current state:
            - URL: {state['url']}
            - Page snapshot (YAML, first 2000 chars): {state['snapshot']}
            - Error count: {state['errors']}

            History (last 5 actions):
            {json.dumps(self.history[-5:], indent=2)}

            Respond with exactly ONE action:
            CLI <command>   — execute a playwright-cli command (use refs from the snapshot)
            PASS <reason>   — test passed, explain why
            FAIL <reason>   — test failed, explain why
            """

            response = self.llm.generate(prompt).strip()
            self.history.append({"step": step, "state_url": state["url"], "action": response})

            # ACT or EVALUATE
            if response.startswith("CLI "):
                command = response[4:]
                output = self.cli(command)
                self.history[-1]["output"] = output
            elif response.startswith("PASS "):
                return {"status": "pass", "reason": response[5:], "steps": step + 1}
            elif response.startswith("FAIL "):
                return {"status": "fail", "reason": response[5:], "steps": step + 1}

        return {"status": "timeout", "reason": f"Exceeded {self.max_steps} steps", "steps": self.max_steps}

Note what the disk-first design does to this loop: the full snapshot only enters the LLM prompt when observe() deliberately includes it. A production version would be smarter still — passing the snapshot path and letting the agent read it only when its reasoning requires.

Error Recovery Patterns

One of the most powerful aspects of mapping ReAct onto a CLI is the agent's ability to recover from errors:

Stale Ref Recovery

Agent tries: playwright-cli click e11
Output: ERROR: ref e11 not found (page changed since last snapshot)

Agent observes: playwright-cli snapshot
→ button "Submit" is now [ref=e31]

Agent reasons: "The page re-rendered and refs changed. The submit button
              is now e31."

Agent acts: playwright-cli click e31
Output: success

Unexpected State Recovery

Agent tries: playwright-cli click e24   # checkout button
Output: success

Agent observes: playwright-cli snapshot
→ heading "Your session has expired. Please log in again."

Agent reasons: "Session expired during checkout. This is an environment
              issue. I need to log in again before continuing the test."

Agent acts:
  playwright-cli state-load auth-state   # restore saved login state
  playwright-cli goto "https://app.example.com/checkout"
  // Then retries the checkout

Performance Considerations

Metric CLI ReAct Agent Direct Playwright Script
Steps per test 8-20 Fixed (known at write time)
Time per step (LLM) 200-500ms 0ms (no reasoning)
Time per step (browser) 100-300ms 100-300ms
Total test time 5-15 seconds 1-3 seconds
Adaptability High None
Debugging difficulty Medium (history + traces) Low (linear script)

The Same Mapping in Commercial SDKs

This phase-to-command mapping is not unique to the Playwright CLI — it is exactly what production agentic browser SDKs expose. Stagehand (Browserbase) collapses Think+Act into a single intent call: act("click the submit button") resolves natural language against the observed page, the same way our agent resolves "click Sign In" to a ref. browser-use does the same in Python at framework scale. Vibium's vibe-check does it with CSS selectors over WebDriver BiDi. Once you understand the mapping in this file, those SDKs are just packaged, hardened versions of the loop you can already build by hand.

Key Takeaway

The Playwright CLI provides a natural mapping from the abstract ReAct pattern to concrete browser automation commands: observation via disk-saved snapshots (read on demand — the token-economics win), action via ref-based click/fill/goto/press, and evaluation via fresh snapshots and eval checks. This mapping turns the theoretical ReAct loop into a practical, implementable test agent — and it is the same mapping that production SDKs like Stagehand and browser-use package behind their intent APIs.