ScreenshotNeo

BlogHow-to

Puppeteer Screenshot Testing: How to Handle Cookie Banners

Hide a consent banner for visual screenshots, or test the real consent controls—without confusing the two. Includes runnable Puppeteer code, reliable state setup, and fixes for common capture failures.

By the ScreenshotNeo team4 October 202610 min read

Direct answer: If a cookie banner is not part of the visual regression baseline, identify its stable, unique selector and hide that element for the screenshot with page.addStyleTag() (or remove only that node with page.evaluate()). If the test is meant to verify consent behavior, leave the banner visible, click its actual accept or reject control, and assert the resulting UI and state. Hiding a banner changes the screenshot; it does not grant or reject consent.

Puppeteer captures a page with Page.screenshot() and an individual element with ElementHandle.screenshot(). Its screenshot guide demonstrates navigation with waitUntil: 'networkidle2'; treat that as an example, not proof that every application or consent manager has finished rendering. Puppeteer Screenshots guide.

1. Choose the test intent first

Goal What to do What the result proves
Stable visual baseline without the banner Verify and hide or remove the specific banner for this capture, then take the screenshot. The page appearance with that node suppressed. It does not prove consent was recorded.
Test accepting or rejecting consent Start in a clean state, interact with the real control, and assert the expected UI and site state. That the tested interaction produced the asserted outcome for this site and scenario.
Test a returning visitor Seed the documented consent state using the site’s supported mechanism, then assert the banner behavior. Behavior for that seeded state. A cookie alone may not represent the site’s complete consent state.

Keep visual-only overrides local to the capture setup and record why they exist. This makes it clear to maintainers that the screenshot intentionally excludes the banner.

2. Install Puppeteer and run a visual-only capture

Install Puppeteer in a Node.js project. The following script navigates to a target page, waits for a stable banner selector, hides only that banner with a capture-scoped stylesheet, and saves a full-page PNG. Replace the URL and selector with values inspected on your target site.

npm install puppeteer

// capture.mjs
import puppeteer from 'puppeteer';

const url = 'https://example.com';
// Inspect the site and replace this with a stable, unique selector.
const bannerSelector = '#cookie-consent-banner';

const browser = await puppeteer.launch({ headless: true });
try {
  const page = await browser.newPage();
  await page.setViewport({ width: 1440, height: 1000, deviceScaleFactor: 1 });

  await page.goto(url, { waitUntil: 'networkidle2', timeout: 45_000 });
  await page.waitForSelector(bannerSelector, { timeout: 10_000 });

  const matches = await page.locator(bannerSelector).count();
  if (matches !== 1) {
    throw new Error(`Expected exactly one banner for ${bannerSelector}; found ${matches}`);
  }

  await page.addStyleTag({ content: `${bannerSelector} { display: none !important; }` });
  await page.screenshot({ path: 'page-without-banner.png', fullPage: true });
} finally {
  await browser.close();
}

page.addStyleTag() adds a stylesheet to the page, and page.evaluate() runs a function in page context. See the addStyleTag API and evaluate API. The explicit selector check prevents silently hiding the wrong number of elements when a site’s markup changes.

Hide versus remove

CSS hiding is usually preferable for screenshot-only changes because it preserves the DOM node and leaves site scripts alone, though layout may still change if the banner normally occupies document space. If a sticky overlay is the problem, hiding it removes the overlay while preserving the rest of the page. Removing the node is another option, but can trigger site behavior that watches DOM changes.

// Alternative: remove exactly the selected banner node for this capture.
const removed = await page.evaluate((selector) => {
  const nodes = document.querySelectorAll(selector);
  if (nodes.length !== 1) return nodes.length;
  nodes[0].remove();
  return 1;
}, bannerSelector);
if (removed !== 1) throw new Error(`Expected one banner node; found ${removed}`);

Do not use broad selectors such as div, generic class fragments, or rules that hide every fixed element. They can suppress unrelated content and make the baseline misleading. Inspect the rendered page, choose a selector tied to the banner, and fail loudly if the expected target is absent or ambiguous.

When consent behavior is the subject, preserve the banner and exercise the actual control exposed by the page. This example is deliberately site-specific: replace the selectors and expected state with the site’s real UI and documented behavior. It uses a fresh browser context so the scenario does not inherit cookies or local storage from an earlier test.

// consent-test.mjs
import assert from 'node:assert/strict';
import puppeteer from 'puppeteer';

const browser = await puppeteer.launch({ headless: true });
try {
  const context = await browser.createBrowserContext();
  try {
    const page = await context.newPage();
    await page.goto('https://example.com', { waitUntil: 'domcontentloaded', timeout: 45_000 });

    const banner = '#cookie-consent-banner'; // Replace with inspected selector.
    const acceptButton = '[data-testid="consent-accept"]'; // Replace with real control.

    await page.waitForSelector(banner, { visible: true, timeout: 10_000 });
    await page.locator(acceptButton).click();
    await page.waitForSelector(banner, { hidden: true, timeout: 10_000 });

    // Assert the state the site actually promises. This example inspects
    // cookies, but the site may use localStorage or another mechanism.
    const cookies = await context.cookies();
    console.log('Cookies after accepting:', cookies.map(({ name }) => name));
    // Add a site-specific assertion for the documented consent state.
  } finally {
    await context.close();
  }
} finally {
  await browser.close();
}

A disappearing banner is a useful UI assertion, but by itself it may not prove that the intended choice was stored. Assert the outcome your application promises, such as the appropriate consent state in its supported storage mechanism. Do not assume one cookie is universal evidence: sites can store state in different ways. Avoid bypassing the UI in a test whose purpose is to verify that UI.

4. Make capture state reproducible

Consent banners often depend on browser storage. A new browser context gives a test isolated cookies and local storage; use it for a fresh-visitor scenario. For a returning-visitor scenario, seed the site’s known state deliberately and document the fixture. Puppeteer guidance provides browser and BrowserContext cookie operations; current guidance marks the Page-level cookie methods deprecated. See the Puppeteer cookie guide and BrowserContext API.

// Example only: use the site's actual cookie name, value, domain, and semantics.
await context.setCookie({
  name: 'consent_state',
  value: 'accepted',
  domain: 'example.com',
  path: '/',
  secure: true,
  httpOnly: false,
  sameSite: 'Lax',
});

const cookies = await context.cookies('https://example.com');
console.log(cookies);

Cookie attributes and validity depend on the target site and URL. Do not copy the illustrative cookie above as a real consent implementation. If the site stores state in local storage or another mechanism, set up that state using a deliberate fixture appropriate to the test. Keep fresh-state and persisted-state cases separate so the order of test execution cannot affect results.

5. Wait for the right state before the screenshot

Choose readiness based on what the test needs. Navigation completion and network idleness do not necessarily mean a client-rendered consent manager, delayed banner, font, or image has reached the desired state. Wait for a specific selector or app condition, then capture. Puppeteer’s screenshot guide shows networkidle2 in its example but does not promise it is right for every page.

await page.goto(url, { waitUntil: 'domcontentloaded', timeout: 45_000 });
await page.waitForSelector('#main-content', { visible: true, timeout: 15_000 });
// Wait for a meaningful application or consent state if the app exposes one.
await page.screenshot({ path: 'ready.png', fullPage: true });
  • domcontentloaded is useful when the application can render before all subresources finish; add explicit condition waits.
  • networkidle2 is a convenient documented example, but persistent requests or later rendering can make it a poor readiness signal.
  • waitForSelector() targets a specific DOM state. Use visibility options when an element must be visible, and wait for hidden state when verifying dismissal.
  • Use a delay only when the app’s known behavior requires it; selector or application-state waits are usually clearer than an arbitrary sleep.

6. Capture a component when that is the test subject

If the visual test is about a page component rather than the whole document, capture the element directly. Puppeteer’s ElementHandle.screenshot() attempts to scroll the element into view if needed. A component screenshot can reduce unrelated page variation, but it will not cover the page-level experience around the banner.

const target = await page.waitForSelector('[data-testid="pricing-card"]', { visible: true });
if (!target) throw new Error('Pricing card was not found');
await target.screenshot({ path: 'pricing-card.png' });

For whole-page captures use page.screenshot({ fullPage: true }); for viewport-only output omit fullPage or set it to false. Set viewport dimensions and device scale factor explicitly when the baseline depends on them. See Page.screenshot.

7. Troubleshoot common failures

Symptom Likely cause Fix
Banner still appears Selector is wrong, the banner is in an iframe or shadow root, or it is inserted after the override. Inspect the live DOM and confirm the selector matches exactly one visible element. Wait for insertion before applying the style. Handle frame or shadow-root content using the relevant frame or component context.
Unrelated content disappears The selector is too broad or matches multiple nodes. Use a stable, unique selector and assert its match count before changing the page.
Banner flashes in the screenshot Screenshot occurs before the banner is hidden or a later script recreates it. Wait for the banner, apply the override, verify its computed visibility or absence, then capture. If the site recreates it, identify the timing and use a narrowly scoped test setup.
Consent test starts without a banner Cookies or local storage persisted between tests. Use a fresh BrowserContext for the new-visitor case; inspect and reset only the relevant state for your application.
Banner disappears but state assertion fails The click may not have completed, the selector may hit the wrong control, or the expected state mechanism is incorrect. Wait for the specific post-click UI, verify the control selector, and assert the site’s documented state rather than assuming a particular cookie.
networkidle2 times out or capture is premature Persistent network traffic prevents idleness, or app rendering continues after it. Choose an appropriate navigation wait condition and add a wait for the exact content or state the screenshot requires.
Screenshot differs across runs Viewport, storage, dynamic content, fonts, animation, or page readiness varies. Fix viewport and browser state, wait on meaningful conditions, and control app data or animation within the test environment where appropriate.
Cookie API call is deprecated Code uses Page-level cookie methods. Use the current BrowserContext or Browser cookie methods documented by Puppeteer.

8. Reliability, performance, and maintenance

  • Reliability: Use a fresh context for independent visitor scenarios, assert selectors before manipulating them, and always close contexts and browsers in finally blocks. A capture should fail visibly when its expected page state is missing.
  • Timing: Waiting only for network quiet can either waste time or miss later UI changes. Wait for the state relevant to the test and set bounded timeouts so failures are diagnosable.
  • Maintenance: Banner markup and consent flows are site-specific. Keep selectors near the test, name their intent, and update them when the application changes. Avoid global browser hacks that silently affect unrelated tests.
  • Cost: Running Puppeteer yourself has no per-screenshot API charge from Puppeteer, but it uses your compute, browser installation, maintenance time, and CI capacity. This is an operational consideration, not a published benchmark.

Or skip the browser setup

ScreenshotNeo is a website screenshot API and MCP server for developers. One GET request returns a PNG, JPEG, WebP, or PDF. Its cookie and consent cleanup accepts the banner like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets before capture; each step can be turned off. Bot checks, CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers say which verdict and billing status applied. Its MCP server gives AI agents tools for screenshots, page information, and PDF capture. It offers 1,000 screenshots a month free with no card; paid plans start at $5 for 3,000.

For a runnable request and the available capture options, see the ScreenshotNeo documentation.

curl -G "https://api.screenshotneo.com/v1/shot" -d access_key=YOUR_API_KEY --data-urlencode url=https://stripe.com -o shot.webp
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)
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 Bun.write('shot.webp', new Uint8Array(await res.arrayBuffer()));

The Python example requires requests. The Node.js example uses built-in fetch and Bun’s file writer; in Node.js, save the returned bytes with your preferred filesystem method. The API also supports full-page and element capture, device and viewport settings, custom CSS and JavaScript, waiting conditions, request blocking, caching, async jobs, bulk capture, and other options; consult the docs for exact parameter names and behavior.

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

Frequently asked questions

Does hiding the banner count as accepting cookies?

No. A CSS override or DOM removal changes what the screenshot displays. It does not establish that the visitor made a consent choice.

Should I remove or hide the banner?

For a visual-only screenshot, narrowly scoped CSS hiding is a simple starting point. Remove the node only when that behavior suits the page and the test; either way, do not present the action as a consent interaction.

Only if that is the state your application documents and promises. Consent implementations can use different storage and state mechanisms, so a cookie check is not universal proof.

Can I screenshot just the banner?

Yes. Locate the banner and call its element handle’s screenshot() method. That is useful when the banner itself is the visual test subject; keep it visible for that capture.