trackmcp
Back to directory
fastslack

mtw-e2e-runner

View on GitHub

JSON-driven E2E test runner with parallel execution, Chrome pool, and Claude Code MCP integration

3 stars JavaScriptOthers Updated Jun 22, 2026
browser-automationbrowserlessclaude-codedockere2e-testingjsonmcpmcp-servermodel-context-protocolnodejspuppeteertest-automationtest-runnervisual-testinglightpanda

Documentation

English ยท

@matware/e2e-runner

The AI-native E2E test runner that writes, runs, and debugs tests for you.


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.

๐ŸŽฌ Write a test by asking โ€” then watch it run

The live dashboard while a suite runs โ€” every step streams a screenshot into the feed, in real time.

With the built-in MCP server, creating a test is a conversation โ€” no docs, no syntax to memorize:

> You: *Create an E2E test for the login flow and run it.*

>

> Claude Code: *writes the test, runs it in a real browser, and reports back โ€”*

> โœ… `login-flow` passed in 2.3s ยท screenshot saved ยท no network errors.

Behind the scenes Claude just wrote and ran this. A test is just JSON โ€” an ordered list of what a user does:

json
[
  { "name": "login-flow", "actions": [
    { "type": "goto", "value": "/login" },
    { "type": "type", "selector": "#email", "value": "user@test.com" },
    { "type": "type", "selector": "#password", "value": "secret" },
    { "type": "click", "text": "Sign In" },
    { "type": "assert_text", "text": "Welcome back" },
    { "type": "screenshot", "value": "logged-in.png" }
  ]}
]

No imports, no `describe`/`it`, no build step. If you can read it you can write it โ€” or just ask.

Connect it to Claude Code (2 commands):

bash
claude plugin marketplace add fastslack/mtw-e2e-runner
claude plugin install e2e-runner@matware

Now say *"create a test for X and run it"* โ€” Claude gets 17 MCP tools, slash commands, and specialized agents.

> Using a different agent (Cursor, Codex, Copilot, 40+ more)? Install the skill: `npx skills add fastslack/mtw-e2e-runner`


๐Ÿ“– Contents

SectionWhat's inside
๐Ÿš€**Install & first test**npm setup ยท run with your own Chrome (no Docker), Obscura, or a Docker pool
โœจ**What you get**feature overview at a glance
โœ๏ธ**Writing tests**test format ยท full action catalog ยท retries ยท serial ยท modules ยท auth ยท hooks
๐Ÿค–**AI integration**Claude Code ยท OpenCode ยท 17 MCP tools ยท visual verification ยท issue-to-test
๐Ÿ“Š**Dashboard & insights**live dashboard ยท learning system ยท network logs ยท screenshot capture
๐ŸŒ**Browser drivers**browserless ยท cdp ยท lightpanda ยท obscura ยท steel
โš™๏ธ**CLI, config & CI**commands ยท flags ยท `e2e.config.js` ยท GitHub Actions ยท programmatic API

๐Ÿš€ Install โ€” it's tiny

bash
npm install --save-dev @matware/e2e-runner
npx e2e-runner init        # scaffolds e2e/ with a sample test + config

Then pick how to run the browser. You don't need Docker unless you want the parallel pool:

Option 1 ยท Use the Chrome you already have โ€” no Docker โญ

Launch any Chromium browser with a debugging port, then point the runner at it:

bash
google-chrome --headless=new --remote-debugging-port=9222 &   # or brave / chromium / msedge
CHROME_POOL_URL=http://localhost:9222 POOL_DRIVER=cdp npx e2e-runner run --all

Or bake it into `e2e.config.js` so you never repeat it:

js
export default {
  baseUrl: 'http://localhost:3000',     // your app โ€” plain localhost, no docker hostname
  poolUrls: ['http://localhost:9222'],
  poolDriver: 'cdp',
};

Nothing to install beyond npm, and `baseUrl` is just `localhost` (the browser is on your machine).

Option 2 ยท Obscura โ€” one tiny binary, no Docker

A single ~30 MB binary with built-in anti-detection. Install once, run it, point the runner at it:

bash
obscura serve --port 9222 --stealth &
CHROME_POOL_URL=http://localhost:9222 POOL_DRIVER=obscura npx e2e-runner run --all

`npx e2e-runner pool start` (with `poolDriver: 'obscura'` in your config) prints the exact install command for your OS.

Option 3 ยท Docker pool โ€” parallel, for CI & big suites

A shared, queue-managed Chrome pool that runs many tests at once:

bash
npx e2e-runner run --all     # the first run auto-starts the Docker pool for you

Requires Docker. Set `baseUrl: 'http://host.docker.internal:3000'` so the containerized Chrome can reach your app.

Why host.docker.internal (Docker option only)?

With 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 & 2 don't have this โ€” the browser is local, so plain `localhost` just works.

Write your first test

Open `e2e/tests/sample.json` โ€” a flow is an ordered list of actions:

json
[
  { "name": "homepage loads", "actions": [
    { "type": "goto", "value": "/" },
    { "type": "assert_text", "text": "Welcome" },
    { "type": "screenshot", "value": "home.png" }
  ]}
]

Run it with `npx e2e-runner run --all`. Results โ€” pass/fail, timing, screenshots, network errors โ€” print to your terminal and to the web dashboard if it's open.

Add OpenCode (optional)

bash
cp node_modules/@matware/e2e-runner/opencode.json ./
mkdir -p .opencode && cp -r node_modules/@matware/e2e-runner/.opencode/* .opencode/

See OPENCODE.md for details.

Updating

Each install method updates separately โ€” bump the one(s) you use:

bash
# npm dependency (per project)
npm install --save-dev @matware/e2e-runner@latest

# Claude Code plugin
claude plugin update e2e-runner@matware

# MCP-only install (npx caches the package โ€” pin @latest to force a refresh)
claude mcp add --transport stdio --scope user e2e-runner \
  -- npx -y -p @matware/e2e-runner@latest e2e-runner-mcp

> [!NOTE]

> 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).


โœจ What you get

๐Ÿงช Zero-code tests โ€” JSON files that anyone on your team can read and write. No JavaScript, no compilation, no framework lock-in.

๐Ÿค– 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.

๐Ÿ› 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*.

๐Ÿ‘๏ธ 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.

๐Ÿง  Learning system โ€” Tracks test stability across runs. Detects flaky tests, unstable selectors, slow APIs, and error patterns โ€” then surfaces actionable insights.

โšก 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.

๐ŸŽฏ 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`.

๐Ÿ“Š Real-time dashboard โ€” Live execution view, run history with pass-rate charts, screenshot gallery with hash-based search, expandable network request logs.

๐Ÿ” Smart retries โ€” Test-level and action-level retries with configurable delays. Flaky tests are detected and flagged automatically.

๐Ÿ“ฆ Reusable modules โ€” Extract common flows (login, navigation, setup) into parameterized modules and reference them with `$use`.

๐Ÿ—๏ธ CI-ready โ€” JUnit XML output, exit code 1 on failure, auto-captured error screenshots. Drop-in GitHub Actions example included.

๐ŸŒ Multi-project โ€” One dashboard aggregates test results from all your projects. One Chrome pool serves them all.

๐Ÿณ Portable โ€” Chrome runs in Docker, tests are JSON files in your repo. Works on any machine with Node.js and Docker.


โœ๏ธ Writing tests

Everything about authoring tests โ€” the file format, the full action vocabulary, retries, state isolation, and reuse. Expand what you need:

Test format & file layout

Each `.json` file in `e2e/tests/` contains an array of tests. Each test has a `name` and sequential `actions`:

json
[
  {
    "name": "homepage-loads",
    "actions": [
      { "type": "goto", "value": "/" },
      { "type": "assert_visible", "selector": "body" },
      { "type": "assert_url", "value": "/" },
      { "type": "screenshot", "value": "homepage.png" }
    ]
  }
]

Suite 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`.

Action catalog โ€” navigation, input & interaction

ActionFieldsDescription
`goto``value`Navigate to URL (relative to `baseUrl` or absolute)
`click``selector` or `text`Click by CSS selector or visible text content. Text mode also takes `scope: "dialog"`, `visible: true`, `last: true`
`type` / `fill``selector`, `value`Clear field and type text
`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
`screenshot``value` (filename)Capture a screenshot
`select``selector`, `value`Select a dropdown option
`clear``selector`Clear an input field
`press``value`Press a keyboard key (`Enter`, `Tab`, etc.)
`scroll``selector` or `value` (px)Scroll to element or by pixel amount
`hover``selector`Hover over an element
`evaluate``value`Execute JavaScript in the browser context
`navigate``value`Browser navigation (`back`, `forward`, `reload`)
`clear_cookies`โ€”Clear all cookies for the current page
`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
`set_storage``value` (`"key=val"`), optional `selector: "session"`Set a `localStorage` key (or `sessionStorage` with `selector: "session"`)
`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

Click by text โ€” when `click` uses `text` instead of `selector`, it searches across common interactive and content elements:

code
button, a, [role="button"], [role="tab"], [role="menuitem"], [role="option"],
[role="listitem"], div[class*="cursor"], span, li, td, th, label, p, h1-h6
json
{ "type": "click", "text": "Sign In" }

Assertions โ€” verify text, elements, URLs, counts & network

ActionFieldsDescription
`assert_text``text`Assert text exists anywhere on the page (substring)
`assert_no_text``text`Assert text does NOT appear anywhere on the page โ€” opposite of `assert_text`
`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
`assert_element_text``selector`, `text`, optional `value: "exact"`Assert element's text contains (or exactly matches) the expected text
`assert_url``value`Assert current URL path or full URL. Paths (`/dashboard`) compare against pathname only
`assert_visible``selector`Assert element exists and is visible
`assert_not_visible``selector`Assert element is hidden or doesn't exist
`assert_attribute``selector`, `value`Check attribute: `"type=email"` for value, `"disabled"` for existence
`assert_class``selector`, `value`Assert element has a CSS class
`assert_input_value``selector`, `value`Assert input/select/textarea `.value` contains text
`assert_matches``selector`, `value` (regex)Assert element text matches a regex pattern
`assert_count``selector`, `value`Assert element count: exact (`"5"`), or operators (`">3"`, `">=1"`, `"

Framework-aware actions โ€” React/MUI without evaluate boilerplate

These actions handle common patterns in React/MUI apps that normally require verbose `evaluate` boilerplate:

ActionFieldsDescription
`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: ""` waits after (debounced autocomplete).
`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.
`click_option``text`Click a `[role="option"]` element by text โ€” common in autocomplete/select dropdowns.
`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`.
`focus_autocomplete``text` (label text)Focus an autocomplete input by its label text. Supports MUI and generic `[role="combobox"]`.
`click_chip``text`Click a chip/tag element by text. Searches `[class*="Chip"]`, `[class*="chip"]`, `[data-chip]`.
`click_icon``value` (icon id), optional `selector` (scope)Click an icon by `data-testid`/`data-icon`/`aria-label`/class fragment or SVG `` โ€” MUI, FontAwesome, Heroicons, etc. Clicks the nearest clickable ancestor (button, link, tab).
`click_menu_item``text`, optional `selector` (scope)Click a menu item by text across `[role="menuitem"]`, `.dropdown-item`, `.menu-item`, MUI `MenuItem`.
`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.
json
// Before: 5 lines of evaluate boilerplate
{ "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}));" }

// After: 1 action
{ "type": "type_react", "selector": "#search", "value": "term" }

Multi-tab actions โ€” popups, OAuth windows & cross-tab flows

ActionFieldsDescription
`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-`
`switch_tab``value`Switch the active tab by label, numeric index, or title/URL match (regex or substring). `"default"` returns to the original tab
`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
`assert_tab_count``value`Assert the number of open tabs: exact (`"2"`) or operators (`">=2"`)
`close_tab`optional `value` (label)Close the current (or named) tab and switch back to the last remaining one

All subsequent actions run in the active tab:

json
{ "type": "click", "text": "Open report" }
{ "type": "wait_for_tab", "text": "report" }
{ "type": "assert_text", "text": "Quarterly results" }
{ "type": "close_tab" }

Retries & flaky detection

Test-level retry โ€” retry an entire test on failure. Set globally via config or per-test:

json
{ "name": "flaky-test", "retries": 3, "timeout": 15000, "actions": [...] }

Tests that pass after retry are flagged as flaky in the report and learning system.

Action-level retry โ€” retry a single action without rerunning the entire test. Useful for timing-sensitive clicks and waits:

json
{ "type": "click", "selector": "#dynamic-btn", "retries": 3 }
{ "type": "wait", "selector": ".lazy-loaded", "retries": 2 }

Set globally: `actionRetries` in config, `--action-retries ` CLI, or `ACTION_RETRIES` env var. Delay between retries: `actionRetryDelay` (default 500ms).

Serial tests โ€” for tests that share state

Tests that share state (e.g., two tests modifying the same record) can race when running in parallel. Mark them as serial:

json
{ "name": "create-patient", "serial": true, "actions": [...] }
{ "name": "verify-patient-list", "serial": true, "actions": [...] }

Serial tests run one at a time after all parallel tests finish โ€” preventing interference without slowing down independent tests.

Testing authenticated apps

The simplest approach โ€” log in via the UI like a real user:

json
{
  "hooks": {
    "beforeEach": [
      { "type": "goto", "value": "/login" },
      { "type": "type", "selector": "#email", "value": "test@example.com" },
      { "type": "type", "selector": "#password", "value": "test-password" },
      { "type": "click", "text": "Sign In" },
      { "type": "wait", "selector": ".dashboard" }
    ]
  },
  "tests": [...]
}

For SPAs with JWT, skip the login form by injecting the token directly:

json
{ "type": "set_storage", "value": "accessToken=eyJhbGciOiJIUzI1NiIs..." }

Or set it globally in config:

js
// e2e.config.js
export default {
  authToken: 'eyJhbGciOiJIUzI1NiIs...',
  authStorageKey: 'accessToken',
};

Each test runs in a fresh browser context, so auth state is automatically clean between tests.

> More strategies: Cookie-based auth, HTTP header injection, OAuth/SSO bypasses, reusable auth modules, and role-based testing โ€” see docs/authentication.md

Reusable modules โ€” extract common flows with $use

Extract common flows into parameterized modules:

json
// e2e/modules/login.json
{
  "$module": "login",
  "description": "Log in via the UI login form",
  "params": {
    "email": { "required": true, "description": "User email" },
    "password": { "required": true, "description": "User password" }
  },
  "actions": [
    { "type": "goto", "value": "/login" },
    { "type": "type", "selector": "#email", "value": "{{email}}" },
    { "type": "type", "selector": "#password", "value": "{{password}}" },
    { "type": "click", "text": "Sign In" },
    { "type": "wait", "value": "2000" }
  ]
}

Use in tests:

json
{
  "name": "dashboard-loads",
  "actions": [
    { "$use": "login", "params": { "email": "user@test.com", "password": "secret" } },
    { "type": "assert_text", "text": "Dashboard" }
  ]
}

Modules support parameter validation (required params fail fast), conditional blocks (`{{#param}}...{{/param}}`), nested composition, and cycle detection.

Hooks โ€” beforeAll / beforeEach / afterEach / afterAll

Run actions at lifecycle points. Define globally in config or per-suite:

json
{
  "hooks": {
    "beforeAll": [{ "type": "goto", "value": "/setup" }],
    "beforeEach": [{ "type": "goto", "value": "/" }],
    "afterEach": [{ "type": "screenshot", "value": "after.png" }],
    "afterAll": []
  },
  "tests": [...]
}

> 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).

Exclude patterns โ€” skip drafts from --all

Skip exploratory or draft tests from `--all` runs:

js
// e2e.config.js
export default {
  exclude: ['explore-*', 'debug-*', 'draft-*'],
};

Individual suite runs (`--suite`) are not affected by exclude patterns.


๐Ÿค– AI integration

The whole point: your agent writes, runs, and verifies tests for you.

Claude Code โ€” plugin install & MCP-only install

bash
claude plugin marketplace add fastslack/mtw-e2e-runner
claude plugin install e2e-runner@matware

This 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).

MCP-only install (tools only, no skill/commands/agents):

bash
claude mcp add --transport stdio --scope user e2e-runner \
  -- npx -y -p @matware/e2e-runner e2e-runner-mcp

OpenCode

bash
cp node_modules/@matware/e2e-runner/opencode.json ./
mkdir -p .opencode && cp -r node_modules/@matware/e2e-runner/.opencode/* .opencode/

See OPENCODE.md for details.

The 17 MCP tools

ToolDescription
`e2e_run`Run tests (all, by suite, or by file)
`e2e_list`List available test suites
`e2e_create_test`Create a new test JSON file
`e2e_create_module`Create a reusable module
`e2e_pool_status`Check Chrome pool health
`e2e_app_pool_status`Inspect the app environment pool (forks, ports, drivers)
`e2e_screenshot`Retrieve a screenshot by hash
`e2e_capture`Capture screenshot of any URL
`e2e_analyze`Extract page structure (interactive elements, forms, headings) and emit test scaffolds
`e2e_dashboard_start`Start web dashboard
`e2e_dashboard_stop`Stop web dashboard
`e2e_dashboard_restart`Restart the dashboard (new project dir/port, clear stale sessions)
`e2e_issue`Fetch issue and generate tests
`e2e_network_logs`Query network logs for a run
`e2e_learnings`Query stability insights
`e2e_vars`Manage SQLite-backed `{{var.KEY}}` project variables
`e2e_neo4j`Manage Neo4j knowledge graph

> Pool start/stop are CLI-only โ€” not exposed via MCP.

Visual verification โ€” describe the page, AI judges it

Describe what the page should look like โ€” AI judges pass/fail from screenshots:

json
{
  "name": "dashboard-loads",
  "expect": "Patient list with at least 3 rows, no error messages, sidebar with navigation links",
  "actions": [
    { "type": "goto", "value": "/dashboard" },
    { "type": "wait", "selector": ".patient-list" }
  ]
}

After 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.

Issue-to-test โ€” turn a bug report into a runnable test

Turn GitHub and GitLab issues into executable E2E tests. Paste an issue URL and get runnable tests โ€” automatically.

How it works:

1. Fetch โ€” Pulls issue details (title, body, labels) via `gh` or `glab` CLI

2. Generate โ€” AI creates JSON test actions based on the issue description

3. Run โ€” Optionally executes the tests immediately to verify if a bug is reproducible

bash
# Fetch and display
e2e-runner issue https://github.com/owner/repo/issues/42

# Generate a test file via Claude API
e2e-runner issue https://github.com/owner/repo/issues/42 --generate

# Generate + run + report
e2e-runner issue https://github.com/owner/repo/issues/42 --verify
# -> "BUG CONFIRMED" or "NOT REPRODUCIBLE"

In Claude Code, just ask:

> "Fetch issue #42 and create E2E tests for it"

Bug verification logic: Generated tests assert the correct behavior. Test failure = bug confirmed. All tests pass = not reproducible.

Auth: GitHub requires `gh` CLI, GitLab requires `glab` CLI. Self-hosted GitLab is supported.


๐Ÿ“Š Dashboard & insights

bash
e2e-runner dashboard                  # Start on default port 8484
e2e-runner dashboard --port 9090      # Custom port

Web dashboard tour โ€” live view, history, gallery, pool status

Live execution โ€” monitor tests in real-time with step-by-step progress, durations, and active worker count.

Test suites โ€” browse all suites across projects. Run a single suite or all tests with one click.

Run history โ€” track pass-rate trends with the built-in chart. Click any row to expand full detail.

Run detail โ€” PASS/FAIL badges, screenshot thumbnails with copyable hashes (`ss:77c28b5a`), formatted console errors, and network request logs.

Screenshot gallery โ€” browse all captured screenshots with hash search (action, error, and verification captures).

Pool status โ€” Chrome pool health: available slots, running sessions, memory pressure.

Learning system โ€” flaky tests, unstable selectors, slow APIs

The runner learns from every test run โ€” building knowledge about your test suite over time. Query insights via the `e2e_learnings` MCP tool:

QueryReturns
`summary`Full health overview: pass rate, flaky tests, unstable selectors, API issues
`flaky`Tests that pass only after retries
`selectors`CSS selectors with high failure rates
`pages`Pages with console errors, network failures, load time issues
`apis`API endpoints with error rates and latency (auto-normalized: UUIDs, hashes, IDs)
`errors`Most frequent error patterns, categorized
`trends`Pass rate over time (auto-switches to hourly when all data is from one day)
`test:`Drill-down history for a specific test
`page:`Drill-down history for a specific page
`selector:`Drill-down history for a specific selector

Storage & export:

  • SQLite (`~/.e2e-runner/dashboard.db`) โ€” default, zero setup
  • Neo4j knowledge graph โ€” optional, for relationship-based analysis. Manage via `e2e_neo4j` MCP tool or `docker compose`
  • Markdown report (`e2e/learnings.md`) โ€” auto-generated after each run

Test narration: Each test run generates a human-readable narrative of what happened step by step, visible in the CLI output and the dashboard.

Network error handling โ€” assertions, global flag, full logging

Explicit assertion โ€” place `assert_no_network_errors` after critical page loads:

json
{ "type": "goto", "value": "/dashboard" },
{ "type": "wait", "selector": ".loaded" },
{ "type": "assert_no_network_errors" }

Global flag โ€” set `failOnNetworkError: true` to automatically fail any test with network errors:

bash
e2e-runner run --all --fail-on-network-error

When disabled (default), the runner still collects and reports network errors โ€” the MCP response includes a warning when tests pass but have network errors.

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.

MCP drill-down flow:

code
1. e2e_run          โ†’ compact networkSummary + runDbId
2. e2e_network_logs(runDbId)                     โ†’ all requests (url, method, status, duration)
3. e2e_network_logs(runDbId, errorsOnly: true)   โ†’ only failed requests
4. e2e_network_logs(runDbId, includeHeaders: true) โ†’ with headers
5. e2e_network_logs(runDbId, includeBodies: true)  โ†’ full request/response bodies

The `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.

Screenshot capture โ€” snapshot any URL on demand

Capture screenshots of any URL on demand โ€” no test suite required:

bash
e2e-runner capture https://example.com
e2e-runner capture https://example.com --full-page --selector ".loaded" --delay 2000

Via MCP, the `e2e_capture` tool supports `authToken` and `authStorageKey` for authenticated pages โ€” it injects the token into localStorage before navigating.

Every 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).


๐ŸŒ Browser drivers

The 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.

DriverEngineDetection probeWhen to use
`browserless`Real Chromium via browserless`/pressure` returns JSONDefault. Production-grade JS execution, screencast, full Chrome behavior
`cdp`Generic CDP-compatible (raw Chrome, etc.)`/json/version` reachableFallback for any CDP server that isn't one of the others
`lightpanda`Lightpanda (Zig)`/json/version` Browser=lightpanda~9ร— faster, ~16ร— less memory than headless Chrome โ€” ideal for high-volume scrape-style tests
`obscura`Obscura (Rust + V8)`/json/version` Browser=obscura~30 MB RAM footprint, built-in anti-detection (`--stealth`), stays close to real Chrome via Puppeteer
`steel`Steel Browser`/v1/sessions` returns JSONManaged session lifecycle, REST API for orchestration

Pick a driver per test / force one per run

json
{
  "tests": [
    {
      "name": "checkout flow (heavy JS, real Chrome)",
      "driver": "browserless",
      "actions": [...]
    },
    {
      "name": "scrape product page (lightweight)",
      "driver": "obscura",
      "fallbackDriver": "cdp",
      "actions": [...]
    }
  ]
}

`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.

Force a driver for a whole run (CLI overrides win over per-test fields โ€” useful for A/B benchmarks):

bash
e2e-runner run --all --driver obscura
e2e-runner run --all --driver obscura --fallback-driver cdp

Running each driver locally

bash
# browserless (default) โ€” managed by `pool start`
e2e-runner pool start

# Lightpanda โ€” pool start uses templates/docker-compose-lightpanda.yml
e2e-runner pool start                 # with poolDriver: 'lightpanda' in config

# Obscura โ€” install the binary and run it yourself
curl -LO https://github.com/h4ckf0r0day/obscura/releases/latest/download/obscura-x86_64-linux.tar.gz
tar xzf obscura-x86_64-linux.tar.gz
./obscura serve --port 9222 --stealth
# then point the runner at it: poolUrls: ['http://localhost:9222'], poolDriver: 'obscura'

โš™๏ธ CLI, config & CI

CLI commands

bash
# Run tests
e2e-runner run --all                  # All suites
e2e-runner run --suite auth           # Single suite
e2e-runner run --tests path/to.json   # Specific file
e2e-runner run --inline ''      # Inline JSON

# Pool management (CLI only, not MCP)
e2e-runner pool start                 # Start Chrome container
e2e-runner pool stop                  # Stop Chrome container
e2e-runner pool status                # Check pool health

# Issue-to-test
e2e-runner issue                 # Fetch issue
e2e-runner issue  --generate     # Generate test via AI
e2e-runner issue  --verify       # Generate + run + report

# Dashboard
e2e-runner dashboard                  # Start web dashboard

# Other
e2e-runner list                       # List available suites
e2e-runner capture               # On-demand screenshot
e2e-runner init                       # Scaffold project

CLI options

FlagDefaultDescription
`--base-url ``http://host.docker.internal:3000`Application base URL
`--pool-url ``ws://localhost:3333`Chrome pool WebSocket URL
`--concurrency ``3`Parallel test workers
`--retries ``0`Retry failed tests N times
`--action-retries ``0`Retry failed actions N times
`--test-timeout ``60000`Per-test timeout
`--timeout ``10000`Default action timeout
`--output ``json`Report: `json`, `junit`, `both`
`--env ``default`Environment profile
`--fail-on-network-error``false`Fail tests with network errors
`--project-name `dir nameProject display name
`--driver `_(per-test)_Force pool driver for the run: `browserless`, `cdp`, `lightpanda`, `obscura`, `steel`
`--fallback-driver `_none_Explicit fallback if no pool with `--driver` is reachable

Configuration โ€” e2e.config.js & priority

Create `e2e.config.js` in your project root:

js
export default {
  baseUrl: 'http://host.docker.internal:3000',
  concurrency: 4,
  retries: 2,
  actionRetries: 1,
  testTimeout: 30000,
  outputFormat: 'both',
  failOnNetworkError: true,
  exclude: ['explore-*', 'debug-*'],

  hooks: {
    beforeEach: [{ type: 'goto', value: '/' }],
  },

  environments: {
    staging: { baseUrl: 'https://staging.example.com' },
    production: { baseUrl: 'https://example.com', concurrency: 5 },
  },
};

Config priority (highest wins):

1. CLI flags

2. Environment variables

3. Config file (`e2e.config.js` or `e2e.config.json`)

4. Defaults

When `--env ` is set, the matching profile overrides everything.

CI/CD โ€” JUnit XML & GitHub Actions

bash
e2e-runner run --all --output junit
yaml
jobs:
  e2e:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
      - run: npm ci
      - run: npx e2e-runner pool start
      - run: npx e2e-runner run --all --output junit
      - uses: mikepenz/action-junit-report@v4
        if: always()
        with:
          report_paths: e2e/screenshots/junit.xml

Programmatic API

js
import { createRunner } from '@matware/e2e-runner';

const runner = await createRunner({ baseUrl: 'http://localhost:3000' });

const report = await runner.runAll();
const report = await runner.runSuite('auth');
const report = await runner.runFile('e2e/tests/login.json');
const report = await runner.runTests([
  { name: 'quick-check', actions: [{ type: 'goto', value: '/' }] },
]);

Requirements

  • Node.js >= 20
  • Docker โ€” only for Option 3 (the parallel Chrome pool). Options 1 & 2 don't need it.

License

Copyright 2026 Matias Aguirre (fastslack) โ€” Matware

Licensed under the Apache License, Version 2.0. See LICENSE for details.

Frequently asked questions

What is mtw-e2e-runner?

mtw-e2e-runner is JSON-driven E2E test runner with parallel execution, Chrome pool, and Claude Code MCP integration

How do I install mtw-e2e-runner?

Open the GitHub repository and follow its README. Most MCP servers are added to your client's MCP config, then called by your agent.

Is mtw-e2e-runner open source?

Yes โ€” it is hosted on GitHub at https://github.com/fastslack/mtw-e2e-runner and has 3 stars.

Related MCP tools

Run your own MCP server? See who uses it and what to fix.

Measure it with TrackMCP