PRACTICAL GUIDE / ElementClickInterceptedException hit test evidence

Prove what covered the button before fixing the click

Capture Selenium's intended click point and the topmost DOM node there, then wait for the real obstruction instead of bypassing user behavior.

By The Testing AcademyUpdated August 4, 202610 min read
All field guides
In this guide6 sections
  1. What Selenium actually tries to click
  2. Capture the click point before the page changes
  3. Read the evidence before choosing a wait
  4. Fix the obstruction, not the click command
  5. Expect a race between observation and action
  6. When bypassing hit testing is the wrong move

What you will learn

  • What Selenium actually tries to click
  • Capture the click point before the page changes
  • Read the evidence before choosing a wait
  • Fix the obstruction, not the click command

The button is visible, enabled, and present in the screenshot, yet click() says another element would receive the event. A loading mask faded to transparent but still owns pointer events at the button's center. Waiting for presence cannot solve an obstruction when the locator already found its target.

What Selenium actually tries to click

Selenium sends the WebDriver Element Click command for the located element. The browser's remote end scrolls the element into view if needed, calculates the center of its first client rectangle inside the viewport, and checks the paint order at that point. If the topmost pointer-interactable element is neither the target nor one of its descendants, WebDriver returns an element click intercepted error.

That mechanism explains several confusing reports:

  • A button can satisfy isDisplayed() and isEnabled() while a modal backdrop sits above it.
  • An overlay with opacity: 0 is visually transparent, but it still intercepts input unless its pointer behavior or layout changes.
  • A child icon returned by the hit test is fine when it belongs to the button. Descendants are part of the target's clickable area.
  • An element with pointer-events: none is skipped by document.elementFromPoint(), so a decorative layer with that style does not own the click.
  • A sticky header may cover the center after WebDriver scrolls the target to the top of a scroll container.

ElementClickInterceptedException is therefore more specific than "the click did not work." Selenium found an element and tried to perform a user-like pointer interaction, but hit testing predicted that another element would receive it. The product outcome was never attempted.

The click point is not always the center of the full bounding box. When part of the element lies outside the viewport, WebDriver uses the center of the visible intersection. Evidence code should make the same adjustment. Document coordinates from a layout report are also insufficient because elementFromPoint() expects viewport-relative coordinates.

Capture the click point before the page changes

An intercepted click often disappears while a screenshot is being uploaded or a retry is starting. Collect geometry and the topmost node immediately, inside the same browser state and test attempt.

This JUnit 5 example creates a page with a five-second loading mask, proves the interception, records the hit test, waits for the actual blocker to disappear, and then uses a normal Selenium click:

Java
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;

import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
import java.time.Duration;
import java.util.Base64;
import java.util.Map;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.openqa.selenium.By;
import org.openqa.selenium.ElementClickInterceptedException;
import org.openqa.selenium.JavascriptExecutor;
import org.openqa.selenium.OutputType;
import org.openqa.selenium.TakesScreenshot;
import org.openqa.selenium.WebDriver;
import org.openqa.selenium.WebElement;
import org.openqa.selenium.chrome.ChromeDriver;
import org.openqa.selenium.support.ui.ExpectedConditions;
import org.openqa.selenium.support.ui.WebDriverWait;

class ClickInterceptionEvidenceTest {
    private final WebDriver driver = new ChromeDriver();

    @AfterEach
    void stopBrowser() {
        driver.quit();
    }

    @Test
    void identifiesTheElementAtTheClickPoint() throws Exception {
        String html = """
            <style>
              #buy { margin: 80px; width: 160px; height: 48px; }
              #mask { position: fixed; inset: 0; z-index: 10;
                      display: grid; place-items: center;
                      background: rgba(0, 0, 0, .15); }
            </style>
            <button id="buy" onclick="this.dataset.state='ordered'">Buy</button>
            <div id="mask" data-testid="loading-mask">Loading</div>
            <script>
              setTimeout(() => document.getElementById('mask').remove(), 5000)
            </script>
            """;
        String page = "data:text/html;base64," + Base64.getEncoder()
            .encodeToString(html.getBytes(StandardCharsets.UTF_8));
        driver.get(page);

        WebElement buy = driver.findElement(By.id("buy"));
        assertThrows(ElementClickInterceptedException.class, buy::click);

        Map<String, Object> hit = hitTest(buy);
        System.out.println("click-hit-test=" + hit);
        saveScreenshot(Path.of("target", "click-intercepted.png"));

        new WebDriverWait(driver, Duration.ofSeconds(7)).until(
            ExpectedConditions.invisibilityOfElementLocated(By.id("mask"))
        );
        buy.click();

        assertEquals("ordered", buy.getAttribute("data-state"));
    }

    @SuppressWarnings("unchecked")
    private Map<String, Object> hitTest(WebElement target) {
        String script = """
            const el = arguments[0];
            const rect = el.getClientRects()[0];
            const describe = node => {
              if (!node) return null;
              const id = node.id ? `#${node.id}` : '';
              const classes = [...node.classList].map(c => `.${c}`).join('');
              return `${node.tagName.toLowerCase()}${id}${classes}`;
            };
            if (!rect) {
              return {hasClientRect: false, target: describe(el)};
            }
            const left = Math.max(0, Math.min(rect.left, rect.right));
            const right = Math.min(innerWidth, Math.max(rect.left, rect.right));
            const top = Math.max(0, Math.min(rect.top, rect.bottom));
            const bottom = Math.min(innerHeight, Math.max(rect.top, rect.bottom));
            const x = Math.floor((left + right) / 2);
            const y = Math.floor((top + bottom) / 2);
            const topmost = document.elementFromPoint(x, y);
            return {
              hasClientRect: true,
              x, y,
              viewport: {width: innerWidth, height: innerHeight},
              rect: {left: rect.left, top: rect.top,
                     width: rect.width, height: rect.height},
              target: describe(el),
              topmost: describe(topmost),
              targetOwnsPoint: topmost === el || el.contains(topmost),
              topmostPointerEvents: topmost
                ? getComputedStyle(topmost).pointerEvents : null
            };
            """;
        return (Map<String, Object>) ((JavascriptExecutor) driver)
            .executeScript(script, target);
    }

    private void saveScreenshot(Path destination) throws Exception {
        Files.createDirectories(destination.getParent());
        Path source = ((TakesScreenshot) driver)
            .getScreenshotAs(OutputType.FILE).toPath();
        Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
    }
}

The important assertion is still the product state, data-state="ordered" in this small example. The hit-test map is failure context, not a replacement for the outcome assertion.

The helper deliberately uses the first item from getClientRects(), not only the union returned by getBoundingClientRect(). An inline link that wraps across two lines can have several painted boxes with empty space inside the union. WebDriver defines interactability from the first client rectangle, so evidence based only on the union can inspect a point the remote end never intended to click.

CSS transforms and browser zoom can also produce fractional rectangle values. Flooring the in-view center follows the WebDriver calculation and gives elementFromPoint() stable viewport coordinates. Record the original rectangle as well as the integer point; otherwise a one-pixel overlap at a responsive breakpoint looks like a random driver failure.

Read the evidence before choosing a wait

Start with the exception text. Chromium-based drivers often identify both the target and the element that would receive the click. Treat that text as a lead, then confirm it with the screenshot and same-moment DOM evidence because messages differ across browsers.

Interpret the fields together:

  • hasClientRect: false is not normal interception evidence. It points toward an unrendered element, a hidden clone, or a detached layout box.
  • A positive rectangle with topmost equal to the target or its descendant means the obstruction moved before evidence capture. Shorten the gap or instrument the page state around the click.
  • A modal backdrop, cookie banner, spinner, toast, or sticky navigation element at topmost gives you a concrete state to wait for or dismiss.
  • A completely unrelated button or hidden template suggests the locator selected the wrong instance.
  • A point at the viewport edge suggests clipping or scroll behavior rather than a full-page overlay.

Run the focused test with stack traces and keep its artifacts separate from retries. In a Maven project, a command such as this preserves the original failing method:

Shell
mvn -Dtest=ClickInterceptionEvidenceTest#identifiesTheElementAtTheClickPoint \
  -DtrimStackTrace=false test

Do not diagnose from a passing retry's screenshot. Animations and transient masks may be gone by then. Name artifacts with the test ID and attempt number so the exception, DOM hit test, and image describe one moment.

Frames are another evidence boundary. document.elementFromPoint() runs in the current browsing context. If Selenium switched into an iframe to find the button, execute the script there as the example does. A top-level script sees the iframe element, not the internal button or its internal blocker.

Fix the obstruction, not the click command

When a known loading mask owns the point, wait for that mask to become invisible or absent. This is stronger than waiting for the button alone because it names the state that blocks user input. Its cost is coupling: a mask selector becomes part of the automation contract and must change when the UI implementation changes.

For a sticky header, scroll the target to a position where its center is uncovered, then keep the normal WebDriver click. Centering with scrollIntoView({block: 'center'}) can help, but it is a browser script and may not reflect how the application itself scrolls nested containers. Prefer a product action that exposes the control naturally when one exists.

For an animation, wait on the state transition that the animation represents, such as a drawer receiving its open state and the backdrop being removed. A fixed sleep pays the full delay on every run and still fails when CI is slower. Disabling all animation in test environments makes runs faster, but it also removes behavior users experience and can conceal focus, layering, and timing defects.

For a wrong locator, make it identify the active component. Waiting longer on the first matching hidden or covered element cannot turn it into the intended element. Scoping a button under the open dialog or active panel costs some selector maintenance, but it makes ownership explicit.

A reusable hit-test wait can be useful for a component library, but it has two limits. It adds JavaScript to every poll, and the page can change after the successful poll but before click() reaches the browser. Keep the captured blocker in timeout diagnostics and set a short, deliberate polling interval rather than treating the helper as a universal cure.

Expect a race between observation and action

Every check followed by a click has a small time-of-check to time-of-use gap. An advertisement, toast, or route transition can cover the point after a wait succeeds. Retrying the click without recording the first blocker may reduce failures while hiding a product race.

If a narrowly scoped retry is justified, retry the complete state observation and normal click, and retain evidence for every failed attempt. Cap it tightly. A click with side effects might have reached the application even if the client later saw a transport error, so generic click retries can create duplicate orders or submissions.

Cross-browser comparison is valuable when evidence says the target owns its center but one driver still reports interception. Use the same viewport and page state. Differences in font metrics, scrollbars, and rendering can move the in-view center, so a result from one browser does not automatically disprove another.

Compare the coordinates in the driver message when it provides them. If they differ from the helper's point, the layout moved between the click and the script, or the browser used a different client rectangle. That discrepancy is useful evidence itself. Capture a monotonic timestamp around both operations and keep transition or animation state in the hit-test map when the component exposes it.

When bypassing hit testing is the wrong move

Do not replace element.click() with executeScript("arguments[0].click()", element) merely to make the exception disappear. That invokes DOM activation without proving a user can reach the control. It can click through consent dialogs, validation layers, and loading masks, turning a genuine usability defect into a passing test.

Coordinate clicks through the Actions API are no better when chosen blindly. They can hit whatever happens to occupy the coordinate and make the test less connected to the intended element. Use pointer actions only when the gesture itself, such as drag, hover, or canvas interaction, is the behavior under test.

Do not remove overlays or rewrite z-index from the test. That mutates the page before the assertion and prevents the test from evaluating production behavior. If the overlay is stuck, the correct result may be a product failure.

Finally, do not add a long wait when the blocker is a required modal. The test must accept, dismiss, or complete that workflow just as a user would. Hit-test evidence is useful because it tells you which of those choices matches the page, instead of rewarding the first workaround that produces a green report.

// FIELD DISPATCH

Get the QA Field Notes

Weekly QA battles, AI testing guides, and interview drills. Free on Substack.

// LIVE COURSE / THE TESTING ACADEMY

Playwright Automation Mastery

Go beyond Selenium. Master Playwright with JS/TS in 90 days.

From the instructor behind this guide.

Playwright jobs are growing 8x faster than Selenium. 90 days / 75+ live hrs / Tue-Thu-Sat 7 AM IST.

Code PROMODE / 10% offJoin the batch

The Testing Academy editorial desk

Practical QA guidance built around test evidence, production tradeoffs, and interview-ready explanations.

Published July 25, 2026 / Reviewed August 4, 2026

PRIMARY REFERENCES

Verify the details at the source

QABattle guides are practical explanations. Product behavior, standards, and APIs can change, so use these primary references for the canonical details.

  1. 01
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  2. 02
    Official selenium.dev reference

    selenium.dev

    Primary documentation selected and verified for the claims in this guide.

  3. 03
    Official w3.org reference

    w3.org

    Primary documentation selected and verified for the claims in this guide.

  4. 04
    Official developer.mozilla.org reference

    developer.mozilla.org

    Primary documentation selected and verified for the claims in this guide.

FAQ / QUICK ANSWERS

Questions testers ask

Why does Selenium say another element would receive the click?

WebDriver hit-tested the target's in-view center and found a different painted element on top. Common causes include modal backdrops, sticky headers, loading masks, and animations that have not finished.

How do I find the element blocking a Selenium click?

Capture the target's client rectangle and call document.elementFromPoint() at its in-view center before the page changes. Save a screenshot and the returned element's short HTML description under the same test attempt.

Will elementToBeClickable prevent ElementClickInterceptedException?

Not in every case. The condition checks visibility and enabled state, but a separate overlay can still own the click point, or it can move into place between the wait and the click.

Is JavaScript click a valid fix for an intercepted Selenium click?

Usually it is evidence destruction, not a fix. JavaScript can invoke the handler through a modal or loading mask that would stop a user, so reserve it for a product contract that explicitly calls for programmatic activation.

What evidence should CI keep for an intercepted click?

Retain the exception text, target locator, click coordinates, target rectangle, topmost node, viewport size, and screenshot. Those records distinguish a genuine obstruction from a wrong locator, zero-size element, or stale reference.