Every pull request on Vercel already produces the perfect test target: a preview deployment - the exact build output, edge config, and environment your users get if the PR merges. Most teams look at it manually, click around for thirty seconds, and approve. This guide wires that preview into visual regression testing instead: every screen of every preview diffed against your production baseline, with the merge blocked until you approve what changed.
The whole setup is one GitHub Actions workflow. No polling, no sleep 60, no testing localhost and hoping the deployed build behaves the same.
Why test the preview instead of localhost?
Running visual tests against a local dev server in CI is better than nothing, but it tests a build that never ships. Preview deployments close that gap:
- Same artifact. The preview is built by the same pipeline as production - framework version, environment variables, edge middleware, image optimization. A diff against the preview is a diff against the release.
- Real network conditions. Fonts, third-party scripts, and API calls resolve the way they will in production, so what you diff is what users render.
- Zero test infrastructure in CI. The app is already deployed. The runner only needs a URL.
This matters double if the PR was written by an AI agent. When Cursor or Claude Code opens a pull request, nobody clicked through the app before the commit - the preview run is the first time the change is actually seen. If something that worked yesterday breaks, this is where you find out - not from a user.
The trigger: deployment_status, not push
The common mistake is triggering visual tests on push and then polling until the preview is live. Vercel already tells GitHub when the preview is ready: it creates a GitHub deployment per preview and emits a deployment_status event when the deploy succeeds. Trigger on that event and the preview URL arrives in the payload:
name: Visual tests on Vercel preview
on:
deployment_status:
jobs:
visual:
if: github.event.deployment_status.state == 'success'
runs-on: ubuntu-latest
steps:
- uses: las-team/lastest/action@main
with:
server-url: ${{ secrets.LASTEST_SERVER_URL }}
runner-token: ${{ secrets.LASTEST_RUNNER_TOKEN }}
preview-url: ${{ github.event.deployment_status.environment_url }}
fail-on-changes: 'true'
github.event.deployment_status.environment_url is the preview URL - https://your-app-git-feat-checkout-yourteam.vercel.app or similar. The Lastest Vercel integration passes it straight to the recorded suite as the base URL, so the same tests that ran against production now replay against the preview. If you drive CI with the CLI runner instead of the Action, the equivalent is --base-url:
pnpm test:visual --repo-id $REPO_ID \
--base-url "$PREVIEW_URL"
What runs against the preview
The suite itself is the one you already have: flows recorded by clicking through your app once (or generated by an AI agent), emitted as deterministic Playwright with self-healing selectors. On each preview run, Lastest walks every recorded flow - home to checkout, not just the page the PR touched - and diffs each screen against the approved production baseline across three engines: pixel, structural, and perceptual. A sub-pixel anti-aliasing wobble classifies as flaky; a shifted checkout button classifies as changed and turns the check red.
Because the comparison target is the production baseline, the verdict answers the only question that matters at merge time: does this PR change what users see, beyond what the author intended?
Blocking the merge
Two settings turn the run from informational into a gate:
fail-on-changes: 'true'on the Action - the check goes red when a non-flaky diff is found.- Add the Lastest check to branch protection (Settings → Branches → Require status checks) - PRs with unapproved visual changes cannot merge.
Approving is a one-click review in the Lastest UI: you see the baseline, the preview screenshot, and the side-by-side diff. Approve, and the check flips green and the diff becomes the new baseline. Reject, and the PR stays blocked with the evidence attached. Reviewers approve what they can see moved - nothing more. The full merge-gate mechanics are covered in the visual regression testing in CI/CD guide.
What testing every preview costs
This pattern only survives contact with a real repo if per-run cost is zero. A team merging ten PRs a day, each with three pushes, runs the suite thirty times daily - with agent-run QA tools that spend AI tokens on every check, the invoice scales with your velocity. Lastest replays are plain Playwright execution: AI is spent once at create-time (and at fix-time when a selector heals), then every replay costs $0 in tokens. Self-hosted, the screenshots are unlimited too - the only bill is your Actions minutes, and Smart Run trims those by reading the git diff and running only the tests the change touched.
Edge cases worth knowing
- Deployment Protection. If your previews are password-protected, issue a protection bypass token in Vercel and inject it as a header or query parameter in the test environment so the runner can reach the preview.
- Production deploys fire deployment_status too. The event fires for every environment. Gate on the environment name (
github.event.deployment_status.environment == 'Preview') if you want preview-only runs, or let production runs refresh your baseline validation - both are legitimate setups. - Dynamic content. Timestamps, avatars, and A/B slots are handled with masks, auto-masking, and perceptual thresholds - configure once per test, not per run.
- Not on Vercel? The pattern is host-agnostic. Netlify, Cloudflare Pages, and Render all expose the deploy-preview URL to CI; pass it as the base URL and the same suite runs unchanged.
Setup checklist
- Record (or generate) your flows once - see the Lastest setup guide.
- Add
LASTEST_SERVER_URLandLASTEST_RUNNER_TOKENas repository secrets. - Commit the
deployment_statusworkflow above. - Enable
fail-on-changesand add the check to branch protection. - Open a PR and watch the preview get tested before anyone reviews it.
From that point on, every Vercel preview deployment - human-written or agent-written - is verified against production before it can merge. The preview was always the right thing to test; now it happens without anyone remembering to click.