Install any skill in seconds. Free to start, no credit card required.
Get Started Free →Captures reproducible evidence screenshots and browser states from web apps in Amp orbs with Playwright, mock authentication, mocked APIs, supervised preview services, and visual verification. Use when a UI change needs review PNGs, browser-state evidence, or reliable Radix Select and AlertDialog captures; especially when portal locators detach, overlays animate, or screenshots must come from an existing single-worker E2E flow.
.claude/skills/minpeter-capturing-ui-screenshots/SKILL.md| Test case | Without → With | Effect | Δ tokens | Δ turns |
|---|---|---|---|---|
| case-04 | ✗→✓ | ▲ Improved | 85% | 0% |
| case-06 | ✗→✓ | ▲ Improved | 59% | 0% |
| case-07 | ✗→✓ | ▲ Improved | 45% | 0% |
| case-08 | ✗→✓ | ▲ Improved | 75% | 0% |
| case-09 | ✗→✓ | ▲ Improved | 75% | 0% |
Produce reviewable PNG evidence from a deterministic local app state. Build, serve, check, and capture sequentially; mock every external dependency; inspect the images; then remove capture-only code and stop the service.
must use the service's private loopback URL from amp orb service status.
fullPage: changing page dimensionscan alter layout and stale overlay positioning.
final opacity. A roughly 500 ms delay is allowed only after those waits to let a known overlay animation and paint settle.
launch parallel captures.
Read the repository's own scripts and E2E setup first. Use its existing E2E-mode build and mock-auth contract rather than inventing production credentials. Run the build by itself and wait for success before starting the preview, for example:
bashpnpm build --mode e2e
Adapt the command to the repository. The requirements are an optimized preview build, E2E/mock-auth mode enabled at build time when the app requires it, and no concurrent build, server, test, or capture jobs.
If Chromium is missing for the installed Playwright revision, install exactly that revision through the project dependency before capturing:
bashnpx playwright install chromium
Do not pin a separate Playwright version.
Start a supervised orb service that consumes Amp's assigned $PORT. Keep the variable expansion inside the service command:
bashamp orb service start ui-capture --portal --command 'pnpm preview --host 0.0.0.0 --port "$PORT" --strictPort' amp orb service status ui-capture amp orb service logs ui-capture
Use the repository's package manager and preview script. Read the assigned port from service status, then require an HTTP 200 over loopback before Playwright starts:
bashcurl -fsS -o /dev/null -w '%{http_code}\n' http://127.0.0.1:<assigned-port>/
The result must be 200. Diagnose status and logs before retrying. Share the portal URL with a human reviewer, but do not make internal Playwright traverse the portal proxy.
Prefer a one-shot script over a new test suite. Put it in a temporary, gitignored location or create a clearly temporary script in the checkout and delete it after capture. Keep screenshots under .amp/in/artifacts/.
Mock authentication before the first navigation with page.addInitScript. Mock network APIs with specific routes and route.fulfill; an auth route must match only /auth/me, not invites or another auth subroute. Use reserved example.com names in reusable examples:
jsimport { chromium } from "playwright"; const baseURL = process.env.CAPTURE_BASE_URL; if (!baseURL) throw new Error("CAPTURE_BASE_URL is required"); const browser = await chromium.launch(); try { const context = await browser.newContext({ viewport: { width: 1440, height: 1000 }, }); const page = await context.newPage(); await page.addInitScript(() => { localStorage.setItem( "example-auth", JSON.stringify({ user: { id: "example-user", email: "you@example.com" } }), ); }); await page.route( /^https:\/\/api\.example\.com\/auth\/me(?:\?.*)?$/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ id: "example-user", email: "you@example.com" }), }), ); await page.route( /^https:\/\/api\.example\.com\/settings\/options(?:\?.*)?$/, (route) => route.fulfill({ status: 200, contentType: "application/json", body: JSON.stringify({ options: ["Default", "Final option"] }), }), ); // Add the app-specific capture sequence from §4 here. } finally { await browser.close(); }
Adapt storage keys, response shapes, and endpoint origins to the app. Keep route regexes anchored and specific. A broad pattern such as **/auth/** can swallow invite subroutes and create misleading UI state.
If captures run inside an existing Playwright suite, preserve serial execution:
tsexport default defineConfig({ fullyParallel: false, workers: 1, });
Also pass --workers=1 when the command permits it. Do not add retries to hide unstable state.
Use a helper whose first act is a fresh navigation. The stable sequence is:
page.goto() the target route and wait for its final app-ready element.it visible and enabled, then click it.
exposes transition styles, also wait for the Select content to be opaque.
viewport screenshot of the open dropdown.
element handle or locator state from before navigation.
Apply, wait for the AlertDialog and its exact warning text to bevisible, then wait about 500 ms and capture the viewport dialog screenshot.
An adaptable helper looks like this:
jsasync function openFinalOption(page) { await page.goto(`${baseURL}/settings`, { waitUntil: "domcontentloaded" }); await page.getByRole("heading", { name: "Example settings", exact: true }) .waitFor({ state: "visible" }); const trigger = page.getByRole("combobox", { name: "Example option" }); await trigger.scrollIntoViewIfNeeded(); await trigger.waitFor({ state: "visible" }); if (!(await trigger.isEnabled())) throw new Error("Select trigger is disabled"); await trigger.click(); const option = page.getByRole("option", { name: "Final option", exact: true }); await option.waitFor({ state: "visible" }); await page.waitForTimeout(500); // Radix overlay animation and final paint only. return option; } await openFinalOption(page); await page.screenshot({ path: ".amp/in/artifacts/select-open.png" }); const option = await openFinalOption(page); // Fresh page state and fresh locators. await option.click(); await page.getByRole("button", { name: "Apply", exact: true }).click(); const dialog = page.getByRole("alertdialog"); await dialog.waitFor({ state: "visible" }); await dialog.getByText("This change affects current sessions.", { exact: true }) .waitFor({ state: "visible" }); await page.waitForTimeout(500); // Dialog animation and final paint only. await page.screenshot({ path: ".amp/in/artifacts/confirm-dialog.png" });
Do not keep retrying a detached portal locator. Detachment means the browsing context or overlay was replaced; repeating the same click indefinitely cannot repair it.
Select, and continue from known state.
single-worker E2E test rather than rebuilding its setup.
Enter, and still wait for the exact option/dialog state.
final overlay to be visible and opaque before a narrowly justified settle.
If those fail, stop and inspect the app state, service logs, route specificity, and screenshot. Do not respond with unbounded locator retries or parallel workers.
view_media on every PNG. Verify the intended overlay is open, labelsare legible, no loading/error state is present, and no sensitive data appears.
sequentially; do not crop away evidence of a bad state.
videos, or test-output directories. Keep requested review PNGs only under .amp/in/artifacts/ and never stage them in a product commit.
git status and ensure capture scripts, reports, browser artifacts,and screenshots are absent from the product diff.
finally, then stop the supervised service:bashamp orb service stop ui-capture
Check service status after stopping it. Cleanup is mandatory: abandoned browsers, preview servers, workers, and retries consume orb resources and can make later work unreliable.
$PORT; status, logs, and HTTP 200 checked..amp/in/artifacts/ and were inspected.| Case | Status | Duration (ms) | Turns | Tokens | Tool calls | ||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| Without | With | Δ | Without | With | Δ | Without | With | Δ | Without | With | Δ | ||
case-01 | fail→fail | 5,465 | 12,163 | +123% | 1 | 1 | 0% | 292 | 2,681 | +818% | 0 | 0 | — |
case-02 | fail→fail | 4,657 | 5,095 | +9% | 1 | 1 | 0% | 271 | 2,692 | +893% | 0 | 0 | — |
case-03 | fail→fail | 5,216 | 7,422 | +42% | 1 | 1 | 0% | 262 | 2,770 | +957% | 0 | 0 | — |
case-04 | fail→pass | 10,006 | 6,165 | -38% | 1 | 1 | 0% | 1,776 | 3,278 | +85% | 0 | 0 | — |
case-05 | pass→pass | 12,081 | 5,512 | -54% | 1 | 1 | 0% | 1,933 | 3,411 | +76% | 0 | 0 | — |
case-06 | fail→pass | 11,380 | 4,534 | -60% | 1 | 1 | 0% | 2,008 | 3,188 | +59% | 0 | 0 | — |
case-07 | fail→pass | 32,245 | 6,522 | -80% | 1 | 1 | 0% | 2,136 | 3,092 | +45% | 0 | 0 | — |
case-08 | fail→pass | 12,597 | 7,366 | -42% | 1 | 1 | 0% | 2,068 | 3,621 | +75% | 0 | 0 | — |
case-09 | fail→pass | 11,270 | 8,105 | -28% | 1 | 1 | 0% | 2,004 | 3,508 | +75% | 0 | 0 | — |
case-10 | fail→pass | 10,499 | 2,669 | -75% | 1 | 1 | 0% | 1,698 | 2,812 | +66% | 0 | 0 | — |
case-11 | fail→pass | 14,158 | 4,237 | -70% | 1 | 1 | 0% | 2,266 | 2,958 | +31% | 0 | 0 | — |
case-12 | pass→pass | 10,306 | 4,605 | -55% | 1 | 1 | 0% | 1,677 | 3,084 | +84% | 0 | 0 | — |
case-13 | fail→pass | 13,007 | 3,176 | -76% | 1 | 1 | 0% | 1,773 | 2,794 | +58% | 0 | 0 | — |
case-14 | fail→pass | 11,755 | 3,772 | -68% | 1 | 1 | 0% | 1,656 | 2,887 | +74% | 0 | 0 | — |
case-15 | fail→pass | 12,761 | 5,660 | -56% | 1 | 1 | 0% | 2,028 | 3,298 | +63% | 0 | 0 | — |
case-16 | fail→pass | 9,457 | 2,815 | -70% | 1 | 1 | 0% | 1,585 | 2,731 | +72% | 0 | 0 | — |
case-17 | fail→pass | 12,852 | 2,330 | -82% | 1 | 1 | 0% | 1,832 | 2,743 | +50% | 0 | 0 | — |
case-18 | pass→pass | 14,076 | 6,867 | -51% | 1 | 1 | 0% | 2,199 | 3,483 | +58% | 0 | 0 | — |
case-19 | pass→pass | 17,384 | 6,597 | -62% | 1 | 1 | 0% | 2,400 | 3,413 | +42% | 0 | 0 | — |
case-20 | fail→pass | 11,476 | 5,029 | -56% | 1 | 1 | 0% | 1,678 | 3,271 | +95% | 0 | 0 | — |
case-21 | fail→fail | 23,952 | 7,154 | -70% | 1 | 1 | 0% | 3,758 | 2,898 | -23% | 0 | 0 | — |
case-22 | fail→fail | 14,948 | 9,994 | -33% | 1 | 1 | 0% | 2,617 | 3,894 | +49% | 0 | 0 | — |
case-23 | fail→fail | 21,037 | 7,841 | -63% | 1 | 1 | 0% | 4,254 | 2,784 | -35% | 0 | 0 | — |
DecimalAI ran this skill against gemini-3.6-flash twice over the same eval suite — once with the skill loaded and once without — and compared the two runs case by case. 23 cases were attempted, and 18 counted toward the lift figure. The other 5 produced results that are not comparable between the two arms, so they are excluded from the headline rather than averaged into it. The headline lift of +57 percentage points is the difference between those two pass rates over the 18 comparable cases.
Without the skill loaded, the model failed this case. With it loaded, the same prompt on the same model passed. This is one improved case from the latest verified run; every case, including any that regressed, is in the table above.
Other measured skills in the registry, with their headline benchmark lift.