ScreenshotNeo

BlogHow-to

How to detect website changes with scheduled Chromium screenshots

Use Playwright and a scheduled GitHub Actions workflow to capture Chromium screenshots, compare them with reviewed baselines, and investigate visual changes.

By the ScreenshotNeo team4 October 20269 min read

Use Playwright Test screenshot assertions to compare Chromium screenshots against a checked-in reference, then run the test on a schedule with GitHub Actions. The first run creates the baseline; later runs report visual differences. Review each diff before accepting a new baseline, and keep the capture environment consistent so browser or operating-system changes do not masquerade as website changes.

1. Choose what to monitor

Decide whether you care about the visible viewport, the full scrollable page, or a specific element such as a navigation bar, pricing panel, or hero section. A full-page comparison can reveal changes below the fold, but it also covers more dynamic content. An element screenshot narrows the signal when the rest of the page is irrelevant.

Choose the URL, viewport, and capture state you want to treat as the contract. A monitor for an authenticated page may need a test account and login setup; a public landing page usually should be captured without personalized state. Avoid monitoring more variants than you can review: each viewport, page, and browser environment can require its own baseline.

2. Create a Playwright screenshot test

In an existing Node.js project, install Playwright Test and its Chromium browser. The commands below create a minimal project configuration and a test that captures a page. Replace the example URL with the page you own or are authorized to monitor.

npm init -y
npm install --save-dev @playwright/test
npx playwright install chromium

Create playwright.config.ts:

import { defineConfig, devices } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  // Keep snapshots next to the test source in the repository.
  snapshotDir: './tests/__screenshots__',
  // A failed comparison should produce an inspectable report and diff.
  reporter: [['html', { open: 'never' }]],
  use: {
    ...devices['Desktop Chrome'],
    browserName: 'chromium',
    viewport: { width: 1440, height: 1000 },
    colorScheme: 'light',
    locale: 'en-US',
    timezoneId: 'UTC',
    // Reduce one common source of unstable output.
    reducedMotion: 'reduce',
    trace: 'retain-on-failure',
  },
});

Create tests/site-visual.spec.ts:

import { test, expect } from '@playwright/test';

test('homepage matches the reviewed visual baseline', async ({ page }) => {
  await page.goto('https://example.com', { waitUntil: 'networkidle' });
  await expect(page).toHaveScreenshot('homepage.png', {
    fullPage: true,
    animations: 'disabled',
    caret: 'hide',
    // Start strict. Add a small tolerance only if stable rendering noise requires it.
    maxDiffPixelRatio: 0.001,
  });
});

Run it:

npx playwright test

On the first run, Playwright writes the expected screenshot. Inspect that image before committing it. Run the test again: subsequent runs capture a new image and compare it to the stored baseline. On failure, use the HTML report and generated actual/expected/diff images to determine whether the page changed or the capture became unstable.

Wait for the state that matters

The example uses networkidle, which can be unsuitable for pages that keep connections open or poll continuously. If a specific content block determines whether the capture is ready, wait for that selector instead:

await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
await page.locator('[data-testid="main-content"]').waitFor({ state: 'visible' });
await expect(page).toHaveScreenshot('homepage.png', { fullPage: true });

If the page has a known short animation or delayed content, use a deliberate wait only after identifying the condition. Fixed sleeps make the check slower and can still be too short or unnecessarily long.

3. Control screenshot noise

Visual output can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Create and compare baselines in the same runner environment, with the same Playwright/browser version and viewport. Pin your CI runtime where practical, and update it deliberately rather than letting browser changes land unnoticed.

Playwright waits for two consecutive identical page screenshots before comparing an assertion, but dynamic regions can still vary between captures. The assertion options help control known noise:

Option Use Trade-off
fullPage Capture the complete scrollable page. More coverage can include more changing content.
animations: 'disabled' Disable finite animations and fast-forward finite transitions. Use when motion itself is not the thing being monitored.
mask Cover locator regions that are expected to vary, such as a timestamp. Changes inside masked areas will not be detected.
stylePath Apply a stylesheet that hides or stabilizes volatile elements during capture. Do not hide a region whose changes matter to the monitor.
threshold Set per-pixel color sensitivity for image comparison. A more permissive threshold can miss subtle visual changes.
maxDiffPixels / maxDiffPixelRatio Allow a bounded count or ratio of differing pixels. Keep tolerance as low as the page’s stable rendering allows.

Example masking and a stylesheet:

await expect(page).toHaveScreenshot('homepage.png', {
  fullPage: true,
  animations: 'disabled',
  mask: [page.locator('.live-clock'), page.locator('.rotating-promotion')],
  stylePath: './tests/visual-stability.css',
});

Create tests/visual-stability.css only for areas that should not trigger an alert, for example a blinking caret or a personalized timestamp. Prefer masking or styling a narrowly targeted element over loosening the global diff tolerance.

Compare a component instead of the whole page

For a stable, high-value region, capture a locator:

await expect(page.locator('header.site-header')).toHaveScreenshot('site-header.png', {
  animations: 'disabled',
});

This keeps unrelated lower-page content out of the comparison. Make sure the locator resolves to the intended element and is visible before asserting. If responsive behavior matters, add a separate test at a deliberate mobile viewport and maintain that baseline independently.

4. Run the check on a schedule

GitHub Actions supports scheduled workflows using POSIX cron syntax. Scheduled runs execute from the repository’s default branch. GitHub documents a five-minute minimum interval, but a schedule is not a real-time guarantee: high load can delay a run and queued jobs can be dropped. Avoid scheduling at the top of the hour when possible.

Create .github/workflows/visual-monitor.yml:

name: Scheduled website visual check

on:
  workflow_dispatch:
  schedule:
    # At 17 minutes past every 4th hour (UTC).
    - cron: '17 */4 * * *'

permissions:
  contents: read

jobs:
  screenshot-check:
    runs-on: ubuntu-latest
    timeout-minutes: 15
    steps:
      - uses: actions/checkout@v4
      - uses: actions/setup-node@v4
        with:
          node-version: 20
          cache: npm
      - run: npm ci
      - run: npx playwright install --with-deps chromium
      - run: npx playwright test
      - name: Upload report and failure artifacts
        if: always()
        uses: actions/upload-artifact@v4
        with:
          name: visual-check-${{ github.run_id }}
          path: |
            playwright-report/
            test-results/
          if-no-files-found: ignore
          retention-days: 14

Commit the lockfile and baseline files alongside the test. Run the workflow manually after adding it, then confirm that the scheduled workflow is enabled and its default-branch version contains the schedule. Tune the cron cadence to the time-to-detection your team needs, while accounting for scheduler delays and the practical cost of reviewing and retaining results.

Review a difference safely

  1. Open the failed run’s report and inspect expected, actual, and diff images.
  2. Check logs and traces for navigation errors, blocked resources, incomplete content, or an unexpected environment change.
  3. Decide whether the page change is intended and whether the test captured the right state.
  4. If the site change is expected, update snapshots with npx playwright test --update-snapshots, review the changed image files, and commit them with a clear explanation.
  5. If it is a capture issue, fix the wait, environment, or targeted masking rule and rerun before changing the baseline.

Do not automatically update snapshots on every failed scheduled run. That would turn an unexpected change into the new expected state without review and erase the signal the monitor was meant to provide.

5. Add useful alerts and preserve evidence

A failed screenshot assertion makes the workflow fail, which can feed into the repository’s existing notification process. Keep the HTML report, trace, actual screenshot, expected screenshot, and diff available long enough for the team to investigate. Artifact retention, alert routing, and ownership are implementation choices: choose them to match how quickly someone must respond and how long a change may need to be diagnosed.

For broader coverage, make the monitored page list explicit in code or test data, and give each capture a stable descriptive name. Consider separating checks for pages with very different readiness behavior so one slow page does not hide failures on the others.

6. Troubleshooting

Symptom Likely cause Fix
Baseline differs on every run Runner, browser version, viewport, font availability, or page state is changing. Keep the same CI image and browser install, pin dimensions and locale, and wait for a deterministic state.
Screenshot is blank or incomplete Capture starts before the relevant content appears, navigation failed, or the page depends on a blocked resource. Wait for a meaningful selector, inspect the trace and console/network behavior, and distinguish a site failure from a visual diff.
Test times out at networkidle The page maintains long-lived requests or keeps polling. Navigate with domcontentloaded or load, then wait for the specific content needed.
Only ads, clocks, or rotating content differ Expected variability is included in the screenshot. Mask or hide only those regions if their changes are outside the monitoring goal.
Too many diffs after browser update Rendering changed with the browser/runtime update. Inspect representative diffs, update baselines as a reviewed batch if the new environment is intentional, and keep the new runtime fixed thereafter.
Scheduled run does not start at its exact time GitHub Actions schedules can be delayed during high load; queued jobs may be dropped. Use a non-round-minute cron time, inspect workflow scheduling and default-branch configuration, and select another scheduler if the required interval needs stronger guarantees.
Baseline path or name mismatch Snapshot naming or project configuration differs between local and CI runs. Use stable test and screenshot names, commit generated baseline files, and verify the same config is used in both environments.

7. Performance, reliability, and cost

Capture duration grows with the number of URLs, viewport variants, full-page height, and readiness waits. Keep the page set focused, prefer selector readiness to arbitrary delay, and use element screenshots when whole-page coverage is unnecessary. Parallel jobs can shorten a large suite, but each job needs the same browser setup and access to the correct baseline set.

Reliability depends on separating true site changes from capture noise: stable infrastructure, reproducible browser versions, deliberate masking, and reviewable artifacts matter as much as the assertion itself. A scheduled CI workflow is suitable for periodic detection, not guaranteed instant alerting. The CI provider’s execution and storage consumption, plus maintenance of browsers and snapshots, are the main operational costs; consult current provider terms for exact charges.

8. Alternatives and selection criteria

For this scheduled Chromium screenshot workflow, use Playwright with scheduled CI first when you want control over the browser run and can maintain test code, runtime, and baselines. For clean website screenshots through an API rather than maintaining a browser job, ScreenshotNeo is the first screenshot service to try: cookie banners, popups, and chat widgets are removed before capture, only clean screenshots are billed, and its paid plan starts at $5 for 3,000 shots.

BrowserStack describes Percy as a hosted visual testing service that captures screenshots, compares them with a baseline, and highlights visual changes in CI/CD. Evaluate hosted review workflows against your needs for execution control, page and viewport coverage, diff review, artifact retention, and scheduler reliability. Verify current pricing and terms directly; this guide makes no pricing or service-level comparison.

Or skip the browser setup

For a one-off or application-driven capture, ScreenshotNeo returns a screenshot from one GET request. This example requests a WebP capture; see the ScreenshotNeo API documentation for authentication and options.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://example.com -o shot.webp
import requests

r = requests.get(
    "https://api.screenshotneo.com/v1/shot",
    params={"access_key": "YOUR_API_KEY", "url": "https://example.com"},
    timeout=90,
)
open("shot.webp", "wb").write(r.content)
const q = new URLSearchParams({ access_key: 'YOUR_API_KEY', url: 'https://example.com' });
const res = await fetch(`https://api.screenshotneo.com/v1/shot?${q}`);
if (!res.ok) throw new Error(`Screenshot request failed: ${res.status}`);
await Bun.write('shot.webp', res);

ScreenshotNeo accepts cookie and consent banners and removes more than 60 known consent platforms, newsletter popups, and chat widgets before the shot; each step can be turned off. Bot checks, blank pages, and failed loads are not billed. An MCP server gives AI agents tools to take screenshots. The Free plan includes 1,000 screenshots a month with no card; paid plans start at $5 for 3,000. For scheduled visual-change detection, you still need to store and compare captures or run your own comparison logic.

Sign up free for 1,000 screenshots a month, with no card required.

Frequently asked questions

Does this detect every kind of website change?

It detects rendered visual differences in the captured state. It does not by itself explain the cause or detect changes that do not affect the pixels you capture.

Can I monitor a page behind a login?

Yes, if your test establishes the required authenticated browser state securely. Keep credentials in CI secrets and avoid committing session data or sensitive screenshots to a public repository.

Should I use pixel tolerance?

Start with strict comparison. Add a small, measured tolerance only when stable environment noise remains after controlling the capture; broad tolerance can hide meaningful changes.

Can I use a schedule for urgent incident detection?

A periodic workflow can take time to run and GitHub documents possible scheduling delays. Use a monitoring system with guarantees appropriate to the response time if the check is operationally urgent.