How to Run Screenshot Regression Tests for a Laravel Website
Catch unintended Laravel UI changes with repeatable browser screenshots, approved baselines, and a practical CI workflow using Pest 4.
To run screenshot regression tests for a Laravel website, use Pest 4 browser testing for new projects: visit representative routes in a real browser and compare their rendered screenshots with approved baselines. Commit those baselines, keep the browser and viewport consistent in CI, and review image diffs before accepting changes. Laravel recommends Pest for new browser testing; Laravel Dusk remains a documented option for existing Dusk suites. Dusk can capture screenshots, but its screenshot method alone is not a baseline comparison system.
This guide builds a minimal Laravel and Pest workflow, explains how to make the output repeatable, and covers Dusk, CI, debugging, failure modes, and when an external screenshot API can help. “Visual regression testing” and “screenshot comparison” are often used for the same baseline-and-diff technique.
1. Choose the right test layer and pages
A visual screenshot test checks how a page is rendered. It does not prove that every business rule, request, or interaction behaves correctly. Use Laravel HTTP tests for request and response behavior, browser tests for user flows and browser-rendered presentation, and visual comparisons to flag changes in appearance. These layers complement one another.
Begin with a small set of pages where visual defects matter: the home page, a high-value account or conversion flow, and a responsive layout. Add states that are likely to break independently, such as a validation error or signed-in dashboard, when those states have stable test data. Avoid trying to snapshot every route on the first pass; a focused set makes reviews and baseline upkeep manageable.
| Need | Use | What it establishes |
|---|---|---|
| Request status, response data, redirects | Laravel HTTP tests | Application request and response behavior |
| Browser interactions and rendered page | Pest 4 browser tests | Real browser behavior and page state |
| Detect changed appearance | Pest screenshot baseline assertion | Whether a captured rendering differs from an approved image |
2. Install Pest 4 browser testing
In an existing Laravel project using Pest 4, install the browser plugin and its Playwright browser dependencies. Run these commands from the project root:
composer require pestphp/pest-plugin-browser --dev
npm install playwright@latest
npx playwright install
The package and browser installation commands are documented by Pest browser testing. Match these dependencies to the versions supported by your project and CI environment, and retain the resulting lockfile changes so local and CI installs resolve consistently. If the project is not yet using Pest 4, first follow the framework’s current Pest upgrade guidance rather than assuming the browser plugin works with an older major version.
3. Write a visual regression test
Create a browser test, for example tests/Browser/VisualRegressionTest.php. Pest’s visit() function opens the route, and assertScreenshotMatches() compares the result to a baseline:
<?php
it('keeps the home page visually consistent', function () {
visit('/')
->assertScreenshotMatches();
});
Run the suite with:
./vendor/bin/pest
The first execution has no approved image to compare, so the visual assertion workflow creates or reports the initial screenshot according to the installed Pest version. Inspect that image, confirm it is the intended design, and add the generated baseline to version control. Subsequent runs compare the current rendering with that checked-in expectation. Keep generated baselines under version control: otherwise different machines may silently compare against different or missing reference images.
For an application route that needs predictable database state, use your regular Laravel testing setup, factories, and database refresh approach. The browser plugin is designed to work with Laravel testing features; see the Pest browser testing documentation for supported setup and current API details. An illustrative test might be:
<?php
use App\Models\Product;
it('keeps the product page visually consistent', function () {
$product = Product::factory()->create([
'name' => 'Example product',
'price' => 2500,
]);
visit('/products/' . $product->slug)
->assertSee('Example product')
->assertScreenshotMatches();
});
Adapt the model fields, route, and factory to your application. The text assertion helps distinguish an unexpected page or failed navigation from a genuine visual difference; the screenshot assertion checks appearance.
Test more than the default viewport
Responsive defects often appear only at narrow or wide widths. Pest supports browser/device and viewport selection; consult its current API for the exact methods and supported presets. Keep each viewport’s baseline intentional and stable. Avoid comparing mobile screenshots generated with one device profile against a different profile on another machine.
Capture a meaningful page state, not an arbitrary instant. Wait for route navigation, content, and any app-specific asynchronous work to settle before taking the screenshot. Use stable seeded data, fixed dates where practical, and deterministic feature flags. If content depends on external services, use test doubles or a controlled test environment instead of relying on a live third-party response.
4. Create and review baselines
- Run each visual test in the intended local or CI environment.
- Open the generated initial image and verify that it shows the correct route, user state, content, and viewport.
- Commit the approved baseline files with the test that owns them.
- When a later run fails, inspect the expected image, actual image, and diff where available.
- Update a baseline only after confirming the design change is intentional and the new image is correct.
A changed image is a review signal, not an automatic defect verdict. A font update, spacing change, image replacement, or intentional redesign can all produce valid diffs. Conversely, approving every changed image without review defeats the check. Make the baseline change visible in the same code review as the UI change.
5. Keep captures repeatable
Screenshot output can vary with the host operating system, browser version, browser settings, hardware, power source, and headless mode. Playwright’s visual comparison documentation calls out these sources of rendering differences and recommends running comparisons in the same environment used to create the baselines.
- Pin the environment: Use a consistent CI image, browser build, fonts, and dependency lockfiles. Generate and update baselines in that same environment.
- Fix the viewport: Set the same width, height, device scale, and browser profile for each run.
- Control data: Seed records, avoid random values, and freeze or normalize timestamps where they affect visible output.
- Wait for readiness: Wait for the page’s actual content and images to finish loading. “Network idle” alone may not be suitable for applications with persistent network activity.
- Stabilize animation: Disable or wait out transitions and carousels when the tool supports it; otherwise capture timing can shift the image.
- Handle volatile content deliberately: Use a fixed fixture, hide or mask a region only if your tool supports it and the hidden area is not part of the visual contract, or exclude that page state from screenshot comparison.
Do not widen a diff threshold simply to silence noisy failures. First identify the source of the difference. Playwright documents screenshot comparison and options such as pixel tolerances and stylesheets that can suppress volatile elements, but those options belong to Playwright’s assertion API; do not assume Pest exposes identical controls. Check the current Pest documentation before adding tool-specific masking or threshold configuration.
6. Run visual checks in CI
Run the same Pest command in CI that developers use locally, after preparing the Laravel application, its database, and the required browser dependencies. Laravel’s testing documentation describes its test commands and test environment, and its Dusk guide includes a CI setup example for Dusk. This is the general sequence for Pest; adapt it to your CI provider and project:
- Check out the commit, install PHP and JavaScript dependencies from lockfiles, and prepare environment variables.
- Set up the database and run any migrations or seed setup required by the test suite.
- Install the browser binaries required by the installed browser tooling.
- Start the Laravel application or use the browser plugin’s supported in-process approach.
- Run
./vendor/bin/pest. - On failure, retain the test output and generated screenshots or other supported debugging artifacts so the reviewer can inspect them.
Keep the baseline files available in the checked-out commit. If CI generates screenshots in a temporary directory, distinguish those actual images from the committed approved references. A CI job should report a mismatch; it should not commit or approve a new baseline automatically.
7. Use Dusk when the project already relies on it
Laravel documents Dusk as a browser automation API and supports screenshot capture, responsive screenshots, and screenshots of a selected element. A basic capture looks like this:
<?php
$browser->visit('/')
->screenshot('home-page');
Dusk’s screenshot() call saves an image; capture alone does not establish that it matches an approved baseline. If your suite already uses Dusk, retain it for browser workflows and add a separately verified image comparison step or library if baseline regression checks are required. Do not describe screenshot capture as built-in Dusk visual comparison without confirming that capability in the current documentation. See Laravel Dusk documentation.
8. Alternative: Playwright snapshots
Teams already using Playwright Test can use its own visual assertions independently of Pest. The following TypeScript example targets a Laravel site running locally and uses a named baseline:
import { test, expect } from '@playwright/test';
test('home page visual baseline', async ({ page }) => {
await page.setViewportSize({ width: 1440, height: 900 });
await page.goto('http://127.0.0.1:8000/');
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
await expect(page).toHaveScreenshot('home-page.png');
});
On the initial run, Playwright creates a reference screenshot that should be reviewed and committed. Later runs compare against it. Its documentation covers snapshot generation, comparison, environment consistency, and update behavior, and the PageAssertions API describes screenshot assertion options. Keep this as a separate option for teams whose browser-test workflow is already Playwright-based; Pest is the more directly Laravel-integrated starting point supported by the cited Laravel guidance.
9. Troubleshooting screenshot failures
| Symptom | Likely cause | What to do |
|---|---|---|
| Baseline or snapshot is missing | The test has not generated its first reference, or the baseline was not committed or found at the expected path. | Run the test in the chosen baseline environment, inspect the generated image, approve it deliberately, and commit it. Check the test tool’s current baseline path rules. |
| Many unrelated pixels change on CI | Browser, OS, fonts, viewport, device scale, or headless settings differ from baseline generation. | Run baseline creation and comparison in the same pinned environment and inspect the environment versions. |
| Screenshot shows a loading state or missing images | Capture happens before app data, fonts, or images finish loading. | Wait for an app-specific ready condition and verify that the route has completed its expected data setup. |
| Only dates, counters, avatars, or ads differ | Visible data changes between runs or comes from an uncontrolled external service. | Use seeded fixtures or a deterministic test service. Mask or hide volatile elements only with documented support and only when appropriate. |
| Wrong page or redirect is captured | Authentication, session, route setup, or test data is missing. | Assert a stable heading or URL before taking the screenshot; establish the intended session and data state. |
| Browser executable cannot be launched | Browser dependencies are not installed or the installed browser version does not match the tool setup. | Run the documented browser installation command in the local and CI environments and check the browser tool’s version requirements. |
| Failures appear intermittent | Race conditions, animations, network responses, random test data, or parallel tests sharing state. | Make the page state deterministic, wait on meaningful readiness conditions, and isolate data or shared resources between tests. |
| Baseline updates conceal a real bug | Snapshots were regenerated and accepted without examining the diff. | Review actual, expected, and diff images in code review; require an explanation for intentional visual changes. |
10. Performance, reliability, and maintenance cost
Browser screenshots cost more runtime than HTTP assertions because they launch or control a browser and render pages. Keep the screenshot suite focused on high-risk pages, avoid redundant viewport/state combinations, and run it alongside rather than in place of fast unit and HTTP tests. Pest 4 documents parallel browser testing; use parallel execution only after ensuring tests do not share mutable data, ports, or state.
Reliability comes primarily from stable inputs and environment control, not from capturing more images. Each baseline has a maintenance cost: UI changes require reviewing and updating expected files, and broad page snapshots can flag many unrelated changes at once. Element-level screenshots can narrow a comparison to a component when the chosen tool supports them, while full-page shots reveal layout changes across the entire route. Choose the smallest capture that still covers the risk.
There is no universal useful pixel threshold: acceptable tolerance depends on browser consistency, fonts, antialiasing, and the page. Avoid treating a tolerance as a substitute for stable rendering. For ordinary Laravel tests, local browser screenshots have no per-image API charge, but they do consume developer and CI compute time. Hosted capture APIs have their own request plans and billing rules; compare the stated plan and billing behavior before adopting one.
Or skip the browser setup
If you need a rendered screenshot for a page review, report, or AI-assisted workflow without installing and maintaining a local browser, ScreenshotNeo provides a website screenshot API and MCP server. It is not a replacement for an assertion against a version-controlled baseline in your Laravel test suite; use your browser tests for that regression gate. The API can capture a URL in one request, while its MCP server gives AI agents a screenshot tool.
See the ScreenshotNeo API documentation for request options. Example cURL request:
curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
Python:
import requests
r = requests.get(
"https://api.screenshotneo.com/v1/shot",
params={"access_key": "YOUR_API_KEY", "url": "https://stripe.com"},
timeout=90,
)
open("shot.webp", "wb").write(r.content)
Node.js:
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://stripe.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await import('node:fs/promises').then(fs => fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer())));
Cookie banners, newsletter popups, and chat widgets are removed before the shot; each cleanup step can be turned off. Bot checks and CAPTCHAs, blank pages, timeouts, failed loads, and cache hits are not billed, and the response identifies page verdict and billing status in headers. AI agents can use its MCP server tools to take screenshots, get page information, and capture PDFs. The free plan includes 1,000 screenshots per month with no card; paid plans start at $5 for 3,000 screenshots. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a screenshot test prove the page works?
No. It detects visible rendering changes. Keep functional browser assertions and Laravel HTTP tests for behavior and response requirements.
Should every Laravel route have a visual baseline?
No. Start with important routes and states where a visual regression would matter, then expand when the value justifies the review and maintenance cost.
Can I use Dusk for screenshot regression testing?
Dusk can capture screenshots. Its documented capture API by itself does not compare those images with approved baselines, so add and verify a comparison layer if that is your goal.
Should expected screenshots be committed?
Yes. Versioned baselines make the approved appearance reviewable and available to later local and CI runs.
Sources
- Laravel Dusk: Pest recommendation, Dusk browser automation and screenshot capture.
- Laravel HTTP tests: request and response testing.
- Pest 4 browser testing announcement: installation and visual regression assertion.
- Pest browser testing: setup, browser workflows, screenshots, and debugging.
- Playwright visual comparisons: baseline comparison and environmental rendering variability.
- Playwright PageAssertions: screenshot assertion API.
- Laravel testing: testing commands and environment.


