How to Click Cookie Popups and Modal Alerts in Playwright
Learn when to click a DOM cookie banner, when to handle a native JavaScript dialog, and how to keep Playwright tests reliable.

Playwright popups fall into two different categories, and the correct API depends on which one you have. A cookie consent banner or custom modal is ordinary HTML in the page, so locate its button and click it. A native JavaScript alert, confirm, prompt, or beforeunload dialog is outside the DOM, so handle it with Playwright’s page.on('dialog') event.
This distinction explains most failed attempts to “click a popup.” A DOM locator cannot find a browser dialog, while a dialog listener cannot click an HTML button. The examples below show both approaches in JavaScript, plus Python equivalents, handling intermittent overlays, debugging failures, and capturing a clean result after consent has been handled.
1. Identify the popup before writing code
Pause the test when the popup appears and inspect what it is:

| What you see | What it is | Playwright API |
|---|---|---|
| A banner with “Accept all” and “Reject” buttons | DOM content | locator.click() |
| A centered HTML dialog with a close button | DOM content | A scoped locator, often getByRole('dialog') |
| A browser message with an OK button outside the page | Native JavaScript dialog | page.on('dialog') |
| A confirmation asking OK or Cancel | Native confirm |
dialog.accept() or dialog.dismiss() |
| A text input in a browser prompt | Native prompt |
dialog.accept('value') |
In headed mode, a native dialog usually blocks the browser page and is not inspectable with the element picker. In a trace or screenshot, an in-page overlay appears as part of the document; a native dialog is represented as a dialog event.
2. Click a cookie banner or HTML modal
Use a user-facing locator first. Playwright recommends accessible roles and names because they describe how a user sees the control and provide auto-waiting and re-resolution when the DOM changes. The locator guidance is covered in the Playwright locators documentation and the best practices guide.
Basic accept example
import { test, expect } from '@playwright/test';
test('accepts the cookie banner', async ({ page }) => {
await page.goto('https://example.com');
await page.getByRole('button', { name: 'Accept all' }).click();
await expect(page.getByRole('button', { name: 'Accept all' })).toBeHidden();
});
Use the exact accessible name exposed by the site. Common names include Accept, Accept all, Allow all cookies, and Agree. If the application uses a different language, use that localized name or a translation-aware test fixture.
Scope the locator to the banner
Several controls may contain the word “Accept.” Scope the search to the banner or dialog container so the test cannot click a different control elsewhere on the page.
const banner = page.getByRole('region', { name: /cookie/i });
await banner.getByRole('button', { name: /accept all/i }).click();
If the container has dialog semantics, use getByRole('dialog'). If it has no useful accessible name, a stable test ID is a good fallback:
const consent = page.getByTestId('cookie-consent');
await consent.getByRole('button', { name: 'Reject optional' }).click();
A CSS or XPath selector is still possible, but avoid chains tied to layout such as div:nth-child(3) > div > button. Those selectors break when a marketing wrapper or an additional button is inserted. Use a stable attribute or test ID when the page’s accessibility tree is not sufficient.
Accept, reject, or manage preferences deliberately
Do not automatically accept consent in a test whose purpose is to verify consent behavior. Choose the action that matches the scenario:
await banner.getByRole('button', { name: 'Reject optional' }).click();
// or
await banner.getByRole('button', { name: 'Manage preferences' }).click();
await page.getByRole('checkbox', { name: 'Analytics cookies' }).uncheck();
await page.getByRole('button', { name: 'Save preferences' }).click();
After clicking, assert a meaningful result: the banner disappears, a preference cookie is set, analytics remains disabled, or navigation proceeds. An assertion prevents a test from passing when the click was intercepted or the page rendered a second consent layer.
3. Handle native JavaScript alerts, confirms, and prompts
Native dialogs are handled through the dialog event API. Register the handler before the action that triggers the dialog. The official Playwright dialogs documentation explains that dialogs are automatically dismissed when no listener is registered. Once a listener exists, it must accept or dismiss every dialog; otherwise, “your action will stall.”
Accept an alert or confirmation
import { test, expect } from '@playwright/test';
test('confirms deletion', async ({ page }) => {
page.on('dialog', async dialog => {
expect(dialog.type()).toBe('confirm');
expect(dialog.message()).toContain('Delete');
await dialog.accept();
});
await page.goto('https://example.com/account');
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Deleted')).toBeVisible();
});
For a cancellation test, call dialog.dismiss() instead:
page.on('dialog', async dialog => {
await dialog.dismiss();
});
await page.getByRole('button', { name: 'Delete' }).click();
await expect(page.getByText('Delete cancelled')).toBeVisible();
Supply text to a prompt
page.on('dialog', async dialog => {
if (dialog.type() === 'prompt') {
await dialog.accept('staging-value');
} else {
await dialog.dismiss();
}
});
await page.getByRole('button', { name: 'Set value' }).click();
Inspect dialog.type() and dialog.message() when one page can emit more than one dialog. If the message is unexpected, fail the test instead of accepting it silently:
page.on('dialog', async dialog => {
if (dialog.type() !== 'confirm' || !dialog.message().includes('Delete')) {
await dialog.dismiss();
throw new Error(`Unexpected dialog: ${dialog.type()} ${dialog.message()}`);
}
await dialog.accept();
});
Python Playwright equivalent
from playwright.sync_api import sync_playwright
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
def handle_dialog(dialog):
if dialog.type == 'prompt':
dialog.accept('staging-value')
else:
dialog.dismiss()
page.on('dialog', handle_dialog)
page.goto('https://example.com')
page.get_by_role('button', name='Delete').click()
browser.close()
4. Deal with popups that appear intermittently
An overlay can appear after navigation, after a delay, or only for a new session. A one-time conditional click is often flaky because it races the overlay. Playwright provides page.addLocatorHandler for this case. The API reference demonstrates registering a handler for an unexpected signup dialog; adapt the trigger and action to your page.
await page.addLocatorHandler(
page.getByText('Sign up to the newsletter'),
async () => {
await page.getByRole('button', { name: 'No thanks' }).click();
}
);
await page.goto('https://example.com/article');
await page.getByRole('heading', { name: 'Article' }).waitFor();
Use a handler only when closing the overlay is outside the behavior being tested. For a newsletter test, keep the overlay visible and assert its controls instead. If the handler’s target can match multiple elements, scope it to a container or use a unique text pattern.
Fallback for optional banners
For a deterministic page where a banner may or may not exist, check visibility with a short timeout rather than adding a long fixed sleep:
const accept = page.getByRole('button', { name: 'Accept all' });
if (await accept.isVisible({ timeout: 2000 }).catch(() => false)) {
await accept.click();
}
Prefer a web-first assertion or locator handler when possible. Fixed delays slow every test and still fail when a consent manager loads more slowly than expected.
5. Wait for the page state after the click
Clicking a consent button can reload the page, set cookies, or remove an iframe. Wait for the state your test needs:
await page.getByRole('button', { name: 'Accept all' }).click();
await expect(page.getByRole('banner')).toBeHidden();
await expect(page.locator('[data-testid="dashboard"]')).toBeVisible();
If clicking starts navigation, combine the click and navigation wait with a locator action and a URL assertion:
await page.getByRole('button', { name: 'Continue' }).click();
await expect(page).toHaveURL(/dashboard/);
Do not use page.waitForTimeout() as the primary synchronization method. It hides the real condition and creates unnecessary runtime variance.
6. Troubleshooting common failures
| Symptom | Likely cause | Fix |
|---|---|---|
| “Locator resolved to 0 elements” | The banner has not loaded, the name is wrong, or consent was already stored. | Inspect the accessible tree, use the exact accessible name, and start with a fresh context when testing first-visit behavior. |
| Strict mode violation | More than one button matches. | Scope to the banner or dialog and make the name or test ID unique. |
| Click intercepted | A second overlay covers the target or an animation is still running. | Locate the visible overlay, wait for its state, and click through the correct handler. |
| Native alert remains open | A dialog listener was added but does not call accept or dismiss. | Handle every event path. Logging the message alone is not enough. |
| The click hangs forever | An unhandled native dialog is blocking the page action. | Register page.on('dialog') before triggering the action and resolve the dialog. |
| Consent appears in one test but not another | Storage state, cookies, or a persistent browser profile differ. | Use an explicit storage-state fixture for accepted consent, or a clean context for first-visit tests. |
| Works locally but fails in CI | Timing, viewport, locale, or geolocation changes the banner. | Set those values explicitly, use role-based locators, and capture a trace on failure. |
Inspect the accessible name
When a role locator fails, run Playwright’s inspector or temporarily print the DOM around the banner. Verify whether the visible label is an accessible name, an aria label, or text inside a nested element. A button rendered as an icon may need getByLabel('Close') rather than getByRole('button', { name: 'Close' }).
Handle iframes
Some consent platforms render inside an iframe. A page locator cannot cross that boundary. Identify the frame and scope the locator:
const frame = page.frameLocator('iframe[title="Consent"]');
await frame.getByRole('button', { name: 'Accept all' }).click();
Use a stable frame selector supplied by the application or consent vendor. Avoid selecting an iframe by an automatically generated numeric index.
7. Reusable fixtures for a test suite
Centralize consent handling so individual tests focus on their behavior. Keep a separate fixture for tests that verify the consent UI itself.
import { test as base } from '@playwright/test';
export const test = base.extend({
page: async ({ page }, use) => {
await page.addLocatorHandler(
page.getByRole('banner', { name: /cookie/i }),
async banner => {
await banner.getByRole('button', { name: 'Accept all' }).click();
}
);
await use(page);
}
});
If the application stores consent in a cookie, an even faster approach is to create storage state once and reuse it for tests that do not cover consent. Keep first-visit and rejection scenarios on a clean context so the suite still exercises the banner deliberately.
8. Performance, reliability, and security notes
- Prefer locators over sleeps. Auto-waiting reduces wasted time and adapts to normal rendering delays.
- Keep handlers narrow. A global dialog handler that accepts every prompt can hide a real application error.
- Use one browser context per scenario. This prevents consent cookies from leaking between tests and makes failures reproducible.
- Set realistic timeouts. A short locator timeout for an optional banner is appropriate; use the normal test timeout for required navigation.
- Do not log secrets. Dialog messages, URLs, and page content can contain tokens or personal data in test environments.
- Capture traces on retry. A trace shows whether the popup was DOM content, an iframe, or a native dialog and which locator matched.
For visual regression or documentation workflows, wait until the consent layer is gone before taking the screenshot. Otherwise the captured image can differ between regions, sessions, or test runs.
9. Or skip the browser setup
If your goal is a clean screenshot rather than an interaction test, ScreenshotNeo handles the browser work through one request. It accepts cookie and consent banners like a visitor, then removes more than 60 known consent platforms, newsletter popups, and chat widgets before capture. Each step can be turned off when you need the original page state.

ScreenshotNeo returns PNG, JPEG, WebP, or PDF. The API also supports full-page captures with lazy images loaded, CSS-selector element captures, dark mode, device presets, custom viewport and retina scale, custom CSS and JavaScript, clicks before capture, hidden selectors, waits, blocked resource types, headers, cookies, user agents, authorization, timezone, geolocation, transparent backgrounds, resizing, caching, signed links, asynchronous jobs, webhooks, bulk capture, and usage reporting. See the ScreenshotNeo API documentation for parameter details.
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}`);
Only clean shots are billed. Bot checks or CAPTCHAs, blank pages, timeouts, failed loads, and cache hits cost nothing, and each response identifies the result with X-Page-Verdict and X-Billed headers. ScreenshotNeo also provides an MCP server with take_screenshot, get_page_info, and capture_pdf tools for 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 shots; yearly billing provides two months free. Create a free account at ScreenshotNeo sign-up.
10. FAQ
Can I use a locator for a JavaScript alert?
No. Native alerts are browser dialogs, not DOM elements. Register a page.on('dialog') handler and accept or dismiss the dialog.
Why does Playwright dismiss my alert without code?
With no dialog listener, Playwright automatically dismisses native dialogs. Add a listener when the test needs to assert the message or choose acceptance.
Should I accept cookies in every test?
Only when consent is part of the scenario. For other tests, reuse a prepared storage state or a fixture, while keeping dedicated first-visit tests on a clean context.
What if the cookie banner is inside an iframe?
Use page.frameLocator() and locate the button inside that frame. A normal page locator cannot cross iframe boundaries.
Can I capture a page after handling the popup?
Yes. Wait for the banner or modal to be hidden, then capture with Playwright or send the URL to ScreenshotNeo when you need a clean image without maintaining browser setup.


