Skip to main content
{anchor}`: the text node is the title, ::after the pitch, the div the button. */ a[href*='utm_campaign=docs_sidebar_banner'] { display: none; justify-items: start; gap: 0.375rem; margin-block: 0.5rem; padding: 1.75rem 0 0.875rem; position: relative; color: var(--e2e-ink) !important; border-top: 1px solid var(--e2e-hairline); background: transparent; font-size: 0.875rem; font-weight: 500; line-height: 1.3; text-decoration: none; overflow-wrap: anywhere; &::after { content: 'TesterArmy runs your critical flows with AI agents and reports back after every deploy.'; margin-top: 0.25rem; color: var(--e2e-muted); font-size: 0.875rem; font-weight: 400; line-height: 1.5; } & > div { --e2e-pixel-unit: 12px; --e2e-pixel-fill: linear-gradient(var(--e2e-ta-orange), var(--e2e-ta-orange)); all: unset; order: 1; display: inline-flex; align-items: center; height: calc(var(--e2e-pixel-unit) * 3); margin-top: 0.375rem; padding: 0 calc(var(--e2e-pixel-unit) + 0.75rem); box-sizing: border-box; /* !important beats the inline background Mintlify sets on hover. */ background: var(--e2e-pixel-fill) center / calc(100% - var(--e2e-pixel-unit) * 2) 100% no-repeat, var(--e2e-pixel-fill) left bottom / var(--e2e-pixel-unit) calc(var(--e2e-pixel-unit) * 2) no-repeat, var(--e2e-pixel-fill) right top / var(--e2e-pixel-unit) var(--e2e-pixel-unit) no-repeat, var(--e2e-pixel-fill) right bottom / var(--e2e-pixel-unit) var(--e2e-pixel-unit) no-repeat !important; color: var(--e2e-ta-orange-ink); font-family: 'DM Mono', ui-monospace, monospace; font-size: 0.875rem; font-weight: 500; line-height: 1rem; letter-spacing: 0.07em; text-transform: uppercase; white-space: nowrap; & > svg { display: none !important; } &::before { content: 'Start testing'; } } &:hover > div, &:focus-visible > div { background: var(--e2e-pixel-fill) center / calc(100% - var(--e2e-pixel-unit) * 2) 100% no-repeat, var(--e2e-pixel-fill) left top / var(--e2e-pixel-unit) var(--e2e-pixel-unit) no-repeat, var(--e2e-pixel-fill) left bottom / var(--e2e-pixel-unit) var(--e2e-pixel-unit) no-repeat, var(--e2e-pixel-fill) right top / var(--e2e-pixel-unit) calc(var(--e2e-pixel-unit) * 2) no-repeat !important; } &:focus-visible { outline: 2px solid var(--e2e-ink); outline-offset: 2px; } @media (min-width: 1280px) { display: grid; position: fixed; top: 17rem; right: 2rem; z-index: 1; box-sizing: border-box; width: 16.5rem; margin: 0; @supports (anchor-name: --e2e-table-of-contents) and (position-anchor: --e2e-table-of-contents) { position-anchor: --e2e-table-of-contents; top: calc(anchor(bottom) + 0.5rem); right: auto; left: anchor(left); } } } @media (min-width: 1280px) { /* Above the z-21 TOC column so the card is clickable, below the z-30 navbar. */ #sidebar { z-index: 22; } @supports (anchor-name: --e2e-table-of-contents) and (position-anchor: --e2e-table-of-contents) { #table-of-contents { anchor-name: --e2e-table-of-contents; } } html:not(:has(#table-of-contents)) a[href*='utm_campaign=docs_sidebar_banner'] { display: none; } } ul:has(> li > a[href*='utm_campaign=docs_sidebar_banner']) + * { margin-top: 0 !important; } #content h2, #content h3 { letter-spacing: -0.01em; } /* The page title sits under a rule in the foreground color. */ #page-title { padding-top: 1rem; border-top: 2px solid var(--e2e-ink); } /* A hairline above every h2 separates sections. */ #content h2 { padding-top: 1rem; border-top: 1px solid var(--e2e-hairline); } /* Code renders in Geist Mono; the GitHub light/dark themes follow the color scheme (styling.codeblocks). */ code, pre, kbd, samp { font-family: 'Geist Mono', ui-monospace, monospace; } /* The code surface is the page's own grey, not the GitHub theme's blue-grey, and one flat fill: Mintlify frames the code in a tinted 2px border around a panel of another color (white in light mode), which square corners turn into a box in a box. In dark mode it paints the panel from an !important `html.dark .code-block-background`; the `body` in the dark selector is what outranks that rule. */ body .code-block, body .code-group, body .code-block-background, html.dark body .code-block-background { background-color: var(--e2e-surface) !important; } /* The fade behind a code block's floating copy button ends in that same grey. */ body .code-block-fade-overlay, html.dark body .code-block-fade-overlay { background-image: linear-gradient(to right, transparent, var(--e2e-surface) 50%) !important; } /* Callouts are a hairline box in the foreground color; the icon tells a note from a warning, not a hue. */ #content .callout { background-color: transparent !important; border-color: var(--e2e-hairline) !important; } #content .callout, #content .callout * { color: var(--e2e-ink) !important; } /* The assistant panel takes the table-of-contents column. Without a cap the content column grows into that space (816px to 1120px at 1920 wide) and every line rewraps; with it, only the panel moves. */ #content-area { max-width: 816px; margin-right: auto; } #content .setup-prompt { position: relative; margin-block: 1.25rem; border: 1px solid var(--e2e-hairline); border-radius: 0; background: transparent; } #content .setup-prompt-header { display: flex; align-items: center; height: 44px; padding-inline: 1rem; border-bottom: 1px solid var(--e2e-hairline); color: var(--e2e-muted); font-size: 0.8125rem; } #content .setup-prompt .prompt { display: block; margin: 0; padding: 1rem 1rem 0; border: 0; background: transparent; color: var(--e2e-ink); font-size: 0.875rem; line-height: 1.7; } #content .setup-prompt [data-component-part='prompt-description'] { max-height: 132px; overflow: hidden; mask-image: linear-gradient(to bottom, black 45%, transparent); } #content .setup-prompt[data-expanded='true'] [data-component-part='prompt-description'] { max-height: none; mask-image: none; } #content .setup-prompt [data-component-part='prompt-action-copy-button'] { width: 24px; height: 24px; padding: 0; border: 1px solid var(--e2e-hairline); border-radius: 0; background: transparent; color: var(--e2e-muted); } #content .setup-prompt [data-component-part='prompt-action-copy-button'] > [aria-hidden] { font-size: 0; } #content .setup-prompt [data-component-part='prompt-action-copy-button'] > [aria-hidden] > span { gap: 0; } #content .setup-prompt [data-component-part='prompt-action-copy-button'] svg { width: 16px; height: 16px; color: currentColor; } #content .setup-prompt [data-component-part='prompt-action-copy-button'] > [aria-hidden] > span:last-child::before { content: ''; width: 16px; height: 16px; background: currentColor; mask: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='black' stroke-width='1.5'%3E%3Crect x='8' y='8' width='12' height='12' rx='2'/%3E%3Cpath d='M16 8V5a2 2 0 0 0-2-2H5a2 2 0 0 0-2 2v9a2 2 0 0 0 2 2h3'/%3E%3C/svg%3E") center / contain no-repeat; } #content .setup-prompt [data-component-part='prompt-actions-wrapper'] { position: absolute; top: 10px; right: 10px; justify-content: flex-end; } #content .setup-prompt [data-component-part='prompt-action-cursor-button'] { display: inline-flex; height: 24px; border-color: var(--e2e-hairline); border-radius: 0; background: transparent; } #content .setup-prompt-toggle { display: block; width: 100%; padding: 0.5rem 1rem 0.75rem; color: var(--e2e-muted); font-size: 0.8125rem; text-align: center; cursor: pointer; } #content .setup-prompt-toggle:hover { color: var(--e2e-ink); } #content .setup-prompt button:focus-visible { outline: 2px solid var(--e2e-ink); outline-offset: 3px; } /* The provider walkthrough on the Models page: separate boxes sharing one selection (snippets/model-provider.jsx). */ #content .provider-part { margin-block: 1.25rem; border: 1px solid var(--e2e-hairline); color: var(--e2e-ink); } #content .provider-part-header { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 0.75rem; min-height: 44px; padding: 0.375rem 1rem; color: var(--e2e-muted); font-size: 0.8125rem; } #content .provider-part:not(.provider-part-select) .provider-part-header { border-bottom: 1px solid var(--e2e-hairline); } #content .provider-part-select .provider-part-header { padding-block: 0.75rem; } #content .provider-part-provider { color: var(--e2e-ink); } #content .provider-part-header select { flex: 1; min-width: 12rem; height: 2.25rem; padding: 0 2rem 0 0.75rem; border: 1px solid var(--e2e-hairline); border-radius: 0; background: var(--e2e-surface) url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16' fill='none' stroke='rgb(115 115 115)' stroke-width='1.5'%3E%3Cpath d='M4 6l4 4 4-4'/%3E%3C/svg%3E") right 0.625rem center / 14px no-repeat; color: var(--e2e-ink); font: inherit; font-size: 0.875rem; appearance: none; cursor: pointer; } #content .provider-part-header select:hover { border-color: var(--e2e-ink); } #content .provider-part-header option, #content .provider-part-header optgroup { background: var(--e2e-surface); color: var(--e2e-ink); } #content .provider-part-header a { color: var(--e2e-muted); text-decoration: underline; text-underline-offset: 3px; } #content .provider-part-header a:hover { color: var(--e2e-ink); } #content .provider-part-tabs { display: flex; } #content .provider-part-tabs button { padding: 0.125rem 0.5rem; border: 1px solid var(--e2e-hairline); color: var(--e2e-muted); font-size: 0.75rem; cursor: pointer; } #content .provider-part-tabs button + button { border-left: 0; } #content .provider-part-tabs button[aria-pressed='true'] { background: var(--e2e-ink); border-color: var(--e2e-ink); color: var(--e2e-surface); } /* Mintlify's CodeBlock inside a part: the part draws the frame and the header, so the block drops its own margins and border. */ #content .provider-part .code-block { margin: 0 !important; padding: 0 !important; border: 0 !important; } #content .provider-part-text { margin: 0; padding: 0.75rem 1rem; font-size: 0.875rem; } #content .provider-part-note { color: var(--e2e-muted); } #content .provider-part .code-block + .provider-part-note { border-top: 1px solid var(--e2e-hairline); } #content .provider-part button:focus-visible, #content .provider-part select:focus-visible, #content .provider-part a:focus-visible { outline: 2px solid var(--e2e-ink); outline-offset: 2px; } #content .video-card { display: flex; align-items: stretch; gap: 1.25rem; margin-block: 1.25rem; padding-right: 1.25rem; border: 1px solid var(--e2e-hairline); border-radius: 0; color: var(--e2e-ink); text-decoration: none; transition: border-color 200ms ease-out; } #content .video-card:hover, #content .video-card:focus-visible { border-color: var(--e2e-ink); } #content .video-card:focus-visible { outline: 2px solid var(--e2e-ink); outline-offset: 2px; } #content .video-card-thumbnail { position: relative; flex-shrink: 0; width: 200px; /* 16:9 at least, taller when the text wraps, so the image always meets the border. */ min-height: 112.5px; overflow: hidden; background: var(--e2e-surface); } /* hqdefault.jpg is 4:3 with letterbox bars; cover crops them away. */ #content .video-card-thumbnail img { position: absolute; inset: 0; display: block; width: 100%; height: 100%; margin: 0; border-radius: 0; object-fit: cover; } /* Bottom right, clear of the title the thumbnail carries on its left. */ #content .video-card-play { position: absolute; right: 8px; bottom: 8px; z-index: 1; width: 28px; height: 28px; } #content .video-card-text { display: flex; flex: 1; flex-direction: column; justify-content: center; gap: 0.25rem; min-width: 0; padding-block: 0.75rem; } #content .video-card-title { font-size: 0.9375rem; font-weight: 500; line-height: 1.4; } #content .video-card-subtitle { color: var(--e2e-muted); font-size: 0.875rem; font-weight: 400; line-height: 1.5; } #content .video-card-arrow { flex-shrink: 0; align-self: center; width: 14px; height: 14px; color: var(--e2e-muted); transition: color 200ms ease-out; } #content .video-card:hover .video-card-arrow, #content .video-card:focus-visible .video-card-arrow { color: var(--e2e-ink); } #content .video-card-new-tab { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; } @media (prefers-reduced-motion: reduce) { #content .video-card, #content .video-card-arrow { transition: none; } } @media (max-width: 640px) { #content .video-card-thumbnail { width: 128px; min-height: 72px; } #content .video-card-subtitle { display: none; } } /* The "Copy page" menu shows titles only. docs.json requires a description on every custom option and Mintlify renders one under each title, so the second line is hidden here, on the built-in items too. The menu has no id; the custom icons identify it. */ [role='menu']:has(img[src*='/images/contextual/']) [role='menuitem'] { align-items: center; } [role='menu']:has(img[src*='/images/contextual/']) [role='menuitem'] > div:first-child { margin-top: 0; } [role='menu']:has(img[src*='/images/contextual/']) [role='menuitem'] > div.flex-col > div:nth-child(2) { display: none; } /* The custom icons load as , so currentColor inside the SVG resolves to black. Match the built-in icons: the item's text color at half opacity, three quarters on hover. */ [role='menuitem'] img[src*='/images/contextual/'] { opacity: 0.5; } .dark [role='menuitem'] img[src*='/images/contextual/'] { filter: invert(1); } [role='menuitem']:hover img[src*='/images/contextual/'] { opacity: 0.75; } /* Card icons from a file load as too. The Expo mark is drawn black for light mode and inverted to white for dark; Kernel's sits on its own green square and reads on both. */ .dark img[src*='/images/integrations/expo.svg'] { filter: invert(1); }
e2e init installs a skill for writing and running tests, plus MCP configuration for inspecting the app. Coding agents use the CLI to run tests and the reports to investigate failures.

The skill

The copy holds SKILL.md and topic references; the link is the layout npx skills add produces, so one copy serves every agent. Run init again after upgrading e2e to refresh it. Without init, npx skills add tester-army/e2e installs the skill from the repository, and npx e2e guide [topic] prints it from the installed package. The package also ships these docs as .mdx pages in its docs/ directory (node_modules/e2e/docs in a single-package project), so an agent can read any page offline. A link such as /reference/cli is docs/reference/cli.mdx. For agents that need an explicit pointer, add this line to your project instructions. init leaves those files unchanged:
AGENTS.md

How an agent looks at the app

e2e mcp lets the coding agent open and interact with the app. Each agent, subagents included, opens its own session, so several can drive the app at once. Its locate tool checks a screen.* locator against the current app and returns the test code when exactly one element matches. init registers the server in .mcp.json for Claude Code and .cursor/mcp.json for Cursor. To register it with Claude Code by hand:
See the MCP server reference for its tools, session rules, and flags. Run tests and read reports through the CLI.

How an agent writes a test

The skill tells the coding agent to read the config and an existing test first. It uses agent.act for one goal at a time and checks the outcome with expect. Known fields and exact values can use screen directly:
tests/checkout.e2e.ts
The main rules are:
  • One agent.act per goal, followed by an expect. The assertion makes the step’s cache entry eligible for replay, and it is what fails when the agent did the wrong thing.
  • No sleeps. Locators poll, actions wait for their target, and expect retries until its timeout.
  • Secrets through credentials.user(name), declared in the config, never literal in a test.

How an agent runs it

Run one file while iterating. For model configuration and credentials, see Models. Fix CLI, config, and collection errors before retrying a run.

How an agent reads the result

Every run that gets to its tests writes .e2e/report.json. For a report that is easier to read, run:
This writes .e2e/summary.md and a page for each failed or flaky test under .e2e/failures/. Start with the failure page. It includes the failing line, steps, recent model turns, and the screen at failure. When reading JSON, check run.errors for run-level failures, then the last attempt of each failed result. Its error, steps, and artifacts show what failed and how far the test got. --reporter json also prints the JSON report to stdout. For model-call details, use --ai-trace and read the trace with unbox-ai. Avoid opening the raw trace in an agent’s context; it repeats prompts and can be several megabytes.

How an agent treats the cache

A verified agent.act can replay from .e2e/cache/ on later runs. Use --no-cache to check the live agent behavior after changing a test or to investigate a replay failure. Pair it with --ai-trace to capture every model call in the flow. Do not edit recordings by hand. A failed read-write run removes implicated entries so a later verified run can replace them. A failure where no model answered implicates nothing and removes none. See Caching agent steps.

CLI reference

init, guide, mcp, and run, flag by flag.

MCP server

Inspect the app before writing a test.

Debugging a run

The report, the artifacts, and the agent flags.