Best way to handle cookie consent in Playwright screenshot tests
Make cookie consent state explicit in Playwright: test first visits, seed returning-visitor state, and keep visual snapshots stable.
The best way to handle cookie consent in Playwright screenshot tests is to make consent state explicit and repeatable. Use a fresh isolated context to test a first visit. For a returning-visitor screenshot, set the application’s real consent cookie or storage value before the page checks it. Keep a separate test for the consent controls when their behavior is in scope. Do not guess a persistence key or globally click away the banner.
This keeps each screenshot tied to a clear scenario: first visit, already-consented visitor, or a user making a choice. It also prevents one test’s saved state from silently changing another test’s result.
1. Choose the state the screenshot should represent
| Scenario | Starting state | What to assert |
|---|---|---|
| First visit | Fresh isolated context with no saved consent | The banner appears; optionally snapshot it or exercise its controls |
| Returning visitor | Application’s documented consent state seeded before navigation | The banner is absent if that is the application’s expected behavior |
| Consent interaction | Fresh state | Use accessible controls, make a choice, and verify the resulting state and UI |
Playwright recommends isolated tests with their own cookies and storage. Its non-persistent BrowserContext gives each test a separate browser session, and supports cookie setup and clearing. See the [Playwright best practices](https://playwright.dev/docs/best-practices) and [BrowserContext API](https://playwright.dev/docs/api/class-browsercontext).
2. Runnable Playwright Test examples
Install Playwright Test in a JavaScript project with npm init playwright@latest, then put the following in a test file such as tests/consent.spec.ts. Run it with npx playwright test. Replace the origin, accessible role/name, cookie name, value, and expected behavior with the application’s real implementation. The returning-visitor example assumes that the consent cookie is valid for the local test origin.
import { test, expect } from '@playwright/test';
const baseURL = 'http://localhost:3000';
test('first visit shows the consent banner', async ({ page }) => {
await page.goto(baseURL);
const banner = page.getByRole('dialog', { name: /cookie|privacy/i });
await expect(banner).toBeVisible();
await expect(page).toHaveScreenshot('home-first-visit.png');
});
test('returning visitor sees the page without the banner', async ({ page }) => {
await page.context().addCookies([{
name: 'consent-cookie-name',
value: 'documented-consent-value',
domain: 'localhost',
path: '/',
}]);
await page.goto(baseURL);
const banner = page.getByRole('dialog', { name: /cookie|privacy/i });
await expect(banner).toBeHidden();
await expect(page).toHaveScreenshot('home-consented.png');
});
test('accepting consent updates the page', async ({ page }) => {
await page.goto(baseURL);
const banner = page.getByRole('dialog', { name: /cookie|privacy/i });
await expect(banner).toBeVisible();
await banner.getByRole('button', { name: /accept all/i }).click();
await expect(banner).toBeHidden();
// Assert the application's real persisted state here if relevant.
await expect(page).toHaveScreenshot('home-after-consent.png');
});
Playwright’s built-in page fixture uses an isolated context for each test. Seeding the cookie before goto() matters when the application checks consent during initial navigation. A domain, path, secure flag, or value that does not match the app’s real cookie can make setup appear to succeed while the app ignores it.
Find the real consent mechanism
- Inspect the application’s consent implementation, configuration, or tests for the cookie or storage key and the accepted value.
- If that is unclear, run the actual consent flow and inspect the browser’s cookies and storage after making a choice.
- Record the exact origin and scope. Cookies are scoped by domain and path; local storage is scoped by origin, including scheme and port.
- Use that state in the returning-visitor setup. Avoid borrowing a key from a consent vendor’s example unless the application actually uses it.
3. Seed localStorage or sessionStorage before app code
If consent is stored in web storage rather than a cookie, the exact key and value still need to come from the application. The setup must run for the page’s origin before the application’s consent code reads storage. One approach is to add an initialization script to the context before navigation:
import { test, expect } from '@playwright/test';
test('returning visitor with consent in localStorage', async ({ context, page }) => {
await context.addInitScript(() => {
// Replace these placeholders with the application's real storage key/value.
localStorage.setItem('documented-consent-key', 'documented-consent-value');
});
await page.goto('http://localhost:3000');
await expect(page.getByRole('dialog', { name: /cookie|privacy/i })).toBeHidden();
await expect(page).toHaveScreenshot('home-consented.png');
});
The initialization script runs in the page before application scripts, and storage is available in the page’s origin. If using a storage-state setup instead, make sure it represents the correct origin and state. Playwright’s current [WebStorage API](https://playwright.dev/docs/api/class-webstorage) supports setting, removing, and clearing items; its clear method was added in v1.61. Check the installed Playwright version before depending on that method.
4. Keep consent behavior tests separate from unrelated screenshots
When a test is about consent itself, leave the banner visible in the first-visit case and interact through user-visible controls. Prefer accessible roles and names, such as getByRole('button', { name: 'Accept all' }), over brittle selectors tied to implementation details. Verify the expected result before capturing the screenshot.
For screenshots of unrelated pages, set the required state as test setup. Opportunistic auto-dismiss code can hide a regression: the banner may not appear, may load late, or its controls may change. A fixed seeded state makes the image’s intent reviewable.
5. Make visual comparisons repeatable
Use expect(page).toHaveScreenshot() for Playwright Test visual assertions. Playwright waits for two consecutive screenshots to match before comparing against the reference. That reduces transient capture differences, but does not control your application data or guarantee the same rendering on every machine. See [visual comparisons](https://playwright.dev/docs/test-snapshots) and the [PageAssertions API](https://playwright.dev/docs/api/class-pageassertions).
- Generate and compare references in a consistent browser and operating-system environment. Playwright notes that OS, browser version, settings, hardware, power source, and headless mode can affect rendering.
- Keep consent state fixed for each screenshot name. A first-visit image and a consented image should have distinct names and references.
- Use screenshot
styleorstylePathonly for unrelated, volatile content such as rotating promotions. Keep suppression rules narrow. - Do not hide the consent banner in a test that checks its appearance, wording, placement, or interaction. Playwright documents screenshot styling in the [Page API](https://playwright.dev/docs/api/class-page).
- Make test data and the page’s external dependencies stable where possible; changing content can invalidate a screenshot even when consent handling is correct.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| The returning-visitor screenshot still shows the banner | The cookie name/value, domain, path, or origin is wrong, or the app reads a different storage key | Inspect the real consent flow and application code; seed the exact state before navigation |
| The banner appears in one test but not another | Tests share a persistent context, state file, or mutable setup | Use isolated contexts and make each test’s intended state explicit |
localStorage setup throws a security or origin error |
Storage was accessed before a page origin existed, or the script ran on a different origin | Use an initialization script or supported storage-state setup for the app’s exact origin |
| Consent is seeded but then disappears or resets | The app validates the stored value, expiry, schema, or related consent fields | Capture the real persisted state after a user choice and reproduce all required fields and cookie attributes |
| The banner assertion times out | The accessible role/name differs, the banner is delayed, or the page failed to load | Inspect the rendered accessibility tree and app behavior; use the real accessible locator and wait for the actual readiness condition |
| Snapshots differ only in CI | Browser, OS, fonts, headless mode, hardware, or dynamic page data differs | Align the visual test environment and stabilize changing content before updating a reference |
| Hiding the banner makes the screenshot pass but breaks the test’s purpose | A broad screenshot style suppresses the subject under test | Remove that style for consent tests; snapshot the banner as part of the intended state |
7. Performance, reliability, and cost
Consent setup itself is a small amount of browser-context work. The larger costs in screenshot suites usually come from launching browsers, navigating pages, waiting for stable content, and storing or reviewing image artifacts. Reuse the Playwright Test worker/browser setup while keeping each test’s context isolated; do not trade away state isolation to avoid setup.
Seed state directly when testing unrelated page visuals. Exercise the real consent flow in a dedicated behavior test when that behavior matters. This reduces dependence on banner timing while preserving coverage of the user journey. Keep screenshot baselines tied to the same browser and OS configuration so environment drift does not look like a product change.
Or skip the browser setup
If you need a clean capture outside a Playwright test, ScreenshotNeo is a website screenshot API and MCP server for developers. It takes a URL and returns PNG, JPEG, WebP, or PDF. The API accepts parameters for other screenshot APIs too, which can make switching easier. See the ScreenshotNeo API 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}`);
const fs = await import('node:fs/promises');
await fs.writeFile('shot.webp', Buffer.from(await res.arrayBuffer()));
- Cookie banners are accepted and removed before capture, along with 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. Responses include
X-Page-VerdictandX-Billedheaders. - An MCP server exposes
take_screenshot,get_page_info, andcapture_pdfto Claude, Cursor, and other MCP clients. - The free plan includes 1,000 screenshots per month with no card. Paid plans start at $5 for 3,000 screenshots; every feature is on every plan.
For Playwright visual regression, keep the browser test when the consent UI or exact browser state is what you need to verify. For a URL-based capture where you want consent clutter removed, try ScreenshotNeo free: 1,000 screenshots a month, no card required.
FAQ
Should I click “Accept” in every screenshot test?
No. Click it in a test that covers the consent interaction. For unrelated screenshots, establish the intended state as setup data.
Can I use one screenshot baseline for both consent states?
No. Treat first-visit and returning-visitor views as separate scenarios with separate reference images.
Does toHaveScreenshot() make captures identical across machines?
No. It waits for consecutive captures to match within the test, but rendering can still differ across environments.
Should screenshot styling hide the consent banner?
Only when the banner is unrelated to the screenshot’s purpose. Leave it visible when consent UI is under test.


