addLocatorHandler in Playwright: How to Get Rid of the Popups That Break Your Tests
A test that passes one run and fails the next because of a random popup? Here's how Playwright's addLocatorHandler fixes it in two lines, plus the options and mistakes you need to know.
If you have a test that passes one time and fails the next, and the code hasn't changed, the cause is very often a popup that appears at random and covers the element you want to click.
The fix in Playwright isn't an if statement, and it isn't a longer wait. The fix is page.addLocatorHandler(): you register once what to do when the popup appears, and Playwright takes care of the rest at any point in the test.
The problem: the Netherlands test
Let's take one example and follow it all the way through. We have an app that shows information about countries. The test is simple: search for Netherlands, then click the like button on its card.
import { test, expect } from '@playwright/test';
test('like the Netherlands', async ({ page }) => {
await page.goto('/');
await page.getByTestId('search-input').fill('Netherlands');
await page.getByTestId('like-button').click();
await expect(page.getByTestId('like-count')).toHaveText('1');
});But the app sometimes shows a promo popup on top of the content. Sometimes it appears after the page loads, sometimes after the search, and sometimes it never appears at all.
- ✅ If the popup doesn't appear, the test passes
- ❌ If it does, it covers the like button, and
click()waits until it times out and then fails
That's the exact definition of a Flaky test: the result depends on luck, not on the quality of the app.
The fixes we usually see, and why they're not enough
Fix one: a condition before the click.
if (await page.getByTestId('promo-popup').isVisible()) {
await page.getByTestId('close-popup').click();
}
await page.getByTestId('like-button').click();The problem is that isVisible() only checks the current moment. If the popup appears half a second after the condition, we're back to the same failure.
Fix two: a fixed wait with waitForTimeout so that "the popup shows up if it's going to". This slows down every run and guarantees nothing, because the popup can arrive later than the wait.
Fix three: repeating the condition before every step. It sort of works, but it turns the test into duplicated code, and it needs changing everywhere whenever the popup changes.
The solution: addLocatorHandler
addLocatorHandler takes two things: a Locator for the element you're watching, and a function that does what you want when it appears.
import { test, expect } from '@playwright/test';
test('like the Netherlands', async ({ page }) => {
const popup = page.getByTestId('promo-popup');
await page.addLocatorHandler(popup, async () => {
await page.getByTestId('close-popup').click();
});
await page.goto('/');
await page.getByTestId('search-input').fill('Netherlands');
await page.getByTestId('like-button').click();
await expect(page.getByTestId('like-count')).toHaveText('1');
});Notice that the test steps themselves didn't change at all. No conditions, no waits. We just added a single registration at the start.
page.goto(). That way it's ready even if the popup appears on the very first page load.What happens at runtime?
- Registration: Playwright stores the Locator and the function, and doesn't run anything yet.
- Before every Action or Assertion: before
click()andfill(), and before every retry of an auto-waitingexpect, Playwright checks whether the popup is visible. - If it appears: it runs your function — clicks the close button — then waits until the popup disappears, then carries on with the original step as if nothing happened.
- If it never appears: the function doesn't run, and the test continues normally.
The result: the test always passes, whether the popup appears or not. And that's the difference between a test the team trusts, and a test they ignore when it fails.
Options you'll need in real projects
Run a limited number of times
Some popups only appear once per session, like a welcome offer. The times option sets how many times the function runs, after which the Handler is removed automatically:
await page.addLocatorHandler(popup, async () => {
await page.getByTestId('close-popup').click();
}, { times: 1 });Don't wait for the popup to disappear
By default, after running the function Playwright waits until the popup disappears. If your function isn't designed to hide it — for example, it clicks something inside it and part of it stays visible — use noWaitAfter:
await page.addLocatorHandler(popup, async () => {
await page.getByTestId('accept-banner').click();
}, { noWaitAfter: true });Stop watching
If part of the test needs the popup visible — for example because you want to test the popup itself — remove the handler before that part:
await page.removeLocatorHandler(popup);Where else can you use it besides promo popups?
Any element that appears at an unexpected time and covers the page:
| Element | What the function does |
|---|---|
| Cookie consent banner | Clicks "Accept" |
| Newsletter sign-up popup | Clicks the close button |
| Feedback survey that appears after a few seconds | Clicks "Later" |
| Support chat window | Minimises it |
Mistakes to watch out for
- Keep the function simple. Close the popup, and that's it. Any extra step, like navigating to another page, changes the page state in the middle of the test and gives you results that are hard to make sense of.
- Pick a precise Locator. If the Locator matches an element that's always on the page, the function will run before every step.
- Don't use it to hide a real problem. If the popup appears somewhere it shouldn't, that's a Bug worth a report, not a Handler.
What's in this for non-programmers?
If you're a manual Tester or a Team Lead, the lesson here is bigger than Playwright: most Flaky tests aren't caused by the app — they're caused by an assumption in the test that the page will always look the same. When a test fails intermittently, ask first: is there something that sometimes appears and sometimes doesn't?
Summary
- Random popups are a common cause of Flaky tests, and conditions and fixed waits don't fix them.
addLocatorHandlerregisters the fix once, and Playwright applies it before every step when needed.- Register it at the start of the test, and use
times,noWaitAfter, andremoveLocatorHandleras the case requires. - Keep the function simple, and don't use it to hide a real Bug.
This topic is one of the advanced lessons in the Master Playwright: The Complete Guide course, along with Custom Fixtures, Routing, and waitForResponse, plus the Page Object Model and a full hands-on project: 274 lessons across 20 sections, 28 of them free that you can start without an account.
