ScreenshotNeo

BlogHow-to

How to Test a Website’s Responsive Images with Screenshot Comparisons

Test responsive images across viewport widths and DPRs with repeatable screenshot baselines, and verify the browser’s choice with currentSrc.

By the ScreenshotNeo team4 October 20269 min read

Test responsive images by capturing the page at meaningful viewport widths and device pixel ratios (DPRs), comparing each capture with a reviewed baseline, and recording the image element’s currentSrc in every case. A screenshot tells you what rendered; currentSrc tells you which resource the browser selected. Assert both so a visually similar but incorrect candidate does not pass unnoticed.

This guide uses Playwright Test and TypeScript. It covers resolution switching with srcset, art direction and format alternatives with <picture>, DPR, screenshot noise, and failure triage.

1. Inventory how each image responds

Before writing tests, identify the behavior you need to protect. Responsive images commonly use resolution switching, art direction, or both. The browser uses the rendered slot and source declarations to choose a resource; with width descriptors, sizes describes the expected slot and works with the candidate widths. DPR affects the pixel resolution sought for that slot. See MDN’s guides to the <img> element and responsive images.

Pattern What to inspect Test assertion
srcset with w descriptors and sizes Candidate intrinsic widths, expected slot size, and the sizes expression At representative widths and DPRs, verify the selected candidate URL and rendered appearance.
srcset with x density descriptors Declared density candidates and fallback src Verify the selected URL at the DPRs your page supports.
<picture> with media conditions Each media condition and its intended crop or composition Exercise every meaningful branch around its breakpoint.
<picture> with type alternatives Each offered format and the <img> fallback Verify the expected branch in browsers that support it, and check the fallback path where relevant.

Width descriptors should match the intrinsic widths of the referenced files. For <picture>, the nested <img> remains the fallback; conditional sources can provide art direction or format selection. Consult MDN on the <picture> element.

2. Choose viewport and DPR cases

Build cases from your actual CSS breakpoints and <picture> media conditions. Include widths just below and above each transition, plus a representative width between transitions when the layout changes continuously. Add DPR values that represent your supported devices and candidate strategy.

A list of popular device names alone can miss a breakpoint or a DPR transition. Viewport width and DPR are separate inputs: changing the viewport does not automatically change the device scale factor. Include zoom, orientation, or network conditions only if the page’s responsive-image behavior is expected to depend on them.

For each case, record:

  • Viewport width and height in CSS pixels, plus device scale factor.
  • Browser and version, and the rendering environment used for the baseline.
  • Image identity, rendered box dimensions, selected currentSrc, and expected candidate or <picture> branch.
  • Screenshot output scale and the baseline name.
  • Relevant page state, such as consent state, loaded test data, and whether lazy content has entered the viewport.

3. Set up Playwright projects for DPR

Install Playwright Test and its browser binaries using the official installation instructions. Define separate projects for the DPR values you want to cover. A project’s use.deviceScaleFactor sets the browser context DPR; keep it stable for each project.

// playwright.config.ts
import { defineConfig } from '@playwright/test';

export default defineConfig({
  testDir: './tests',
  projects: [
    {
      name: 'chromium-dpr1',
      use: { browserName: 'chromium', deviceScaleFactor: 1 },
    },
    {
      name: 'chromium-dpr2',
      use: { browserName: 'chromium', deviceScaleFactor: 2 },
    },
  ],
  expect: {
    toHaveScreenshot: {
      // Keep screenshot pixel scale explicit for repeatable output.
      scale: 'css',
      animations: 'disabled',
    },
  },
});

With scale: 'css', Playwright writes one output pixel per CSS pixel. scale: 'device' writes device pixels, so a high-DPI context produces a larger screenshot. Choose deliberately and use the same setting for baseline generation and comparison. See the Playwright screenshot assertion options and screenshot parameters.

4. Write a test that checks source and appearance

The following test assumes the application is running locally and the hero image has data-testid="hero-image". Adjust the URL, selector, and expected path fragment to match the app. The assertion checks the selected resource separately from the visual baseline.

// tests/responsive-image.spec.ts
import { test, expect } from '@playwright/test';

test('hero image uses the expected mobile candidate', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 844 });
  await page.goto('http://127.0.0.1:3000/page-under-test', {
    waitUntil: 'networkidle',
  });

  const hero = page.getByTestId('hero-image');
  await expect(hero).toBeVisible();

  // Ensure the chosen image has loaded before reading its source or capturing.
  await expect.poll(() =>
    hero.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0)
  ).toBe(true);

  const details = await hero.evaluate((img: HTMLImageElement) => ({
    currentSrc: img.currentSrc,
    renderedWidth: img.getBoundingClientRect().width,
    renderedHeight: img.getBoundingClientRect().height,
    naturalWidth: img.naturalWidth,
    naturalHeight: img.naturalHeight,
  }));

  expect(details.currentSrc).toContain('hero-mobile');
  expect(details.renderedWidth).toBeGreaterThan(0);
  await expect(page).toHaveScreenshot('hero-mobile.png');
});

Run it with npx playwright test. On the first run, Playwright creates a reference screenshot; subsequent runs compare the page with that baseline. Review and commit approved baselines with the test. For component-level checks, capture the image or containing component with a locator screenshot assertion rather than the full page when unrelated page content creates noise.

currentSrc is the browser-selected URL for an image element. It can differ from the literal src attribute when responsive sources are present. Record it with the rendered box and inspect the markup when selection surprises you. See MDN’s currentSrc reference.

5. Capture every meaningful branch

Use data-driven cases for widths around real breakpoints. Keep each baseline’s name descriptive of the branch and viewport. For example:

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

const cases = [
  { name: 'narrow-mobile', width: 375, height: 812, source: 'hero-mobile' },
  { name: 'wide-mobile', width: 430, height: 932, source: 'hero-mobile' },
  { name: 'tablet', width: 768, height: 1024, source: 'hero-tablet' },
  { name: 'desktop', width: 1280, height: 800, source: 'hero-desktop' },
];

for (const item of cases) {
  test(`hero responsive image: ${item.name}`, async ({ page }) => {
    await page.setViewportSize({ width: item.width, height: item.height });
    await page.goto('http://127.0.0.1:3000/page-under-test');
    const hero = page.getByTestId('hero-image');
    await expect(hero).toBeVisible();
    await expect.poll(() =>
      hero.evaluate((img: HTMLImageElement) => img.complete && img.naturalWidth > 0)
    ).toBe(true);
    const selected = await hero.evaluate((img: HTMLImageElement) => img.currentSrc);
    expect(selected).toContain(item.source);
    await expect(page).toHaveScreenshot(`hero-${item.name}.png`);
  });
}

Run the width cases in each configured DPR project. This produces explicit coverage of both axes without assuming that a viewport preset implies a particular DPR.

6. Control screenshot noise and review diffs

Visual comparisons are meaningful only when the page and renderer are stable. Keep browser version, operating system or container, fonts, browser settings, test data, and page state consistent between baseline creation and comparison. Differences in headless mode, hardware, power state, and rendering settings can also affect pixels. Playwright documents its visual comparison workflow and controls.

Playwright waits for consecutive screenshots to match before saving a new screenshot. Its comparisons use pixelmatch and expose controls such as maxDiffPixels, maxDiffPixelRatio, and color threshold. Use these only to accommodate understood rendering variation. A permissive threshold can hide a wrong source or a real layout defect.

await expect(page).toHaveScreenshot('hero-mobile.png', {
  maxDiffPixelRatio: 0.001,
});

Prefer stabilizing the cause of noise over broadening the threshold. Disable animations, wait for image loading and the application’s stable state, and use Playwright’s screenshot style or stylePath option to hide known volatile content such as clocks or rotating promotions where appropriate. Avoid hiding the responsive image or layout under test.

7. Diagnose failures systematically

  1. Confirm the page state. Check that navigation completed, the right test data loaded, and lazy-loaded content entered the viewport. Wait for the image to be complete and have a nonzero naturalWidth.
  2. Check source selection. Log currentSrc, rendered box size, and DPR. Inspect srcset descriptors, sizes, intrinsic file widths, and each <picture> media and type condition.
  3. Inspect the visual diff. Look for a crop or art-direction change, blur, a missing image, unexpected whitespace, or a layout shift. Determine whether the source is wrong, the CSS slot changed, or the intended design changed.
  4. Check environment drift. Compare browser/version, fonts, OS or container, DPR, screenshot scale, and browser settings with the baseline environment.
  5. Update carefully. Adjust tolerances only for understood and reviewed noise. Replace a baseline only after deciding that the visual change is intended.

Common errors

Symptom Likely cause Fix
currentSrc is empty or unexpected The image has not loaded, the selector found a different element, or the declared candidates and conditions do not match expectations. Wait for visibility and successful image loading; verify the element identity, then inspect srcset, sizes, and <picture> conditions.
Expected mobile image appears at desktop width A media condition or breakpoint differs from the test’s assumption, or the viewport was not set before navigation/layout. Set viewport before navigation, use widths on both sides of the actual condition, and assert the expected source branch.
Screenshot dimensions differ between DPR runs Screenshot scale is device pixels or the context DPR changed. Set scale explicitly and keep DPR fixed per project; use CSS scale for CSS-pixel output.
Image looks soft despite a passing source assertion The selected candidate may be too small for the rendered slot and DPR, or CSS may enlarge it. Compare candidate intrinsic width with the slot and density target; review the screenshot at the intended scale.
Intermittent pixel diffs Animations, rotating content, fonts, timing, or external data vary between runs. Stabilize page state and rendering environment; disable or mask only known unrelated volatility.
Baseline changes across machines Browser, OS, fonts, rendering settings, or hardware differ. Generate and compare baselines in the same pinned browser/container environment.
All widths select the same image sizes or source conditions may describe a constant slot, the CSS layout may not change as expected, or candidates may be equivalent. Measure the rendered box at each case and compare it with the declared source-size and media rules.

8. Performance, reliability, and maintenance

Screenshot comparison is most useful when cases reflect real transitions rather than every possible pixel width. Cover each breakpoint boundary, important intermediate layout, and supported DPR class. This keeps runtime and baseline review manageable while protecting the image behavior that can change.

Wait for a stable page and image rather than relying on a fixed delay alone. Network-idle navigation can help for pages whose state settles that way, but applications with ongoing requests should wait for a specific readiness signal. Keep captures isolated from unrelated dynamic content. Pin the browser and execution environment used to create and check baselines; regenerate baselines deliberately after a browser or rendering environment change.

Store source-selection assertions alongside visual baselines. A pixel comparison catches crop, sizing, and layout changes; the URL assertion catches a source-selection regression even when two candidates look similar. Review diffs before accepting baseline updates so intended design changes and accidental regressions do not become indistinguishable.

9. Or skip the browser setup

If you need a clean capture for a page or want screenshots without maintaining capture infrastructure, ScreenshotNeo is a website screenshot API and MCP server. It can capture image formats or PDFs through one GET request. The API captures rendered output; for responsive-image tests that need to prove which candidate the browser chose, keep a browser assertion for currentSrc as described above.

Example request for a page screenshot (replace the key and target URL):

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

See the ScreenshotNeo API documentation for request options and supported formats. Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed. Its MCP server lets AI agents take screenshots. The free plan includes 1,000 screenshots per month with no card, and paid plans start at $5 for 3,000.

Create a free ScreenshotNeo account to get 1,000 screenshots a month with no card.

FAQ

Does a screenshot tell me which srcset candidate loaded?

No. Read the image element’s currentSrc and assert it separately from the screenshot.

Does changing the viewport change DPR?

No. Configure the browser context’s device scale factor for DPR coverage; viewport dimensions are a separate setting.

Should I compare full pages or just the image?

Capture the smallest area that proves the behavior. Use a component or image capture when the surrounding page adds unrelated visual noise, and use full-page captures when page layout or lazy-loaded content is part of the requirement.

Should a test assert an exact image URL?

Assert the intended candidate or branch at the level your implementation promises. A stable path fragment or explicit expected URL can catch selection regressions; update it when a deliberate asset change is reviewed.