Skip to content

Commit c2f04de

Browse files
dgrlread
andauthored
Issue 604 addendum (#619)
* Change shadow root query function names (#604) Update the query function names for shadow roots to be closer to the API names used in the Web Driver spec. * Rework get-element-shadow-root(-el) to use Web Driver APIs Shifts from using get-element-property-el to using a new get-element-shadow-root* based on the underlying Web Driver Get Element Shadow Root API endpoint. * Use unwrap-webdriver-object for find-element(s)-from-shadow-root* Previous find-element(s)-from-shadow-root* functions used the first / second idiom to extract the value of the first pair in a map. This is brittle and may not work in the future for some reason. This is replaced with unwrap-webdriver-object looking for a specific item type tag in the map, as specified by the Web Driver spec. * Update get-element-shadow-root* to use get-element-property-el Originally, tried using Web Driver Get Element Shadow Root API call, but the error handling for this was too inconsistent across multiple browers. Using get-element-property-el works much more consistently and avoids having to deal with exception processing as much. * Update shadow-root functions to work with query's vector syntax * woops, missed committing something here! * address lint error from eastwood * Don't need `:ELEMENT` from chrome/edge anymore Now that we are in "w3c mode" for chrome and edge we do not need to special case them anymore. * Use new `web-element-identifier` constant Use this new constant to replace magic number in existing code. * Add date to comment on WebDriver behaviours To make it easy to see when this was last checked. --------- Co-authored-by: lread <[email protected]>
1 parent e58ff88 commit c2f04de

File tree

4 files changed

+209
-46
lines changed

4 files changed

+209
-46
lines changed

doc/01-user-guide.adoc

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -919,28 +919,28 @@ You can test whether an element is a shadow root host using `has-shadow-root?` a
919919
;; => false
920920
----
921921

922-
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`.
922+
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`.
923923

924-
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.
924+
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.
925925
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.
926926

927-
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.
927+
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.
928928

929929
[source,clojure]
930930
----
931-
(def in-shadow (e/query-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"}))
931+
(def in-shadow (e/query-from-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"}))
932932
(e/get-element-text-el driver in-shadow)
933933
;; => "I'm in the shadow DOM"
934934
935-
(->> (e/query-all-shadow-root driver {:id "shadow-root-host"} {:css "span"})
935+
(->> (e/query-all-from-shadow-root driver {:id "shadow-root-host"} {:css "span"})
936936
(map #(e/get-element-text-el driver %)))
937937
;; => ("I'm in the shadow DOM" "I'm also in the shadow DOM")
938938
939939
(def shadow-root (e/get-element-shadow-root-el driver host))
940-
(e/get-element-text-el driver (e/query-shadow-root-el driver shadow-root {:css "#in-shadow"}))
940+
(e/get-element-text-el driver (e/query-from-shadow-root-el driver shadow-root {:css "#in-shadow"}))
941941
;; => "I'm in the shadow DOM"
942942
943-
(->> (e/query-all-shadow-root-el driver shadow-root {:css "span"})
943+
(->> (e/query-all-from-shadow-root-el driver shadow-root {:css "span"})
944944
(map #(e/get-element-text-el driver %)))
945945
;; > ("I'm in the shadow DOM" "I'm also in the shadow DOM")
946946
----

env/test/resources/static/test.html

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,18 @@ <h3>Shadow DOM</h3>
254254
<template shadowrootmode="open">
255255
<span id="in-shadow">I'm in the shadow DOM</span>
256256
<span id="also-in-shadow">I'm also in the shadow DOM</span>
257+
<div id="level-1">
258+
Level 1 text.
259+
<div id="level-2">
260+
Level 2 text.
261+
<div id="level-3">
262+
Level 3 text.
263+
</div>
264+
<div id="level-3-all">
265+
<span>1</span><span>2</span><span>3</span>
266+
</div>
267+
</div>
268+
</div>
257269
</template>
258270
</div>
259271

src/etaoin/api.clj

Lines changed: 115 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@
2424
2525
**Querying/Selecting DOM Elements**
2626
- [[query]] [[query-all]] [[query-tree]]
27-
- [[query-shadow-root]] [[query-shadow-root-el]] [[query-all-shadow-root]] [[query-all-shadow-root-el]]
27+
- [[query-from-shadow-root]] [[query-from-shadow-root-el]] [[query-all-from-shadow-root]] [[query-all-from-shadow-root-el]]
2828
- [[has-shadow-root?]] [[has-shadow-root-el?]]
2929
- [[exists?]] [[absent?]]
3030
- [[displayed?]] [[displayed-el?]] [[enabled?]] [[enabled-el?]] [[disabled?]] [[invisible?]] [[visible?]]
@@ -196,6 +196,12 @@
196196
;; assume same idea as chrome (TBD)
197197
:capabilities {:ms:edgeOptions {:w3c true}}}})
198198

199+
;; Web Driver identifiers used as object type tags
200+
;; See: https://www.w3.org/TR/webdriver2/#elements
201+
(def ^:private shadow-root-identifier :shadow-6066-11e4-a52e-4f735466cecf)
202+
;; See: https://www.w3.org/TR/webdriver2/#shadow-root
203+
(def ^:private web-element-identifier :element-6066-11e4-a52e-4f735466cecf)
204+
199205
;;
200206
;; utils
201207
;;
@@ -211,6 +217,20 @@
211217
(when (get-method feature (:type driver))
212218
true))
213219

220+
(defn- unwrap-webdriver-object
221+
"Unwraps an object tagged with `identifier` from a Web Driver JSON object,
222+
`web-driver-obj`. If `web-driver-obj` is not tagged with
223+
`identifier` (i.e., the specified identifer is not present), throw
224+
an exception."
225+
[web-driver-obj identifier]
226+
(let [obj (get web-driver-obj identifier ::not-found)]
227+
(if (= obj ::not-found)
228+
(throw (ex-info (str "Could not find object tagged with " identifier
229+
" in " (str web-driver-obj))
230+
{:web-driver-obj web-driver-obj
231+
:identifier identifier}))
232+
obj)))
233+
214234
;;
215235
;; api
216236
;;
@@ -545,6 +565,27 @@
545565
:value
546566
(mapv (comp second first))))
547567

568+
(defn- follow-path-from-element*
569+
"Starting at `el`, search for the first query in `path`, then from the
570+
resulting element, search for the next, and so on. If `path` is
571+
empty, returns `el`. A member of the `path` is limited to:
572+
573+
* a keyword (converted to an element ID)
574+
* a string (converted to an XPath expression)
575+
* a map using {:xpath ...} (converted to XPath)
576+
* a map using {:css ...} (converted to CSS)
577+
* a map following the Etaoin map syntax (converted to the driver default, typically XPath)
578+
579+
Things that are not supported as `path` elements:
580+
* `query`'s `:active` keyword
581+
* other sequences"
582+
[driver el path]
583+
(reduce (fn [el q]
584+
(let [[loc term] (query/expand driver q)]
585+
(find-element-from* driver el loc term)))
586+
el
587+
path))
588+
548589
;;
549590
;; Querying elements (high-level API)
550591
;;
@@ -1348,15 +1389,48 @@
13481389
[driver q]
13491390
(get-element-value-el driver (query driver q)))
13501391

1392+
(defmulti ^:private get-element-shadow-root*
1393+
"Returns the shadow root element associated with the specified shadow
1394+
root host element, `el`, or `nil` if the specified element is not a
1395+
shadow root host."
1396+
dispatch-driver)
1397+
1398+
(defmethod get-element-shadow-root*
1399+
:default
1400+
[driver el]
1401+
;; Note that we're using get-element-property-el here, rather than
1402+
;; executing a Web Driver Get Element Shadow Root API call. This is
1403+
;; because the error handling for this API call is inconsistent
1404+
;; across drivers whereas getting the property is consistent and
1405+
;; probably not as brittle as drivers are updated.
1406+
;;
1407+
;; Specifically, if the element does not have a shadow root, then
1408+
;; when executing a Get Element Shadow Root API call... (as of August 2024)
1409+
;; * Firefox: throws 404
1410+
;; * Safari: returns {:value nil}
1411+
;; * Chrome: throws HTTP status 200, Web Driver status 65
1412+
;; * Edge: throws HTTP 200, Web Driver status 65
1413+
;;
1414+
;; My guess is that Chrome and Edge are probably behaving correctly
1415+
;; and Firefox and Safari are not.
1416+
;;
1417+
;; Perhaps update this at a later date when drivers better conform
1418+
;; to the standard.
1419+
(when-let [root (get-element-property-el driver el "shadowRoot")]
1420+
(unwrap-webdriver-object root shadow-root-identifier)))
1421+
1422+
(defmethod get-element-shadow-root*
1423+
:safari
1424+
[driver el]
1425+
;; Safari gives us the shadow root in a non-standard wrapper
1426+
(when-let [root (get-element-property-el driver el "shadowRoot")]
1427+
(-> root first second)))
13511428

1352-
;; TODO: Dev Doc: Why not: /session/{session id}/element/{element id}/shadow from w3c spec?
13531429
(defn get-element-shadow-root-el
13541430
"Returns the shadow root for the specified element or `nil` if the
13551431
element does not have a shadow root."
13561432
[driver el]
1357-
(-> (get-element-property-el driver el "shadowRoot")
1358-
first
1359-
second))
1433+
(get-element-shadow-root* driver el))
13601434

13611435
(defn get-element-shadow-root
13621436
"Returns the shadow root for the first element matching the query, or
@@ -1378,8 +1452,7 @@
13781452
:path [:session (:session driver) :shadow shadow-root-el :element]
13791453
:data {:using locator :value term}})
13801454
:value
1381-
first
1382-
second))
1455+
(unwrap-webdriver-object web-element-identifier)))
13831456

13841457
(defn- find-elements-from-shadow-root*
13851458
[driver shadow-root-el locator term]
@@ -1389,35 +1462,51 @@
13891462
:path [:session (:session driver) :shadow shadow-root-el :elements]
13901463
:data {:using locator :value term}})
13911464
:value
1392-
(mapv (comp second first))))
1465+
(mapv #(unwrap-webdriver-object % web-element-identifier))))
13931466

1394-
(defn query-shadow-root-el
1467+
(defn query-from-shadow-root-el
13951468
"Queries the shadow DOM rooted at `shadow-root-el`, looking for the
13961469
first element specified by `shadow-q`.
13971470
13981471
The `shadow-q` parameter is similar to the `q` parameter of
13991472
the [[query]] function, but some drivers may limit it to specific
14001473
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
14011474
1475+
Note that `shadow-q` does not support `query`'s `:active` keyword.
1476+
14021477
https://www.w3.org/TR/webdriver2/#dfn-find-element-from-shadow-root"
14031478
[driver shadow-root-el shadow-q]
1404-
(let [[loc term] (query/expand driver shadow-q)]
1405-
(find-element-from-shadow-root* driver shadow-root-el loc term)))
1479+
(if (sequential? shadow-q)
1480+
(let [q1-el (query-from-shadow-root-el driver shadow-root-el (first shadow-q))]
1481+
(follow-path-from-element* driver q1-el (next shadow-q)))
1482+
(let [[loc term] (query/expand driver shadow-q)]
1483+
(find-element-from-shadow-root* driver shadow-root-el loc term))))
14061484

1407-
(defn query-all-shadow-root-el
1485+
(defn query-all-from-shadow-root-el
14081486
"Queries the shadow DOM rooted at `shadow-root-el`, looking for all
14091487
elements specified by `shadow-q`.
14101488
14111489
The `shadow-q` parameter is similar to the `q` parameter of
14121490
the [[query]] function, but some drivers may limit it to specific
14131491
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
14141492
1493+
Note that `shadow-q` does not support `query`'s `:active` keyword.
1494+
14151495
https://www.w3.org/TR/webdriver2/#dfn-find-elements-from-shadow-root"
14161496
[driver shadow-root-el shadow-q]
1417-
(let [[loc term] (query/expand driver shadow-q)]
1418-
(find-elements-from-shadow-root* driver shadow-root-el loc term)))
1419-
1420-
(defn query-shadow-root
1497+
(if (sequential? shadow-q)
1498+
(let [last-q (last shadow-q)
1499+
but-last-q (butlast shadow-q)]
1500+
(if-let [first-q (first but-last-q)]
1501+
(let [first-el (query-from-shadow-root-el driver shadow-root-el first-q)
1502+
but-last-el (follow-path-from-element* driver first-el (next but-last-q))
1503+
[loc term] (query/expand driver last-q)]
1504+
(find-elements-from* driver but-last-el loc term))
1505+
(query-all-from-shadow-root-el driver shadow-root-el last-q)))
1506+
(let [[loc term] (query/expand driver shadow-q)]
1507+
(find-elements-from-shadow-root* driver shadow-root-el loc term))))
1508+
1509+
(defn query-from-shadow-root
14211510
"First, conducts a standard search (as if by [[query]]) for an element
14221511
with a shadow root. Then, from that shadow root element, conducts a
14231512
search of the shadow DOM for the first element matching `shadow-q`.
@@ -1426,11 +1515,12 @@
14261515
14271516
The `shadow-q` parameter is similar to the `q` parameter of
14281517
the [[query]] function, but some drivers may limit it to specific
1429-
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information."
1518+
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
1519+
Note that `shadow-q` does not support `query`'s `:active` keyword."
14301520
[driver q shadow-q]
1431-
(query-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
1521+
(query-from-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
14321522

1433-
(defn query-all-shadow-root
1523+
(defn query-all-from-shadow-root
14341524
"First, conducts a standard search (as if by [[query]]) for an element
14351525
with a shadow root. Then, from that shadow root element, conducts a
14361526
search of the shadow DOM for all elements matching `shadow-q`.
@@ -1439,9 +1529,10 @@
14391529
14401530
The `shadow-q` parameter is similar to the `q` parameter of
14411531
the [[query]] function, but some drivers may limit it to specific
1442-
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information."
1532+
formats (e.g., CSS). See [this note](/doc/01-user-guide.adoc#shadow-root-browser-limitations) for more information.
1533+
Note that `shadow-q` does not support `query`'s `:active` keyword."
14431534
[driver q shadow-q]
1444-
(query-all-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
1535+
(query-all-from-shadow-root-el driver (get-element-shadow-root driver q) shadow-q))
14451536

14461537
;;
14471538
;; cookies
@@ -1543,7 +1634,7 @@
15431634
(defn el->ref
15441635
"Return map representing an element reference for WebDriver.
15451636
1546-
The magic `:element-` constant in source is taken from the [WebDriver Spec](https://www.w3.org/TR/webdriver/#elements).
1637+
The magic `:element-` constant in source is taken from the [WebDriver Spec](https://www.w3.org/TR/webdriver2/#elements).
15471638
15481639
Passing the element reference map to `js-execute` automatically expands it
15491640
into a DOM node. For example:
@@ -1556,8 +1647,8 @@
15561647
(js-execute driver \"arguments[0].scrollIntoView()\", (el->ref el))
15571648
```"
15581649
[el]
1559-
{:ELEMENT el
1560-
:element-6066-11e4-a52e-4f735466cecf el})
1650+
{:ELEMENT el
1651+
web-element-identifier el})
15611652

15621653
(defn js-execute
15631654
"Return result of `driver` executing Javascript `script` with `args` synchronously in the browser.

0 commit comments

Comments
 (0)