{"id":50864248,"url":"https://github.com/gsaini/playwright-mcp-getting-started","last_synced_at":"2026-06-14T23:34:29.849Z","repository":{"id":358519646,"uuid":"1241054966","full_name":"gsaini/playwright-mcp-getting-started","owner":"gsaini","description":"Drive Playwright MCP from a deterministic Node.js client to validate a frontend app — no LLM in the loop. Demonstrates three assertion styles (snapshot refs, CSS selectors, page evaluate) across 9 end-to-end scenarios.","archived":false,"fork":false,"pushed_at":"2026-06-13T04:40:01.000Z","size":223,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":0,"default_branch":"main","last_synced_at":"2026-06-14T23:34:28.806Z","etag":null,"topics":["biome","browser-automation","demo","e2e-testing","frontend-testing","mcp","model-context-protocol","no-llm","nodejs","playwright","playwright-mcp","pnpm","react","react-router","tailwind-css","tailwindcss","vite"],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/gsaini.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2026-05-16T22:49:22.000Z","updated_at":"2026-06-13T04:40:05.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/gsaini/playwright-mcp-getting-started","commit_stats":null,"previous_names":["gsaini/playwright-mcp-getting-started"],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/gsaini/playwright-mcp-getting-started","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fplaywright-mcp-getting-started","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fplaywright-mcp-getting-started/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fplaywright-mcp-getting-started/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fplaywright-mcp-getting-started/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/gsaini","download_url":"https://codeload.github.com/gsaini/playwright-mcp-getting-started/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/gsaini%2Fplaywright-mcp-getting-started/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":34342089,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-06-14T02:00:07.365Z","response_time":62,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["biome","browser-automation","demo","e2e-testing","frontend-testing","mcp","model-context-protocol","no-llm","nodejs","playwright","playwright-mcp","pnpm","react","react-router","tailwind-css","tailwindcss","vite"],"created_at":"2026-06-14T23:34:29.201Z","updated_at":"2026-06-14T23:34:29.825Z","avatar_url":"https://github.com/gsaini.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Validating a frontend with Playwright MCP — no LLM in the loop\n\n![Node.js](https://img.shields.io/badge/Node.js-24-339933?style=for-the-badge\u0026logo=node.js\u0026logoColor=white)\n![pnpm](https://img.shields.io/badge/pnpm-11-F69220?style=for-the-badge\u0026logo=pnpm\u0026logoColor=white)\n![React](https://img.shields.io/badge/React-19-61DAFB?style=for-the-badge\u0026logo=react\u0026logoColor=black)\n![Vite](https://img.shields.io/badge/Vite-8-646CFF?style=for-the-badge\u0026logo=vite\u0026logoColor=white)\n![Tailwind](https://img.shields.io/badge/Tailwind-4-38BDF8?style=for-the-badge\u0026logo=tailwindcss\u0026logoColor=white)\n![Playwright](https://img.shields.io/badge/Playwright_MCP-2EAD33?style=for-the-badge\u0026logo=playwright\u0026logoColor=white)\n![MCP](https://img.shields.io/badge/MCP-Compatible-1F6FEB?style=for-the-badge\u0026logo=anthropic\u0026logoColor=white)\n![Biome](https://img.shields.io/badge/Biome-Linted-60A5FA?style=for-the-badge\u0026logo=biome\u0026logoColor=white)\n![Tests](https://img.shields.io/badge/Tests-32_passing-2EA043?style=for-the-badge\u0026logo=githubactions\u0026logoColor=white)\n\nThis demo shows that the **Model Context Protocol** is a transport, not a\nruntime detail of any particular AI. Anything that speaks MCP can drive an MCP\nserver. Here, a plain Node.js script — no Claude, no OpenAI, no inference of\nany kind — drives [`@playwright/mcp`](https://github.com/microsoft/playwright-mcp)\nto run an end-to-end validation of **Nimbus Gear**, a React + Tailwind 4 demo\nstore.\n\n```text\n ┌──────────────────────┐     stdio / JSON-RPC      ┌──────────────────────┐\n │  validator/run.mjs   │ ────────────────────────► │  @playwright/mcp     │\n │  (deterministic)     │ ◄──────────────────────── │  (Chromium driver)   │\n └──────────────────────┘   tools/list, tools/call  └──────────────────────┘\n            │                                                  │\n            │  asserts on returned text / JSON                 │  navigates,\n            ▼                                                  ▼  clicks, types\n   PASS / FAIL summary                              Vite dev server :5173\n                                                    (React 19 + Tailwind 4)\n```\n\nThe validator decides which tool to call next using ordinary control flow,\nexactly as a hand-written E2E test would. The MCP server is the only \"smart\"\npiece — it knows how to drive Chromium.\n\n## The demo app — Nimbus Gear\n\nA small React storefront with:\n\n- Mock username/password auth (demo / demo) with protected routes\n- Product catalogue with **search**, **category filter**, and **sort**\n- Product detail pages with quantity stepper, \"Add to cart\", \"Buy now\"\n- Shopping cart with line-quantity controls, subtotals, and a Remove action\n- Multi-field **checkout** with inline validation\n- Order success page with generated order number\n- **Light / System / Dark** theme toggle (persisted to localStorage)\n\nBuilt with React 19, React Router 7, Vite 8, and Tailwind CSS 4 (CSS-only\ntheming via `@theme` + a `dark` custom variant).\n\n## Layout\n\n| Path | What it is |\n| --- | --- |\n| [app/index.html](app/index.html), [app/vite.config.js](app/vite.config.js) | Vite entrypoint + config |\n| [app/src/main.jsx](app/src/main.jsx) | React root, provider tree |\n| [app/src/App.jsx](app/src/App.jsx) | Router with protected routes |\n| [app/src/routes/](app/src/routes/) | 6 route components (Login, Catalog, ProductDetail, Cart, Checkout, OrderSuccess) |\n| [app/src/components/](app/src/components/) | Header, ThemeToggle, ProductCard, ProtectedRoute |\n| [app/src/hooks/](app/src/hooks/) | `useAuth`, `useCart`, `useTheme` contexts |\n| [app/src/data/products.js](app/src/data/products.js) | In-memory product catalogue |\n| [app/src/styles.css](app/src/styles.css) | Tailwind import + theme tokens (light/dark) |\n| [validator/run.mjs](validator/run.mjs) | Orchestrator — connect, run groups, summarise |\n| [validator/lib/mcp-client.mjs](validator/lib/mcp-client.mjs) | Wraps the official `@modelcontextprotocol/sdk` `Client` |\n| [validator/lib/snapshot.mjs](validator/lib/snapshot.mjs) | Parses the YAML-ish accessibility tree returned by `browser_snapshot` |\n| [validator/lib/helpers.mjs](validator/lib/helpers.mjs) | High-level helpers — `clickByRole`, `typeSelector`, `evaluate`, `setReactInputValue`, … |\n| [validator/lib/harness.mjs](validator/lib/harness.mjs) | Scenario runner + assertions (`assert`, `assertEqual`, `assertContains`) |\n| [validator/scenarios/auth.mjs](validator/scenarios/auth.mjs) | Login, validation, redirect |\n| [validator/scenarios/catalog.mjs](validator/scenarios/catalog.mjs) | Search, filter, sort, navigation |\n| [validator/scenarios/cart.mjs](validator/scenarios/cart.mjs) | Add, quantity, remove, totals |\n| [validator/scenarios/checkout.mjs](validator/scenarios/checkout.mjs) | Form validation, happy path, order number |\n| [validator/scenarios/theme.mjs](validator/scenarios/theme.mjs) | Light / dark / system, persistence |\n| [validator/scenarios/visual.mjs](validator/scenarios/visual.mjs) | Full-page screenshots |\n| [validator/features/*.feature](validator/features/) | Plain-English specs (hand-written) — compile to `.mjs` via `pnpm spec:compile` |\n| [tools/compile-scenarios/](tools/compile-scenarios/) | LLM-powered `.feature` → `.mjs` compiler (Anthropic / Ollama) |\n| [biome.json](biome.json) | Biome config (lint + format) |\n\n## Quick start\n\n```bash\npnpm install\npnpm exec playwright install chromium   # one-time, ~150 MB\npnpm demo                               # spawns Vite, runs all scenarios, tears down\n```\n\nIf Vite is already running and you just want to iterate on tests:\n\n```bash\npnpm app          # terminal 1\npnpm validate     # terminal 2\n```\n\nPass `--headed` to watch the browser:\n\n```bash\nnode validator/run.mjs --start-app --headed\n```\n\nScreenshots land in [screenshots/](screenshots/) (catalogue in light + dark\nthemes, plus a product-detail capture).\n\n### Plain-English specs (optional)\n\nTwo directories, two responsibilities:\n\n```text\nvalidator/features/   intent-level .feature specs   (hand-written; source of truth)\nvalidator/scenarios/  generated .mjs scenarios      (committed; CI runs these)\n```\n\nThe compiler is an **agentic LLM** that drives the live app via Playwright\nMCP at *compile time* to discover the real DOM, then emits a deterministic\n`.mjs`. At *runtime* (`pnpm demo`) the saved `.mjs` runs with no LLM —\nthat's the whole point.\n\n```text\nCOMPILE TIME (occasional)                   RUNTIME (every pnpm demo / CI run)\n┌────────────┐                              ┌──────────────┐\n│ .feature   │ ─────┐                       │ scenarios/   │\n│ (intent)   │      ▼                       │ *.mjs        │\n└────────────┘  ┌────────┐                  │ (committed)  │\n                │  LLM   │                  └──────┬───────┘\n                │ + tools│                         │\n                └───┬────┘                         ▼\n                    │ browser_navigate,    ┌──────────────────┐\n                    ▼ snapshot, click…    │  validator/run   │\n       ┌──────────────────────┐            │  (deterministic) │\n       │  @playwright/mcp     │ ◄───────── └─────┬────────────┘\n       │  ↳ Vite app on :5173 │                  │ same MCP server\n       └──────────────────────┘ ◄────────────────┘ same tools\n                    │\n                    ▼ write_scenario(code)\n              scenarios/*.mjs\n```\n\n#### Compiling\n\n```bash\n# Anthropic (default)\nANTHROPIC_API_KEY=sk-... pnpm spec:compile\n\n# Local Ollama (no API key, no network egress; needs a tool-use-capable model)\npnpm spec:compile:ollama --model=qwen2.5-coder:14b\n\n# Print the plan only — no Vite spawn, no model call\npnpm spec:compile:dry-run\n\n# Subset of features\npnpm spec:compile --only=auth,catalog\n\n# Raise the per-feature tool-use cap if a complex feature needs it\npnpm spec:compile --max-turns=80\n```\n\nThe compiler is intentionally **SDK-agnostic** — it speaks raw HTTP to both\nproviders, so the project carries no `@anthropic-ai/sdk` / `ollama` package\nto track or upgrade.\n\n#### What the compiler does, step by step\n\n1. Spawns `vite app` (the demo app) and `@playwright/mcp` (the browser\n   driver).\n2. For each `.feature` file, navigates the browser to a fresh state and\n   hands the LLM a tool-use loop with these tools:\n   - `browser_navigate`, `browser_snapshot`, `browser_click`,\n     `browser_type`, `browser_press_key`, `browser_wait_for`,\n     `browser_evaluate` — proxied straight through to MCP.\n   - `write_scenario({ code })` — terminal tool. The LLM calls this exactly\n     once when it has explored enough to write a complete `.mjs`.\n3. Captures the `code` argument, prepends an `AUTO-GENERATED` header, and\n   writes `validator/scenarios/\u003cname\u003e.mjs`.\n4. Tears down Vite + MCP.\n\n#### Determinism trade-offs\n\n- **At compile time** the LLM observes a *live* browser. JSX with dynamic\n  classNames, conditional rendering, computed `aria-label` strings — all\n  resolved. The model writes selectors against what it *saw*, not what\n  the source code *says*.\n- **At runtime** the generated `.mjs` is plain code. Same Playwright MCP\n  server, same helpers, no model — every run is identical given the same\n  app build.\n- **Cost** is paid once per compile and amortized over every CI run.\n  Anthropic-side caching makes files 2–N in a batch ~10× cheaper than file 1\n  (look for `cache hit` in the per-feature log line).\n\nEdit the `.feature` and recompile — do not hand-edit the generated `.mjs`.\n\n#### Tagging scenarios\n\nTags use standard Gherkin `@tag` syntax — one or more whitespace-separated\ntags on the line directly above a `Feature:` or `Scenario:`. Tags on\n`Feature:` cascade to every scenario in the file; tags on `Scenario:` are\nlocal to that scenario.\n\nKeep tags to **two axes** that map to real commands. Avoid area tags\n(`@auth`, `@cart`) — the filename already encodes that, and `--only=` covers\nfile-level filtering.\n\n| Axis | Tag | Meaning |\n| --- | --- | --- |\n| Priority | `@smoke` | Runs on every PR — keep the set tiny and fast |\n| Priority | `@regression` | Full nightly / pre-release suite |\n| Priority | `@slow` | Expensive scenarios (e.g. visual diffs) — skip in fast loops |\n| Lifecycle | `@wip` | Compiler skips; not ready for CI |\n| Lifecycle | `@flaky` | Validator soft-fails or retries; under investigation |\n\nExample:\n\n```gherkin\n@smoke\nFeature: Authentication\n  ...\n\n  Scenario: valid credentials redirect to the catalogue\n    ...\n\n  @flaky\n  Scenario: session persists across reloads\n    ...\n```\n\nFilter at the command line — `--tags=` intersects with `--only=`:\n\n```bash\npnpm spec:compile --tags=@smoke              # compile smoke set only\npnpm spec:compile --tags=@smoke,@regression  # union: either tag\npnpm spec:compile --tags=@smoke --tags-not=@flaky  # smoke minus flaky\npnpm validate --tags=@smoke                  # filter the run, not the compile\n```\n\nThe compiler emits resolved tags into the generated `.mjs` as a\n`tags: [...]` array on each `scenario(...)` call, so the validator can\nfilter at run time without re-parsing `.feature` files. Resolved means\ncascade-merged: a `@smoke` `Feature:` with a `@flaky` `Scenario:` ends up\nwith `tags: [\"smoke\", \"flaky\"]`.\n\n**When to skip tags entirely**: if file-level (`--only=auth,catalog`)\ngranularity is always enough, don't introduce tags — they're a second\nfiltering surface that pays off only once you need *sub-file* control\n(one slow scenario inside an otherwise fast feature, a single flaky case\nyou want to quarantine without yanking its siblings).\n\n### Lint \u0026 format\n\n[Biome](https://biomejs.dev) handles JS / JSX / JSON;\n[markdownlint-cli2](https://github.com/DavidAnson/markdownlint-cli2) handles\nMarkdown.\n\n```bash\npnpm lint         # Biome lint\npnpm lint:fix     # Biome lint + autofix\npnpm lint:md      # markdownlint\npnpm lint:md:fix  # markdownlint + autofix\npnpm format       # Biome format (rewrites to canonical style)\npnpm check        # Biome check + markdownlint — CI-friendly, runs both gates\n```\n\nRule configuration:\n\n- Biome: [biome.json](biome.json) — JSX-aware, ES module style, 100-col wrap.\n- markdownlint: [.markdownlint-cli2.jsonc](.markdownlint-cli2.jsonc) — default\n  rule set with `MD013` (line length) and `MD033` (inline HTML) disabled\n  because the README intentionally uses wide prose and shields.io badge\n  markup. `MD024` is restricted to `siblings_only` so the same heading\n  text can appear under different parents. OpenSpec-managed markdown\n  (`.claude/commands/opsx/`, `.claude/skills/openspec-*/`, `openspec/`) is\n  excluded — it has its own `openspec validate`.\n\n## Spec-driven changes (OpenSpec)\n\nLarger changes are planned with\n[OpenSpec](https://github.com/Fission-AI/OpenSpec), a spec-driven-development\nlayer for AI coding assistants. You describe a change in plain English;\nOpenSpec scaffolds a proposal, design, task list, and delta specs under\n`openspec/`. You implement against the tasks, then archive — merging the\ndeltas into the source-of-truth specs in `openspec/specs/`.\n\nIt's wired into Claude Code as slash commands (restart the IDE after install\nto load them):\n\n| Command | What it does |\n| --- | --- |\n| `/opsx:propose \"\u003cidea\u003e\"` | Create a change, generate proposal + design + tasks |\n| `/opsx:apply` | Implement the tasks for a change |\n| `/opsx:archive` | Archive a finished change, merge its delta specs |\n| `/opsx:explore`, `/opsx:sync` | Browse changes / sync deltas into main specs |\n\nOpenSpec is a **devDependency**, so the CLI runs through pnpm:\n\n```bash\npnpm exec openspec list        # active changes\npnpm exec openspec validate    # structural check of specs + changes\n```\n\nThe generated slash commands call `pnpm exec openspec` for the same reason.\nThe [pnpm-workspace.yaml](pnpm-workspace.yaml) entry approves OpenSpec's\ncosmetic postinstall so pnpm 11's build-script gate doesn't block `pnpm exec`.\n\n## MCP-LIVE — authoring with a live browser\n\n**MCP-LIVE** = author / repair compiled tests with a real browser open,\nobserving the real DOM at each step. The `mcp__playwright__*` tool family\ndrives a Playwright browser session against the running app\n(`http://localhost:5173` locally, `https://\u003cenv\u003e.frado.ai` in deployed\nenvironments), takes DOM snapshots, runs `browser_evaluate` to inspect\nspecific nodes, and the resulting locators go straight into the committed\ntest files — [validator/scenarios/](validator/scenarios/) `*.mjs` in this\nrepo (the same role `tests-compiled/**/*.spec.ts` plays in TypeScript\nPlaywright layouts).\n\n**It's not** a separate runtime. The compiled tests still execute via plain\nPlaywright in CI. MCP-LIVE only changes how the tests are *written*.\n\n```text\nAUTHORING (MCP-LIVE)                       RUNTIME (CI / pnpm demo)\n┌──────────────────┐                       ┌──────────────────┐\n│ human or LLM     │                       │ scenarios/*.mjs  │\n│ at the keyboard  │                       │  (committed)     │\n└────────┬─────────┘                       └────────┬─────────┘\n         │ mcp__playwright__*                       │ plain Playwright\n         ▼  (snapshot, evaluate, click)             ▼  (no MCP author loop)\n┌──────────────────┐                       ┌──────────────────┐\n│ live browser     │                       │ headless browser │\n│ on real app      │                       │ on real app      │\n└──────────────────┘                       └──────────────────┘\n         │\n         ▼  paste locator\n   scenarios/*.mjs\n```\n\nTwo forms of the same pattern:\n\n- **Automated** — the [LLM compiler](#what-the-compiler-does-step-by-step)\n  drives the MCP browser, explores, and emits the `.mjs`. Run it when a\n  `.feature` changes.\n- **Manual** — open the MCP browser yourself when a selector goes flaky or\n  you're sketching a new scenario. Snapshot the page, `browser_evaluate`\n  the node you care about, paste the locator into the scenario file. Same\n  tools, same DOM truth — just no model in the loop.\n\nIn both cases the artifact that ships is a plain Playwright file; MCP is\nthe authoring surface, not the runtime.\n\n## What the demo validates\n\nSix feature groups, **32 scenarios** total:\n\n| Group | Scenarios | Coverage |\n| --- | --- | --- |\n| `auth` | 4 | Redirect on no-auth, form fields render, bad creds rejected, good creds redirect |\n| `catalog` | 9 | Initial render, filter pill, search, empty state, sort, out-of-stock badge, deep link |\n| `cart` | 7 | Quantity stepper, add toast, badge sync, multi-product cart, totals math, remove, checkout CTA |\n| `checkout` | 5 | All-empty errors, email format error, valid submission, order number format, cart cleared |\n| `theme` | 4 | Light removes `.dark`, Dark adds it, persistence, System defers to OS |\n| `visual` | 3 | Light + dark catalogue screenshots, product detail screenshot |\n\n## Three patterns for asserting state\n\nThe helpers in [validator/lib/helpers.mjs](validator/lib/helpers.mjs) deliberately\nsupport three interaction styles. Pick whichever fits the element at hand:\n\n**1. Snapshot tree** — `browser_snapshot` returns an ARIA-style tree. Find a\nnode by `role` + `name`. Resilient to layout/CSS changes.\n\n```js\nconst { nodes } = await snapshot(mcp);\nfindOne(nodes, \"heading\", \"Welcome back\");\nawait clickByRole(mcp, \"button\", \"Sign in\");\n```\n\n**2. CSS selector via `target`** — every interactive tool's `target` parameter\nalso accepts a unique selector. Useful when an element has a stable\n`id` / `data-testid` but a noisy accessible name.\n\n```js\nawait clickSelector(mcp, '#filters button[aria-pressed=\"false\"]:nth-child(2)');\nawait typeSelector(mcp, \"#field-email\", \"demo@nimbus.gear\");\n```\n\n**3. Page evaluate** — run JS in the page and JSON-decode the result. Best\nfor precise, structural assertions.\n\n```js\nconst lines = await evaluate(mcp, () =\u003e\n  Array.from(document.querySelectorAll('[data-testid=\"cart-line\"]')).map((li) =\u003e ({\n    id: li.dataset.productId,\n    qty: Number(li.querySelector('[data-testid=\"line-qty\"]').textContent),\n  }))\n);\nassertEqual(lines, [{ id: \"headphones-aurora\", qty: 2 }]);\n```\n\nA fourth helper, `setReactInputValue`, uses the native value setter from\n`HTMLInputElement.prototype` so programmatic value changes correctly trigger\nReact's controlled-input tracker (a well-known React quirk that bites people\ntrying to clear an input via `el.value = \"\"`).\n\n## Why \"no LLM\"?\n\nA few practical reasons to drive Playwright MCP from a non-AI client:\n\n- **CI determinism** — the same inputs always run the same scenarios. No\n  sampling, no token budget, no \"the agent decided to skip a step today.\"\n- **Cost \u0026 speed** — no inference calls. The full 32-scenario suite runs in\n  about 70 seconds, most of which is real browser time.\n- **Auditability** — the test file *is* the spec. Reviewers see exactly what\n  ran, in what order, with what assertions.\n- **MCP server reuse** — your team already runs `@playwright/mcp` for an AI\n  agent? The exact same server now also powers your test suite.\n\nLLM-driven exploration is great for *finding* bugs you didn't know to look\nfor. Deterministic MCP clients are great for *preventing regressions* on bugs\nyou already fixed. They are complementary, not alternatives.\n\n## Extending\n\n- **Add a scenario**: append another `await scenario(\"…\", …)` block in the\n  appropriate file under [validator/scenarios/](validator/scenarios/).\n- **Add a feature group**: create `validator/scenarios/myFeature.mjs`, export\n  a `myFeatureScenarios(mcp, …)` function, re-export it from\n  [validator/scenarios/index.mjs](validator/scenarios/index.mjs), and call\n  it from [validator/run.mjs](validator/run.mjs).\n- **Add a helper**: drop it in [validator/lib/helpers.mjs](validator/lib/helpers.mjs).\n  Keep it generic — anything app-specific belongs in the scenario file.\n- **Validate a different app**: change `APP_URL` (or set the env var) and\n  rewrite the scenarios. None of the plumbing in `lib/` is app-specific.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgsaini%2Fplaywright-mcp-getting-started","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgsaini%2Fplaywright-mcp-getting-started","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgsaini%2Fplaywright-mcp-getting-started/lists"}