diff --git a/doc/01-user-guide.adoc b/doc/01-user-guide.adoc
index e096f9e..950592f 100644
--- a/doc/01-user-guide.adoc
+++ b/doc/01-user-guide.adoc
@@ -919,28 +919,28 @@ You can test whether an element is a shadow root host using `has-shadow-root?` a
;; => false
----
-Now that you know how to retrieve the shadow root, you can query elements in the shadow DOM using `query-shadow-root`, `query-all-shadow-root`, `query-shadow-root-el`, and `query-all-shadow-root-el`.
+Now that you know how to retrieve the shadow root, you can query elements in the shadow DOM using `query-from-shadow-root`, `query-all-from-shadow-root`, `query-from-shadow-root-el`, and `query-all-from-shadow-root-el`.
-For `query-shadow-root` and `query-all-shadow-root`, the `q` parameter specifies a query of the _normal_ DOM to find the shadow root host.
+For `query-from-shadow-root` and `query-all-from-shadow-root`, the `q` parameter specifies a query of the _normal_ DOM to find the shadow root host.
If the host is identified, the `shadow-q` parameter is a query that is executed within the shadow DOM rooted at the shadow root host.
-The `query-shadow-root-el` and `query-all-shadow-root-el` allow you to specify the shadow root host element directly, rather than querying for it.
+The `query-from-shadow-root-el` and `query-all-from-shadow-root-el` allow you to specify the shadow root host element directly, rather than querying for it.
[source,clojure]
----
-(def in-shadow (e/query-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"}))
+(def in-shadow (e/query-from-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"}))
(e/get-element-text-el driver in-shadow)
;; => "I'm in the shadow DOM"
-(->> (e/query-all-shadow-root driver {:id "shadow-root-host"} {:css "span"})
+(->> (e/query-all-from-shadow-root driver {:id "shadow-root-host"} {:css "span"})
(map #(e/get-element-text-el driver %)))
;; => ("I'm in the shadow DOM" "I'm also in the shadow DOM")
(def shadow-root (e/get-element-shadow-root-el driver host))
-(e/get-element-text-el driver (e/query-shadow-root-el driver shadow-root {:css "#in-shadow"}))
+(e/get-element-text-el driver (e/query-from-shadow-root-el driver shadow-root {:css "#in-shadow"}))
;; => "I'm in the shadow DOM"
-(->> (e/query-all-shadow-root-el driver shadow-root {:css "span"})
+(->> (e/query-all-from-shadow-root-el driver shadow-root {:css "span"})
(map #(e/get-element-text-el driver %)))
;; > ("I'm in the shadow DOM" "I'm also in the shadow DOM")
----
diff --git a/env/test/resources/static/test.html b/env/test/resources/static/test.html
index 43b86b4..a310cf8 100644
--- a/env/test/resources/static/test.html
+++ b/env/test/resources/static/test.html
@@ -254,6 +254,18 @@
Shadow DOM
I'm in the shadow DOM
I'm also in the shadow DOM
+
+ Level 1 text.
+
+ Level 2 text.
+
+ Level 3 text.
+
+
+ 123
+
+
+
diff --git a/src/etaoin/api.clj b/src/etaoin/api.clj
index a4a1c84..0a12a12 100644
--- a/src/etaoin/api.clj
+++ b/src/etaoin/api.clj
@@ -24,7 +24,7 @@
**Querying/Selecting DOM Elements**
- [[query]] [[query-all]] [[query-tree]]
- - [[query-shadow-root]] [[query-shadow-root-el]] [[query-all-shadow-root]] [[query-all-shadow-root-el]]
+ - [[query-from-shadow-root]] [[query-from-shadow-root-el]] [[query-all-from-shadow-root]] [[query-all-from-shadow-root-el]]
- [[has-shadow-root?]] [[has-shadow-root-el?]]
- [[exists?]] [[absent?]]
- [[displayed?]] [[displayed-el?]] [[enabled?]] [[enabled-el?]] [[disabled?]] [[invisible?]] [[visible?]]
@@ -196,6 +196,12 @@
;; assume same idea as chrome (TBD)
:capabilities {:ms:edgeOptions {:w3c true}}}})
+;; Web Driver identifiers used as object type tags
+;; See: https://www.w3.org/TR/webdriver2/#elements
+(def ^:private shadow-root-identifier :shadow-6066-11e4-a52e-4f735466cecf)
+;; See: https://www.w3.org/TR/webdriver2/#shadow-root
+(def ^:private web-element-identifier :element-6066-11e4-a52e-4f735466cecf)
+
;;
;; utils
;;
@@ -211,6 +217,20 @@
(when (get-method feature (:type driver))
true))
+(defn- unwrap-webdriver-object
+ "Unwraps an object tagged with `identifier` from a Web Driver JSON object,
+ `web-driver-obj`. If `web-driver-obj` is not tagged with
+ `identifier` (i.e., the specified identifer is not present), throw
+ an exception."
+ [web-driver-obj identifier]
+ (let [obj (get web-driver-obj identifier ::not-found)]
+ (if (= obj ::not-found)
+ (throw (ex-info (str "Could not find object tagged with " identifier
+ " in " (str web-driver-obj))
+ {:web-driver-obj web-driver-obj
+ :identifier identifier}))
+ obj)))
+
;;
;; api
;;
@@ -545,6 +565,27 @@
:value
(mapv (comp second first))))
+(defn- follow-path-from-element*
+ "Starting at `el`, search for the first query in `path`, then from the
+ resulting element, search for the next, and so on. If `path` is
+ empty, returns `el`. A member of the `path` is limited to:
+
+ * a keyword (converted to an element ID)
+ * a string (converted to an XPath expression)
+ * a map using {:xpath ...} (converted to XPath)
+ * a map using {:css ...} (converted to CSS)
+ * a map following the Etaoin map syntax (converted to the driver default, typically XPath)
+
+ Things that are not supported as `path` elements:
+ * `query`'s `:active` keyword
+ * other sequences"
+ [driver el path]
+ (reduce (fn [el q]
+ (let [[loc term] (query/expand driver q)]
+ (find-element-from* driver el loc term)))
+ el
+ path))
+
;;
;; Querying elements (high-level API)
;;
@@ -1348,15 +1389,48 @@
[driver q]
(get-element-value-el driver (query driver q)))
+(defmulti ^:private get-element-shadow-root*
+ "Returns the shadow root element associated with the specified shadow
+ root host element, `el`, or `nil` if the specified element is not a
+ shadow root host."
+ dispatch-driver)
+
+(defmethod get-element-shadow-root*
+ :default
+ [driver el]
+ ;; Note that we're using get-element-property-el here, rather than
+ ;; executing a Web Driver Get Element Shadow Root API call. This is
+ ;; because the error handling for this API call is inconsistent
+ ;; across drivers whereas getting the property is consistent and
+ ;; probably not as brittle as drivers are updated.
+ ;;
+ ;; Specifically, if the element does not have a shadow root, then
+ ;; when executing a Get Element Shadow Root API call... (as of August 2024)
+ ;; * Firefox: throws 404
+ ;; * Safari: returns {:value nil}
+ ;; * Chrome: throws HTTP status 200, Web Driver status 65
+ ;; * Edge: throws HTTP 200, Web Driver status 65
+ ;;
+ ;; My guess is that Chrome and Edge are probably behaving correctly
+ ;; and Firefox and Safari are not.
+ ;;
+ ;; Perhaps update this at a later date when drivers better conform
+ ;; to the standard.
+ (when-let [root (get-element-property-el driver el "shadowRoot")]
+ (unwrap-webdriver-object root shadow-root-identifier)))
+
+(defmethod get-element-shadow-root*
+ :safari
+ [driver el]
+ ;; Safari gives us the shadow root in a non-standard wrapper
+ (when-let [root (get-element-property-el driver el "shadowRoot")]
+ (-> root first second)))
-;; TODO: Dev Doc: Why not: /session/{session id}/element/{element id}/shadow from w3c spec?
(defn get-element-shadow-root-el
"Returns the shadow root for the specified element or `nil` if the
element does not have a shadow root."
[driver el]
- (-> (get-element-property-el driver el "shadowRoot")
- first
- second))
+ (get-element-shadow-root* driver el))
(defn get-element-shadow-root
"Returns the shadow root for the first element matching the query, or
@@ -1378,8 +1452,7 @@
:path [:session (:session driver) :shadow shadow-root-el :element]
:data {:using locator :value term}})
:value
- first
- second))
+ (unwrap-webdriver-object web-element-identifier)))
(defn- find-elements-from-shadow-root*
[driver shadow-root-el locator term]
@@ -1389,9 +1462,9 @@
:path [:session (:session driver) :shadow shadow-root-el :elements]
:data {:using locator :value term}})
:value
- (mapv (comp second first))))
+ (mapv #(unwrap-webdriver-object % web-element-identifier))))
-(defn query-shadow-root-el
+(defn query-from-shadow-root-el
"Queries the shadow DOM rooted at `shadow-root-el`, looking for the
first element specified by `shadow-q`.
@@ -1399,12 +1472,17 @@
the [[query]] function, but some drivers may limit it to specific
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
+ Note that `shadow-q` does not support `query`'s `:active` keyword.
+
https://www.w3.org/TR/webdriver2/#dfn-find-element-from-shadow-root"
[driver shadow-root-el shadow-q]
- (let [[loc term] (query/expand driver shadow-q)]
- (find-element-from-shadow-root* driver shadow-root-el loc term)))
+ (if (sequential? shadow-q)
+ (let [q1-el (query-from-shadow-root-el driver shadow-root-el (first shadow-q))]
+ (follow-path-from-element* driver q1-el (next shadow-q)))
+ (let [[loc term] (query/expand driver shadow-q)]
+ (find-element-from-shadow-root* driver shadow-root-el loc term))))
-(defn query-all-shadow-root-el
+(defn query-all-from-shadow-root-el
"Queries the shadow DOM rooted at `shadow-root-el`, looking for all
elements specified by `shadow-q`.
@@ -1412,12 +1490,23 @@
the [[query]] function, but some drivers may limit it to specific
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
+ Note that `shadow-q` does not support `query`'s `:active` keyword.
+
https://www.w3.org/TR/webdriver2/#dfn-find-elements-from-shadow-root"
[driver shadow-root-el shadow-q]
- (let [[loc term] (query/expand driver shadow-q)]
- (find-elements-from-shadow-root* driver shadow-root-el loc term)))
-
-(defn query-shadow-root
+ (if (sequential? shadow-q)
+ (let [last-q (last shadow-q)
+ but-last-q (butlast shadow-q)]
+ (if-let [first-q (first but-last-q)]
+ (let [first-el (query-from-shadow-root-el driver shadow-root-el first-q)
+ but-last-el (follow-path-from-element* driver first-el (next but-last-q))
+ [loc term] (query/expand driver last-q)]
+ (find-elements-from* driver but-last-el loc term))
+ (query-all-from-shadow-root-el driver shadow-root-el last-q)))
+ (let [[loc term] (query/expand driver shadow-q)]
+ (find-elements-from-shadow-root* driver shadow-root-el loc term))))
+
+(defn query-from-shadow-root
"First, conducts a standard search (as if by [[query]]) for an element
with a shadow root. Then, from that shadow root element, conducts a
search of the shadow DOM for the first element matching `shadow-q`.
@@ -1426,11 +1515,12 @@
The `shadow-q` parameter is similar to the `q` parameter of
the [[query]] function, but some drivers may limit it to specific
- formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information."
+ formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
+ Note that `shadow-q` does not support `query`'s `:active` keyword."
[driver q shadow-q]
- (query-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
+ (query-from-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
-(defn query-all-shadow-root
+(defn query-all-from-shadow-root
"First, conducts a standard search (as if by [[query]]) for an element
with a shadow root. Then, from that shadow root element, conducts a
search of the shadow DOM for all elements matching `shadow-q`.
@@ -1439,9 +1529,10 @@
The `shadow-q` parameter is similar to the `q` parameter of
the [[query]] function, but some drivers may limit it to specific
- formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information."
+ formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
+ Note that `shadow-q` does not support `query`'s `:active` keyword."
[driver q shadow-q]
- (query-all-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
+ (query-all-from-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
;;
;; cookies
@@ -1543,7 +1634,7 @@
(defn el->ref
"Return map representing an element reference for WebDriver.
- The magic `:element-` constant in source is taken from the [WebDriver Spec](https://www.w3.org/TR/webdriver/#elements).
+ The magic `:element-` constant in source is taken from the [WebDriver Spec](https://www.w3.org/TR/webdriver2/#elements).
Passing the element reference map to `js-execute` automatically expands it
into a DOM node. For example:
@@ -1556,8 +1647,8 @@
(js-execute driver \"arguments[0].scrollIntoView()\", (el->ref el))
```"
[el]
- {:ELEMENT el
- :element-6066-11e4-a52e-4f735466cecf el})
+ {:ELEMENT el
+ web-element-identifier el})
(defn js-execute
"Return result of `driver` executing Javascript `script` with `args` synchronously in the browser.
diff --git a/test/etaoin/api_test.clj b/test/etaoin/api_test.clj
index 361098f..8c96945 100644
--- a/test/etaoin/api_test.clj
+++ b/test/etaoin/api_test.clj
@@ -918,32 +918,92 @@
(e/query *driver* {:id "shadow-root-host"})))))
(testing "whether an element has a shadow root"
(is (e/has-shadow-root? *driver* {:id "shadow-root-host"}))
- (is (e/has-shadow-root-el? *driver* (e/query *driver* {:id "shadow-root-host"}))))
+ (is (e/has-shadow-root-el? *driver* (e/query *driver* {:id "shadow-root-host"})))
+ (is (not (e/has-shadow-root? *driver* :not-in-shadow)))
+ (is (not (e/has-shadow-root-el? *driver* (e/query *driver* :not-in-shadow)))))
(let [shadow-root (e/get-element-shadow-root *driver* {:id "shadow-root-host"})]
(testing "querying the shadow root element for a single element"
(is (= "I'm in the shadow DOM"
- (->> (e/query-shadow-root-el *driver*
- shadow-root
- {:css "#in-shadow"})
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ {:css "#in-shadow"})
(e/get-element-text-el *driver*))))
(is (= "I'm also in the shadow DOM"
- (->> (e/query-shadow-root-el *driver*
- shadow-root
- {:css "#also-in-shadow"})
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ {:css "#also-in-shadow"})
(e/get-element-text-el *driver*)))))
+ (testing "vector syntax for single element shadow root queries"
+ (is (thrown? Exception
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ [])
+ (e/get-element-text-el *driver*))))
+ (is (= "Level 3 text."
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-3"}])
+ (e/get-element-text-el *driver*)
+ str/trim)))
+ (is (= "Level 3 text."
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-2"}
+ {:css "#level-3"}])
+ (e/get-element-text-el *driver*)
+ str/trim)))
+ (is (= "Level 3 text."
+ (->> (e/query-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-1"}
+ {:css "#level-2"}
+ {:css "#level-3"}])
+ (e/get-element-text-el *driver*)
+ str/trim))))
(testing "querying the shadow root element for multiple elements"
- (is (= ["I'm in the shadow DOM" "I'm also in the shadow DOM"]
- (->> (e/query-all-shadow-root-el *driver*
- shadow-root
- {:css "span"})
- (mapv #(e/get-element-text-el *driver* %)))))))
+ (is (= ["I'm in the shadow DOM" "I'm also in the shadow DOM" "1" "2" "3"]
+ (->> (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ {:css "span"})
+ (mapv #(e/get-element-text-el *driver* %))))))
+ (testing "vector syntax for -all shadow root queries"
+ (is (thrown? Exception
+ (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ [])))
+ (is (= ["I'm in the shadow DOM" "I'm also in the shadow DOM" "1" "2" "3"]
+ (->> (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "span"}])
+ (map #(e/get-element-text-el *driver* %)))))
+ (is (= ["1" "2" "3"]
+ (->> (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-3-all"}
+ {:css "span"}])
+ (map #(e/get-element-text-el *driver* %)))))
+ (is (= ["1" "2" "3"]
+ (->> (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-2"}
+ {:css "#level-3-all"}
+ {:css "span"}])
+ (map #(e/get-element-text-el *driver* %)))))
+ (is (= ["1" "2" "3"]
+ (->> (e/query-all-from-shadow-root-el *driver*
+ shadow-root
+ [{:css "#level-1"}
+ {:css "#level-2"}
+ {:css "#level-3-all"}
+ {:css "span"}])
+ (map #(e/get-element-text-el *driver* %)))))))
(testing "querying the shadow root element"
(is (= "I'm in the shadow DOM"
- (->> (e/query-shadow-root *driver* {:id "shadow-root-host"} {:css "#in-shadow"})
+ (->> (e/query-from-shadow-root *driver* {:id "shadow-root-host"} {:css "#in-shadow"})
(e/get-element-text-el *driver*)))))
(testing "querying the shadow root element for multiple elements"
- (is (= ["I'm in the shadow DOM" "I'm also in the shadow DOM"]
- (->> (e/query-all-shadow-root *driver* {:id "shadow-root-host"} {:css "span"})
+ (is (= ["I'm in the shadow DOM" "I'm also in the shadow DOM" "1" "2" "3"]
+ (->> (e/query-all-from-shadow-root *driver* {:id "shadow-root-host"} {:css "span"})
(mapv #(e/get-element-text-el *driver* %)))))))
(deftest test-timeouts