Resource library

QA How-To

How to Fix Playwright "element intercepts pointer events"

Fix Playwright element intercepts pointer events errors by diagnosing overlays, sticky headers, tooltips, and responsive sheets with runnable tests and traces.

23 min read | 3,179 words

TL;DR

To fix Playwright element intercepts pointer events, inspect the click call log or trace for the named blocker. Wait for temporary masks, close intentional dialogs, or repair persistent CSS overlap, then assert the click's visible result.

Key Takeaways

  • Read the intercepting element in the click call log before changing the locator or timeout.
  • Wait for loading masks to disappear and close dialogs through their visible controls.
  • Fix fixed headers and transparent toast roots in the application's CSS when they permanently block input.
  • Test tooltip and consent overlays at the viewport and pointer state that reveal them.
  • Use a trace to inspect the click point, blocker, and action sequence in CI.
  • Assert the click's user-visible outcome without relying on force or synthetic DOM clicks.

To fix Playwright element intercepts pointer events, identify which element covers the click point and remove the obstruction through the same action a user would take. The message appears in a locator.click() call log when a button is visible and enabled, but a loading mask, dialog backdrop, fixed header, toast layer, tooltip, or consent sheet would receive the pointer event instead.

Locator.click: Timeout 30000ms exceeded.
<div id="toast-container"></div> intercepts pointer events

The toast-container line comes from a reported Playwright failure. The element name and timeout vary. Inspect the named blocker, not just the button. The Playwright actionability guide defines the receiving-events check, and the Trace Viewer guide shows the action log.

TL;DR

  1. Run the failing test with npx playwright test tests/intercept.spec.ts --trace on and open its trace. Find the element named immediately before intercepts pointer events.
  2. If the blocker belongs to the intended flow, close it or wait for it to disappear: await expect(page.getByTestId('loading-mask')).toBeHidden().
  3. If a permanent decoration covers usable controls, fix the application's layout or hit testing. Keep an assertion for the click's result, such as await expect(page.getByRole('status')).toHaveText('Saved').

A locator already waits for click actionability. A long wait cannot fix a permanent overlay; force: true may pass without proving the control is usable.

What the Error Actually Means

Playwright finds an action point inside the target and asks the browser which element would receive a pointer event there. If another element wins hit testing, Playwright retries until its action timeout. The log reports that intercepting element. A visible button can therefore be unclickable; toBeVisible() does not assert that it is the topmost hit target. Likewise, toBeEnabled() concerns disabled state, not what sits above it.

A subtree intercepts pointer events line means the hit target lies inside the named ancestor. Inspect its dimensions, stacking context, and pointer-events CSS. A full-viewport transparent container can block clicks; applying pointer-events: none to a real dialog can break the product.

Playwright's action guide documents scrolling, force, and dispatchEvent('click'); the last two do not prove a human can click through an obstruction. A different log may call for the viewport error guide or visibility error guide.

In an existing Playwright project, run npm ci and npx playwright install chromium. Save each independent block at its stated path under tests/. The examples use page.setContent(), so they need no application server. Match the installed package to your lockfile.

Root-Cause Decision Table

Symptom Root cause Fix
Log names .loading-mask; button becomes usable after data arrives A transient loading layer still covers the action point Wait for the mask to be hidden or for a specific ready state
Log names a dialog backdrop; target is on the underlying page An active modal intentionally blocks background actions Finish or close the modal before clicking the page
Log names a header after Playwright scrolls the target Fixed or sticky navigation overlaps content Correct the layout offset and test the control at the affected viewport
Log names a toast container although the toast looks small A transparent notification root occupies a large hit area Reduce its box or make only decorative space transparent to pointer events
Log names a tooltip after hover Hover creates a visual layer over the target Move the tooltip or make its noninteractive surface ignore pointer events
Failure occurs only on a narrow viewport; log names consent UI A responsive bottom sheet covers the target Handle consent through the visible control, then assert the sheet is gone
Log names a suggestion list after typing An open autocomplete menu overlays a nearby action Select an option or dismiss the menu before continuing
Log names an iframe or its wrapper An embedded promotion occupies the action point Close the interstitial through its visible dismiss control

The trace's Action snapshot shows the click point; its Log panel names the blocker. Changing selectors rarely helps when the correct target is covered. See the role locator guide for target selection.

1. Fix Playwright Element Intercepts Pointer Events From a Loading Mask

A Save button can render before a request finishes. A full-page mask then owns its click point even though the button is enabled. Wait for the mask to leave; networkidle is a poor proxy on a polling page.

Save as tests/loading-mask.spec.ts. Prepare starts a short load; the test waits for the mask to leave before saving.

import { test, expect } from '@playwright/test';

test('save after the loading mask leaves', async ({ page }) => {
  await page.setContent(`
    <style>
      #mask { position: fixed; inset: 0; background: #0004; z-index: 10; }
      #mask[hidden] { display: none; }
    </style>
    <button id="prepare">Prepare</button>
    <button id="save">Save</button>
    <p role="status">Idle</p>
    <div id="mask" data-testid="loading-mask" hidden></div>
    <script>
      document.querySelector('#prepare').onclick = () => {
        document.querySelector('#mask').hidden = false;
        setTimeout(() => { document.querySelector('#mask').hidden = true; }, 150);
      };
      document.querySelector('#save').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Saved';
      };
    </script>
  `);

  await page.getByRole('button', { name: 'Prepare' }).click();
  await expect(page.getByTestId('loading-mask')).toBeHidden();
  await page.getByRole('button', { name: 'Save' }).click();
  await expect(page.getByRole('status')).toHaveText('Saved');
});

Verify with npx playwright test tests/loading-mask.spec.ts. Expect one pass. If the mask never hides, the assertion identifies the failing state boundary before the click.

A legitimate loading mask should keep blocking input. If it stays up, inspect the request and state transition. Use the response-waiting guide for a named API response, then assert the visible ready state.

For a real save page, distinguish the mask's two transitions: it appears when the operation starts and disappears only after the UI can accept another action. If it briefly vanishes between requests, a bare hidden assertion may pass too early. Assert an accompanying ready label, enabled action, or loaded data value that cannot occur during the intermediate state. If the request fails, verify the error message and keep the Save action blocked or recoverable according to the product design. That state contract also helps developers locate an application bug when the mask remains mounted.

2. Close the Dialog Backdrop Before Clicking the Page

A modal deliberately blocks background actions. Close Settings before clicking Delete behind it. Clicking through the backdrop would test behavior the interface prevents.

Save as tests/dialog-backdrop.spec.ts. The test closes the semantic dialog, checks that its backdrop is hidden, then verifies Delete's effect.

import { test, expect } from '@playwright/test';

test('close settings before using background action', async ({ page }) => {
  await page.setContent(`
    <style>
      #backdrop { position: fixed; inset: 0; background: #0006; z-index: 20; }
      #backdrop[hidden] { display: none; }
      [role=dialog] { margin: 5rem auto; padding: 1rem; background: white; width: 15rem; }
    </style>
    <button id="open">Settings</button>
    <button id="delete">Delete</button>
    <p role="status">Present</p>
    <div id="backdrop" data-testid="backdrop" hidden>
      <section role="dialog" aria-label="Settings">
        <button id="close">Close</button>
      </section>
    </div>
    <script>
      document.querySelector('#open').onclick = () => {
        document.querySelector('#backdrop').hidden = false;
      };
      document.querySelector('#close').onclick = () => {
        document.querySelector('#backdrop').hidden = true;
      };
      document.querySelector('#delete').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Deleted';
      };
    </script>
  `);

  await page.getByRole('button', { name: 'Settings' }).click();
  const dialog = page.getByRole('dialog', { name: 'Settings' });
  await expect(dialog).toBeVisible();
  await dialog.getByRole('button', { name: 'Close' }).click();
  await expect(page.getByTestId('backdrop')).toBeHidden();
  await page.getByRole('button', { name: 'Delete' }).click();
  await expect(page.getByRole('status')).toHaveText('Deleted');
});

Verify with npx playwright test tests/dialog-backdrop.spec.ts; expect one pass. Use the actual Cancel, Escape, or submit path in your product. An invisible but mounted backdrop can still block input. Scope repeated buttons to the active dialog; see locator filtering.

If Close only hides the dialog panel and leaves the backdrop, check whether an exit animation or portal cleanup is incomplete. The backdrop must stop receiving events when the modal closes, even if it remains in the DOM for a fade. Assert the condition that matters for the user: the background action becomes reachable. For nested dialogs, close the topmost one first and confirm focus returns to the expected control before acting on the underlying page. Skipping these checks can disguise a broken focus trap as a click timeout.

3. Repair a Sticky Header That Covers the Click Point

A fixed header can overlap content after scrolling. An in-viewport target may still lie under it. Capture the failing viewport and inspect the trace's click point before changing timeouts.

Reserve space for the fixed header and add a scroll offset for scrolled targets. Save the fixture as tests/sticky-header.spec.ts; it exercises a below-fold button at a compact viewport.

import { test, expect } from '@playwright/test';

test('fixed header does not cover the action', async ({ page }) => {
  await page.setViewportSize({ width: 390, height: 640 });
  await page.setContent(`
    <style>
      :root { --header-height: 72px; }
      header { position: fixed; top: 0; left: 0; right: 0;
               height: var(--header-height); background: navy; z-index: 5; }
      main { padding-top: var(--header-height); }
      #apply { scroll-margin-top: calc(var(--header-height) + 16px); }
      .spacer { height: 800px; }
    </style>
    <header aria-label="Site navigation"></header>
    <main>
      <div class="spacer"></div>
      <button id="apply">Apply</button>
      <p role="status">Not applied</p>
    </main>
    <script>
      document.querySelector('#apply').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Applied';
      };
    </script>
  `);

  const action = page.getByRole('button', { name: 'Apply' });
  await action.scrollIntoViewIfNeeded();
  await action.click();
  await expect(page.getByRole('status')).toHaveText('Applied');
});

Verify with npx playwright test tests/sticky-header.spec.ts; expect a pass at 390 by 640 pixels. Apply the CSS to the actual component and rerun its failing viewport. Avoid coordinate clicks tied to one layout. See the responsive layout testing guide for breakpoint coverage.

A header can create a stacking context through transform, filter, or positioned ancestors, so changing the button's z-index may have no effect. Inspect the header and content rectangles at the failed click point. If the button is partly visible below the header, a manual coordinate could pass while the layout still clips its label or focus ring. Prefer a layout repair that leaves the entire control readable and reachable. Test keyboard focus as well when the same fixed header obscures focused controls after tabbing.

4. Fix Playwright Element Intercepts Pointer Events From a Toast Container

A small toast can have a full-screen invisible root. Inspect the root's dimensions and pointer-events. Decorative wrapper space can ignore pointer input while toast buttons still receive it.

Save as tests/toast-container.spec.ts. The test checks the background action and the toast's Dismiss control.

import { test, expect } from '@playwright/test';

test('toast wrapper does not block the page', async ({ page }) => {
  await page.setContent(`
    <style>
      #toast-root { position: fixed; inset: 0; z-index: 30;
                    pointer-events: none; }
      #toast { position: absolute; top: 12px; right: 12px;
               padding: 12px; background: #eee; pointer-events: auto; }
    </style>
    <button id="continue">Continue</button>
    <p role="status">Waiting</p>
    <div id="toast-root">
      <div id="toast" role="status" aria-label="Notice">
        Notice <button id="dismiss">Dismiss</button>
      </div>
    </div>
    <script>
      document.querySelector('#continue').onclick = () => {
        document.querySelector('p[role=status]').textContent = 'Continued';
      };
      document.querySelector('#dismiss').onclick = () => {
        document.querySelector('#toast').remove();
      };
    </script>
  `);

  await page.getByRole('button', { name: 'Continue' }).click();
  await expect(page.locator('p[role=status]')).toHaveText('Continued');
  await page.getByRole('button', { name: 'Dismiss' }).click();
  await expect(page.getByRole('button', { name: 'Dismiss' })).toHaveCount(0);
});

Verify with npx playwright test tests/toast-container.spec.ts; both buttons should work. Check whether your UI library supports a narrower portal before adding CSS. A different locator cannot repair the same covered click point.

Use DevTools to inspect the toast root's computed bounding rectangle, not just the painted notification. A wrapper with inset: 0 can fill the viewport even when its background is transparent. The example restores pointer-events: auto on the interactive toast because inherited pointer behavior would otherwise make Dismiss unusable. If the toast intentionally blocks the page, such as a critical confirmation requiring a decision, treat it as a dialog instead of making the wrapper transparent. Its behavior determines the correct fix.

5. Move or Neutralize a Hover Tooltip That Covers Its Trigger

Hover can reveal a tooltip between click checks. If it covers the trigger, it becomes the hit target. Move a decorative tooltip away or make it ignore pointer events.

Save as tests/hover-tooltip.spec.ts. The test reveals the overlapping tooltip before clicking and asserting the result.

import { test, expect } from '@playwright/test';

test('decorative tooltip leaves its trigger clickable', async ({ page }) => {
  await page.setContent(`
    <style>
      #help { position: relative; margin: 60px; }
      #tip { display: none; position: absolute; left: 0; top: 0;
             z-index: 10; background: gold; pointer-events: none; }
      #help:hover #tip { display: block; }
    </style>
    <div id="help">
      <button id="details">Details</button>
      <span id="tip" role="tooltip">Shows details</span>
    </div>
    <p role="status">Closed</p>
    <script>
      document.querySelector('#details').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Opened';
      };
    </script>
  `);

  const trigger = page.getByRole('button', { name: 'Details' });
  await trigger.hover();
  await expect(page.getByRole('tooltip')).toBeVisible();
  await trigger.click();
  await expect(page.getByRole('status')).toHaveText('Opened');
});

Verify with npx playwright test tests/hover-tooltip.spec.ts; it should pass with the tooltip visible. An interactive popover needs a reachable position rather than pointer-events: none. A passing hover alone does not prove the click is reachable; see the hover guide.

Check whether the tooltip appears on keyboard focus too. A pure tooltip is descriptive content, so its placement should preserve the trigger's pointer target and avoid covering nearby controls. A menu or help panel with selectable content is a different component; its items must remain clickable and focusable. If moving the pointer away collapses the layer immediately, do not solve the test by moving the mouse to an artificial corner. Reproduce the user's path and correct the hover geometry that blocks the trigger.

6. Clear a Responsive Consent Sheet Through Its Own Control

A bottom consent sheet can cover a call-to-action on mobile but not desktop. Choose Accept or Reject, assert the sheet closes, then use the underlying control. Do not preseed consent if this test covers the decision flow.

Save as tests/consent-sheet.spec.ts. Continue sits behind the sheet at a phone viewport.

import { test, expect } from '@playwright/test';

test('mobile consent sheet is handled before continuing', async ({ page }) => {
  await page.setViewportSize({ width: 375, height: 667 });
  await page.setContent(`
    <style>
      #continue { position: fixed; bottom: 20px; left: 20px; }
      #consent { position: fixed; bottom: 0; left: 0; right: 0;
                 height: 180px; padding: 12px; background: white;
                 border-top: 1px solid #555; z-index: 40; }
      #consent[hidden] { display: none; }
    </style>
    <button id="continue">Continue</button>
    <p role="status">Waiting</p>
    <section id="consent" aria-label="Cookie choices" data-testid="consent-sheet">
      <p>Choose whether to accept cookies.</p>
      <button id="accept">Accept</button>
    </section>
    <script>
      document.querySelector('#accept').onclick = () => {
        document.querySelector('#consent').hidden = true;
      };
      document.querySelector('#continue').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Continued';
      };
    </script>
  `);

  await page.getByRole('button', { name: 'Accept' }).click();
  await expect(page.getByTestId('consent-sheet')).toBeHidden();
  await page.getByRole('button', { name: 'Continue' }).click();
  await expect(page.getByRole('status')).toHaveText('Continued');
});

Verify with npx playwright test tests/consent-sheet.spec.ts; expect a pass at 375 by 667 pixels. If an accepted sheet still blocks input, inspect its backdrop. See device emulation for broader device projects.

A fresh browser context has no consent choice unless your fixture adds one. Local manual browsing often does, which explains why the same test can fail only in CI. Decide which state each test requires and set it before navigation only for flows unrelated to consent. Keep a separate case that starts clean, makes a real choice, and verifies the choice persists after reload. That prevents a storage shortcut from hiding a broken Accept button or a sheet that reappears on every page.

7. Select From an Autocomplete Menu Before Continuing

An autocomplete menu can extend over a button below its input. After typing, the button is still visible through a translucent suggestion panel, but the panel receives the click. Complete the selection first. Hiding the list with CSS solely in the test would skip the form state the user must establish.

Save as tests/autocomplete-menu.spec.ts. Selecting Alpha closes the listbox and records the choice. The submit outcome proves the chosen value reached the form action.

import { test, expect } from '@playwright/test';

test('select suggestion before submitting', async ({ page }) => {
  await page.setContent(`
    <style>
      #menu { position: absolute; top: 35px; left: 0; width: 240px;
              height: 120px; background: white; z-index: 10; }
      #menu[hidden] { display: none; }
      #submit { position: absolute; top: 75px; left: 20px; }
    </style>
    <input id="search" aria-label="Product" />
    <div id="menu" role="listbox" hidden>
      <div role="option" tabindex="0">Alpha</div>
    </div>
    <button id="submit">Submit</button>
    <p role="status">No selection</p>
    <script>
      document.querySelector('#search').oninput = () => {
        document.querySelector('#menu').hidden = false;
      };
      document.querySelector('[role=option]').onclick = () => {
        document.querySelector('#search').value = 'Alpha';
        document.querySelector('#menu').hidden = true;
      };
      document.querySelector('#submit').onclick = () => {
        document.querySelector('[role=status]').textContent =
          'Submitted ' + document.querySelector('#search').value;
      };
    </script>
  `);

  await page.getByRole('textbox', { name: 'Product' }).fill('Al');
  await page.getByRole('option', { name: 'Alpha' }).click();
  await expect(page.getByRole('listbox')).toBeHidden();
  await page.getByRole('button', { name: 'Submit' }).click();
  await expect(page.getByRole('status')).toHaveText('Submitted Alpha');
});

Verify with npx playwright test tests/autocomplete-menu.spec.ts; expect one pass. If the intended workflow is to dismiss suggestions without selecting one, use the application's supported Escape or outside-click behavior and assert the listbox closes. Check that focus and the input value remain correct before submitting.

Autocomplete menus can be portaled outside the form, so a DOM descendant selector may miss them even though they visibly overlap Submit. Use the accessible listbox and option roles to select the intended suggestion. Then check both the input value and the closed menu state. If the panel remains open after selection, the component may have a real blur or state synchronization bug. Clicking Submit with force would bypass the symptom and risk submitting a partial query rather than the selected entity.

8. Close an Embedded Interstitial That Covers the Page

An iframe can cover a target even when its content looks like a small promotion. The frame's outer rectangle participates in hit testing. Changing the target's locator will not move the iframe. If the product offers a dismiss control, use it and verify the frame disappears. If it has no usable dismiss path and hides essential controls, raise a product defect.

Save as tests/interstitial-frame.spec.ts. The overlay iframe covers the page, while its Close promotion button sits above the frame. The test takes the visible exit path and then verifies the underlying action.

import { test, expect } from '@playwright/test';

test('dismiss embedded promotion before continuing', async ({ page }) => {
  await page.setContent(`
    <style>
      #promo { position: fixed; inset: 0; width: 100%; height: 100%;
               border: 0; z-index: 20; }
      #close-promo { position: fixed; top: 10px; right: 10px; z-index: 21; }
    </style>
    <button id="continue">Continue</button>
    <p role="status">Waiting</p>
    <iframe id="promo" title="Promotion" srcdoc="<p>Promotion</p>"></iframe>
    <button id="close-promo">Close promotion</button>
    <script>
      document.querySelector('#close-promo').onclick = () => {
        document.querySelector('#promo').remove();
        document.querySelector('#close-promo').remove();
      };
      document.querySelector('#continue').onclick = () => {
        document.querySelector('[role=status]').textContent = 'Continued';
      };
    </script>
  `);

  await page.getByRole('button', { name: 'Close promotion' }).click();
  await expect(page.getByTitle('Promotion')).toHaveCount(0);
  await page.getByRole('button', { name: 'Continue' }).click();
  await expect(page.getByRole('status')).toHaveText('Continued');
});

Verify with npx playwright test tests/interstitial-frame.spec.ts; expect one pass. In the real application, the frame may load from another origin, but the parent's dismiss button remains accessible without reading frame internals. Preserve that boundary in your test.

An iframe's content can be cross-origin while its bounding box is still visible to the parent page and Playwright's trace. Check the outer frame's width, height, and stacking order. If the product intends only a small embedded card, size its iframe to that card rather than stretching an invisible frame across the viewport. If the promotion is a deliberate full-screen interstitial, its close control must be discoverable and keyboard reachable. Removing the iframe with page.evaluate() in the test would skip that user path.

CI and Docker Variants

CI can change click geometry through viewport, fonts, fresh consent state, or slower responses. Reproduce the same browser project, viewport, and storage state. Run the focused spec before the full flow.

npm ci
npx playwright install --with-deps chromium
npx playwright test tests/consent-sheet.spec.ts --trace on --workers=1
npx playwright show-report

The command records a trace. show-report opens the HTML report if configured; otherwise use npx playwright show-trace path/to/trace.zip with the emitted path. Upload test-results/ on CI failure. The trace-on-retry guide describes a smaller artifact strategy.

For Docker, the browser image and the project's installed Playwright package must match. Substitute your installed package version into the placeholder before running this command; do not paste the angle brackets literally. The working directory is mounted into the container, so npm ci installs the test package there. --ipc=host follows Playwright's Docker guidance for Chromium. If your project relies on a local app server, run that server in the container or expose it through an address the container can reach; localhost inside Docker refers to the container.

docker run --rm --ipc=host \
  -v "$PWD:/work" -w /work \
  mcr.microsoft.com/playwright:v<your-playwright-version>-noble \
  /bin/sh -lc 'npm ci && npx playwright test tests/consent-sheet.spec.ts --trace on'

The image supplies browsers and system dependencies; your project supplies @playwright/test. Choose a published matching tag per Playwright Docker. For environment setup, see Docker for Playwright.

How to Verify the Fix

Rerun the failing action with --trace on. Inspect the Action snapshot for the target and click point, then the Log panel for the blocker. Assert the effect of the action, such as a saved record or navigation, rather than just a returned click().

Use the following focused commands after you create the example files above:

npx playwright test tests/loading-mask.spec.ts tests/dialog-backdrop.spec.ts --trace on
npx playwright test tests/sticky-header.spec.ts tests/toast-container.spec.ts --trace on
npx playwright test tests/hover-tooltip.spec.ts tests/consent-sheet.spec.ts --trace on
npx playwright test tests/autocomplete-menu.spec.ts tests/interstitial-frame.spec.ts --trace on

The four runs should pass without force or fixed sleeps. Repeat the original test at its failing viewport. If it still fails, compare the new blocker name. For a persistent blocker, inspect its computed box and pointer-events state.

For intermittent failures, try npx playwright test tests/consent-sheet.spec.ts --repeat-each=10 --workers=1. This illustrative sample cannot prove a race is gone. Keep the outcome assertion and CI artifacts; see the CI test setup guide.

Prevent It From Coming Back

Model visible UI gates explicitly. When a button must wait for loading, the app should expose a stable loading indicator or disabled state and remove it when ready. When a modal is open, tests should operate within that modal until it closes. When a toast is decorative, its outer hit area should not cover the page. These are product behaviors worth reviewing, not merely test timing details.

Keep one regression case for each affected geometry. A sticky header bug warrants the narrow viewport and scrolled location that produced it. A consent bug warrants a fresh browser context with no stored decision. A hover bug warrants a pointer movement before the click. Tests with only toBeVisible() miss these failures because visibility and hit testing are separate. Add an outcome assertion so a click that silently lands elsewhere cannot pass.

Record a trace on first failure or first retry in your test configuration, and retain CI artifacts for the failing job. Review large z-index layers and full-screen wrappers during UI changes. For overlays that intentionally block the page, ensure they have a usable close or completion path. For notification portals, test both underlying actions and interactive toast controls after any CSS pointer-events change. This keeps the product usable for mouse users while giving Playwright a reliable action point.

Interview Questions and Answers

The structured interview questions below cover click actionability, why a visible button can be covered, when CSS needs repair, and how to investigate CI-only failures. A strong answer names the blocker from the trace, explains whether it is intentional, and connects the fix to an assertion of the user's outcome. Practice that reasoning against a failing trace, then compare it with the Playwright debugging interview questions.

Common Mistakes

  • Adding waitForTimeout(3000) before every click. A permanent header or modal remains after the delay, while a variable load can last longer. Assert the state that actually removes the blocker.
  • Using force: true as a blanket fix. It can conceal a user-facing defect and turns a pointer interaction test into a weaker assertion.
  • Dispatching a click event and treating it as proof of reachability. It tests an event handler, not the browser's hit target.
  • Increasing the action timeout without reading the call log. More time only repeats the same blocked action when the overlay is permanent.
  • Setting pointer-events: none on an entire interactive dialog or consent sheet. That can let input leak through and make visible controls unusable.
  • Replacing a role locator with a CSS ID when the trace already resolved the correct button. Locator precision does not remove a layer above the button.
  • Asserting only that a button is visible. Check the action outcome and, when relevant, that a loading indicator, modal, or sheet has closed.
  • Debugging only the screenshot. The log identifies the intercepting element and the trace shows the click point, including layers that are transparent in a screenshot.

Conclusion

Fix Playwright element intercepts pointer events by identifying the reported hit target, then following or repairing the UI state that puts it above the intended control. Wait for transient masks, close deliberate modal gates, and correct persistent CSS overlap. Keep the original failing viewport and a meaningful post-click assertion in the regression test. The next practical step is to open one trace, name the blocker, and apply the matching fix from the decision table.

Interview Questions and Answers

What actionability condition detects an intercepting element?

Playwright's Receives Events check determines whether the target is the hit target at the action point. A visible, stable, enabled button can fail that check when another element is stacked over it. The call log identifies the element that intercepts input.

Why is toBeVisible insufficient before a click?

It verifies rendering, not hit testing. A transparent backdrop may cover a visible control. I assert the overlay is gone when appropriate, click the target, and verify the user-visible result.

How would you handle a loading mask that intermittently covers Save?

I would identify the mask in the trace and wait for its disappearance or a specific ready state. Then I would click Save and assert persistence or confirmation. If the mask never clears, I would investigate the application request or state transition rather than add a sleep.

When would you change CSS instead of test code?

If a fixed header or invisible wrapper permanently covers a usable control, real users face the same obstruction. I would reserve layout space or narrow the wrapper's hit area, then keep a viewport-specific regression test. An intentional modal backdrop calls for a different sequence in the test.

What is the risk of force clicking?

A forced click bypasses the receiving-events check. The test may pass although a mouse user cannot reach the target. I would use it only for an explicitly non-user-like operation and would not treat it as a UI reachability test.

How would you investigate a CI-only pointer interception?

I would record a trace in CI, inspect the failed action's click point and blocker, and compare viewport, browser project, stored consent, and loading state with local execution. I would then reproduce the relevant geometry in a focused test and assert the actual outcome.

Why is dispatchEvent('click') different from locator.click()?

dispatchEvent invokes a DOM event on the selected element. locator.click performs browser input after actionability checks, including whether the element receives events at the chosen point. The former can test an event handler while missing a user-facing overlay defect.

Frequently Asked Questions

What does element intercepts pointer events mean in Playwright?

Another element would receive the pointer event at the click point. Playwright keeps checking until the click timeout and reports the blocking element in its call log. The target can be visible and enabled throughout.

Why does a visible button fail Playwright's click check?

Visibility only describes how the button renders. A backdrop, header, toast root, or tooltip can be stacked above its click point. The receiving-events actionability check catches that difference.

Should I use force true to bypass an overlay?

Use it only when you intentionally want to bypass normal hit testing and do not claim the result proves a user can click. For a user-flow test, clear the overlay or fix the layout and assert the action outcome.

Will waitForTimeout fix an intercepting element?

It might accidentally outlast a transient mask, but it cannot repair a permanent overlap and can still fail on a slower run. Wait for the loading indicator or modal to reach the state that makes the control usable.

How do I find which element blocks the click?

Run the focused test with `--trace on` and open the failed action in Trace Viewer. The Log panel names the intercepting element, while the Action snapshot shows the target and attempted click point.

Can a toast block clicks outside its visible box?

Yes. A transparent, full-screen toast root can receive pointer events across the page. Reduce its hit area or set pointer-events to none on decorative wrapper space while preserving input on toast buttons.

Why does the problem happen only in CI or Docker?

A different viewport, fresh consent state, font rendering, or slower UI transition can change which layer sits at the click point. Compare the CI trace and environment to the local run before increasing timeouts.

Related Guides