{
  "markdown": "<p align=\"right\">\n  <strong>English</strong> · <a href=\"LEEME.md\">Español</a>\n</p>\n\n<h1 align=\"center\">@matware/e2e-runner</h1>\n\n<p align=\"center\">\n  <strong>The AI-native E2E test runner that writes, runs, and debugs tests for you.</strong>\n</p>\n\n<p align=\"center\">\n  <a href=\"https://www.npmjs.com/package/@matware/e2e-runner\"><img src=\"https://img.shields.io/npm/v/@matware/e2e-runner?color=blue\" alt=\"npm version\" /></a>\n  <img src=\"https://img.shields.io/node/v/@matware/e2e-runner\" alt=\"node version\" />\n  <a href=\"https://www.npmjs.com/package/@matware/e2e-runner\"><img src=\"https://img.shields.io/npm/dm/@matware/e2e-runner\" alt=\"npm downloads\" /></a>\n  <a href=\"https://hub.docker.com/r/fastslack/e2e-runner-mcp\"><img src=\"https://img.shields.io/docker/pulls/fastslack/e2e-runner-mcp\" alt=\"Docker pulls\" /></a>\n  <a href=\"https://github.com/fastslack/mtw-e2e-runner/stargazers\"><img src=\"https://img.shields.io/github/stars/fastslack/mtw-e2e-runner\" alt=\"GitHub stars\" /></a>\n  <a href=\"LICENSE\"><img src=\"https://img.shields.io/npm/l/@matware/e2e-runner\" alt=\"license\" /></a>\n  <img src=\"https://img.shields.io/badge/MCP-compatible-green\" alt=\"MCP compatible\" />\n  <img src=\"https://img.shields.io/badge/AI--native-Claude%20Code-blueviolet\" alt=\"AI native\" />\n  <img src=\"https://img.shields.io/badge/AI--native-OpenCode-orange\" alt=\"OpenCode compatible\" />\n  <a href=\"https://skills.sh\"><img src=\"https://img.shields.io/badge/skills.sh-e2e--testing-ff6600\" alt=\"Agent Skills\" /></a>\n</p>\n\n---\n\n**E2E Runner** lets you test your web app without writing test code. Tests are plain JSON — and you don't even have to write that yourself: **just ask Claude Code.**\n\n## 🎬 Write a test by asking — then watch it run\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/demo-live.gif\" alt=\"Live dashboard streaming screenshots as a test suite runs\" width=\"820\" />\n  <br/><sub><em>The live dashboard while a suite runs — every step streams a screenshot into the feed, in real time.</em></sub>\n</p>\n\nWith the built-in [MCP server](https://modelcontextprotocol.io/), creating a test is a conversation — no docs, no syntax to memorize:\n\n> **You:** *Create an E2E test for the login flow and run it.*\n>\n> **Claude Code:** *writes the test, runs it in a real browser, and reports back —*\n> ✅ `login-flow` passed in 2.3s · screenshot saved · no network errors.\n\nBehind the scenes Claude just wrote and ran this. A test is **just JSON** — an ordered list of what a user does:\n\n```json\n[\n  { \"name\": \"login-flow\", \"actions\": [\n    { \"type\": \"goto\", \"value\": \"/login\" },\n    { \"type\": \"type\", \"selector\": \"#email\", \"value\": \"user@test.com\" },\n    { \"type\": \"type\", \"selector\": \"#password\", \"value\": \"secret\" },\n    { \"type\": \"click\", \"text\": \"Sign In\" },\n    { \"type\": \"assert_text\", \"text\": \"Welcome back\" },\n    { \"type\": \"screenshot\", \"value\": \"logged-in.png\" }\n  ]}\n]\n```\n\nNo imports, no `describe`/`it`, no build step. If you can read it you can write it — or just ask.\n\n**Connect it to Claude Code (2 commands):**\n\n```bash\nclaude plugin marketplace add fastslack/mtw-e2e-runner\nclaude plugin install e2e-runner@matware\n```\n\nNow say *\"create a test for X and run it\"* — Claude gets 17 MCP tools, slash commands, and specialized agents.\n\n> Using a different agent (Cursor, Codex, Copilot, [40+ more](https://github.com/vercel-labs/skills#supported-agents))? Install the skill: `npx skills add fastslack/mtw-e2e-runner`\n\n---\n\n## 📖 Contents\n\n|   | Section | What's inside |\n|---|---------|---------------|\n| 🚀 | **[Install &amp; first test](#install)** | npm setup · run with your own Chrome (no Docker), Obscura, or a Docker pool |\n| ✨ | **[What you get](#features)** | feature overview at a glance |\n| ✍️ | **[Writing tests](#writing-tests)** | test format · full action catalog · retries · serial · modules · auth · hooks |\n| 🤖 | **[AI integration](#ai)** | Claude Code · OpenCode · 17 MCP tools · visual verification · issue-to-test |\n| 📊 | **[Dashboard &amp; insights](#dashboard)** | live dashboard · learning system · network logs · screenshot capture |\n| 🌐 | **[Browser drivers](#drivers)** | browserless · cdp · lightpanda · obscura · steel |\n| ⚙️ | **[CLI, config &amp; CI](#reference)** | commands · flags · `e2e.config.js` · GitHub Actions · programmatic API |\n\n---\n\n<a name=\"install\"></a>\n\n## 🚀 Install — it's tiny\n\n```bash\nnpm install --save-dev @matware/e2e-runner\nnpx e2e-runner init        # scaffolds e2e/ with a sample test + config\n```\n\nThen pick how to run the browser. **You don't need Docker** unless you want the parallel pool:\n\n### Option 1 · Use the Chrome you already have — no Docker ⭐\n\nLaunch any Chromium browser with a debugging port, then point the runner at it:\n\n```bash\ngoogle-chrome --headless=new --remote-debugging-port=9222 &   # or brave / chromium / msedge\nCHROME_POOL_URL=http://localhost:9222 POOL_DRIVER=cdp npx e2e-runner run --all\n```\n\nOr bake it into `e2e.config.js` so you never repeat it:\n\n```js\nexport default {\n  baseUrl: 'http://localhost:3000',     // your app — plain localhost, no docker hostname\n  poolUrls: ['http://localhost:9222'],\n  poolDriver: 'cdp',\n};\n```\n\nNothing to install beyond npm, and `baseUrl` is just `localhost` (the browser is on your machine).\n\n### Option 2 · Obscura — one tiny binary, no Docker\n\nA single ~30 MB binary with built-in anti-detection. Install once, run it, point the runner at it:\n\n```bash\nobscura serve --port 9222 --stealth &\nCHROME_POOL_URL=http://localhost:9222 POOL_DRIVER=obscura npx e2e-runner run --all\n```\n\n`npx e2e-runner pool start` (with `poolDriver: 'obscura'` in your config) prints the exact install command for your OS.\n\n### Option 3 · Docker pool — parallel, for CI &amp; big suites\n\nA shared, queue-managed Chrome pool that runs many tests at once:\n\n```bash\nnpx e2e-runner run --all     # the first run auto-starts the Docker pool for you\n```\n\nRequires Docker. Set `baseUrl: 'http://host.docker.internal:3000'` so the containerized Chrome can reach your app.\n\n<details>\n<summary><strong>Why <code>host.docker.internal</code> (Docker option only)?</strong></summary>\n\n<br/>\n\nWith the Docker pool, Chrome runs inside a container, so `localhost` there means the container — not your machine. `host.docker.internal` bridges to your host. On Linux (Docker Engine, not Docker Desktop) add `--add-host=host.docker.internal:host-gateway`, or use your LAN IP. Options 1 &amp; 2 don't have this — the browser is local, so plain `localhost` just works.\n\n</details>\n\n### Write your first test\n\nOpen `e2e/tests/sample.json` — a flow is an ordered list of actions:\n\n```json\n[\n  { \"name\": \"homepage loads\", \"actions\": [\n    { \"type\": \"goto\", \"value\": \"/\" },\n    { \"type\": \"assert_text\", \"text\": \"Welcome\" },\n    { \"type\": \"screenshot\", \"value\": \"home.png\" }\n  ]}\n]\n```\n\nRun it with `npx e2e-runner run --all`. Results — pass/fail, timing, screenshots, network errors — print to your terminal and to the [web dashboard](#dashboard) if it's open.\n\n<details>\n<summary><strong>Add OpenCode</strong> (optional)</summary>\n\n<br/>\n\n```bash\ncp node_modules/@matware/e2e-runner/opencode.json ./\nmkdir -p .opencode && cp -r node_modules/@matware/e2e-runner/.opencode/* .opencode/\n```\n\nSee [OPENCODE.md](OPENCODE.md) for details.\n\n</details>\n\n### Updating\n\nEach install method updates separately — bump the one(s) you use:\n\n```bash\n# npm dependency (per project)\nnpm install --save-dev @matware/e2e-runner@latest\n\n# Claude Code plugin\nclaude plugin update e2e-runner@matware\n\n# MCP-only install (npx caches the package — pin @latest to force a refresh)\nclaude mcp add --transport stdio --scope user e2e-runner \\\n  -- npx -y -p @matware/e2e-runner@latest e2e-runner-mcp\n```\n\n> [!NOTE]\n> Two gotchas: **(1)** `npx` prefers a copy found in the project's `node_modules` over its own cache — if a project pins an old version, the MCP server and dashboard run that old version, so update the project dependency too. **(2)** Already-running processes keep the old code in memory: after updating, restart the dashboard and reconnect the MCP server (`/mcp` → `e2e-runner` → Reconnect, or restart your session).\n\n---\n\n<a name=\"features\"></a>\n\n## ✨ What you get\n\n🧪 **Zero-code tests** — JSON files that anyone on your team can read and write. No JavaScript, no compilation, no framework lock-in.\n\n🤖 **AI-powered testing** — Claude Code creates, executes, and debugs tests natively through 17 MCP tools. Ask it to \"test the checkout flow\" and it builds the JSON, runs it, and reports back.\n\n🐛 **Issue-to-Test pipeline** — Paste a GitHub or GitLab issue URL. The runner fetches it, generates E2E tests, runs them, and tells you: *bug confirmed* or *not reproducible*.\n\n👁️ **Visual verification** — Describe what the page should look like in plain English. The AI captures a screenshot and judges pass/fail against your description. No pixel-diffing setup needed.\n\n🧠 **Learning system** — Tracks test stability across runs. Detects flaky tests, unstable selectors, slow APIs, and error patterns — then surfaces actionable insights.\n\n⚡ **Parallel execution** — Run N tests simultaneously against a shared browser pool (browserless, raw CDP, Lightpanda, Obscura, or Steel). Serial mode available for tests that share state.\n\n🎯 **Pluggable browser drivers** — Pick the engine that fits each test: real Chrome via browserless, Lightpanda or Obscura for fast lightweight runs, Steel for managed sessions. Set `driver` per test or override the whole run with `--driver`.\n\n📊 **Real-time dashboard** — Live execution view, run history with pass-rate charts, screenshot gallery with hash-based search, expandable network request logs.\n\n🔁 **Smart retries** — Test-level and action-level retries with configurable delays. Flaky tests are detected and flagged automatically.\n\n📦 **Reusable modules** — Extract common flows (login, navigation, setup) into parameterized modules and reference them with `$use`.\n\n🏗️ **CI-ready** — JUnit XML output, exit code 1 on failure, auto-captured error screenshots. Drop-in GitHub Actions example included.\n\n🌐 **Multi-project** — One dashboard aggregates test results from all your projects. One Chrome pool serves them all.\n\n🐳 **Portable** — Chrome runs in Docker, tests are JSON files in your repo. Works on any machine with Node.js and Docker.\n\n---\n\n<a name=\"writing-tests\"></a>\n\n## ✍️ Writing tests\n\nEverything about authoring tests — the file format, the full action vocabulary, retries, state isolation, and reuse. Expand what you need:\n\n<details>\n<summary><strong>Test format &amp; file layout</strong></summary>\n\n<br/>\n\nEach `.json` file in `e2e/tests/` contains an array of tests. Each test has a `name` and sequential `actions`:\n\n```json\n[\n  {\n    \"name\": \"homepage-loads\",\n    \"actions\": [\n      { \"type\": \"goto\", \"value\": \"/\" },\n      { \"type\": \"assert_visible\", \"selector\": \"body\" },\n      { \"type\": \"assert_url\", \"value\": \"/\" },\n      { \"type\": \"screenshot\", \"value\": \"homepage.png\" }\n    ]\n  }\n]\n```\n\nSuite files can have numeric prefixes for ordering (`01-auth.json`, `02-dashboard.json`). The `--suite` flag matches with or without the prefix, so `--suite auth` finds `01-auth.json`.\n\n</details>\n\n<details>\n<summary><strong>Action catalog</strong> — navigation, input &amp; interaction</summary>\n\n<br/>\n\n| Action | Fields | Description |\n|--------|--------|-------------|\n| `goto` | `value` | Navigate to URL (relative to `baseUrl` or absolute) |\n| `click` | `selector` or `text` | Click by CSS selector or visible text content. Text mode also takes `scope: \"dialog\"`, `visible: true`, `last: true` |\n| `type` / `fill` | `selector`, `value` | Clear field and type text |\n| `wait` | `selector`, `text`, `gone`, or `value` (ms) | Wait for element/text to appear, for `gone` to disappear (spinner/dialog), or fixed delay. Prefer conditions over fixed `value` sleeps |\n| `screenshot` | `value` (filename) | Capture a screenshot |\n| `select` | `selector`, `value` | Select a dropdown option |\n| `clear` | `selector` | Clear an input field |\n| `press` | `value` | Press a keyboard key (`Enter`, `Tab`, etc.) |\n| `scroll` | `selector` or `value` (px) | Scroll to element or by pixel amount |\n| `hover` | `selector` | Hover over an element |\n| `evaluate` | `value` | Execute JavaScript in the browser context |\n| `navigate` | `value` | Browser navigation (`back`, `forward`, `reload`) |\n| `clear_cookies` | — | Clear all cookies for the current page |\n| `wait_network_idle` | optional `value` (idle ms, default 500), `timeout` | Wait until the network has been idle for `value` ms — useful after actions that trigger background requests |\n| `set_storage` | `value` (`\"key=val\"`), optional `selector: \"session\"` | Set a `localStorage` key (or `sessionStorage` with `selector: \"session\"`) |\n| `gql` | `value` (query), optional `text` (variables JSON), optional `selector` (assertion) | Run a GraphQL query/mutation via in-page `fetch`, with the auth token read from `localStorage`. Fails on GraphQL errors. `selector` is a JS expression asserted against the response `r` (e.g. `\"r.data.users.length > 0\"`). Installs `window.__e2eGql` for later `evaluate` steps |\n\n**Click by text** — when `click` uses `text` instead of `selector`, it searches across common interactive and content elements:\n\n```\nbutton, a, [role=\"button\"], [role=\"tab\"], [role=\"menuitem\"], [role=\"option\"],\n[role=\"listitem\"], div[class*=\"cursor\"], span, li, td, th, label, p, h1-h6\n```\n\n```json\n{ \"type\": \"click\", \"text\": \"Sign In\" }\n```\n\n</details>\n\n<details>\n<summary><strong>Assertions</strong> — verify text, elements, URLs, counts &amp; network</summary>\n\n<br/>\n\n| Action | Fields | Description |\n|--------|--------|-------------|\n| `assert_text` | `text` | Assert text exists anywhere on the page (substring) |\n| `assert_no_text` | `text` | Assert text does NOT appear anywhere on the page — opposite of `assert_text` |\n| `assert_text_in` | `selector`, `text`, optional `value: \"exact\"` | Assert text inside a scoped container. `text` is a case-insensitive regex by default; `value: \"exact\"` switches to case-sensitive substring |\n| `assert_element_text` | `selector`, `text`, optional `value: \"exact\"` | Assert element's text contains (or exactly matches) the expected text |\n| `assert_url` | `value` | Assert current URL path or full URL. Paths (`/dashboard`) compare against pathname only |\n| `assert_visible` | `selector` | Assert element exists and is visible |\n| `assert_not_visible` | `selector` | Assert element is hidden or doesn't exist |\n| `assert_attribute` | `selector`, `value` | Check attribute: `\"type=email\"` for value, `\"disabled\"` for existence |\n| `assert_class` | `selector`, `value` | Assert element has a CSS class |\n| `assert_input_value` | `selector`, `value` | Assert input/select/textarea `.value` contains text |\n| `assert_matches` | `selector`, `value` (regex) | Assert element text matches a regex pattern |\n| `assert_count` | `selector`, `value` | Assert element count: exact (`\"5\"`), or operators (`\">3\"`, `\">=1\"`, `\"<10\"`) |\n| `assert_no_network_errors` | — | Fail if any network requests failed (e.g. `ERR_CONNECTION_REFUSED`) |\n| `assert_storage` | `value` (`\"key\"` or `\"key=expected\"`), optional `selector: \"session\"` | Assert a `localStorage`/`sessionStorage` key exists or has a specific value |\n| `assert_visual` | `value` (golden image), optional `selector`, `text` (max diff, e.g. `\"0.02\"`), `fullPage`, `maskRegions`, `threshold` | Visual regression: compare a screenshot against a golden reference. The first run saves the golden; later runs fail if more pixels differ than the threshold (default 2%) and write a diff image |\n| `get_text` | `selector` | Extract element text (non-assertion, never fails). Result: `{ value: \"...\" }` |\n\n</details>\n\n<details>\n<summary><strong>Framework-aware actions</strong> — React/MUI without <code>evaluate</code> boilerplate</summary>\n\n<br/>\n\nThese actions handle common patterns in React/MUI apps that normally require verbose `evaluate` boilerplate:\n\n| Action | Fields | Description |\n|--------|--------|-------------|\n| `type_react` | `selector`, `value`, optional `blur`, `waitAfter` | Type into React controlled inputs using the native value setter. Dispatches `input` + `change` events so React state updates correctly. `blur: true` commits on blur; `waitAfter: \"<ms>\"` waits after (debounced autocomplete). |\n| `click_regex` | `text` (regex), optional `selector`, optional `value: \"last\"` | Click element whose textContent matches a regex (case-insensitive). Default: first match. Use `value: \"last\"` for last match. |\n| `click_option` | `text` | Click a `[role=\"option\"]` element by text — common in autocomplete/select dropdowns. |\n| `select_combobox` | `text`, optional `selector`, `filter`, `openWait`/`filterWait`/`waitAfter` | Open a MUI Autocomplete/Select, optionally type `filter`, then click the option matching `text`. Falls back across `[role=\"option\"]`, `.MuiAutocomplete-option`, `li.MuiMenuItem-root`. |\n| `focus_autocomplete` | `text` (label text) | Focus an autocomplete input by its label text. Supports MUI and generic `[role=\"combobox\"]`. |\n| `click_chip` | `text` | Click a chip/tag element by text. Searches `[class*=\"Chip\"]`, `[class*=\"chip\"]`, `[data-chip]`. |\n| `click_icon` | `value` (icon id), optional `selector` (scope) | Click an icon by `data-testid`/`data-icon`/`aria-label`/class fragment or SVG `<title>` — MUI, FontAwesome, Heroicons, etc. Clicks the nearest clickable ancestor (button, link, tab). |\n| `click_menu_item` | `text`, optional `selector` (scope) | Click a menu item by text across `[role=\"menuitem\"]`, `.dropdown-item`, `.menu-item`, MUI `MenuItem`. |\n| `click_in_context` | `text` (container text), `selector` (child) | Click a child element inside the smallest container matching `text` — e.g. the delete button of one specific card/row. |\n\n```json\n// Before: 5 lines of evaluate boilerplate\n{ \"type\": \"evaluate\", \"value\": \"const input = document.querySelector('#search'); const nativeSet = Object.getOwnPropertyDescriptor(window.HTMLInputElement.prototype, 'value').set; nativeSet.call(input, 'term'); input.dispatchEvent(new Event('input', {bubbles: true})); input.dispatchEvent(new Event('change', {bubbles: true}));\" }\n\n// After: 1 action\n{ \"type\": \"type_react\", \"selector\": \"#search\", \"value\": \"term\" }\n```\n\n</details>\n\n<details>\n<summary><strong>Multi-tab actions</strong> — popups, OAuth windows &amp; cross-tab flows</summary>\n\n<br/>\n\n| Action | Fields | Description |\n|--------|--------|-------------|\n| `open_tab` | `value` (URL), optional `text` (label) | Open a new tab and navigate to the URL (relative to `baseUrl` or absolute). Label defaults to `tab-<n>` |\n| `switch_tab` | `value` | Switch the active tab by label, numeric index, or title/URL match (regex or substring). `\"default\"` returns to the original tab |\n| `wait_for_tab` | optional `text` (label), `timeout` | Wait for a new tab/popup opened by the app (`window.open`, `target=\"_blank\"`) and make it the active tab |\n| `assert_tab_count` | `value` | Assert the number of open tabs: exact (`\"2\"`) or operators (`\">=2\"`) |\n| `close_tab` | optional `value` (label) | Close the current (or named) tab and switch back to the last remaining one |\n\nAll subsequent actions run in the active tab:\n\n```json\n{ \"type\": \"click\", \"text\": \"Open report\" }\n{ \"type\": \"wait_for_tab\", \"text\": \"report\" }\n{ \"type\": \"assert_text\", \"text\": \"Quarterly results\" }\n{ \"type\": \"close_tab\" }\n```\n\n</details>\n\n<details>\n<summary><strong>Retries &amp; flaky detection</strong></summary>\n\n<br/>\n\n**Test-level retry** — retry an entire test on failure. Set globally via config or per-test:\n\n```json\n{ \"name\": \"flaky-test\", \"retries\": 3, \"timeout\": 15000, \"actions\": [...] }\n```\n\nTests that pass after retry are flagged as **flaky** in the report and learning system.\n\n**Action-level retry** — retry a single action without rerunning the entire test. Useful for timing-sensitive clicks and waits:\n\n```json\n{ \"type\": \"click\", \"selector\": \"#dynamic-btn\", \"retries\": 3 }\n{ \"type\": \"wait\", \"selector\": \".lazy-loaded\", \"retries\": 2 }\n```\n\nSet globally: `actionRetries` in config, `--action-retries <n>` CLI, or `ACTION_RETRIES` env var. Delay between retries: `actionRetryDelay` (default 500ms).\n\n</details>\n\n<details>\n<summary><strong>Serial tests</strong> — for tests that share state</summary>\n\n<br/>\n\nTests that share state (e.g., two tests modifying the same record) can race when running in parallel. Mark them as serial:\n\n```json\n{ \"name\": \"create-patient\", \"serial\": true, \"actions\": [...] }\n{ \"name\": \"verify-patient-list\", \"serial\": true, \"actions\": [...] }\n```\n\nSerial tests run one at a time **after** all parallel tests finish — preventing interference without slowing down independent tests.\n\n</details>\n\n<details>\n<summary><strong>Testing authenticated apps</strong></summary>\n\n<br/>\n\nThe simplest approach — log in via the UI like a real user:\n\n```json\n{\n  \"hooks\": {\n    \"beforeEach\": [\n      { \"type\": \"goto\", \"value\": \"/login\" },\n      { \"type\": \"type\", \"selector\": \"#email\", \"value\": \"test@example.com\" },\n      { \"type\": \"type\", \"selector\": \"#password\", \"value\": \"test-password\" },\n      { \"type\": \"click\", \"text\": \"Sign In\" },\n      { \"type\": \"wait\", \"selector\": \".dashboard\" }\n    ]\n  },\n  \"tests\": [...]\n}\n```\n\nFor SPAs with JWT, skip the login form by injecting the token directly:\n\n```json\n{ \"type\": \"set_storage\", \"value\": \"accessToken=eyJhbGciOiJIUzI1NiIs...\" }\n```\n\nOr set it globally in config:\n\n```js\n// e2e.config.js\nexport default {\n  authToken: 'eyJhbGciOiJIUzI1NiIs...',\n  authStorageKey: 'accessToken',\n};\n```\n\nEach test runs in a **fresh browser context**, so auth state is automatically clean between tests.\n\n> **More strategies:** Cookie-based auth, HTTP header injection, OAuth/SSO bypasses, reusable auth modules, and role-based testing — see [docs/authentication.md](docs/authentication.md)\n\n</details>\n\n<details>\n<summary><strong>Reusable modules</strong> — extract common flows with <code>$use</code></summary>\n\n<br/>\n\nExtract common flows into parameterized modules:\n\n```json\n// e2e/modules/login.json\n{\n  \"$module\": \"login\",\n  \"description\": \"Log in via the UI login form\",\n  \"params\": {\n    \"email\": { \"required\": true, \"description\": \"User email\" },\n    \"password\": { \"required\": true, \"description\": \"User password\" }\n  },\n  \"actions\": [\n    { \"type\": \"goto\", \"value\": \"/login\" },\n    { \"type\": \"type\", \"selector\": \"#email\", \"value\": \"{{email}}\" },\n    { \"type\": \"type\", \"selector\": \"#password\", \"value\": \"{{password}}\" },\n    { \"type\": \"click\", \"text\": \"Sign In\" },\n    { \"type\": \"wait\", \"value\": \"2000\" }\n  ]\n}\n```\n\nUse in tests:\n\n```json\n{\n  \"name\": \"dashboard-loads\",\n  \"actions\": [\n    { \"$use\": \"login\", \"params\": { \"email\": \"user@test.com\", \"password\": \"secret\" } },\n    { \"type\": \"assert_text\", \"text\": \"Dashboard\" }\n  ]\n}\n```\n\nModules support parameter validation (required params fail fast), conditional blocks (`{{#param}}...{{/param}}`), nested composition, and cycle detection.\n\n</details>\n\n<details>\n<summary><strong>Hooks</strong> — beforeAll / beforeEach / afterEach / afterAll</summary>\n\n<br/>\n\nRun actions at lifecycle points. Define globally in config or per-suite:\n\n```json\n{\n  \"hooks\": {\n    \"beforeAll\": [{ \"type\": \"goto\", \"value\": \"/setup\" }],\n    \"beforeEach\": [{ \"type\": \"goto\", \"value\": \"/\" }],\n    \"afterEach\": [{ \"type\": \"screenshot\", \"value\": \"after.png\" }],\n    \"afterAll\": []\n  },\n  \"tests\": [...]\n}\n```\n\n> **Important:** `beforeAll` runs on a separate browser page that is closed before tests start. Use `beforeEach` for state that tests need (cookies, localStorage, auth tokens).\n\n</details>\n\n<details>\n<summary><strong>Exclude patterns</strong> — skip drafts from <code>--all</code></summary>\n\n<br/>\n\nSkip exploratory or draft tests from `--all` runs:\n\n```js\n// e2e.config.js\nexport default {\n  exclude: ['explore-*', 'debug-*', 'draft-*'],\n};\n```\n\nIndividual suite runs (`--suite`) are not affected by exclude patterns.\n\n</details>\n\n---\n\n<a name=\"ai\"></a>\n\n## 🤖 AI integration\n\nThe whole point: your agent writes, runs, and verifies tests for you.\n\n<details>\n<summary><strong>Claude Code</strong> — plugin install &amp; MCP-only install</summary>\n\n<br/>\n\n```bash\nclaude plugin marketplace add fastslack/mtw-e2e-runner\nclaude plugin install e2e-runner@matware\n```\n\nThis gives Claude 17 MCP tools, a workflow skill, 4 slash commands (`/e2e-runner:run`, `/e2e-runner:create-test`, `/e2e-runner:verify-issue`, `/e2e-runner:capture`), and 3 specialized agents (test-analyzer, test-creator, test-improver).\n\n**MCP-only install** (tools only, no skill/commands/agents):\n\n```bash\nclaude mcp add --transport stdio --scope user e2e-runner \\\n  -- npx -y -p @matware/e2e-runner e2e-runner-mcp\n```\n\n</details>\n\n<details>\n<summary><strong>OpenCode</strong></summary>\n\n<br/>\n\n```bash\ncp node_modules/@matware/e2e-runner/opencode.json ./\nmkdir -p .opencode && cp -r node_modules/@matware/e2e-runner/.opencode/* .opencode/\n```\n\nSee [OPENCODE.md](OPENCODE.md) for details.\n\n</details>\n\n<details>\n<summary><strong>The 17 MCP tools</strong></summary>\n\n<br/>\n\n| Tool | Description |\n|------|-------------|\n| `e2e_run` | Run tests (all, by suite, or by file) |\n| `e2e_list` | List available test suites |\n| `e2e_create_test` | Create a new test JSON file |\n| `e2e_create_module` | Create a reusable module |\n| `e2e_pool_status` | Check Chrome pool health |\n| `e2e_app_pool_status` | Inspect the app environment pool (forks, ports, drivers) |\n| `e2e_screenshot` | Retrieve a screenshot by hash |\n| `e2e_capture` | Capture screenshot of any URL |\n| `e2e_analyze` | Extract page structure (interactive elements, forms, headings) and emit test scaffolds |\n| `e2e_dashboard_start` | Start web dashboard |\n| `e2e_dashboard_stop` | Stop web dashboard |\n| `e2e_dashboard_restart` | Restart the dashboard (new project dir/port, clear stale sessions) |\n| `e2e_issue` | Fetch issue and generate tests |\n| `e2e_network_logs` | Query network logs for a run |\n| `e2e_learnings` | Query stability insights |\n| `e2e_vars` | Manage SQLite-backed `{{var.KEY}}` project variables |\n| `e2e_neo4j` | Manage Neo4j knowledge graph |\n\n> Pool start/stop are CLI-only — not exposed via MCP.\n\n</details>\n\n<details>\n<summary><strong>Visual verification</strong> — describe the page, AI judges it</summary>\n\n<br/>\n\nDescribe what the page should look like — AI judges pass/fail from screenshots:\n\n```json\n{\n  \"name\": \"dashboard-loads\",\n  \"expect\": \"Patient list with at least 3 rows, no error messages, sidebar with navigation links\",\n  \"actions\": [\n    { \"type\": \"goto\", \"value\": \"/dashboard\" },\n    { \"type\": \"wait\", \"selector\": \".patient-list\" }\n  ]\n}\n```\n\nAfter test actions complete, the runner auto-captures a verification screenshot. The MCP response includes the screenshot hash — Claude Code retrieves it and visually verifies against your `expect` description. No API key required.\n\n</details>\n\n<details>\n<summary><strong>Issue-to-test</strong> — turn a bug report into a runnable test</summary>\n\n<br/>\n\nTurn GitHub and GitLab issues into executable E2E tests. Paste an issue URL and get runnable tests — automatically.\n\n**How it works:**\n\n1. **Fetch** — Pulls issue details (title, body, labels) via `gh` or `glab` CLI\n2. **Generate** — AI creates JSON test actions based on the issue description\n3. **Run** — Optionally executes the tests immediately to verify if a bug is reproducible\n\n```bash\n# Fetch and display\ne2e-runner issue https://github.com/owner/repo/issues/42\n\n# Generate a test file via Claude API\ne2e-runner issue https://github.com/owner/repo/issues/42 --generate\n\n# Generate + run + report\ne2e-runner issue https://github.com/owner/repo/issues/42 --verify\n# -> \"BUG CONFIRMED\" or \"NOT REPRODUCIBLE\"\n```\n\nIn Claude Code, just ask:\n> \"Fetch issue #42 and create E2E tests for it\"\n\n**Bug verification logic:** Generated tests assert the **correct** behavior. Test failure = bug confirmed. All tests pass = not reproducible.\n\n**Auth:** GitHub requires `gh` CLI, GitLab requires `glab` CLI. Self-hosted GitLab is supported.\n\n</details>\n\n---\n\n<a name=\"dashboard\"></a>\n\n## 📊 Dashboard &amp; insights\n\n```bash\ne2e-runner dashboard                  # Start on default port 8484\ne2e-runner dashboard --port 9090      # Custom port\n```\n\n<details>\n<summary><strong>Web dashboard tour</strong> — live view, history, gallery, pool status</summary>\n\n<br/>\n\n**Live execution** — monitor tests in real-time with step-by-step progress, durations, and active worker count.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-live-running.png\" alt=\"Dashboard - Live test execution\" width=\"800\" />\n</p>\n\n**Test suites** — browse all suites across projects. Run a single suite or all tests with one click.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-suites.png\" alt=\"Dashboard - Test suites grid\" width=\"800\" />\n</p>\n\n**Run history** — track pass-rate trends with the built-in chart. Click any row to expand full detail.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-runs.png\" alt=\"Dashboard - Run history\" width=\"800\" />\n</p>\n\n**Run detail** — PASS/FAIL badges, screenshot thumbnails with copyable hashes (`ss:77c28b5a`), formatted console errors, and network request logs.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-run-detail.png\" alt=\"Dashboard - Run detail\" width=\"800\" />\n</p>\n\n**Screenshot gallery** — browse all captured screenshots with hash search (action, error, and verification captures).\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-screenshots-gallery.png\" alt=\"Dashboard - Screenshot gallery\" width=\"800\" />\n</p>\n\n**Pool status** — Chrome pool health: available slots, running sessions, memory pressure.\n\n<p align=\"center\">\n  <img src=\"https://raw.githubusercontent.com/fastslack/mtw-e2e-runner/main/docs/screenshots/blog-dashboard-pool-status.png\" alt=\"Dashboard - Pool status\" width=\"800\" />\n</p>\n\n</details>\n\n<details>\n<summary><strong>Learning system</strong> — flaky tests, unstable selectors, slow APIs</summary>\n\n<br/>\n\nThe runner learns from every test run — building knowledge about your test suite over time. Query insights via the `e2e_learnings` MCP tool:\n\n| Query | Returns |\n|-------|---------|\n| `summary` | Full health overview: pass rate, flaky tests, unstable selectors, API issues |\n| `flaky` | Tests that pass only after retries |\n| `selectors` | CSS selectors with high failure rates |\n| `pages` | Pages with console errors, network failures, load time issues |\n| `apis` | API endpoints with error rates and latency (auto-normalized: UUIDs, hashes, IDs) |\n| `errors` | Most frequent error patterns, categorized |\n| `trends` | Pass rate over time (auto-switches to hourly when all data is from one day) |\n| `test:<name>` | Drill-down history for a specific test |\n| `page:<path>` | Drill-down history for a specific page |\n| `selector:<value>` | Drill-down history for a specific selector |\n\n**Storage &amp; export:**\n- SQLite (`~/.e2e-runner/dashboard.db`) — default, zero setup\n- Neo4j knowledge graph — optional, for relationship-based analysis. Manage via `e2e_neo4j` MCP tool or `docker compose`\n- Markdown report (`e2e/learnings.md`) — auto-generated after each run\n\n**Test narration:** Each test run generates a human-readable narrative of what happened step by step, visible in the CLI output and the dashboard.\n\n</details>\n\n<details>\n<summary><strong>Network error handling</strong> — assertions, global flag, full logging</summary>\n\n<br/>\n\n**Explicit assertion** — place `assert_no_network_errors` after critical page loads:\n\n```json\n{ \"type\": \"goto\", \"value\": \"/dashboard\" },\n{ \"type\": \"wait\", \"selector\": \".loaded\" },\n{ \"type\": \"assert_no_network_errors\" }\n```\n\n**Global flag** — set `failOnNetworkError: true` to automatically fail any test with network errors:\n\n```bash\ne2e-runner run --all --fail-on-network-error\n```\n\nWhen disabled (default), the runner still collects and reports network errors — the MCP response includes a warning when tests pass but have network errors.\n\n**Full network logging** — all XHR/fetch requests are captured with URL, method, status, duration, request/response headers, and response body (truncated at 50KB). Viewable in the dashboard with expandable request detail rows.\n\nMCP drill-down flow:\n\n```\n1. e2e_run          → compact networkSummary + runDbId\n2. e2e_network_logs(runDbId)                     → all requests (url, method, status, duration)\n3. e2e_network_logs(runDbId, errorsOnly: true)   → only failed requests\n4. e2e_network_logs(runDbId, includeHeaders: true) → with headers\n5. e2e_network_logs(runDbId, includeBodies: true)  → full request/response bodies\n```\n\nThe `e2e_run` response stays compact (~5KB) regardless of how many requests were captured. Use `e2e_network_logs` with the returned `runDbId` to drill into details on demand.\n\n</details>\n\n<details>\n<summary><strong>Screenshot capture</strong> — snapshot any URL on demand</summary>\n\n<br/>\n\nCapture screenshots of any URL on demand — no test suite required:\n\n```bash\ne2e-runner capture https://example.com\ne2e-runner capture https://example.com --full-page --selector \".loaded\" --delay 2000\n```\n\nVia MCP, the `e2e_capture` tool supports `authToken` and `authStorageKey` for authenticated pages — it injects the token into localStorage before navigating.\n\nEvery screenshot gets a deterministic hash (`ss:a3f2b1c9`). Use `e2e_screenshot` to retrieve any screenshot by hash — it returns the image with metadata (test name, step, type).\n\n</details>\n\n---\n\n<a name=\"drivers\"></a>\n\n## 🌐 Browser drivers\n\nThe runner can talk to multiple browser engines through different drivers. The default is **`auto`** — it probes each pool URL and picks the right driver per pool.\n\n| Driver | Engine | Detection probe | When to use |\n|--------|--------|-----------------|-------------|\n| `browserless` | Real Chromium via [browserless](https://www.browserless.io/) | `/pressure` returns JSON | Default. Production-grade JS execution, screencast, full Chrome behavior |\n| `cdp` | Generic CDP-compatible (raw Chrome, etc.) | `/json/version` reachable | Fallback for any CDP server that isn't one of the others |\n| `lightpanda` | [Lightpanda](https://lightpanda.io) (Zig) | `/json/version` Browser=lightpanda | ~9× faster, ~16× less memory than headless Chrome — ideal for high-volume scrape-style tests |\n| `obscura` | [Obscura](https://github.com/h4ckf0r0day/obscura) (Rust + V8) | `/json/version` Browser=obscura | ~30 MB RAM footprint, built-in anti-detection (`--stealth`), stays close to real Chrome via Puppeteer |\n| `steel` | [Steel Browser](https://steel.dev) | `/v1/sessions` returns JSON | Managed session lifecycle, REST API for orchestration |\n\n<details>\n<summary><strong>Pick a driver per test / force one per run</strong></summary>\n\n<br/>\n\n```json\n{\n  \"tests\": [\n    {\n      \"name\": \"checkout flow (heavy JS, real Chrome)\",\n      \"driver\": \"browserless\",\n      \"actions\": [...]\n    },\n    {\n      \"name\": \"scrape product page (lightweight)\",\n      \"driver\": \"obscura\",\n      \"fallbackDriver\": \"cdp\",\n      \"actions\": [...]\n    }\n  ]\n}\n```\n\n`driver` is optional. If set, only pools whose detected driver matches become candidates. `fallbackDriver` is **explicit opt-in** — without it, a missing target driver fails the test with a clear message. Pool busyness does **not** trigger fallback; the runner waits inside the filtered set.\n\nForce a driver for a whole run (CLI overrides win over per-test fields — useful for A/B benchmarks):\n\n```bash\ne2e-runner run --all --driver obscura\ne2e-runner run --all --driver obscura --fallback-driver cdp\n```\n\n</details>\n\n<details>\n<summary><strong>Running each driver locally</strong></summary>\n\n<br/>\n\n```bash\n# browserless (default) — managed by `pool start`\ne2e-runner pool start\n\n# Lightpanda — pool start uses templates/docker-compose-lightpanda.yml\ne2e-runner pool start                 # with poolDriver: 'lightpanda' in config\n\n# Obscura — install the binary and run it yourself\ncurl -LO https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-x86_64-linux.tar.gz\ntar xzf obscura-x86_64-linux.tar.gz\n./obscura serve --port 9222 --stealth\n# then point the runner at it: poolUrls: ['http://localhost:9222'], poolDriver: 'obscura'\n```\n\n</details>\n\n---\n\n<a name=\"reference\"></a>\n\n## ⚙️ CLI, config &amp; CI\n\n<details>\n<summary><strong>CLI commands</strong></summary>\n\n<br/>\n\n```bash\n# Run tests\ne2e-runner run --all                  # All suites\ne2e-runner run --suite auth           # Single suite\ne2e-runner run --tests path/to.json   # Specific file\ne2e-runner run --inline '<json>'      # Inline JSON\n\n# Pool management (CLI only, not MCP)\ne2e-runner pool start                 # Start Chrome container\ne2e-runner pool stop                  # Stop Chrome container\ne2e-runner pool status                # Check pool health\n\n# Issue-to-test\ne2e-runner issue <url>                # Fetch issue\ne2e-runner issue <url> --generate     # Generate test via AI\ne2e-runner issue <url> --verify       # Generate + run + report\n\n# Dashboard\ne2e-runner dashboard                  # Start web dashboard\n\n# Other\ne2e-runner list                       # List available suites\ne2e-runner capture <url>              # On-demand screenshot\ne2e-runner init                       # Scaffold project\n```\n\n</details>\n\n<details>\n<summary><strong>CLI options</strong></summary>\n\n<br/>\n\n| Flag | Default | Description |\n|------|---------|-------------|\n| `--base-url <url>` | `http://host.docker.internal:3000` | Application base URL |\n| `--pool-url <ws>` | `ws://localhost:3333` | Chrome pool WebSocket URL |\n| `--concurrency <n>` | `3` | Parallel test workers |\n| `--retries <n>` | `0` | Retry failed tests N times |\n| `--action-retries <n>` | `0` | Retry failed actions N times |\n| `--test-timeout <ms>` | `60000` | Per-test timeout |\n| `--timeout <ms>` | `10000` | Default action timeout |\n| `--output <format>` | `json` | Report: `json`, `junit`, `both` |\n| `--env <name>` | `default` | Environment profile |\n| `--fail-on-network-error` | `false` | Fail tests with network errors |\n| `--project-name <name>` | dir name | Project display name |\n| `--driver <name>` | _(per-test)_ | Force pool driver for the run: `browserless`, `cdp`, `lightpanda`, `obscura`, `steel` |\n| `--fallback-driver <name>` | _none_ | Explicit fallback if no pool with `--driver` is reachable |\n\n</details>\n\n<details>\n<summary><strong>Configuration</strong> — <code>e2e.config.js</code> &amp; priority</summary>\n\n<br/>\n\nCreate `e2e.config.js` in your project root:\n\n```js\nexport default {\n  baseUrl: 'http://host.docker.internal:3000',\n  concurrency: 4,\n  retries: 2,\n  actionRetries: 1,\n  testTimeout: 30000,\n  outputFormat: 'both',\n  failOnNetworkError: true,\n  exclude: ['explore-*', 'debug-*'],\n\n  hooks: {\n    beforeEach: [{ type: 'goto', value: '/' }],\n  },\n\n  environments: {\n    staging: { baseUrl: 'https://staging.example.com' },\n    production: { baseUrl: 'https://example.com', concurrency: 5 },\n  },\n};\n```\n\n**Config priority (highest wins):**\n\n1. CLI flags\n2. Environment variables\n3. Config file (`e2e.config.js` or `e2e.config.json`)\n4. Defaults\n\nWhen `--env <name>` is set, the matching profile overrides everything.\n\n</details>\n\n<details>\n<summary><strong>CI/CD</strong> — JUnit XML &amp; GitHub Actions</summary>\n\n<br/>\n\n```bash\ne2e-runner run --all --output junit\n```\n\n```yaml\njobs:\n  e2e:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v4\n      - uses: actions/setup-node@v4\n        with:\n          node-version: 20\n      - run: npm ci\n      - run: npx e2e-runner pool start\n      - run: npx e2e-runner run --all --output junit\n      - uses: mikepenz/action-junit-report@v4\n        if: always()\n        with:\n          report_paths: e2e/screenshots/junit.xml\n```\n\n</details>\n\n<details>\n<summary><strong>Programmatic API</strong></summary>\n\n<br/>\n\n```js\nimport { createRunner } from '@matware/e2e-runner';\n\nconst runner = await createRunner({ baseUrl: 'http://localhost:3000' });\n\nconst report = await runner.runAll();\nconst report = await runner.runSuite('auth');\nconst report = await runner.runFile('e2e/tests/login.json');\nconst report = await runner.runTests([\n  { name: 'quick-check', actions: [{ type: 'goto', value: '/' }] },\n]);\n```\n\n</details>\n\n---\n\n## Requirements\n\n- **Node.js** >= 20\n- **Docker** — only for [Option 3](#install) (the parallel Chrome pool). Options 1 &amp; 2 don't need it.\n\n## License\n\nCopyright 2026 Matias Aguirre (fastslack) — Matware\n\nLicensed under the Apache License, Version 2.0. See [LICENSE](LICENSE) for details.\n",
  "bytes": 40829,
  "sha": "875ea7472a18fbcd9282f3a1a861fcb680ad7333cb4477a8135aa9914f10d29f",
  "repo_slug": "fastslack/mtw-e2e-runner",
  "fonte": "repo",
  "truncated": false,
  "api": "https://agentalog.com/api/listings/plg_fastslack_mtw_e2e_runner_mtw_e2e_runner_8edfde18/readme"
}