How to Capture a Web Page Screenshot After a Cookie Banner Choice Is Stored in localStorage
Dismiss the consent banner, verify the saved choice, and capture a repeatable screenshot with Playwright. Learn when to restore browser state or seed localStorage.
To capture a page after a cookie banner choice, make the choice through the site’s real consent controls, wait until the banner is gone and the page has reached the state you want, then call Playwright’s page.screenshot(). For repeat runs, save and restore the browser context’s storage state. Seed localStorage before page scripts only when you know the site’s real origin, key, and value.
Consent storage is specific to a website origin. A value saved for one origin will not automatically apply to another, and guessing a consent manager’s key or serialized value can produce a misleading result. The reliable default is to make the choice through the banner once, verify what rendered, and reuse the resulting browser state.
1. Make the choice through the consent banner
Install Playwright and its Chromium browser if they are not already available:
npm install playwright
npx playwright install chromium
Save this as capture-after-consent.mjs. Replace the example URL, button name, and banner locator with the target site’s actual address and accessible controls. The example uses a Reject action; use the choice appropriate to your test.
import { chromium } from 'playwright';
const url = 'https://example.com';
const browser = await chromium.launch();
try {
const context = await browser.newContext();
const page = await context.newPage();
await page.goto(url, { waitUntil: 'domcontentloaded' });
// Replace this role and accessible name with the real consent control.
await page.getByRole('button', { name: 'Reject optional cookies' }).click();
// Replace the dialog locator if the target site uses another structure.
await page.getByRole('dialog', { name: /cookie/i })
.waitFor({ state: 'hidden' });
// Capture the current viewport, or set fullPage: true for the full document.
await page.screenshot({ path: 'page-after-choice.png', fullPage: true });
} finally {
await browser.close();
}
The button and dialog selectors are examples, not universal selectors. Inspect the page’s accessible names and structure, then use locators for its actual controls. If clicking opens a preference panel first, select the intended options and click the site’s Save or Confirm button. Wait for a meaningful outcome, such as the banner becoming hidden or a known post-choice element appearing; an arbitrary sleep can be too short on a slow page and waste time on a fast one.
2. Save and restore the browser state for later runs
A fresh browser context starts with separate browser storage. After making the choice once, save the context’s storage state and pass it when creating a later context. Playwright documents storage-state reuse in its authentication guidance.
For the first run, save the state after the banner choice has been made:
// After clicking the site's consent control and verifying the banner is hidden:
await context.storageState({ path: 'consent-state.json' });
For a later run, create the context with that state before opening the page:
import { chromium } from 'playwright';
const browser = await chromium.launch();
try {
const context = await browser.newContext({
storageState: 'consent-state.json'
});
const page = await context.newPage();
await page.goto('https://example.com', { waitUntil: 'domcontentloaded' });
// Verify the expected post-consent state on every run.
await page.getByRole('dialog', { name: /cookie/i })
.waitFor({ state: 'hidden' });
await page.screenshot({ path: 'restored-state.png', fullPage: true });
} finally {
await browser.close();
}
Keep the saved state in an appropriate location for your project: it may contain cookies or other browser storage in addition to consent data. Recreate it through the site’s UI if the site changes its consent behavior or the saved state stops producing the expected page.
3. Seed localStorage before the site’s scripts (when you know the schema)
Direct seeding can make a controlled test deterministic, but only if you know the exact origin, storage key, and serialized value the site expects. Playwright’s page.addInitScript() runs after the document is created and before its page scripts execute, which makes it suitable for setting a known value early. See the Playwright Page API.
import { chromium } from 'playwright';
const origin = 'https://example.com';
const browser = await chromium.launch();
try {
const context = await browser.newContext();
const page = await context.newPage();
// These are placeholders. Use the target site's real key and exact value.
await page.addInitScript(({ expectedOrigin, key, value }) => {
if (window.location.origin === expectedOrigin) {
window.localStorage.setItem(key, value);
}
}, {
expectedOrigin: origin,
key: 'REPLACE_WITH_REAL_CONSENT_KEY',
value: 'REPLACE_WITH_REAL_SERIALIZED_VALUE'
});
await page.goto(origin, { waitUntil: 'domcontentloaded' });
// Storage being present does not prove the site accepted it. Verify the UI.
await page.getByRole('dialog', { name: /cookie/i })
.waitFor({ state: 'hidden' });
await page.screenshot({ path: 'seeded-consent.png', fullPage: true });
} finally {
await browser.close();
}
The origin guard prevents writing this value on unrelated origins if the site navigates elsewhere. Do not set localStorage while on about:blank and assume it applies to the website: storage is scoped to the target site’s origin. If the site’s consent data is stored in cookies or another mechanism, localStorage seeding will not reproduce it.
4. Choose the right capture and verify it
| Need | Approach | Tradeoff |
|---|---|---|
| Reproduce a real visitor choice | Click the actual banner control, then save storage state | Requires an initial run through the banner; does not require guessing the storage format |
| Repeat a controlled fixture | Seed the known localStorage value with addInitScript() |
Requires the exact origin and schema; can break if the site changes its representation |
| Capture visible content | page.screenshot({ path: 'page.png' }) |
Captures the viewport only |
| Capture below the fold | page.screenshot({ path: 'page.png', fullPage: true }) |
Produces a taller image and may expose lazy-loading or layout changes |
Before saving a baseline or using the image downstream, check that the banner is absent and the intended content is visible. If you know the site’s actual key, you can inspect it on the page’s origin as an additional diagnostic:
const storedValue = await page.evaluate(() =>
localStorage.getItem('REPLACE_WITH_REAL_CONSENT_KEY')
);
console.log(storedValue);
Do not treat a non-null value as proof that the consent choice took effect: the application may reject, migrate, or interpret the value differently. Check the rendered page too.
For automated visual comparisons, Playwright’s expect(page).toHaveScreenshot() waits for two consecutive screenshots to match before comparing with a reference. Keep the browser and execution environment consistent for meaningful diffs. Playwright notes that host OS, browser version, settings, hardware, power source, and headless mode can affect rendering; see its visual comparison guidance. Disable animations or mask dynamic regions only when that matches what the test is meant to verify.
5. Other runnable capture examples
Python with Playwright
Install the Python package and browser:
pip install playwright
playwright install chromium
import asyncio
from playwright.async_api import async_playwright
async def main():
async with async_playwright() as p:
browser = await p.chromium.launch()
try:
context = await browser.new_context()
page = await context.new_page()
await page.goto('https://example.com', wait_until='domcontentloaded')
# Replace with the target site's real consent action and dialog.
await page.get_by_role('button', name='Reject optional cookies').click()
await page.get_by_role('dialog', name='cookie', exact=False).wait_for(
state='hidden'
)
await page.screenshot(path='page-after-choice.png', full_page=True)
await context.storage_state(path='consent-state.json')
finally:
await browser.close()
asyncio.run(main())
cURL
cURL makes an HTTP request; it does not run the site’s JavaScript or click a consent banner. It can retrieve a screenshot from a screenshot service that accepts an HTTP request, but it cannot perform the Playwright workflow by itself. The hosted option below shows a cURL capture call.
Puppeteer
Puppeteer offers page and element screenshots as well. Its official screenshots guide covers viewport and full-page captures. The same core sequence applies: navigate, interact with the real consent controls, wait for the resulting state, then capture.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The banner is still in the screenshot | The click did not match the real control, or the banner hides asynchronously | Inspect the accessible name and locator; wait for the actual banner or dialog to become hidden after the choice |
| The button locator times out | The control has a different label, is inside an iframe, or has not appeared yet | Inspect the rendered page and frame structure; wait for the actual control and target its correct frame and accessible name |
| The banner returns in a new run | The new context has fresh storage, state was saved before the choice, or the wrong origin was visited | Save state after verifying the choice; load it into the new context before navigation; confirm the URL origin |
| Seeded localStorage has no effect | The key/value is wrong, the site uses another storage mechanism, or the value was set on the wrong origin | Make a real UI choice and inspect the resulting storage; seed only the known schema before site scripts |
| The saved state loads but the banner appears | The site’s consent representation changed, expired, or is not included in the saved state | Recreate the state through the live UI and verify the new run’s rendered page |
| Full-page capture misses content | Content is loaded lazily or only after scrolling | Scroll through the page and wait for expected content before the full-page screenshot |
| Visual diffs are inconsistent | Browser or host rendering varies, or dynamic content changes between runs | Use a consistent browser and environment; stabilize content, and mask or disable motion only when appropriate |
7. Performance, reliability, and cost
For repeat runs, reusing saved storage avoids repeating the consent interaction and makes the starting state explicit. Direct seeding can reduce setup steps in controlled tests, but only when the schema is known and verified. Neither approach removes the need to wait for the page to render the state you intend to capture. Full-page screenshots can take longer and create larger files than viewport captures, especially on long pages; choose the smallest capture that answers the test’s question.
Use explicit waits for observable outcomes rather than fixed delays. Keep screenshots and storage state tied to the browser version and environment used for comparison. The Playwright visual comparison documentation describes why rendering can vary across environments. For hosted captures, cost depends on the provider’s terms and whether it charges for failed or unhelpful renders; check those terms before relying on a service.
Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. One GET request returns an image or PDF; its pre-capture consent handling accepts cookie banners like a visitor and removes 60+ known consent platforms, newsletter popups, and chat widgets. Each step can be turned off. Bot checks, blank pages, timeouts, failed loads, and cache hits cost nothing, and response headers identify the page verdict and billing status. Its MCP server gives AI agents tools to take screenshots, inspect page info, and capture PDFs.
For a basic capture, use cURL, Python, or Node.js. See the ScreenshotNeo API documentation for request options, including consent controls.
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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
Cookie banners, popups, and chat widgets are removed before the shot; bot checks, blank pages, and failed loads are never billed; an MCP server lets AI agents take screenshots. The Free plan includes 1,000 screenshots a month with no card, and paid plans start at $5 for 3,000. Sign up for 1,000 free screenshots a month, with no card required.
FAQ
Does a localStorage value automatically carry across browser contexts?
No. A new context has separate storage unless you restore saved state or set the value for the site’s origin.
Should I accept or reject cookies for a screenshot test?
Use the choice the test is meant to represent. The important part is to use the real site control and verify the page’s resulting state.
Can I capture only one element?
Yes. Playwright can capture a locator’s element rather than the full page. This is useful when the test is scoped to a component; ensure it is visible and in the expected state before capturing.
Why does a screenshot differ on another machine?
Browser rendering can vary with the operating system, browser version, settings, hardware, power source, and headless mode. Keep the environment consistent when comparing baselines.


