From f372df21760174e8cdfa6c594c983d20797bd5a2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Sautter?= <joerg.sautter@gmx.net>
Date: Sun, 20 Jul 2025 15:49:22 +0200
Subject: [PATCH 1/3] [atoms] create the new is-pointer-reachable atom

---
 Rakefile                                      |  3 +++
 .../org/openqa/selenium/remote/BUILD.bazel    |  7 +++++++
 javascript/atoms/dom.js                       |  9 +++++++++
 javascript/atoms/fragments/BUILD.bazel        | 19 +++++++++++++++++++
 4 files changed, 38 insertions(+)

diff --git a/Rakefile b/Rakefile
index 3f7f80059c6ed..244d61b263010 100644
--- a/Rakefile
+++ b/Rakefile
@@ -461,6 +461,7 @@ namespace :node do
   atom_list = %w[
     //javascript/atoms/fragments:find-elements
     //javascript/atoms/fragments:is-displayed
+    //javascript/atoms/fragments:is-pointer-reachable
     //javascript/webdriver/atoms:get-attribute
   ]
 
@@ -469,6 +470,7 @@ namespace :node do
     mkdir_p base_dir
 
     ['bazel-bin/javascript/atoms/fragments/is-displayed.js',
+     'bazel-bin/javascript/webdriver/atoms/is-pointer-reachable.js',
      'bazel-bin/javascript/webdriver/atoms/get-attribute.js',
      'bazel-bin/javascript/atoms/fragments/find-elements.js'].each do |atom|
       name = File.basename(atom)
@@ -733,6 +735,7 @@ namespace :rb do
     {
       '//javascript/atoms/fragments:find-elements': 'findElements.js',
       '//javascript/atoms/fragments:is-displayed': 'isDisplayed.js',
+      '//javascript/atoms/fragments:is-pointer-reachable': 'isPointerReachable.js',
       '//javascript/webdriver/atoms:get-attribute': 'getAttribute.js'
     }.each do |target, name|
       puts "Generating #{target} as #{name}"
diff --git a/java/src/org/openqa/selenium/remote/BUILD.bazel b/java/src/org/openqa/selenium/remote/BUILD.bazel
index e1bac58daa94f..d1d160c3bc7f9 100644
--- a/java/src/org/openqa/selenium/remote/BUILD.bazel
+++ b/java/src/org/openqa/selenium/remote/BUILD.bazel
@@ -52,6 +52,7 @@ java_library(
         ":bidi-mutation-listener",
         ":get-attribute",
         ":is-displayed",
+        ":is-pointer-reachable",
     ],
     visibility = [
         "//java/src/org/openqa/selenium/bidi:__pkg__",
@@ -94,6 +95,12 @@ copy_file(
     out = "isDisplayed.js",
 )
 
+copy_file(
+    name = "is-pointer-reachable",
+    src = "//javascript/atoms/fragments:is-pointer-reachable.js",
+    out = "isPointerReachable.js",
+)
+
 copy_file(
     name = "bidi-mutation-listener",
     src = "//javascript/bidi-support:bidi-mutation-listener.js",
diff --git a/javascript/atoms/dom.js b/javascript/atoms/dom.js
index d9eef5fb7b500..0a9afe68db63a 100644
--- a/javascript/atoms/dom.js
+++ b/javascript/atoms/dom.js
@@ -616,6 +616,15 @@ bot.dom.isShown = function (elem, opt_ignoreOpacity) {
   return bot.dom.isShown_(elem, !!opt_ignoreOpacity, displayed);
 };
 
+/**
+ * @param {!Element} elem The element to check.
+ * @param {boolean} scrollToElement Scroll to the element before checking it.
+ * @return {?Object} An object describing whether the element is partially hidden
+ *      as a JSON string.
+ */
+bot.dom.isPointerReachable = function (elem, scrollToElement) {
+    return {};
+};
 
 /**
  * The kind of overflow area in which an element may be located. NONE if it does
diff --git a/javascript/atoms/fragments/BUILD.bazel b/javascript/atoms/fragments/BUILD.bazel
index cf18aed671e80..511ab79364637 100644
--- a/javascript/atoms/fragments/BUILD.bazel
+++ b/javascript/atoms/fragments/BUILD.bazel
@@ -130,6 +130,25 @@ closure_fragment(
     ],
 )
 
+
+closure_fragment(
+    name = "is-pointer-reachable",
+    function = "bot.dom.isPointerReachable",
+    module = "bot.dom",
+    visibility = [
+        "//dotnet/src/webdriver:__pkg__",
+        "//java/src/org/openqa/selenium/remote:__pkg__",
+        "//javascript/chrome-driver:__pkg__",
+        "//javascript/ie-driver:__pkg__",
+        "//javascript/selenium-webdriver/lib/atoms:__pkg__",
+        "//py:__pkg__",
+        "//rb/lib/selenium/webdriver/atoms:__pkg__",
+    ],
+    deps = [
+        "//javascript/atoms:dom",
+    ],
+)
+
 closure_fragment(
     name = "is-editable",
     function = "bot.dom.isEditable",

From d3aec767bb27605c14a41b5dc6f51de1dbe87e4b Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Sautter?= <joerg.sautter@gmx.net>
Date: Sun, 20 Jul 2025 16:09:58 +0200
Subject: [PATCH 2/3] [java] make the is-pointer-reachable atom callable

---
 java/src/org/openqa/selenium/Position.java    | 30 +++++++++++++++++++
 java/src/org/openqa/selenium/WebElement.java  | 14 +++++++++
 .../openqa/selenium/remote/DriverCommand.java |  6 ++++
 .../selenium/remote/RemoteWebElement.java     | 17 +++++++++++
 .../remote/codec/w3c/W3CHttpCommandCodec.java |  6 ++++
 .../openqa/selenium/WrappedWebElement.java    |  6 ++++
 .../selenium/remote/RemoteWebElementTest.java | 12 ++++++++
 .../selenium/testing/StaticResources.java     |  4 +++
 8 files changed, 95 insertions(+)
 create mode 100644 java/src/org/openqa/selenium/Position.java

diff --git a/java/src/org/openqa/selenium/Position.java b/java/src/org/openqa/selenium/Position.java
new file mode 100644
index 0000000000000..f8dd53dee40f9
--- /dev/null
+++ b/java/src/org/openqa/selenium/Position.java
@@ -0,0 +1,30 @@
+// Licensed to the Software Freedom Conservancy (SFC) under one
+// or more contributor license agreements.  See the NOTICE file
+// distributed with this work for additional information
+// regarding copyright ownership.  The SFC licenses this file
+// to you under the Apache License, Version 2.0 (the
+// "License"); you may not use this file except in compliance
+// with the License.  You may obtain a copy of the License at
+//
+//   http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing,
+// software distributed under the License is distributed on an
+// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+// KIND, either express or implied.  See the License for the
+// specific language governing permissions and limitations
+// under the License.
+
+package org.openqa.selenium;
+
+public enum Position {
+  UpperLeft,
+  UpperCenter,
+  UpperRight,
+  CenterLeft,
+  CenterCenter,
+  CenterRight,
+  LowerLeft,
+  LowerCenter,
+  LowerRight
+}
diff --git a/java/src/org/openqa/selenium/WebElement.java b/java/src/org/openqa/selenium/WebElement.java
index 8f9029ec2fd97..e9609fc358ce9 100644
--- a/java/src/org/openqa/selenium/WebElement.java
+++ b/java/src/org/openqa/selenium/WebElement.java
@@ -18,6 +18,7 @@
 package org.openqa.selenium;
 
 import java.util.List;
+import java.util.Map;
 import org.jspecify.annotations.NullMarked;
 import org.jspecify.annotations.Nullable;
 
@@ -285,6 +286,19 @@ default SearchContext getShadowRoot() {
    */
   boolean isDisplayed();
 
+  /**
+   * Is this element pointer reachable inside the current viewport or not? Will return null in case
+   * the element is bigger than the viewport or one of its scrollable parent containers. The check
+   * will check the upper left, upper center, upper right, center left, center center, center right,
+   * lower left, lower center, lower right points of the element for interactability. In case the
+   * element is hidden by one of its children, the check will still pass.
+   *
+   * @param scrollToElement scroll to the element before checking the state
+   * @return The overlapping elements of this element, in case the elements are not one of its
+   *     children or null in case the element is too big to reach all of its borders.
+   */
+  Map<Position, WebElement> isPointerReachable(boolean scrollToElement);
+
   /**
    * Where on the page is the top left-hand corner of the rendered element?
    *
diff --git a/java/src/org/openqa/selenium/remote/DriverCommand.java b/java/src/org/openqa/selenium/remote/DriverCommand.java
index fffb7cb3b9d70..0b7ba543dc307 100644
--- a/java/src/org/openqa/selenium/remote/DriverCommand.java
+++ b/java/src/org/openqa/selenium/remote/DriverCommand.java
@@ -83,6 +83,7 @@ public interface DriverCommand {
   String IS_ELEMENT_SELECTED = "isElementSelected";
   String IS_ELEMENT_ENABLED = "isElementEnabled";
   String IS_ELEMENT_DISPLAYED = "isElementDisplayed";
+  String IS_ELEMENT_POINTER_REACHABLE = "isElementPointerReachable";
   String GET_ELEMENT_RECT = "getElementRect";
   String GET_ELEMENT_LOCATION = "getElementLocation";
   String GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW = "getElementLocationOnceScrolledIntoView";
@@ -285,6 +286,11 @@ static CommandPayload IS_ELEMENT_DISPLAYED(String id) {
     return new CommandPayload(IS_ELEMENT_DISPLAYED, Map.of("id", id));
   }
 
+  static CommandPayload IS_ELEMENT_POINTER_REACHABLE(String id, boolean scrollToElement) {
+    return new CommandPayload(
+        IS_ELEMENT_POINTER_REACHABLE, Map.of("id", id, "scroll", scrollToElement));
+  }
+
   static CommandPayload GET_ELEMENT_RECT(String id) {
     return new CommandPayload(GET_ELEMENT_RECT, Map.of("id", id));
   }
diff --git a/java/src/org/openqa/selenium/remote/RemoteWebElement.java b/java/src/org/openqa/selenium/remote/RemoteWebElement.java
index 599f895fd42a0..904b246e1d1b3 100644
--- a/java/src/org/openqa/selenium/remote/RemoteWebElement.java
+++ b/java/src/org/openqa/selenium/remote/RemoteWebElement.java
@@ -32,6 +32,7 @@
 import org.openqa.selenium.JavascriptException;
 import org.openqa.selenium.OutputType;
 import org.openqa.selenium.Point;
+import org.openqa.selenium.Position;
 import org.openqa.selenium.Rectangle;
 import org.openqa.selenium.SearchContext;
 import org.openqa.selenium.TakesScreenshot;
@@ -291,6 +292,22 @@ public boolean isDisplayed() {
     }
   }
 
+  @Override
+  public Map<Position, WebElement> isPointerReachable(boolean scrollToElement) {
+    Object value =
+        execute(DriverCommand.IS_ELEMENT_POINTER_REACHABLE(id, scrollToElement)).getValue();
+    try {
+      if (value == null) {
+        return null;
+      }
+      return ((Map<String, WebElement>) value)
+          .entrySet().stream()
+              .collect(Collectors.toMap((e) -> Position.valueOf(e.getKey()), Map.Entry::getValue));
+    } catch (ClassCastException ex) {
+      throw new WebDriverException("Returned value cannot be converted to Map: " + value, ex);
+    }
+  }
+
   @Override
   @SuppressWarnings({"unchecked"})
   public Point getLocation() {
diff --git a/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java b/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java
index 970cd2035c6e4..a333c02bc5326 100644
--- a/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java
+++ b/java/src/org/openqa/selenium/remote/codec/w3c/W3CHttpCommandCodec.java
@@ -57,6 +57,7 @@
 import static org.openqa.selenium.remote.DriverCommand.GET_SESSION_STORAGE_SIZE;
 import static org.openqa.selenium.remote.DriverCommand.GET_WINDOW_HANDLES;
 import static org.openqa.selenium.remote.DriverCommand.IS_ELEMENT_DISPLAYED;
+import static org.openqa.selenium.remote.DriverCommand.IS_ELEMENT_POINTER_REACHABLE;
 import static org.openqa.selenium.remote.DriverCommand.MAXIMIZE_CURRENT_WINDOW;
 import static org.openqa.selenium.remote.DriverCommand.MINIMIZE_CURRENT_WINDOW;
 import static org.openqa.selenium.remote.DriverCommand.PRINT_PAGE;
@@ -111,6 +112,7 @@ public W3CHttpCommandCodec() {
     alias(GET_ELEMENT_LOCATION_ONCE_SCROLLED_INTO_VIEW, EXECUTE_SCRIPT);
     alias(GET_ELEMENT_SIZE, GET_ELEMENT_RECT);
     alias(IS_ELEMENT_DISPLAYED, EXECUTE_SCRIPT);
+    alias(IS_ELEMENT_POINTER_REACHABLE, EXECUTE_SCRIPT);
     alias(SUBMIT_ELEMENT, EXECUTE_SCRIPT);
 
     defineCommand(EXECUTE_SCRIPT, post(sessionId + "/execute/sync"));
@@ -269,6 +271,10 @@ public W3CHttpCommandCodec() {
       case IS_ELEMENT_DISPLAYED:
         return executeAtom("isDisplayed.js", asElement(parameters.get("id")));
 
+      case IS_ELEMENT_POINTER_REACHABLE:
+        return executeAtom(
+            "isPointerReachable.js", asElement(parameters.get("id")), parameters.get("scroll"));
+
       case SEND_KEYS_TO_ELEMENT:
         // When converted from JSON, this is a list, not an array
         Object rawValue = parameters.get("value");
diff --git a/java/test/org/openqa/selenium/WrappedWebElement.java b/java/test/org/openqa/selenium/WrappedWebElement.java
index 1e568bf98d079..e838be8e8d1bd 100644
--- a/java/test/org/openqa/selenium/WrappedWebElement.java
+++ b/java/test/org/openqa/selenium/WrappedWebElement.java
@@ -18,6 +18,7 @@
 package org.openqa.selenium;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * A WebElement that wraps another WebElement, for purposes of testing that JSON converters
@@ -96,6 +97,11 @@ public boolean isDisplayed() {
     throw new UnsupportedOperationException();
   }
 
+  @Override
+  public Map<Position, WebElement> isPointerReachable(boolean scrollToElement) {
+    throw new UnsupportedOperationException();
+  }
+
   @Override
   public Point getLocation() {
     throw new UnsupportedOperationException();
diff --git a/java/test/org/openqa/selenium/remote/RemoteWebElementTest.java b/java/test/org/openqa/selenium/remote/RemoteWebElementTest.java
index d593c44293c4a..720a1f444aa98 100644
--- a/java/test/org/openqa/selenium/remote/RemoteWebElementTest.java
+++ b/java/test/org/openqa/selenium/remote/RemoteWebElementTest.java
@@ -288,6 +288,18 @@ void canHandleIsDisplayedCommand() {
             DriverCommand.IS_ELEMENT_DISPLAYED, ImmutableMap.of("id", fixture.element.getId())));
   }
 
+  @Test
+  void canHandleIsPointerReachableCommand() {
+    WebElementFixture fixture = new WebElementFixture(echoCapabilities, valueResponder(null));
+
+    assertThat(fixture.element.isPointerReachable(true)).isNull();
+
+    fixture.verifyCommands(
+        new CommandPayload(
+            DriverCommand.IS_ELEMENT_POINTER_REACHABLE,
+            ImmutableMap.of("id", fixture.element.getId(), "scroll", true)));
+  }
+
   @Test
   void canHandleGeTextCommand() {
     WebElementFixture fixture = new WebElementFixture(echoCapabilities, valueResponder("test"));
diff --git a/java/test/org/openqa/selenium/testing/StaticResources.java b/java/test/org/openqa/selenium/testing/StaticResources.java
index 03b46fbe4f4bf..773c647005ff6 100644
--- a/java/test/org/openqa/selenium/testing/StaticResources.java
+++ b/java/test/org/openqa/selenium/testing/StaticResources.java
@@ -51,6 +51,10 @@ static void ensureAvailable() {
     // W3C emulation
     bazel.build("//javascript/atoms/fragments:is-displayed");
     copy("javascript/atoms/fragments/is-displayed.js", "org/openqa/selenium/remote/isDisplayed.js");
+    bazel.build("//javascript/atoms/fragments:is-pointer-reachable");
+    copy(
+        "javascript/atoms/fragments/is-pointer-reachable.js",
+        "org/openqa/selenium/remote/isPointerReachable.js");
     bazel.build("//javascript/webdriver/atoms:get-attribute");
     copy(
         "javascript/webdriver/atoms/get-attribute.js",

From 1aaa04814dd3f489545616cb99fd27fbb1fabec4 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?J=C3=B6rg=20Sautter?= <joerg.sautter@gmx.net>
Date: Sun, 20 Jul 2025 16:37:10 +0200
Subject: [PATCH 3/3] [atom] a first draft of the atom

---
 javascript/atoms/dom.js | 58 ++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 57 insertions(+), 1 deletion(-)

diff --git a/javascript/atoms/dom.js b/javascript/atoms/dom.js
index 0a9afe68db63a..7fed5ca8bc376 100644
--- a/javascript/atoms/dom.js
+++ b/javascript/atoms/dom.js
@@ -623,7 +623,63 @@ bot.dom.isShown = function (elem, opt_ignoreOpacity) {
  *      as a JSON string.
  */
 bot.dom.isPointerReachable = function (elem, scrollToElement) {
-    return {};
+    if (scrollToElement) {
+      // use center to avoid issues with overlapping fixed elements like header or footers
+      elem.scrollIntoView({
+        "behavior": "instant",
+        "block": "center",
+        "inline": "center"
+      });
+    }
+
+    function checkCoordinates(doc, x, y) {
+      // ensure the point is inside the viewport
+      if (doc.elementFromPoint(x, y) === null) {
+        return null;
+      }
+
+      // for chrome / edge the doc.elementFromPoint() does not allways return doc.elementsFromPoint()[0], perhaps a broken fastpath?
+      var elements = doc.elementsFromPoint(x, y);
+
+      if (elements.length == 0) {
+        return true;
+      }
+
+      var e = elements[0];
+
+      // check any of the parents is the element
+      while (e) {
+        if (elem === e) {
+          return true;
+        }
+        e = e.parentElement;
+      }
+
+      return elements[0];
+    }
+
+    var box = elem.getBoundingClientRect();
+
+    // +1 / -1 to avoid issues due to floating numbers of the box
+    var result = Object.entries({
+      "UpperLeft": checkCoordinates(elem.ownerDocument, Math.floor(box.left + 1), Math.floor(box.top + 1)),
+      "UpperCenter": checkCoordinates(elem.ownerDocument, Math.floor(box.left + box.width / 2 + 1), Math.floor(box.top + 1)),
+      "UpperRight": checkCoordinates(elem.ownerDocument, Math.floor(box.right - 1), Math.floor(box.top + 1)),
+      "CenterLeft": checkCoordinates(elem.ownerDocument, Math.floor(box.left + 1), Math.floor(box.top + box.height / 2 + 1)),
+      "CenterCenter": checkCoordinates(elem.ownerDocument, Math.floor(box.left + box.width / 2 + 1), Math.floor(box.top + box.height / 2 + 1)),
+      "CenterRight": checkCoordinates(elem.ownerDocument, Math.floor(box.right - 1), Math.floor(box.top + box.height / 2 + 1)),
+      "LowerLeft": checkCoordinates(elem.ownerDocument, Math.floor(box.left + 1), Math.floor(box.bottom - 1)),
+      "LowerCenter": checkCoordinates(elem.ownerDocument, Math.floor(box.left + box.width / 2 + 1), Math.floor(box.bottom - 1)),
+      "LowerRight": checkCoordinates(elem.ownerDocument, Math.floor(box.right - 1), Math.floor(box.bottom - 1)),
+    });
+
+    // remove all positions where we detected nothing
+    return result.reduce((filtered, [key, value]) => {
+      if (filtered !== true && value !== null) {
+        filtered[key] = value;
+      }
+      return filtered;
+    }, {});
 };
 
 /**
