Playwright_PPT / Playwright MCP + Advance Framework Creation with AI Agents & CLI
Document Diagrams Workflow
Workshop Document · v1 · 2026

Playwright MCP + Advance Playwright Framework Creation with Playwright AI Agents & CLI

A teaching document for the Testing Academy workshop. Students learn what an LLM is, where it falls short, how MCP fills the gap, and how a single architecture.md file converts a natural-language prompt into a real, opinionated Playwright framework — Pages, Modules, fixtures, custom reporter, plus the AI agents and CLI workflow that make it all promptable.

Audience · QA engineers, automation testers Outcome · prompt → spec → run → report Stack · Playwright · TypeScript · Claude · MCP · VWO Length · 8 chapters, ~2 hours
Disclaimer

This roadmap only works if you can give 1 hour per day or 4–5 hours per week (Sat/Sun). The goal is hands-on conversion: by the end of the session every student should have shipped one real test (invalid login on app.vwo.com) without typing code.

Table of Contents

01 · What is an LLM, really?

A Large Language Model is a statistical reader-writer. It has read a lot of the internet, compressed that reading into hundreds of billions of weights, and given a prompt it predicts the next most-likely token, again and again, until a stop signal arrives.

That is the entire trick. There is no database lookup, no live browser, no knowledge base. Everything else — chat memory, tools, web access, “agents” — is scaffolding we build around that core loop.

For a tester, the most useful framing is this: an LLM is a brilliant intern. Confident, articulate, well-read, and unable to verify anything it claims unless we give it a way to check.

How “next token” actually works

The headline claim — “predicts the next most-likely token” — is easier to feel with one concrete example. Take the phrase below. The LLM looks at every word in its vocabulary (~50,000 of them) and assigns each a probability. The token closest to 1.0 wins:

Figure 1.0 — Next-token probabilities for one prompt
the cat will sit on the ___
mat
0.62
floor
0.14
table
0.08
chair
0.06
sofa
0.04
roof
0.03
desk
0.02
… 50,000+ others
0.01
The model picks mat — not because it “knows” the answer, but because mat is statistically the heaviest weight in this context.

Lower the temperature and the model picks mat almost every time. Raise it, and it starts choosing floor or table sometimes — the same weights, just read with more randomness. Either way, there is no understanding here. There are weights.

Key insight

Every “answer” an LLM gives is a chain of these weighted picks, one token after another. Saying “the LLM thinks” is a useful shorthand — but underneath, it’s 50,000 numbers, ranked, and the top one wins.

The vocabulary you actually need

TermPlain definitionWhy a tester cares
TokenA chunk of text, ~¾ of an English word.Cost, speed and context windows are measured in tokens.
Context windowHow much text the model can see at once.Long DOMs and screenshots eat this fast.
System promptHidden instructions a tool sets before your turn.It is where rules for your agent live.
TemperatureHow randomly the model picks the next token.For test code, keep it low. Determinism > creativity.
Tool callThe model emits a structured JSON request.The entire bridge between an LLM and Playwright.

One round-trip, visualised

Figure 1.1 — A single LLM turn with optional tool use
flowchart LR U([User prompt]) --> M[LLM] S[/System prompt + tool schemas/] --> M M --> O([Response text]) M -. optional .-> T["Tool call (JSON)"] T --> R[Runtime executes tool] R --> M
An LLM by itself is one-shot. Tools are how the loop becomes useful for testing.
Teach this slowly

Most students arrive thinking the LLM is the agent. It isn’t. The agent is the runtime around it — the loop that takes a tool call, executes it, hands the result back, and asks the model what to do next. Once they see this, MCP makes sense in five minutes instead of fifty.


02 · Where LLMs hit a wall

A model that has read the internet is still a model that cannot do anything in the world. It cannot click a button, read a database row, or know what time it is. Pretending otherwise is the source of most bad demos.

Six honest limitations

  1. No live world. Without a tool, the model can’t browse, query or execute.
  2. Hallucinations. It will confidently invent selectors, library APIs and URLs. Test code makes hallucinations easy to catch — they fail fast.
  3. Knowledge cutoff. Anything released after training is invisible. New Playwright APIs, your last sprint’s DOM — none of it is in the weights.
  4. No memory. Each request starts blank. State must be carried in the prompt or persisted by the surrounding runtime.
  5. Limited context. A whole repo will not fit. You must choose what to show — itself a skill.
  6. Cannot verify itself. It cannot run the test it just wrote. It needs an executor to close the loop. That executor is the entire point of MCP.
Every limitation in that list is a gap you can fill with a tool. — The reason this whole document exists.

What this means for testers

Testing is a closed-loop discipline. You write a check, run it, observe the result, and decide. An LLM alone can only do the first step. The remaining three — run, observe, decide — require something on the other end of a tool call. For us that thing is a browser, driven by Playwright, exposed through MCP.


Bridge — From LLM to AI Agent (and why MCP matters)

An LLM by itself is just a brain in a jar — brilliant, but trapped. Add memory and tools, and the brain becomes an agent: something that can act in the world, remember what it did, and decide what to do next.

The formula on the whiteboard

LLM + Memory + Tools = AI Agent

The brain-and-body analogy

Picture two robots. One has a head — and nothing else. It can think, but it can’t do. The other has a head, a body, hands holding tools, and legs to walk. Same brain in both. The difference is what surrounds it.

brain only

LLM = brain only

  • Reads, writes, reasons
  • Predicts the next token
  • Articulate and well-read
cannot click · cannot fetch · forgets every turn
MEMORY TOOL TOOL brain + memory + tools

AI Agent = brain + memory + tools

  • Same LLM brain at the core
  • + Hands & legs (tools — browser, files, APIs)
  • + Notebook (memory across turns & sessions)
can click · can fetch · remembers why it’s doing this

So… why do MCPs matter?

Once you accept that an agent needs tools, the next question is: how do you wire those tools up? The hard way is to write custom integration code for every tool the agent might need — a bespoke browser library, a bespoke file API, a bespoke database client. Every host (Claude Code, Cursor, Claude Desktop) has to redo the work for every tool. It does not scale.

MCP solves this with one standard protocol. Write a tool server once; every MCP-aware agent can use it. Ship a Playwright MCP server, and Claude Code, Cursor and Claude Desktop all get browser automation for free. Switch from Claude to GPT — your servers keep working.

Without MCP
  • Custom glue code per tool, per host
  • Each app reinvents browser control, file I/O, API calls
  • No shared discovery — model can’t list available tools
  • Switching hosts means rewriting every integration
  • Tool authors target one app at a time
With MCP
  • One protocol (JSON-RPC) for every tool, every host
  • Playwright MCP works in Claude Code, Cursor, Claude Desktop
  • Model auto-discovers tool schemas at startup
  • Switch hosts; servers keep working unchanged
  • Tool authors ship once, reach every MCP client
The headline

Without MCP, every agent reinvents the wheel for every tool. With MCP, every agent gets every tool for free. The next chapter is the technical detail of how that protocol actually works.


03 · The Model Context Protocol (MCP)

MCP is an open protocol — JSON-RPC over stdio or HTTP — that lets any LLM client talk to any tool server in a uniform way. A Playwright MCP server exposes the browser; a database MCP server exposes SQL; a filesystem MCP server exposes your repo. The model picks tools by name, the runtime executes them.

The one-line analogy: MCP is a USB hub for AI

Before USB, every peripheral had its own cable. Printer port. Mouse port. Modem port. Each cable, each driver, each shape — bespoke. USB collapsed that mess into one shared socket: any device, one cable, plug-and-play.

MCP is the USB hub for AI tools. Slack, Jira, your database, Playwright — each is a different “peripheral.” MCP is the one socket the LLM speaks. Plug a server in, and the model can use it without anyone writing custom glue.

Figure 3.0 — MCP as a USB hub for AI
flowchart LR L[LLM
Claude · GPT · Gemini]:::brain L ===|one protocol| HUB{{"MCP
(the USB hub)"}}:::hub HUB --- T1[Slack server]:::tool HUB --- T2[Jira server]:::tool HUB --- T3[Database server]:::tool HUB --- T4[Playwright server]:::tool HUB --- T5[GitHub server]:::tool HUB --- T6[Filesystem server]:::tool classDef brain fill:#fff,stroke:#111,stroke-width:2px,color:#111; classDef hub fill:#1aa163,stroke:#0e6c43,stroke-width:2px,color:#fff; classDef tool fill:#f6f7f9,stroke:#111,stroke-width:1.5px,color:#111;
One protocol, many tools. Add a peripheral, the LLM gets the capability — no rewiring.

Three roles, one handshake

Figure 3.1 — Anatomy of an MCP connection
flowchart TB subgraph H["HOST · e.g. Claude Code / Cursor"] UI([Chat UI / IDE]) LLM[LLM core] UI --- LLM C1[/MCP client #1/] C2[/MCP client #2/] C3[/MCP client #3/] LLM --- C1 LLM --- C2 LLM --- C3 end C1 <-->|JSON-RPC| S1[Playwright MCP server] C2 <-->|JSON-RPC| S2[Filesystem MCP server] C3 <-->|JSON-RPC| S3[GitHub MCP server] S1 --> B[(Browser · Chromium / Firefox / WebKit)] S2 --> F[(Repo files)] S3 --> G[(api.github.com)]
One host, many clients, many servers. Each server is a discrete capability the LLM can call by name.

What a server can offer

PrimitiveWhat it isExample from Playwright MCP
ToolsFunctions the LLM can call.browser_navigate, browser_click, browser_snapshot
ResourcesRead-only data the LLM can pull.Accessibility tree, console logs, network requests.
PromptsReusable, parameterised instructions."login_test" with placeholders for username and password.

Sequence of one tool call

Figure 3.2 — One full round-trip, end to end
sequenceDiagram autonumber participant U as You participant H as Host (Claude Code) participant L as LLM participant C as MCP Client participant S as Playwright MCP Server participant B as Browser U->>H: "Open vwo.com and log in with bad creds" H->>L: prompt + tool schemas L-->>H: tool_call: browser_navigate(url) H->>C: forward call C->>S: JSON-RPC: browser_navigate S->>B: page.goto(url) B-->>S: page loaded + a11y snapshot S-->>C: result (snapshot ref) C-->>L: result L-->>H: tool_call: browser_click(ref="email field") Note over L,B: ...loop until task is done... L-->>H: final natural-language report H-->>U: test journey completed
The model chooses the next move; the server executes it; the result feeds the next decision.

The only config you really need

The host holds a small JSON file that lists the servers it should launch. For Claude Code, this lives in .mcp.json at your project root.

.mcp.json// Tells the host which MCP servers to start for this project.
{
  "mcpServers": {
    "playwright": {
      "command": "npx",
      "args": ["-y", "@playwright/mcp@latest"]
    }
  }
}

Save the file, restart your host, and you will see a new set of tools — browser_navigate, browser_click, browser_snapshot, browser_evaluate — available to the model.

Verify the connection

In Claude Code, run /mcp. You should see playwright listed as connected. If it isn’t, fix this before moving on — every later chapter assumes the bridge is alive.


04 · Hands-on — Playwright MCP × VWO

The first real test. We will not write a single line of TypeScript. We will write a paragraph, hand it to the LLM, and watch the browser drive itself through a real login flow against a real product.

The scenario

Figure 4.1 — Lab flow
flowchart LR P[/Your prompt — natural language/] --> CC[Claude Code] CC -->|tool calls| MCP[Playwright MCP server] MCP -->|CDP| BR[(Chromium @ app.vwo.com)] BR -->|snapshot + console + screenshot| MCP MCP -->|results| CC CC --> R[/Test report + screenshot.png/]
No new tooling. Just prompt → model → server → browser → back again.

The prompt — copy this verbatim in class

Use the Playwright MCP to open https://app.vwo.com/. Wait until the login form is visible. Enter the email [email protected] and the password WrongPassword!23. Click the Sign in button. Wait for the error toast or inline error to appear. Take a full-page screenshot named vwo-invalid-login.png. Finally, report which selector you used for the email field, the password field and the error element — and tell me the exact text of the error you saw.

What the underlying tool calls look like

trace excerpt — what the LLM emitted// 1. Discover the page
browser_navigate({ url: "https://app.vwo.com/" })
browser_snapshot()  // → returns a11y tree with refs

// 2. Type credentials
browser_type({ ref: "e23", text: "[email protected]" })
browser_type({ ref: "e31", text: "WrongPassword!23" })

// 3. Submit and wait
browser_click({ ref: "e44" })
browser_wait_for({ text: "Invalid email or password" })

// 4. Evidence
browser_take_screenshot({ filename: "vwo-invalid-login.png", fullPage: true })

Notice three things students always miss on first viewing:

Common stumble

If the page lazy-loads its login widget, an early prompt will fail with “field not found.” Don’t panic — add “wait for the email input to be visible before typing” and re-run. Also a chance to teach the difference between presence and visibility.


05 · The architecture.md blueprint

A trace from Chapter 04 is not a framework. It is a script. To get from script to framework — Pages, Modules, fixtures, environments, retry policy, reporting — we need to give the LLM something it can read and respect: an architecture.md.

Think of it as a constitution. The model reads it before every test it generates, and writes code that fits. The architecture used in this workshop is the Testing Academy 8-layer Pages + Modules framework by Pramod Dutta — a strict separation-of-concerns pattern that scales to large teams.

The 8 layers

Layer 01

Configuration

playwright.config.ts, tsconfig.json, .env, src/config/index.ts. 60s timeout, parallel, multi-browser.

Layer 02

Pages — locators only

Locators as arrow functions, simple UI actions. No business logic. No assertions.

Layer 03

Modules — business logic

doLogin(), addProductToCart(), completeCheckout(). Use Pages, never locate directly.

Layer 04

Utilities

Logger, WaitHelper, DataGenerator, ApiHelper, retries.

Layer 05

API testing

AuthApi, ProductApi, OrderApi. Same patterns, REST instead of UI.

Layer 06

Custom Fixtures

loginPage, loginModule, authenticatedPage. Compose, don’t inherit.

Layer 07

Reporting

HTML, JSON, screenshots/videos on failure, traces, plus the custom CustomTTAReporter.

Layer 08

CI/CD

Multi-browser, env selection, artifact publishing, parallel workers.

Folder shape

Figure 5.1 — Folder structure produced from architecture.md
flowchart TB A[("architecture.md")] --> S["src/"] S --> P["pages/
LoginPage · HomePage · ProductPage"] S --> M["modules/
LoginModule · ProductModule · CheckoutModule"] S --> U["utils/
Logger · WaitHelper · DataGenerator · CustomTTAReporter"] S --> API["api/
AuthApi · ProductApi · OrderApi"] S --> FX["fixtures/
auth.fixture.ts · index.ts"] S --> CFG["config/
authors · test-groups"] S --> TD["testdata/
users.json · environments.ts"] A --> T["tests/
login.spec · product.spec · checkout.spec"] A --> CFG2["playwright.config.ts"] A --> R["tta-report/
(generated)"]
One markdown file → a complete, opinionated layout. Every later prompt obeys this shape.

The execution flow

Figure 5.2 — How a single test executes through the layers
flowchart LR TS[Test Specs] --> FX[Fixtures] FX --> MD[Modules] MD --> PG[Pages] PG --> BR[Browser] BR --> RP[Reports]
Tests never touch Pages directly — they go through Modules via fixtures.

The contract — give this to the LLM

architecture.md (excerpt)# Playwright Test Automation Framework — Architecture

## Principles
- TypeScript only. Strict mode on.
- Pages = locators only (arrow functions). No logic. No assertions.
- Modules = business logic. Use Pages. Never locate directly.
- Tests = use fixtures. Use test.step(). Use tags (@P0, @P1, @Login).
- Test data lives in src/testdata/. Never inline.
- Every test produces evidence: screenshot + trace on failure.

## Naming
- Pages:    *Page.ts    (LoginPage.ts)
- Modules:  *Module.ts  (LoginModule.ts)
- Tests:    *.spec.ts   (login.spec.ts)
- APIs:     *Api.ts     (AuthApi.ts)
- Helpers:  *Helper.ts  (WaitHelper.ts)

## Required configurations
- TS path aliases: @pages, @modules, @fixtures, @utils, @api
- Multi-browser: chromium, firefox, webkit, mobile-chrome
- Parallel execution enabled
- HTML + JSON reporting + CustomTTAReporter
- Screenshots/videos on failure, trace on first retry

Now re-prompt with the contract loaded

Read architecture.md. Convert the invalid-login flow we just executed into the framework. Create src/pages/LoginPage.ts (locators only), src/modules/LoginModule.ts (business logic), wire them into src/fixtures/auth.fixture.ts as loginPage and loginModule, then add src/tests/login.spec.ts tagged @P0 @Login using test.step(). Follow every principle in the file. Run it once and show me the output.

Before vs After — same scenario, two worlds

Before · raw script
  • Selectors hard-coded in the test
  • Credentials inline
  • One file does everything
  • No retries, no fixtures, no tags
  • Breaks on a DOM change
  • Cannot scale to 100+ tests
After · 8-layer framework
  • LoginPage owns the locators
  • LoginModule owns doLogin()
  • Test = 6 lines, reads as English
  • Fixture composes page + module + auth
  • Tags @P0 @Login drive selective runs
  • test.step() shows up in the HTML report

The generated code, in full

src/pages/LoginPage.ts — Layer 02 (locators only)import type { Page } from "@playwright/test";

export class LoginPage {
  constructor(private page: Page) {}

  // Locators as arrow functions — no logic, no assertions
  emailInput    = () => this.page.getByRole("textbox", { name: "Email" });
  passwordInput = () => this.page.getByRole("textbox", { name: "Password" });
  signInButton  = () => this.page.getByRole("button", { name: "Sign in" });
  errorMessage  = () => this.page.getByText("Invalid email or password");

  async goto() { await this.page.goto("https://app.vwo.com/"); }
  async fillEmail(v: string)    { await this.emailInput().fill(v); }
  async fillPassword(v: string) { await this.passwordInput().fill(v); }
  async submit()             { await this.signInButton().click(); }
}
src/modules/LoginModule.ts — Layer 03 (business logic)import type { Page } from "@playwright/test";
import { LoginPage } from "@pages/LoginPage";

export class LoginModule {
  constructor(
    private page: Page,
    private loginPage: LoginPage
  ) {}

  // Business logic — uses LoginPage methods, never locates directly
  async doInvalidLogin(email: string, password: string) {
    await this.loginPage.goto();
    await this.loginPage.fillEmail(email);
    await this.loginPage.fillPassword(password);
    await this.loginPage.submit();
  }
}
src/fixtures/auth.fixture.ts — Layer 06 (fixtures)import { test as base } from "@playwright/test";
import { LoginPage }   from "@pages/LoginPage";
import { LoginModule } from "@modules/LoginModule";

type Fixtures = {
  loginPage: LoginPage;
  loginModule: LoginModule;
};

export const test = base.extend<Fixtures>({
  loginPage:   async ({ page }, use) => await use(new LoginPage(page)),
  loginModule: async ({ page, loginPage }, use) => await use(new LoginModule(page, loginPage))
});
export { expect } from "@playwright/test";
src/tests/login.spec.ts — the test, six linesimport { test, expect } from "@fixtures";
import { invalidUser } from "@testdata/users";

test.describe("@P0 @Login", () => {
  test("shows error on invalid credentials", async ({ loginModule, loginPage }) => {

    await test.step("submit invalid credentials", async () => {
      await loginModule.doInvalidLogin(invalidUser.email, invalidUser.password);
    });

    await test.step("verify the inline error appears", async () => {
      await expect(loginPage.errorMessage()).toBeVisible();
    });
  });
});
A framework is a contract written in markdown. The code is just a faithful printing of it. — Chapter 05, the punchline.

Three rules students must internalise


06 · Custom Reporter — CustomTTAReporter

Playwright already ships excellent reporters. The point of building your own is not to compete — it is to teach students how the test runtime exposes its lifecycle. Once they have seen the events, they own the workflow.

Figure 6.1 — Reporter event lifecycle
stateDiagram-v2 [*] --> onBegin onBegin --> onTestBegin onTestBegin --> onStdOut onStdOut --> onStdErr onStdErr --> onTestEnd onTestEnd --> onTestBegin: more tests onTestEnd --> onEnd: all done onEnd --> [*]
Six callbacks. Implement any subset. Emit anything you want.
src/utils/CustomTTAReporter.tsimport type {
  Reporter, FullConfig, Suite, TestCase, TestResult, FullResult
} from "@playwright/test/reporter";
import fs from "node:fs";

class CustomTTAReporter implements Reporter {
  private rows: any[] = [];

  onBegin(_: FullConfig, suite: Suite) {
    console.log(`▸ ${suite.allTests().length} tests queued`);
  }

  onTestEnd(test: TestCase, result: TestResult) {
    this.rows.push({
      title:    test.title,
      tags:     test.titlePath().join(" › "),
      status:   result.status,
      duration: result.duration,
      retry:    result.retry,
      errors:   result.errors.map(e => e.message),
      steps:    result.steps.map(s => ({ title: s.title, duration: s.duration })),
      attachments: result.attachments.map(a => a.path)
    });
  }

  async onEnd(result: FullResult) {
    fs.mkdirSync("tta-report", { recursive: true });
    fs.writeFileSync("tta-report/index.html", render(this.rows, result));
    fs.writeFileSync("tta-report/results.json", JSON.stringify(this.rows, null, 2));
    if (result.status === "failed") await notifySlack(this.rows);
  }
}

export default CustomTTAReporter;
playwright.config.ts (snippet)export default defineConfig({
  reporter: [
    ["list"],
    ["html", { open: "never" }],
    ["json", { outputFile: "test-results/results.json" }],
    ["./src/utils/CustomTTAReporter.ts"]
  ]
});
Teaching beat

Have a student rename one test to fail on purpose. Re-run. Watch the Slack ping fire and the tta-report/index.html render. The room finally sees the lifecycle as something they can program — not a black box.


07 · Playwright Test Agents — Planner, Generator, Healer

Playwright now ships its own official AI agents. Three of them, each with a single job, designed to chain together. Run them independently, sequentially, or in a loop — and they will produce, run, and self-heal a complete test suite for your product.

All three are configured with one command and run inside your IDE — no separate service, no separate process. They are agent definitions shipped by Microsoft that any modern coding host (VS Code, Claude Code, OpenCode / Cursor) can pick up. Source: playwright.dev/docs/test-agents.

The three agents at a glance

Agent 01

🎭 Planner

Explores the app and writes a Markdown test plan. Reads your seed test to bootstrap the environment, then walks the product and proposes the scenarios worth covering — in plain English, in specs/.

Inyour request · seed test · optional PRD
Outspecs/basic-operations.md
Agent 02

🎭 Generator

Turns the Markdown plan into Playwright test files. As it generates, it verifies selectors and assertions live — actually performing the scenarios in a browser to make sure the resulting tests would pass.

Ina plan from specs/
Outa test suite under tests/
Agent 03

🎭 Healer

Runs the suite and automatically repairs failing tests. Replays the failing steps, inspects the live UI, suggests a patch, re-runs — until pass, or until the guardrail decides the functionality itself is broken (and skips the test).

Ina failing test name
Outa passing test (or a skipped one with notes)

One command to install the agents

Pick the host you actually use. The CLI writes the right agent definitions for that host. You can re-run this whenever Playwright updates to pick up new tools.

terminal — choose your host# For VS Code (requires v1.105+)
npx playwright init-agents --loop=vscode

# For Claude Code
npx playwright init-agents --loop=claude

# For OpenCode / Cursor and other forks
npx playwright init-agents --loop=opencode
Re-run after every Playwright upgrade

The agent definitions are tied to the version of Playwright on disk. New tools or instructions ship with new releases. Re-run init-agents after every upgrade so your agents pick them up.

The chain — how they hand off

Figure 7.1 — Planner → Generator → Healer
flowchart LR REQ([your request]):::input --> P["🎭 Planner"]:::pln SEED[(seed.spec.ts)]:::seed --> P P --> SPEC[/"specs/*.md
markdown plan"/]:::doc SPEC --> G["🎭 Generator"]:::gen G --> TESTS[/"tests/*.spec.ts
playwright suite"/]:::doc TESTS --> RUN[npx playwright test]:::run RUN -->|failure| H["🎭 Healer"]:::heal H -->|patch| TESTS RUN -->|all pass| DONE([✓ shipped]):::ok classDef input fill:#fff,stroke:#111; classDef seed fill:#f6f7f9,stroke:#111; classDef pln fill:#1aa163,stroke:#0e6c43,color:#fff; classDef gen fill:#1aa163,stroke:#0e6c43,color:#fff; classDef heal fill:#1aa163,stroke:#0e6c43,color:#fff; classDef doc fill:#fff,stroke:#111; classDef run fill:#fff,stroke:#111; classDef ok fill:#e5f4ec,stroke:#1aa163;
Three small agents. Sequential when you want control, looped when you want autonomy.

The artifact layout they produce

repo layout after init-agentsrepo/
  .github/             # agent definitions Playwright wrote
  specs/               # human-readable plans (Planner output)
    basic-operations.md
    checkout-flow.md
  tests/               # generated Playwright tests (Generator output)
    seed.spec.ts       # your environment bootstrap
    basic-operations.spec.ts
    checkout-flow.spec.ts
  playwright.config.ts

The seed test — the most important file in the kit

The seed test is the contract you give the agents. It does three things at once: bootstraps the environment (logs in, seeds data, sets cookies), shows the agents your custom fixtures, and serves as the style example they imitate when generating new tests.

tests/seed.spec.tsimport { test, expect } from "./fixtures";

test("seed", async ({ page }) => {
  // Whatever a generated test will need: auth, base URL, fixtures.
  // Agents read this file to understand your conventions before they write code.
  await page.goto("https://app.vwo.com/");
  await expect(page).toHaveTitle(/VWO/);
});

An end-to-end run — invoking each agent

Once init-agents has run, the three agents appear in your IDE’s agent picker. The typical session looks like this:

Ask the Planner for a plan

Open the Planner agent. Prompt: “Write a test plan for the invalid-login flow on app.vwo.com.” It will explore the page, then save specs/login.md.

Hand the plan to the Generator

Open the Generator agent. Prompt: “Generate tests from specs/login.md.” It writes tests/login.spec.ts, verifying selectors live as it goes.

Run the suite

npx playwright test. If everything is green, you’re done. Ship it.

If something fails, dispatch the Healer

Open the Healer. Hand it the failing test name. It replays, inspects, patches and re-runs — or skips with a clear note explaining why the feature itself looks broken.

Test Agents vs MCP — when to reach for which

Playwright Test AgentsPlaywright MCP
Purpose Produce, run and heal a test suite. Drive a single browser session for any task.
Output Files in specs/ and tests/. Tool-call traces; whatever the LLM decides.
Granularity Three role-specialised agents. One general-purpose toolset.
Best for Building and maintaining a real test suite. Discovery, exploration, one-off automation.
Setup npx playwright init-agents --loop=… .mcp.json with the playwright server.
Honest limitation

Agents are non-deterministic by nature. Use Test Agents for generation and healing; use the resulting .spec.ts files for your CI smoke suite. The agents author the tests — they don’t replace them at runtime.


08 · Playwright CLI + CodeGen

The third leg of the stool — and quietly, the most cost-effective. If MCP is the LLM driving the browser, the Playwright CLI is the original surface: a fast, scriptable, stateless toolkit that runs locally with zero token cost. Pair it with the LLM, and you get the cheapest path from idea to shipped test.

What is the Playwright CLI?

The CLI is the npx playwright command family — everything that ships in the @playwright/test package. Each command does one thing and exits. There is no server, no daemon, no LLM. You hand it arguments, it does the work.

It is how 90% of Playwright work happened before MCP existed. And it is still the most token-efficient way to do most tasks. The CLI is what your CI runs every hour. MCP is what you reach for when you need exploration.

Source & history

The CLI started life as a separate repo — github.com/microsoft/playwright-cli — which was later merged into the main @playwright/test package. The link is still useful for reading the original design, the codegen internals, and the early history of npx playwright codegen.

The three surfaces, ranked by cost

SurfaceWhat it isBest forToken cost
MCP LLM driving a long-running server that controls the browser Discovery, exploratory testing, unknown pages High
CLI Direct commands you (or the LLM) invoke Recording, running, debugging stable flows Low
Agents LLM in a verified loop with tools Failure triage, self-healing locators Variable

The headline — token efficiency, MCP vs CLI

Same task. Same end result. Two very different bills. For the invalid-login VWO scenario from Chapter 04:

Playwright MCP heavy
~114,000
tokens consumed

Each browser_snapshot sends the full a11y tree to the model. Each tool call is a round-trip. Discovery is great — but expensive.

Playwright CLI ~4× cheaper
~27,000
tokens consumed

Codegen records selectors locally. The LLM only sees the final recorded script — no DOM snapshots, no tool round-trips. Convert and ship.

Rule of thumb

Use MCP for the first time you touch a flow (discovery). Use the CLI + codegen every time after (cheap repetition). Use Agents only when something fails and you want triage.

The 43 commands — full cheat sheet

Every Playwright CLI command and flag a working tester needs, grouped by purpose. Print this and pin it.

Running tests · 12 commands
npx playwright test
Run all tests across all projects.
npx playwright test login.spec.ts
Run a single spec file.
npx playwright test --headed
Run with the browser window visible.
npx playwright test --debug
Open the Playwright Inspector for step-through.
npx playwright test --ui
Open UI mode — time-travel debugger + watch.
npx playwright test --grep "@P0"
Run only tests matching a tag/pattern.
npx playwright test --grep-invert "@slow"
Run everything except matches.
npx playwright test --workers=4
Parallelism: how many workers to use.
npx playwright test --workers=1
Single worker — great for live demos.
npx playwright test --project=chromium
Restrict to one project from the config.
npx playwright test --retries=2
Retry failing tests N times.
npx playwright test --max-failures=5
Stop the run after N failures.
Test selection & control · 8 commands
npx playwright test --list
List tests without running them.
npx playwright test --shard=2/4
Run shard 2 of 4 (CI sharding).
npx playwright test --timeout=60000
Per-test timeout in ms.
npx playwright test --forbid-only
Fail the run if any .only exists.
npx playwright test --quiet
Reduce console output.
npx playwright test --update-snapshots
Refresh visual / text snapshots.
npx playwright test --reporter=html
Override config to use one reporter.
npx playwright test --output=results/
Custom directory for artifacts.
CodeGen & recording · 6 commands
npx playwright codegen
Open the recorder with a blank URL.
npx playwright codegen https://app.vwo.com/
Open the recorder against a URL.
npx playwright codegen --output=test.spec.ts
Save the recording to a file directly.
npx playwright codegen --target=python
Generate Python (or java, csharp, javascript).
npx playwright codegen --device="iPhone 13"
Record under mobile emulation.
npx playwright codegen --color-scheme=dark
Record against the dark theme.
Reports & traces · 4 commands
npx playwright show-report
Open the last HTML report.
npx playwright show-report ./report
Open a specific report directory.
npx playwright show-trace trace.zip
Open the trace inspector for one trace.
npx playwright show-trace --port=8080 trace.zip
Same, but on a custom port.
Browsers & OS deps · 6 commands
npx playwright install
Download all browser binaries.
npx playwright install chromium
Install just one browser.
npx playwright install --with-deps
Browsers + OS dependencies (Linux/CI).
npx playwright install-deps
OS-level dependencies only.
npx playwright uninstall
Remove all installed browsers.
npx playwright install --dry-run
Preview what would be installed.
Project setup · 3 commands
npm init playwright@latest
Bootstrap a new Playwright project.
npx playwright --version
Print the installed version.
npx playwright --help
General CLI help.
MCP & extras · 4 commands
npx @playwright/mcp@latest
Run the Playwright MCP server.
npx @playwright/mcp --port=8931
MCP on a custom HTTP port.
npx @playwright/mcp --headless
MCP, headless browser.
npx @playwright/mcp --browser=firefox
MCP using Firefox instead of Chromium.

The Playwright extension for VS Code

Two minutes to install, ten years of saved time. The extension turns VS Code into a Playwright control room — no terminal needed for most workflows.

Open the extensions sidebar

+ + X on macOS, or Ctrl + Shift + X on Windows / Linux.

Search “Playwright Test for VSCode”

Publisher: Microsoft. Look for the official blue verified checkmark — there are knock-offs.

Click Install, then reload the window

You will see a flask-shaped icon appear in the activity bar — that is the test explorer.

What you get the moment it is installed:

Using the CLI + a prompt — the cheap path for VWO invalid login

This is the workflow you graduate to once a page is stable. Same VWO scenario as Chapter 04, but no MCP — and roughly 4× cheaper.

Figure 8.1 — The CLI + LLM workflow (no MCP)
flowchart LR C([codegen]) --> H[/raw recorded steps/] H --> L[LLM + architecture.md] L --> S[/framework-shaped .spec.ts/] S --> R[playwright test] R --> A[/tta-report + screenshots/] A -. failure? .-> AG[Agent triage] AG --> L
Record → convert → run → triage. Cheap, repeatable, deterministic.

Run codegen against the URL

npx playwright codegen https://app.vwo.com/

Click through the invalid-login flow

Type a bad email, a bad password, hit Sign In. Watch the recorder write code in real time.

Stop. Copy the recorded script.

The recorder gives you a clean .spec.ts with locators it picked itself.

Paste into Claude Code with this prompt

The LLM converts it into framework shape — without ever touching the browser.

Run the converted spec headed

npx playwright test src/tests/login.spec.ts --headed --workers=1

Here is a Playwright codegen recording for the invalid-login flow on app.vwo.com. Convert it into a framework-shaped spec following architecture.md: put the locators into src/pages/LoginPage.ts, the business logic into src/modules/LoginModule.ts, wire them into the loginPage and loginModule fixtures, and write a tagged spec at src/tests/login.spec.ts with @P0 @Login. Use test.step() around each logical action. Then run it once.

The LLM now has the exact selectors and the exact steps from your recording. No DOM snapshots, no tool round-trips, no browser_navigate calls. Just text in, framework-shaped code out. ~27K tokens. Done.

When to use which surface

If you’re…Use thisWhy
Touching a new page for the first time MCP The model needs to see the DOM to discover selectors.
Adding the next test on a known page CLI + codegen Selectors already known — pay the cheaper bill.
Triaging a flaky failure Agent Loop + verifier finds root cause without your time.
Running CI CLI only Deterministic. No LLM in the hot path.
Building a new framework MCP + architecture.md Pay once for the scaffolding, then live on the cheap surfaces.
Prompt the framework. Record the journey. Let the agents triage the failures. The keyboard becomes optional — and the bill becomes manageable. — What this whole document was leading to.
End-of-class check

By the end of the session, every student should be able to: (a) describe MCP in one sentence, (b) point at the four pieces of an agent, (c) explain why architecture.md matters, (d) ship a working VWO login spec without typing code, and (e) name two reasons to prefer the CLI over MCP for repeat work. If four of five land, you taught well.


Closing — the shape of the workflow

Playwright MCP, AI Agents and CodeGen are three different surfaces of the same underlying idea: the LLM should be the author, the keyboard should be the editor, and the framework — the contract written in architecture.md — should be the constitution everything else obeys.

Figure C — The full workflow at a glance
flowchart LR H([Human]) -->|prompts| L[LLM] ARC[("architecture.md")] --> L L -->|tool calls| MCP[Playwright MCP] MCP --> BR[(Browser)] BR --> EVT[(Events + snapshots)] EVT --> L L --> SPEC[/Framework-shaped specs/] SPEC --> RUN[playwright test] RUN --> REP[CustomTTAReporter] RUN --> AGENT[Agent for failures] AGENT --> L
Everything we covered, on one page. This is the picture students should leave the room with.

The companion artifacts in this kit: