Waitcher Docs Open Waitcher

Visual checks

A page can answer 200, pass every assertion and still look broken: a stylesheet that did not load, a banner covering the buy button, a layout that collapsed on phones. A visual check catches that. On a schedule, Waitcher opens one page in a fresh browser, takes a screenshot of the viewport or of one element, and compares it pixel by pixel with a screenshot you approved. A difference over your tolerance fails the run, with the old image, the new one and the difference kept as evidence.

A browser checkA visual check
What you writeA test with steps and assertionsA few lines of JSON: the page, the viewport, what to ignore
What it provesA journey worksA page looks the way you approved it
What breaks itA changed element, flow or replyAny visible change, including ones no assertion names
EvidenceReport, trace, screenshotsBaseline, new capture, difference image and its numbers

How a run works

  1. Waitcher starts a fresh, single-use sandbox (a microVM on Cloudflare) with a pinned Chromium.
  2. It opens your page at a fixed viewport (desktop 1280x800 or mobile 390x844), in light mode with reduced motion, waits for load, fonts and your ready selector, stops animations and hides the text caret.
  3. It paints your masks gray and captures the viewport or the one element you chose.
  4. It compares the capture with the approved baseline and passes when the share of changed pixels stays within your tolerance. Content that arrives late gets up to 10 seconds to settle before a difference counts.
  5. The sandbox is destroyed. Every run keeps its comparison: the baseline, the capture and how much differed. A failed run also keeps the difference image.

Underneath, a visual check is a small browser test that Waitcher writes from your settings and runs with the same e2e framework as your own tests. It uses the framework's toHaveScreenshot, which you can also call yourself inside a journey (below).

A visual check has no script, no sign-in and no custom headers: it sees what an anonymous visitor sees on a public HTTPS page.

Set one up

In waitcher.json, a visual entry needs only a key and a page:

{ "key": "home-look", "type": "visual", "path": "/" }

That captures the desktop viewport of the environment's baseUrl every 30 minutes (every 2 hours on the free plan, see limits). Add a visual block to change what is captured:

{
  "key": "pricing-mobile",
  "type": "visual",
  "path": "/pricing",
  "interval": 7200,
  "visual": {
    "checkpoint": { "kind": "element", "selector": "main" },
    "viewport": "mobile",
    "ready": { "selector": "main h1" },
    "masks": ["[data-testid=visitor-count]", ".carousel"],
    "thresholds": { "maxDiffRatio": 0.002 }
  }
}

Deploy it like any other monitor (Repository workflow):

waitcher deploy plan --env production
waitcher deploy apply --env production --based-on BASED_ON --smoke

For one check outside a repository, waitcher monitor init --type visual --target https://example.com/ --file visual.json writes a complete paused example for monitor apply.

Create the first baseline

A new visual monitor has nothing to compare with. Scheduled runs never create a baseline on their own: until one exists, a run ends as error with "First capture of main-web-linux: approve it as the baseline to start comparing". That is missing coverage, not a failure, and it opens no incident. There are two ways to set the baseline:

Capture and approve in one step. Start a run that updates the baseline. It takes the capture and makes it the approved baseline:

waitcher run start --id MONITOR_ID --update-baselines --wait
waitcher visual list --monitor-id MONITOR_ID
waitcher visual image --id COMPARISON_ID --role candidate --file ./private/baseline.png
waitcher monitor resume --id MONITOR_ID

In the web app this is Run and update baseline in the monitor's details. Look at the image afterwards; if it is wrong (a cookie banner, a half-loaded page), fix the settings and update again.

Look first, then approve. Run the check normally, inspect the first capture and approve it:

waitcher run start --id MONITOR_ID --wait
waitcher visual list --monitor-id MONITOR_ID
waitcher visual image --id COMPARISON_ID --role candidate --file ./private/first.png
waitcher visual approve --id COMPARISON_ID --expected-revision 0

In the web app, the run's report shows the capture, what to check in it, and Approve and start the schedule. With deploy apply --smoke, a first capture is reported as baseline_review with these commands.

From then on every run compares against that baseline and keeps the comparison: the baseline, the capture and how much differed, for passing runs too.

When a run fails

A failed visual run means the page changed more than your tolerance allows. Read what changed before deciding:

waitcher visual get --id COMPARISON_ID          # changed pixels and their share
waitcher visual image --id COMPARISON_ID --role diff --file ./private/diff.png
waitcher visual image --id COMPARISON_ID --role original --file ./private/before.png

In the web app, the run's report in the monitor's details shows how much changed against what you allow, the baseline, the new capture and the difference image, and the changed share of recent runs. The report of a visual run that changed: 5.938% of the image changed against 0.1% allowed, the approved baseline beside the new capture, and the three choices

Under the images it names the three things a change can be, each with its next step: Run again, Approve as the new baseline, or Edit masks and tolerance. Compare images opens the latest report. The Visual screen shows the same and is where you restore an earlier baseline.

Adopting an existing visual monitor into a project (deploy plan --adopt) starts it without a baseline: update the baseline once after the deploy.

waitcher visual restore returns to an earlier approved baseline. Stale approvals are refused with a conflict; reload and decide again instead of retrying.

Screenshots inside a browser test

A visual check sees one public page as an anonymous visitor. To compare a screen that needs a sign-in or a few clicks first, assert it inside a browser test with the framework's toHaveScreenshot:

import { test, expect } from "e2e";

test("dashboard looks right", async ({ app, screen }) => {
  await app.open("/login");
  // ... sign in, open the dashboard ...
  await expect(screen).toHaveScreenshot("dashboard.png");
  await expect(screen.getByRole("navigation")).toHaveScreenshot("nav.png", {
    maxDiffPixelRatio: 0.01,
    mask: [screen.getByTestId("last-updated")],
  });
});

The viewport, masks and tolerance are the test's own; the framework's options are threshold, maxDiffPixels, maxDiffPixelRatio, mask and maskColor. Deploy the test as an ordinary browser monitor. Waitcher keeps the baselines, so there are no PNG files to commit:

A monitor keeps up to 10 stored screenshots, 12 MiB together. Baselines follow the screenshot's name: renaming it in the test starts a new one. These runs are ordinary browser runs for your plan.

Settings

FieldValuesDefault
checkpoint{ "kind": "page" } for the viewport (no full-page scrolling), or { "kind": "element", "selector": CSS } for one elementPage
viewportdesktop (1280x800) or mobile (390x844)desktop
localeen-US or pl-PLen-US
timezoneUTC, Europe/Warsaw or America/New_YorkUTC
ready.selectorAn element that must be visible before the capturebody
masksUp to 10 CSS selectors painted gray, 50 elements in totalNone
captureBudgetMs5,000 to 45,00030,000
thresholds.pixelThreshold0 to 1: how different one pixel must be to count0.1
thresholds.maxDiffRatio0 to 0.1: the share of changed pixels a run may have and still pass0.001

Selectors must match: a checkpoint or ready selector that matches no element or several, or a mask that matches nothing, ends the run as error without a comparison. A capture of a different size than its baseline fails.

Limits