Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Laravel Visual Diff Laravel Package

beyondcode/laravel-visual-diff

Laravel Visual Diff by Beyond Code captures snapshots of your app's rendered pages and highlights exactly what changed between runs, making it easy to catch unintended UI regressions during development, testing, and code review.

View on GitHub
Deep Wiki
Context7

Getting Started

beyondcode/laravel-visual-diff is a dev-only test helper. Its whole value proposition is one loop: render a page → save the pixels as a baseline → render again later → compare against the baseline → emit a diff image you can look at. Everything in the package is plumbing around that loop: config for the browser, config for where snapshots live, config for how much pixel noise counts as "different," and a small comparison UI. If you keep that loop in your head, the package is small enough to read in an afternoon.

Check compatibility before you type anything else. The latest release (1.1.0, August 2019) exists specifically to add Laravel 5.7 and 5.8 support — that is the ceiling, not the floor. The composer constraints are Laravel-5-era. On Laravel 6+ / PHP 8.x you should expect composer require to fail resolution outright, or to force --ignore-platform-reqs and discover runtime breakage. Decide your path up front: stay pinned to the 5.x line, fork and widen the constraints yourself (and own the maintenance), or pick a maintained tool. Discovering this after you've written ten visual tests is the classic waste of a sprint.

Assuming you're on a compatible app:

composer require beyondcode/laravel-visual-diff:^1.1 --dev
php artisan vendor:publish --provider="BeyondCode\VisualDiff\VisualDiffServiceProvider"

Then verify the real prerequisite before writing a single test: a working headless Chrome/Chromium binary on the machine. Most "the package is broken" reports are actually "no browser binary" or "Chrome can't launch in this slim Docker image." Take a screenshot manually — outside the package — first. Once that works, the package will work.

The published config is the control panel, and four values determine almost all behavior:

  • the browser binary path,
  • the snapshot storage directory (baselines vs. current vs. diff outputs),
  • the viewport size,
  • the diff tolerance threshold.

Read those four, change nothing else, and go write the smallest possible test: one public page, one viewport, no auth. The integration reads like a Dusk interaction:

$this->browse(function (Browser $browser) {
    $browser->visit('/')->visualDiff('homepage');
});

Then run the three-run onboarding loop:

  1. Run once — writes the baseline. Nothing to compare against yet.
  2. Run again — should be a clean no-op. If it isn't, your page is non-deterministic already; fix that before going further.
  3. Change one CSS value and run — you should get a diff artifact showing exactly the changed region.

That three-run loop is the entire onboarding. Once you've opened a diff artifact and gone "yep, that's my border-radius," you understand the tool's day-to-day shape.

Where to look when something is off, in order: the published config → the snapshot directory (are you comparing the baselines you think you are?) → the package source. It's small, archived, and will never be documented better than it is coded.

Implementation Patterns

Treat baselines as source-controlled assertions. Commit the baseline directory to git. Updating a baseline should be an explicit, reviewable change in the PR — never a silent side effect of running tests locally. A common convention is a dedicated script (composer visual:update) so nobody regenerates baselines mid-debug and commits a wall of PNGs.

Keep the suite deliberately tiny. Visual diffing is slow and brittle relative to a feature test: each capture pays browser startup. The pattern that actually pays off is a visual smoke suite — landing page, login, dashboard, checkout, settings. The screens you'd notice immediately in production. Put them in their own test suite/directory so they run on a different cadence (pre-merge or nightly) than your fast unit suite.

Layer captures onto Dusk journeys you already have. The strongest integration is to add the capture at the end of a browser flow that already logged in, seeded state, and navigated. You get a screenshot of a realistic authenticated state instead of a login page over an empty database. Visual tests that bootstrap their own state from scratch are where the flakiness comes from.

Encode page + state + viewport in the snapshot name. checkout-empty-mobile, dashboard-desktop, settings-populated-desktop. This lets one test cover multiple viewports without ambiguous filenames, and it makes a CI failure log readable without opening the artifact.

Deterministic fixtures are not optional. Seed a fixed database, freeze time, disable animations and carousels, stub third-party scripts. Anything that varies between two runs — a timestamp, a remote avatar URL, an A/B bucket, a randomized "recommended for you" list — produces a diff that has nothing to do with your code. Do this at the application layer, not by squinting at the tolerance threshold.

Wrap the package in one trait. Because the package is archived and won't get fixes, isolate it. A CapturesVisualDiffs trait that applies your masking, your naming convention, and your viewport matrix means the package is touched in exactly one file across your codebase — so swapping the underlying tool later is a one-file change rather than a grep-and-replace across the test suite.

trait CapturesVisualDiffs
{
    protected function captureVisual(Browser $browser, string $page, string $state = 'default'): void
    {
        $browser->script('document.querySelectorAll(".cookie-banner, .toast").forEach(el => el.remove())');

        $browser->visualDiff(sprintf('%s-%s-%s', $page, $state, $this->viewportName()));
    }
}

CI pattern. Run in a container image that pins the exact browser version used locally. Upload the diff artifacts as build artifacts. Fail the job on any non-empty diff. The artifact is the entire point — a bare "diff detected" message is useless to a reviewer without the image they can open and eyeball. Keep the current/diff output directories out of git; only baselines belong in the repo.

Review workflow. The diff image is what you paste into the PR when asking for design sign-off, and it's the justification for a baseline update. That's the whole social value of the tool: it turns "looks fine on my machine" into a shared artifact.

Gotchas and Tips

It is archived, unmaintained, and tops out at Laravel 5.8. Release 1.1.0 is a compatibility bump for 5.7/5.8 — not a revival. There have been no releases since August 2019 and the repo has zero dependents. Don't build a critical regression gate on it. If visual testing genuinely matters to your team, evaluate Playwright screenshots, BackstopJS, or a hosted service as the primary, and treat this package as a convenience or a reference implementation of a Laravel-native approach.

Pin the version hard. With no future releases to bail you out, ^1.1 (or exact 1.1.0) plus a committed composer.lock is the only thing between you and an irreproducible environment. If you fork, alias your fork under the package name so the rest of the codebase is untouched and the swap is invisible.

Cross-machine rendering is the number-one source of false positives. Fonts, anti-aliasing, subpixel rendering, GPU/rasterization flags, and device pixel ratio all differ between macOS, Linux, and CI. A baseline generated on a Mac will fail on Linux. Pin one container image and generate baselines inside it. If you can't, accept that you'll regenerate baselines every time you switch environments, and budget for it.

Freeze non-determinism at the source, then lean on tolerance. Laravel's time-travel helpers for clocks, fixed fixtures for data, injected CSS or a config-level "hide selectors" list for cookie banners, ads, and animated elements. Then use the tolerance threshold for what's left — antialiasing noise. Tolerance handles rendering noise; masking handles content noise. They are not interchangeable, and using tolerance to paper over a live clock will produce a permanently red suite.

The headless browser is the real dependency. Assertion-looking failures are frequently "Chrome not found," missing shared libraries in a slim Docker base image, or a Puppeteer/Node version mismatch. Debug by capturing a screenshot outside the package. If that fails, the package was never the problem.

Snapshot bloat accumulates quietly. PNG baselines grow with every page you add. Prune removed pages from the baseline directory, keep the suite small, and don't check in current/diff outputs. Consider a .gitattributes marking the baseline directory as binary so GitHub's diff view doesn't churn.

A diff-viewer route is a production risk. If the package (or your config) registers a route for browsing results, make sure the service provider is only registered in local/testing and never ships enabled. Verify with php artisan route:list in a production-like environment — this is a two-minute check that prevents a very embarrassing one.

Extension points are thin, so don't plan on upstream fixes landing. The realistic customization surfaces are the published config (browser path, viewport, storage, tolerance) and your own wrapper trait. Design around that: assume you own the masking logic, the naming scheme, and the viewport matrix.

Don't put this in every test. Runtime is dominated by browser startup. Batch multiple captures per browser session where the API allows it, never run the visual suite in watch mode alongside unit tests, and never let it block your fast feedback loop.

Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
dbravoan/dbcms-admin-theme-dbcms
dbravoan/dbcms-theme-db-cyber
dbravoan/dbcms-blocks-basic
dbravoan/dbcms-module-content-audit
dbravoan/dbcms-module-ai-assistant
paygate-to/paygate-php
padosoft/askmydocs-connector-base
defstudio/supercharged-carbon
bernskioldmedia/laravel-matomo
bernskioldmedia/laravel-harvest
bernskiold/laravel-data-scrubber
bernskiold/laravel-currency-converter
bernskiold/laravel-campaign-monitor
bernskioldmedia/laravel-autotranslate
siberfx/telegram-bot
roy-sandip/laravel-view-extended
gregsphotography/site-builder-core
roy-sandip/laravel-view
anodyne/blade-fluentui-icons
webx-ui/module-catalog-collections