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); } /* The Next.js wordmark needs the same contrast in dark mode. */ .dark img[src*='/images/examples/nextjs.svg'] { filter: invert(1); } /* Keep the wide Next.js wordmark legible alongside the square marks. */ #content [data-component-part='card-icon']:has(> img[src*='/images/examples/nextjs.svg']), #content img[src*='/images/examples/nextjs.svg'] { width: 80px; }
@e2e-dev/github posts test results as a pull request comment, including failures, source links, and links to workflow artifacts. Reruns update the same comment, and a --last-failed rerun updates it as one run. The reporter also writes the results to the job summary.

Setup

e2e.config.ts
Give the workflow permission to write pull request comments and pass GITHUB_TOKEN to the test step:
.github/workflows/e2e.yml
Upload artifacts even when a retry passes. Each failure’s evidence links open the run’s Artifacts section; inside the download, every test has a directory named after it and the agent that ran it (web/tests_billing.e2e.ts__upgrades/default/attempt-0/), so keep path: .e2e/artifacts and the download lines up with the report. A --last-failed rerun keeps the first run’s directories and puts its own under rerun-1/, so a flaky test’s failed attempt still links to its own files. A project below the checkout root, such as packages/e2e-tests, gets source links under that path. The checkout root is the nearest directory holding .git inside GITHUB_WORKSPACE, so actions/checkout with a path input works too.

The comment

The comment shows test counts and agent usage, followed by each failure’s error, source line, and evidence links. Flaky tests and the full test list are collapsed; the list is one table with a row per file above its tests, and its agent column appears only when the run used the agent. Run-level errors appear above the test failures. Large reports are shortened to fit GitHub’s comment limit. Text from the app under test that GitHub would turn into a mention, an issue link, or a URL is shown as code, so a screen reading @someone or https://... notifies no one and links nowhere. The built-in markdown reporter writes the same summary to .e2e/summary.md.

Rerunning the failed tests

A step that gives failed tests a second chance keeps one comment for both passes:
The comment covers the whole suite, not only the tests the rerun ran again. A test that failed and then passed shows as flaky, a test that failed twice stays failed, one that was interrupted and then passed shows as passed, and the rest keep their first-pass results. The comment stays red while the rerun left out something it owed: a failed test, one --max-failures stopped before it ran, or a failed beforeAll or afterAll whose tests another filter kept out of the rerun. It shows ⏹️ when the rerun left out a test the first pass never finished. The rerun’s own exit code counts only what it ran. A failed attempt’s evidence links to the first run’s files, which the rerun keeps.

Matrix jobs

The reporter identifies a comment by project, workflow, and job. Matrix jobs share those identifiers, so give each matrix entry a distinct key:
Set MATRIX_BROWSER: ${{ matrix.browser }} in the step’s env. The key also appears in the headline, such as e2e chromium: 77 passed. Keep it stable across runs so reruns update the existing comment.

When nothing is posted

The reporter never changes the run’s status or exit code. When it cannot post, it says why in one row under the summary:
The token is read when the run finishes and is excluded from the report. GitHub Enterprise Server uses GITHUB_SERVER_URL and GITHUB_API_URL.