Invisible Playwright
feder-cr/invisible_dots
Use the Dot's browser identities for any task on a website or search on the web: which identity to use, opening and closing one, and when a plain download is simpler than a browser.
Capture desktop+mobile screenshots of Bike Index pages from the local bin/dev server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images.
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a claude-codeProject install by default; add -g for ~/.claude/skills/.
$ gh skill install bikeindex/bike_index frontend-screenshots --agent claude-codeProject scope by default; add --scope user for a personal install. Needs GitHub CLI 2.90.0 or later (public preview).
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .claude/skills && cp -r skills-src/.claude/skills/frontend-screenshots .claude/skills/frontend-screenshots && rm -rf skills-srcUse ~/.claude/skills/ instead of .claude/skills for a personal install. The folder must contain SKILL.md.
Claude Code skills documentation · loads skills from .claude/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .claude/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.Claude Code copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$skill-installer install https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshotsType this inside Codex. $skill-installer <name> installs a curated skill from openai/skills. The installer writes to $CODEX_HOME/skills (default ~/.codex/skills). Restart Codex if the skill does not show up.
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a codexProject install goes to .agents/skills/; add -g for ~/.codex/skills/.
$ gh skill install bikeindex/bike_index frontend-screenshots --agent codexProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .agents/skills && cp -r skills-src/.claude/skills/frontend-screenshots .agents/skills/frontend-screenshots && rm -rf skills-srcUse ~/.agents/skills/ instead of .agents/skills for a personal install.
Codex skills documentation · loads skills from .agents/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .agents/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.Codex copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a cursorProject install goes to .agents/skills/; add -g for ~/.cursor/skills/.
$ gh skill install bikeindex/bike_index frontend-screenshots --agent cursorProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .cursor/skills && cp -r skills-src/.claude/skills/frontend-screenshots .cursor/skills/frontend-screenshots && rm -rf skills-srcUse ~/.cursor/skills/ instead of .cursor/skills for a personal install.
Cursor skills documentation · loads skills from .cursor/skills/, .agents/skills/, .claude/skills/, .codex/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .cursor/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.Cursor copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gemini skills install https://github.com/bikeindex/bike_index.git --path .claude/skills/frontend-screenshots--scope user (default) or --scope workspace; --path is the subfolder of the repo that holds the skill; --consent skips the security confirmation prompt.
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a gemini-cliProject install goes to .agents/skills/; add -g for ~/.gemini/skills/.
$ gh skill install bikeindex/bike_index frontend-screenshots --agent gemini-cliProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .gemini/skills && cp -r skills-src/.claude/skills/frontend-screenshots .gemini/skills/frontend-screenshots && rm -rf skills-srcUse ~/.gemini/skills/ instead of .gemini/skills for a personal install, then run /skills reload.
Gemini CLI skills documentation · loads skills from .gemini/skills/, .agents/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .gemini/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.Gemini CLI copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ gh skill install bikeindex/bike_index frontend-screenshotsInstalls for Copilot at project scope by default; add --scope user for a personal install. Preview a skill first with gh skill preview. Needs GitHub CLI 2.90.0 or later (public preview).
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a github-copilotProject install goes to .agents/skills/; add -g for ~/.copilot/skills/.
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .github/skills && cp -r skills-src/.claude/skills/frontend-screenshots .github/skills/frontend-screenshots && rm -rf skills-srcUse ~/.copilot/skills/ instead of .github/skills for a personal install. Commit .github/skills so cloud agent and code review can use it.
GitHub Copilot skills documentation · loads skills from .github/skills/, .claude/skills/, .agents/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .github/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.GitHub Copilot copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
$ npx skills add bikeindex/bike_index --skill frontend-screenshots -a opencodeOpenCode documents no install command of its own. Project install goes to .agents/skills/; add -g for ~/.config/opencode/skills/.
$ gh skill install bikeindex/bike_index frontend-screenshots --agent opencodeProject scope by default (.agents/skills/); add --scope user for a personal install.
$ git clone --depth 1 https://github.com/bikeindex/bike_index.git skills-src && mkdir -p .opencode/skills && cp -r skills-src/.claude/skills/frontend-screenshots .opencode/skills/frontend-screenshots && rm -rf skills-srcUse ~/.config/opencode/skills/ instead of .opencode/skills for a personal install.
OpenCode skills documentation · loads skills from .opencode/skills/, .claude/skills/, .agents/skills/
Install the "frontend-screenshots" agent skill from https://github.com/bikeindex/bike_index/tree/main/.claude/skills/frontend-screenshots into .opencode/skills/frontend-screenshots/ in this project. Copy the whole folder (SKILL.md and every file beside it), keep the folder name "frontend-screenshots", then confirm the skill loads.OpenCode copies the folder itself, the same result as the manual copy. Check what it changed before you commit it.
frontend-screenshotsCapture desktop+mobile screenshots of Bike Index pages from the local bin/dev server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images.
Frontend Screenshots is an agent skill from bikeindex/bike_index. Capture desktop+mobile screenshots of Bike Index pages from the local bin/dev server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images. Use whenever a task needs screenshots of local pages — PR documentation, bug repros, before/after comparisons across branches, design review, demos — including mid-interaction states like an open dropdown, a modal showing, a form mid-fill, or a hover. Use it even when the user just says "grab a screenshot" or "show me what this looks like"…
Its SKILL.md is about 6.6k tokens, which your agent loads only when the skill is triggered. It is a single SKILL.md file with no bundled scripts.
It sits in Testing & QA, covering Browser testing and Design review and critique. It works with Playwright and Model Context Protocol. The repository describes itself as: All the code for Bike Index, because we love you. The licence is AGPL-3.0.
2 steps, taken from the first numbered list in SKILL.md.
Read from SKILL.md and the folder at commit b9be85f. It shows what the files ask for, not the result of running them.
Pre-approves these tools, so the agent can use them without asking each time:
BashReadToolSearchmcp__playwright__browser_navigatemcp__playwright__browser_resizemcp__playwright__browser_evaluatemcp__playwright__browser_take_screenshotmcp__playwright__browser_snapshotmcp__playwright__browser_wait_formcp__playwright__browser_console_messages…and 5 more on the same allowed-tools line.
From allowed-tools in the SKILL.md frontmatter.
Shell commands in SKILL.md call:
gitmagickcurlbundlerailsrubyclaudeFrom the folder's file list and the shell code blocks in SKILL.md.
Hosts in commands or code, which the agent is likely to contact:
bikebook-catalog.bikeindex.orgFrom URLs in SKILL.md, links to its own repository left out.
Names no API keys, tokens, secrets or passwords.
From names ending in _API_KEY, _TOKEN, _SECRET, _KEY or _PASSWORD in SKILL.md.
Frontend Screenshots loads about 6.6k tokens when it runs. Until then it costs about 238 tokens; SKILL.md has 3,405 words of instructions outside code blocks.
Estimates: characters ÷ 4, the usual rule of thumb; real counts depend on the model's tokenizer. Scripts and assets cost tokens only if the agent reads them.
The automated check noted patterns worth knowing about, such as sudo or a known installer.
allowed-tools: Bash, Read, ToolSearch, mcp__playwright__browser_navigate, mcp__playwright__browser_resize, mcp__plaAutomated static check — not a guarantee. Review scripts before installing. It scans the text of SKILL.md for risky patterns (piping downloads into a shell, reading credential files, hidden Unicode, destructive commands); files beside SKILL.md are not scanned.
The full file from bikeindex/bike_index at commit b9be85f, republished under its AGPL-3.0 licence (© bikeindex). 3,405 words, ~6,579 tokens.
.claude/skills/frontend-screenshots/SKILL.md (or your agent's skills folder).Drive Playwright MCP to capture screenshots of pages served by bin/dev. Callers
pass (url-path, page-slug) pairs, optionally with per-URL interaction steps, and
get back local PNG paths.
tmp/pr_screenshots/<branch>-<page>-<timestamp>-{desktop,mobile}.png, where <branch>=$(git rev-parse --abbrev-ref HEAD | tr '/' '-') and <timestamp>=$(date +%Y%m%d-%H%M%S). Cross-branch shots get an extra -base- segment.
Every browser_take_screenshot anywhere passes a filename: starting with tmp/ — including a one-off tmp/tooltip-hover.png for visual verification that has nothing to do with a PR. The MCP tool's root is the project root, so a bare tooltip.png lands in the working tree and shows up in git status; tmp/ is gitignored.
eval "$(ruby bin/env --export)" so $BASE_URL is set.curl -fs "$BASE_URL/" >/dev/null — run it every time, even if an earlier check in the session failed; the user may have started it since. If it fails now, stop and ask the user to start it — unless this is a spawned .claude/worktrees/… checkout or the web sandbox, where you start it yourself. bin/env resolves $DEV_PORT/$BASE_URL from the workspace ID, so whoever starts bin/dev binds the same port and DB this skill expects.ruby bin/env --export names a WORKSPACE_ID first — without one the curl reaches the main checkout. See the sandbox-test-setup skill. The web sandbox (/home/user/bike_index) gets a WORKSPACE_ID like anywhere else — its setup script runs bin/workspace_setup — but it's the only checkout in that container, so whatever $BASE_URL resolves to is the right one.CheckPending only re-raises once the evented file watcher notices db/migrate
moved — so a passing curl can be followed by ActiveRecord::PendingMigrationError on every page.
bundle exec rails db:migrate, and read log/development.log before blaming the capture.
A gem the merge bumped does the same: the running server keeps the version it booted with, so a
method the new one adds is a NoMethodError until bin/rails restart.mcp__playwright__* tools aren't registered, tell the user to run claude mcp add playwright -- npx -y @playwright/mcp@latest and restart.Bike.count comes
back 0 in a workspace whose db:seed never ran, so only preview routes render. Seed it (it's the
per-workspace throwaway DB), or capture previews. In the web sandbox the seed is already running
in the background — wait on /tmp/seed.status (sandbox-test-setup) rather than starting a
second one, which dies on duplicates.Pick the user the caller specified, or default to user@bikeindex.org (lowest privilege; most non-org-affiliated pages render for them). All seeded users use password pleaseplease12, and db/seeds/seed_test_users.rb is the list of record:
user@bikeindex.org — no org memberships. Default. Use for personal pages (/my_account, /bikes/new) or to show how an org-less account sees a route.member@brakebills.edu — member (not admin) of Brakebills. Use to capture the non-admin view of an org.admin@bikeindex.org — SuperuserAbility; effectively admin of every org. Use when capturing admin-only menu items, /admin/... routes, or org pages where you want the fully-loaded sidebar.dev@bikeindex.org — SuperuserAbility and developer. Use for the pages gated on both: the Dev: navbar entries and /admin/organizations/:slug/custom_layouts/..., which redirect for admin@.:anonymous — skip sign-in entirely. Use for public pages where the signed-out rendering is the point.Signed-out is the normal starting state, not a blocker: if a page redirects to /session/new or /session/magic_link (or #navUserSettingLink has no email), sign in. In development every page carries a "sign in as superadmin" button in the top banner — one click, no credentials, and it lands back on the page you were on; use it whenever the target needs a superuser. Otherwise drive the sign-in form via Playwright with the seed credentials above — don't ask the user to sign in manually, and don't skip the screenshot for lack of a session. It's two steps (email → Continue → password). The fields are input[name="session[email]"] and input[name="session[password]"] — the scope is session, not user, which is the guess that costs a round trip. Both submits need addressing by value — input[name='commit'][value='Continue'] then input[name='commit'][value='Log in']. A [type=submit] selector fails strict mode on either, and so does input[name='commit'] on the second step, where "Email me the link" is the other match. Only ever authenticate against the local dev server ($BASE_URL / localhost) — never sign in to any other host, and never create, promote, or impersonate users to bypass auth.
Picking an org slug. When the URL is org-scoped (/o/<slug>/...) and the caller didn't specify a slug, default to brakebills
Verify identity before capturing. The gate isn't about whether to authenticate — signing in with seed credentials is expected. It's about confirming the session and its data are seed-only, so no PII lands in an uploaded image. After signing in, check:
document.getElementById('navUserSettingLink')?.dataset.emailIf it's set but not one of the seeded emails, stop and ask — you're signed in as a non-seed user (PII risk on upload). If it's undefined when you expected a session, sign-in didn't take (often the seeds haven't run — bundle exec rails db:seed); retry the sign-in, don't capture signed-out. For :anonymous, expect undefined and confirm before continuing.
The admin layout has no #navUserSettingLink, so on an /admin/... route it reads undefined for a session that's fine — reaching the page at all proves superuser. Confirm the data instead: every email the page renders should be a seeded one — @bikeindex.org, member@brakebills.edu, or the user@fakegmail.com / user1@gmail.com / user_2@gmail.com bike owners.
(document.body.innerText.match(/[\w.+-]+@[\w.-]+/g) || []).slice(0, 8)Don't capture if any on-page data looks non-seeded. Even signed in as a seed user, if a page shows records that don't look like seed data (unfamiliar names/emails, real-looking user content), stop and ask — the dev DB may have been loaded with production data, and screenshots are permanent once uploaded.
Make the directory and clear stale shots: mkdir -p tmp/pr_screenshots && rm -f tmp/pr_screenshots/<branch>-<page>-*.png 2>/dev/null || true — on the branch capture only: the pattern matches -base- shots too, so a cross-branch rerun would delete branch shots its caller hasn't posted yet. browser_take_screenshot errors with ENOENT rather than creating the directory, so a fresh workspace fails on the first capture.
Two viewports — resize once each, then walk every URL:
browser_resize 1440×900 → for each URL: navigate → settle → hide the footer → browser_take_screenshot (fullPage: true) to ...-desktop.png.browser_resize 390×844 → same loop, also fullPage: true → ...-mobile.png.Full page, minus the footer, review-app banner and profiler badge, no target: arg. Capture the whole page (fullPage: true) at both viewports so nothing below the fold is cut off, but first hide the site footer (identical on every page, just padding), the #review-app-banner topbar and the .profiler-results badge (both dev-only chrome that isn't part of the real page). Keep the footer when the diff changes it — the reason to hide it is that it carries no information, which stops being true the moment it's the subject. The profiler badge reports this request's timing, so leaving it in makes every before/after pair differ on a number no reviewer cares about. After each navigation — and again immediately before the shot, since rack-mini-profiler injects .profiler-results after load — run:
browser_evaluate: () => {
document.querySelectorAll('.close, [data-dismiss="modal"], [aria-label="Close"]').forEach(c => c.click());
document.querySelectorAll('.modal-backdrop').forEach(b => b.style.setProperty('display', 'none'));
document.body.classList.remove('modal-open');
document.querySelector('.primary-footer')?.style.setProperty('display', 'none');
document.getElementById('review-app-banner')?.style.setProperty('display', 'none');
// A same-origin iframe (the legacy org add-a-bike page, the embeds) carries its own badge
[document, ...[...document.querySelectorAll('iframe')].map(f => f.contentDocument).filter(Boolean)]
.forEach(d => d.querySelector('.profiler-results')?.style.setProperty('display', 'none'));
return document.body.scrollHeight; // content height with the chrome gone
}The donation modal is why that starts with a dismiss: a seeded user who hasn't donated gets it over the page on /my_account and friends, and it covers the whole shot rather than sitting in a corner. Drop the dismiss when a modal is what the diff changes — the same rule as the footer, and it bites harder here, since the dismiss also sets hideDonationModal and the base-branch shot of a modal that was the whole point comes back without it.
If the returned content height is less than the viewport height, browser_resize the height down to it before the shot (the <html> element's near-black background fills the gap otherwise), then resize back to the standard viewport before the next URL. Taller-than-viewport pages need no resize — fullPage scroll-stitches them.
An org-sidebar page taller than the viewport needs that resize upward instead. The sidebar is position: fixed, so fullPage stitching leaves it at viewport height over the same near-black background — resize up to the content height before the shot.
The sidebar scrolls inside itself, so body.scrollHeight doesn't say whether its lower rows are in the shot. At 1440×900 its own scroller overflows, and a row near the bottom captures as absent. Measure the row you're there for and resize the viewport height past its getBoundingClientRect().bottom. On mobile the sidebar is behind button[aria-label="Menu"] — open it, and run the same open-the-menu step on the base branch so the pair compares like for like. That state is an overlay taller than the viewport over a much longer page, which is one of the two cases to capture fullPage: false — the other is below.
Viewport-only is the caller's call, never yours — except when the diff's subject is a position: fixed or sticky element. fullPage paints it once, where it sits at scroll 0, and nowhere else: on the 10,007px /accept_vendor_terms its bottom bar landed at y=798 with the remaining 9,100px of that column bare. Capture those at fullPage: false, scrolled to where the element pins. When the caller asks for it — "viewport only", "above the fold", "just the mobile viewport" — drop fullPage for the size they named and leave the other one full page.
/bikebook renders whatever catalog the dev server points it at. With BIKEBOOK_CATALOG_DIRECTORY set it's a local /bikebook_catalog/ that can trail or lead the published one by schema versions, so a capture of a schema change against the wrong one shows nothing new. Compare the two manifests' schema_version and capture against the one the diff reads — when that's the published one, page.route /bikebook_catalog/ to https://bikebook-catalog.bikeindex.org/catalog/ in browser_run_code_unsafe, on both branches.
Settle before the screenshot. Stimulus + Chartkick render after document load; either browser_wait_for on a known element or pause ~500ms–1s. Otherwise charts capture mid-draw. loading="lazy" images capture blank below the fold — fullPage stitching never scrolls them into view (the search results, marketplace and /stolen recoveries use it). Set loading = 'eager' on them and wait for every naturalWidth before the shot.
Mid-interaction states are in scope. When the caller asks for a dropdown open, a modal showing, a hover state, a partially-filled form, etc., drive Playwright between settle and the screenshot — browser_click, browser_type, browser_press_key, browser_hover, then wait for the UI to reach the target state (browser_wait_for on a marker element, or check via browser_evaluate) before browser_take_screenshot. Treat the interaction sequence as part of the page-slug — e.g. capture combobox-open after clicking + typing, distinct from a static search-registrations page-load shot. For cross-branch comparisons, run the same interaction sequence on each branch so the screenshots actually compare like-for-like.
A loading state is captured by holding the request open, not by racing it. In browser_run_code_unsafe, page.route the frame's URL and delay with await page.waitForTimeout(90000) before route.continue() — setTimeout isn't defined there — then trigger the fetch the way the frame does, by re-setting its src. Load the results first: a frame that goes busy from an empty page captures a header reading "0 matches".
One capture's ?organization_id= or ?view_as=<org> changes what the next param-less URL renders. set_passive_organization writes the org into the session, and /registrations/:id with no params then resolves through default_view_for to that org's admin view — so a /registrations/54 shot taken after a view_as=brakebills.staff one is the org page, in the org layout, at the same URL. Nothing errors, and the pair only looks wrong once you open it. Navigate ?organization_id=false before any capture whose URL carries no view_as, and remember the session survives the base-branch checkout, so the branch and base loops can drift apart on this if their orders differ.
localStorage survives that checkout too, and a mid-capture interaction writes to it. The org search column toggle persists the checked set to orgRegistrationColumns, so a base loop run after a branch loop that toggled columns loads the branch's column set — the pair then compares different tables at the same URL. Clear the key, or re-apply the interaction, at the start of each loop rather than once per run.
Being signed in is that same drifting state, and it reaches every page. A run that captures public pages signed out and then signs in for one org-scoped page leaves the session behind for whatever it captures next — so the base loop's public pages come back with a signed-in navbar against a signed-out branch shot, and the pair differs on chrome the PR never touched. Capture each loop's pages in the same order, and /goodbye back to signed out before the public ones.
An element missing from the shot may be a stale asset build, not the code. bin/dev's watchers don't pick up a new @theme token, so a class keyed off one (tw:navbar:block!) is absent from what the server serves while the specs — whose builds you regenerated — pass. Confirm with getComputedStyle on the element, then run bin/rails tailwindcss:build (or dartsass:build for a .scss edit); sprockets serves the new digest on the next request, so this needs no bin/dev restart and isn't assets:precompile.
Sanity-check each PNG: under ~5 KB usually means the page errored. Pull browser_console_messages and look only for uncaught exceptions from app code (Stimulus registration failures, TypeErrors in app/javascript/**) — asset 404s and third-party deprecation warnings are noise. To diagnose a failed capture: HTTP status via curl -s -o /dev/null -w "%{http_code}\n" "$BASE_URL/<path>", response body via curl -s "$BASE_URL/<path>" | head -200, full backtrace via tail -200 log/development.log.
A 429 mid-capture is rack-attack, not a broken page. requests/ip allows a burst per 20 seconds (config/initializers/rack_attack.rb), which a loop of fetches from browser_evaluate blows through — navigate the pages you're capturing rather than probing them in bulk, and wait the window out rather than retrying.
An email renders at $BASE_URL/rails/mailers/<mailer>/<action>, but that route is preview chrome around an iframe — append &part=text%2Fhtml for the email body alone, which is what to capture. Every OrganizedMailerPreview action takes a record id (?bike_id=75&part=text%2Fhtml); spec/mailers/previews/ is the list of actions and their params. Pick the record for the state you need — finished_registration renders a different email for a claimed ownership than an unclaimed one.
Some components only render in a context you can't reproduce on a normal dev page — gated by an env var (e.g. the review-app banner needs REVIEW_APP), a feature flag, or a hard-to-reach error/empty state. When a component has a ViewComponent/Lookbook preview, screenshot the preview URL instead of hunting for a page that happens to render it:
$BASE_URL/rails/view_components/<preview_path>/<scenario><preview_path> is the preview class underscored with the Preview suffix dropped, and <scenario> is the preview method. SharedBlocks::ReviewAppBanner::ComponentPreview#superadmin_signed_in → /rails/view_components/shared_blocks/review_app_banner/component/superadmin_signed_in. If a scenario doesn't exist yet, add a method to the component's *_preview.rb first — a preview that renders the exact state (pass the args that trigger it) is often the fastest path to a clean shot.
Use this bare route, not Lookbook's /lookbook/inspect/..., which wraps the component in its own browser chrome. /lookbook/preview/... is the one route that puts a whole @!group on a single page — /lookbook/preview/ui/tooltip/variants for UI::Tooltip::ComponentPreview's # @!group Variants. Reach for it when the shot needs several scenarios side by side; the component's system spec usually already visits it. It takes a group, not a scenario, and drops the trailing component — /lookbook/preview/ui/tooltip/component/variants and /lookbook/preview/ui/tooltip/<scenario> both 404, which reads as an unregistered group rather than a wrong path.
On a dev server that's been up a while, the group page stops picking up newly added scenarios — it renders every other one, which reads as a broken preview rather than a stale registry (bin/rails restart clears it; a fresh server picks them up within a request or two). The bare /rails/view_components/… route stays current either way, since config/initializers/lookbook.rb patches __vc_load_previews to re-resolve through the autoloader — capture a new scenario there.
The preview page loads Tailwind and renders the component standalone (no site chrome), so a preview that fits the viewport captures at fullPage: false; a small ViewComponent render-timing line at the bottom is harmless. A preview taller than the viewport still captures fullPage: true — page-sized components (a whole registration step, a long form) put the changed field below 900px, and cropping it out is the one thing the shot exists to show. Measure before choosing:
() => document.querySelector('<selector for what changed>').getBoundingClientRect().top + window.scrollYA legacy-styled component needs the display option in the URL. layouts/component_preview only includes revised/kelsey_styles when Lookbook passes it, and the bare route passes nothing — so a preview whose class carries # @display legacy_stylesheet true renders unstyled (the navbar's logo fills the viewport) unless you append it yourself:
$BASE_URL/rails/view_components/<preview_path>/<scenario>?lookbook%5Bdisplay%5D%5Blegacy_stylesheet%5D=trueEverything else still applies — same PII/seed-data gate, same (url-path, page-slug) naming (use a slug like banner-signed-in).
Previews that query the dev DB (e.g. User.admins.first) render nothing when that data is missing — if the state doesn't appear, seed first with bundle exec rails db:seed. This is component-only: a preview can't show layout/stacking against the rest of the page (e.g. a navbar z-index fix), so use a real page for those.
When the caller wants before/after, repeat the capture loop against the base ref. The caller passes the base — origin/main by default, or the PR's actual base when it isn't main (a stacked PR's base often isn't). Set BASE_REF to that remote ref (e.g. origin/main, origin/sethherr/feature-x) and use it throughout; git fetch origin first so it's current.
Capture the base at what the branch actually merged, not at the ref's tip. A fetch moves origin/main to commits the branch hasn't taken, so a base capture there renders the base's newer work and the diff attributes it to this PR. Check git rev-list --count HEAD..$BASE_REF before detaching: non-zero means merge first, or detach at $(git merge-base HEAD $BASE_REF) instead. On a busy repo the base can move between the branch capture and the base capture of the same run.
The detached checkout in step 4 is a sanctioned exception to "never change branch" — don't stop and ask for it. It detaches at a remote ref, reads, and returns to the same branch within this section, committing nothing. Nothing here licenses any other checkout, git checkout -b, or one that outlives the capture.
git status — abort if there are uncommitted changes.$BASE_REF, or $(git merge-base HEAD $BASE_REF) when the branch is behind it. Call that $BASE_AT.db/migrate/ between the branch and $BASE_AT, not $BASE_REF; abort if it changed — a branch-only migration leaves the DB schema ahead of the base's code, so base pages can error. A migration that only shows up against the ref's tip belongs to commits the branch never took, and detaching at the merge-base is what resolves it; aborting there abandons a capture that was fine. So does aborting on a migration that only adds a table, or a column with a default — nothing on the base reads either, so load the target page after detaching and abort only if it errors.BRANCH=$(git rev-parse --abbrev-ref HEAD), git checkout --detach $BASE_AT (detached — checking out a branch name fails if a sibling worktree holds it; detached HEAD is allowed concurrently and is the same code), navigate the browser to force Rails to reload the changed files — the watcher can lag that first request, so confirm the page shows the base's markup (the changed element gone) and re-navigate if it doesn't — repeat capture into ...-base-... filenames, then git checkout $BRANCH.A Gemfile.lock diff is not a reason to abort.
Don't call a pair identical with cmp. Two captures of the same code routinely differ by a few dozen bytes, so byte-equality reports a change that isn't one (and its absence proves nothing). Compare pixels, and establish the noise floor before reading anything into a number — recapture one page without changing branches, and treat that count as zero:
magick compare -metric AE <base>.png <branch>.png null: # differing pixel count
magick compare <base>.png <branch>.png -compose src d.png && magick identify -format '%@' d.png # where they differThe bounding box is what settles it: dev-only chrome that slipped past the hide step lands in one small box, a real change doesn't.
The seeded DB persists across checkouts, so the existing session usually still works. Preview routes (/rails/view_components/..., /lookbook/...) reload across the checkout like ordinary pages, so their before/after works against any $BASE_REF too.
Once every screenshot is captured, quit Chrome with browser_close — including when the capture failed partway. Leaving it running holds the shared browser profile lock, so the next browser_navigate (this skill or another) fails with "Browser is already in use".
Who closes is decided by who invoked you, so you never have to be told. Invoked by the user — "grab a screenshot of X" — you're the last one in the browser: close it. Invoked by a workflow that captures again straight afterwards — the pr screenshot phase, which captures the base next — leave it open; closing between the two just pays the startup again.
© bikeindex, AGPL-3.0. Rendered from Markdown: HTML in the file is shown as text, images as links, and headings moved down two levels. Raw file
Just SKILL.md in .claude/skills/frontend-screenshots of bikeindex/bike_index.
Open the folder on GitHubat commit b9be85f
Frontend Screenshots next to the 5 skills that share the most tags, products or categories with it. Stars are the repository's; “used in” counts other GitHub owners with a copy.
| Skill | Stars | Used in | Tokens | Auto-check | Licence | Repo updated |
|---|---|---|---|---|---|---|
| Frontend Screenshots this skillbikeindex/bike_index | 308 | — | ~6.6k | Automated safety check: Notes | AGPL-3.0 | |
| Invisible Playwrightfeder-cr/invisible_dots | 32k | — | ~573 | Automated safety check: Pass | MIT | |
| Playwright E2E Testsonyx-dot-app/onyx | 32k | 1 repos | ~2.8k | Automated safety check: Notes | Custom licence | |
| Shogun Screenshotyohey-w/multi-agent-shogun | 1.4k | — | ~901 | Automated safety check: Notes | MIT | |
| Playwright Debugvoicetreelab/voicetree | 924 | — | ~1.2k | Automated safety check: Pass | Custom licence | |
| Hands On Testktnyt/cclsp | 675 | — | ~1.7k | Automated safety check: Pass | MIT |
feder-cr/invisible_dots
Use the Dot's browser identities for any task on a website or search on the web: which identity to use, opening and closing one, and when a plain download is simpler than a browser.
onyx-dot-app/onyx
Write and maintain Playwright end-to-end tests for the Onyx application.
yohey-w/multi-agent-shogun
スクリーンショットの取得・加工を行う。ローカルスクショから最新画像を取得、 PlaywrightでWebページをキャプチャ、画像のトリミング・リサイズ、機微情報を黒塗りマスキング。
voicetreelab/voicetree
This skill should be used when the user asks to "debug the electron app", "connect playwright to VoiceTree", "take screenshots of the running app", "interact with the live UI", "inspect the running…
ktnyt/cclsp
Performs manual hands-on testing of a web application using playwright-cli.
Chorus-AIDLC/Chorus
A skill your agent uses when manually verifying a Chorus frontend change in a real browser — finding local login credentials, driving the running dev server with the Playwright MCP, logging in…
bikeindex/bike_index
Read live production data from Bike Index through the admin OAuth token — Sidekiq and PgHero status, and the user-submitted bug reports — the same data as the cookie-gated dashboards, but…
bikeindex/bike_index
Embed a local image file into an existing GitHub PR — either in the PR body or as a comment.
bikeindex/bike_index
Add a manufacturer to Bike Index in production through the admin OAuth token (POST /admin/manufacturers).
bikeindex/bike_index
Create or update a pull request for the current branch. An agent skill from bikeindex/bike_index.
bikeindex/bike_index
How to fix a test that fails intermittently in Bike Index — one that passes locally but fails on CI, fails on one shard, passes on re-run, or is already tagged :flaky.
bikeindex/bike_index
Investigate and fix a specific Honeybadger exception in the Bike Index app — pull the fault, read its backtrace, find the offending code, write the fix.
Works with
Categories
Capture desktop+mobile screenshots of Bike Index pages from the local bin/dev server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images. Frontend Screenshots is an agent skill from bikeindex/bike_index. Capture desktop+mobile screenshots of Bike Index pages from the local bin/dev server via Playwright MCP, with a seeded-user identity gate that keeps PII out of uploaded images.
Frontend Screenshots fits situations like: A task needs screenshots of local pages — PR documentation; before/after comparisons across branches; demos — including mid-interaction states like an open dropdown; A modal showing.
Run `npx skills add bikeindex/bike_index --skill frontend-screenshots -a claude-code`. Or copy the skill folder (.claude/skills/frontend-screenshots in bikeindex/bike_index) into .claude/skills/frontend-screenshots in your project. Claude Code loads it when a task matches its description.
Run `npx skills add bikeindex/bike_index --skill frontend-screenshots -a codex`. Or copy the skill folder (.claude/skills/frontend-screenshots in bikeindex/bike_index) into .agents/skills/frontend-screenshots in your project. Codex loads it when a task matches its description.
Cursor, Gemini CLI, GitHub Copilot and OpenCode also load SKILL.md folders. With the skills CLI, run `npx skills add bikeindex/bike_index --skill frontend-screenshots -a cursor` (or -a gemini-cli, github-copilot or opencode for the others). To copy it by hand, put the folder in .cursor/skills/frontend-screenshots, .gemini/skills/frontend-screenshots, .github/skills/frontend-screenshots and .opencode/skills/frontend-screenshots in your project.
Going by SKILL.md and its folder, Frontend Screenshots needs the command-line tools its instructions call (git, magick, curl, bundle, rails and ruby). Our summary lists: Node.js. Its frontmatter pre-approves these tools: Bash, Read, ToolSearch, mcp__playwright__browser_navigate, mcp__playwright__browser_resize, mcp__playwright__browser_evaluate, mcp__playwright__browser_take_screenshot, mcp__playwright__browser_snapshot, mcp__playwright__browser_wait_for, mcp__playwright__browser_console_messages, mcp__playwright__browser_click, mcp__playwright__browser_type, mcp__playwright__browser_press_key, mcp__playwright__browser_hover, mcp__playwright__browser_close.
SKILL.md names 1 domain. In commands or code: bikebook-catalog.bikeindex.org; the agent is likely to contact it when it follows the instructions. This is read from the text; nothing was executed.
Our automated static check of SKILL.md found notes only (pre-approves every shell command (allowed-tools: bash)), nothing it rates as a warning. It is not a guarantee. Review the folder before installing.
Frontend Screenshots is published under the AGPL-3.0 licence (the repository's licence). It allows redistribution, so the full SKILL.md is shown on this page.
About 6.6k tokens (SKILL.md is roughly 26k characters). Agents keep only the skill's name and description in context until a task matches; then they load SKILL.md in full.
Skills that share tags, products or a category with Frontend Screenshots: Invisible Playwright (feder-cr/invisible_dots, 32k stars), Playwright E2E Tests (onyx-dot-app/onyx, 32k stars), Shogun Screenshot (yohey-w/multi-agent-shogun, 1.4k stars) and Playwright Debug (voicetreelab/voicetree, 924 stars). The comparison table on this page puts their stars, adoption, token cost, safety result and licence side by side.
bikeindex (a GitHub organization) maintains it in bikeindex/bike_index, which has 308 GitHub stars. The repository holds 11 skills in this directory. The repository was last updated on October 10, 2026.
Source: bikeindex/bike_index on GitHub. Facts on this page come from the repository at the commit we read; the author's words are quoted as theirs.