How to Test Cookie Banner Screenshots When Consent Is Stored in a Cookie
Set up clean, repeatable first-visit and saved-consent states before capturing cookie banner screenshots with Cypress or Playwright.
To test cookie banner screenshots reliably, set the consent state deliberately before each capture. Use a clean browser context or clear the consent cookie for the first-visit state; initialize the real consent cookie before navigation for the returning-visitor state. Assert both the cookie and the expected banner behavior before saving or comparing a screenshot.
Do not guess the cookie value. Find the name, format, domain, path, and expiry in your consent implementation, or obtain a valid value by accepting consent through the application. The examples below use cookieConsent=accepted and [data-testid="cookie-banner"] as placeholders; replace them with your app’s actual values.
1. Define the states you need to capture
A screenshot only describes the browser state that produced it. A useful test suite treats consent as explicit test setup, not incidental state left by a previous run.
| Starting state | Expected behavior | What to assert |
|---|---|---|
| No consent cookie | Banner appears | Banner is visible; cookie is absent until a choice is made |
| Accepted consent saved | Banner stays hidden or saved settings are shown | Cookie value matches the accepted state; UI matches app behavior |
| Rejected or customized choice | UI reflects the saved choice | Stored value and preference summary match the choice |
| Malformed, stale, or expired value | App follows its recovery policy | Banner or preference UI matches that policy |
Test only variants your product supports. A malformed or expired value can be useful when the application defines recovery behavior. These are product behavior tests, not claims about legal compliance.
2. Find the real cookie and its scope
- Open the application in a clean browser profile and inspect its consent code or browser storage after making a choice.
- Record the cookie’s exact name and value format. Some consent systems store a simple string; others store encoded JSON, categories, or a versioned payload.
- Record its domain and path. A cookie set for
www.example.commay not be sent toapp.example.com. Host-only and domain cookies also behave differently across subdomains. - Check whether the cookie is set by the server in the initial response or by client-side code after the page loads. This determines whether it must exist before navigation.
- Check expiry and whether local storage or session storage also affects the banner. Clearing only one persistence mechanism may not create a true first visit.
Prefer a cookie value produced by the real consent flow. If you seed a value directly for a test, use a valid fixture from your application’s consent logic so the test models a state the app actually understands.
3. Cypress: capture first-visit and saved-consent states
Cypress clears cookies, local storage, and session storage between tests by default. Set up each case explicitly so one test cannot accidentally provide the state for another. Cypress documents cy.setCookie() options for domain, path, expiry, secure, HTTP-only, SameSite, and host-only scope; its default cookie domain behavior can include subdomains. See the cy.setCookie() documentation and Cypress FAQ on test isolation.
// cypress/e2e/cookie-banner.cy.js
const appUrl = 'https://www.example.com/';
const cookieName = 'cookieConsent';
const banner = '[data-testid="cookie-banner"]';
describe('cookie banner screenshots', () => {
it('shows the banner to a first-time visitor', () => {
cy.visit(appUrl);
cy.clearCookie(cookieName);
cy.clearLocalStorage();
cy.clearAllSessionStorage();
cy.reload();
cy.get(banner).should('be.visible');
cy.getCookie(cookieName).should('be.null');
cy.get(banner).screenshot('cookie-banner-first-visit');
});
it('uses a previously saved accepted choice', () => {
// Visit first so Cypress knows the application host for cookie scope.
cy.visit(appUrl);
cy.setCookie(cookieName, 'accepted', {
path: '/',
secure: true,
sameSite: 'lax',
hostOnly: true,
});
cy.getCookie(cookieName).should('have.property', 'value', 'accepted');
cy.reload();
cy.get(banner).should('not.exist');
cy.getCookie(cookieName).should('have.property', 'value', 'accepted');
cy.screenshot('cookie-banner-consented');
});
});
The first case visits and clears state before reloading so client-side consent initialization sees the clean state. If you need a genuinely cookie-free initial HTTP request, establish a clean context before the first visit instead of clearing after navigation. For the saved state, the example visits to establish the host, sets the cookie, and reloads; if your server must receive the cookie on the very first request, set it before visiting with an explicit domain matching the app origin.
For instance, before a visit to https://www.example.com, Cypress is initially on about:blank; its default cookie domain may therefore be the spec host. Cypress documents that you should pass the intended domain when setting a cookie before navigation. If testing another origin, use Cypress’s cross-origin support as required by the app and test setup. Confirm the cookie is present for the origin you are about to capture.
For a host-only cookie, use hostOnly: true and the exact host. For a shared domain cookie, set the intended domain and verify behavior on each relevant subdomain. Do not disable test isolation just to preserve a consent choice: that makes the result depend on test order. If a suite intentionally disables isolation, restore consent and all related storage explicitly in every test.
4. Playwright: initialize the cookie before navigation
Playwright tests use isolated browser contexts. A clean context gives the first-visit case a fresh cookie jar; context.addCookies() seeds the saved-consent case before the page request. Use the URL form or the cookie’s domain and path so its scope matches the tested origin.
// tests/cookie-banner.spec.js
import { test, expect } from '@playwright/test';
const appUrl = 'https://www.example.com/';
const banner = '[data-testid="cookie-banner"]';
test('shows the banner to a first-time visitor', async ({ page }) => {
await page.goto(appUrl);
await expect(page.locator(banner)).toBeVisible();
await expect(page).toHaveScreenshot('cookie-banner-first-visit.png');
});
test('uses previously saved consent', async ({ browser }) => {
const context = await browser.newContext();
await context.addCookies([
{
name: 'cookieConsent',
value: 'accepted',
url: appUrl,
sameSite: 'Lax',
secure: true,
httpOnly: false,
},
]);
const page = await context.newPage();
await page.goto(appUrl);
await expect(page.context().cookies(appUrl)).resolves.toEqual(
expect.arrayContaining([
expect.objectContaining({ name: 'cookieConsent', value: 'accepted' }),
]),
);
await expect(page.locator(banner)).toHaveCount(0);
await expect(page).toHaveScreenshot('cookie-banner-consented.png');
await context.close();
});
For a persistent domain cookie, use domain and path instead of url, following the cookie’s real scope. For a host-only cookie, use the URL for the exact host. If your application requires the cookie on the first request, adding it to the context before page.goto() is essential.
toHaveScreenshot() waits for a stable screenshot comparison according to Playwright’s visual comparison behavior. Keep the browser and operating system consistent between reference generation and CI: rendering may vary with OS, browser version, settings, hardware, power source, and headless mode. See Playwright visual comparisons and Playwright best practices.
5. Make the screenshot comparison deterministic
- Use a fixed viewport. The banner may reflow, change placement, or switch buttons at responsive breakpoints.
- Wait on meaningful conditions. Assert banner visibility or absence and wait for consent initialization, fonts, and relevant page content. Avoid arbitrary long sleeps when an element or app-ready condition can be awaited.
- Settle motion. Disable or wait out animations and transitions where they make captures vary. Keep the same browser, browser version, operating system, and headless configuration for baseline and comparison runs.
- Keep the page inputs stable. Use deterministic content, locale, timezone, feature flags, and test data if those can affect the banner or surrounding page.
- Review diffs before updating baselines. A changed screenshot may reveal a real regression, a cookie not applied to the request, or a rendering environment change. Confirm the cause before accepting a new reference image.
Assert behavior first, pixels second. If the banner assertion fails, debug state setup before interpreting the image diff. For large pages, capture the banner element when the goal is its appearance; capture the full page when you need to verify layout interaction with the rest of the page.
6. Troubleshooting
| Symptom | Likely cause | Fix |
|---|---|---|
| Banner appears in the saved-consent screenshot | Cookie name or value is wrong, cookie is scoped to another host/path, or the app reads another storage key | Inspect the real consent flow and storage. Verify the cookie on the exact origin and assert its value before navigation or reload. |
| Banner is missing in the first-visit screenshot | Cookie or local/session storage survived setup, or the banner is delayed | Clear all consent-related storage in an isolated context, reload if needed, and wait for the app’s visible state. |
| Cookie looks set but server renders the wrong page | It was added after the initial request, but server rendering needs it in the request | Seed the cookie into the browser context before navigation, or set it before the first visit with the correct domain. |
| Cypress sets the cookie on localhost or reports a cross-origin error | The current page origin does not match the application host | Visit the application first or pass an explicit domain before visiting; follow Cypress cross-origin rules for other origins. |
| Cookie works on one subdomain only | Host-only/domain scope differs from the app’s actual cookie | Match the application’s domain and path. Cypress supports hostOnly; verify each origin separately. |
| Screenshot diff changes on every CI run | Browser/OS variance, animation, dynamic content, fonts, viewport, or asynchronous initialization | Pin the rendering environment, fix viewport and test data, wait on app state, and control motion. |
| Malformed value produces unexpected behavior | The fixture is not a valid format or the app has an undocumented recovery path | Use a value generated by the app’s own consent logic and document expected invalid-state behavior. |
7. Privacy, reliability, and cost considerations
Keep consent fixtures and browser state out of public repositories if they contain real identifiers or other sensitive values. Playwright warns that saved browser state can contain sensitive cookies and headers; handle such state files as credentials and exclude them from source control. If Cypress screenshots or replay data are uploaded to Cypress Cloud, consider what test content is stored and use its documented masking or blackout controls where needed. See Playwright authentication state guidance and Cypress Cloud data storage and controls.
For reliability, favor independent state setup, explicit UI and cookie assertions, and a stable browser image in CI. This avoids flaky dependence on test order and makes failures diagnosable. Browser-based tests consume CI time and resources; keep the matrix focused on real consent choices and run broader browser/viewport combinations where they provide value. No framework-specific runtime or pricing benchmark is implied here.
8. Or skip the browser setup
ScreenshotNeo is a website screenshot API and MCP server. Its one-call API is useful when you need a page image without managing a browser in your own test harness. See the ScreenshotNeo API documentation for the request options.
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}`);
Replace the example target with your page URL. ScreenshotNeo removes cookie banners, newsletter popups, and chat widgets 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; paid plans start at $5 for 3,000 screenshots. The API supports PNG, JPEG, WebP, or PDF output and offers controls such as viewport, full-page capture, custom CSS, waits, and cookies. Cookie removal is enabled for clean shots, so this is suited to clean-page captures rather than verifying the banner’s visible first-visit state.
Create a free ScreenshotNeo account for 1,000 screenshots a month with no card.
FAQ
How do I make a cookie banner show up in a screenshot test?
Start from a clean browser context and remove every storage value your app uses for consent, then assert that the banner is visible before capturing.
How do I set a cookie before a Cypress or Playwright screenshot?
In Cypress, use cy.setCookie() with the app’s actual scope and value. In Playwright, call context.addCookies() before navigating to the page.
Should I use a saved browser profile for this test?
Usually, separate explicit contexts or per-test setup are easier to reason about. Reusable state is appropriate only when its contents are controlled and kept private.
Does a screenshot prove the consent implementation is correct?
No. It verifies a rendered state. Test the consent behavior and storage rules separately, and assess legal requirements with appropriate jurisdiction-specific research.


