Skip to content

Commit 963ce17

Browse files
committed
Merge remote-tracking branch 'origin/master' into lread-discover-safaridriver-logs
* origin/master: Ignore emacs backup files (#609) Add :fn/index as alias for :index in map syntax (#603) (#608) doc: thank Dave Roberts for contribution [skip ci] (#607) Add support for shadow DOM #604 (#606) doc: user-guide: add list of :fn/* (#605)
2 parents 7f31fea + c199428 commit 963ce17

File tree

9 files changed

+355
-12
lines changed

9 files changed

+355
-12
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pom.xml.asc
99
.hgignore
1010
.hg/
1111
.DS_Store
12+
*~
1213

1314
gh-pages
1415
node_modules
@@ -34,3 +35,4 @@ build.xml
3435
/fiddle/
3536
/.cljdoc-preview
3637
/.vscode
38+

CHANGELOG.adoc

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,18 @@ A release with an intentional breaking changes is marked with:
1212
* https://github.com/clj-commons/etaoin/issues/566[#566]: Recognize `:driver-log-level` for Edge
1313
* https://github.com/clj-commons/etaoin/issues/563[#563]: Support `"debug"` `:driver-log-level` for Safari
1414
* https://github.com/clj-commons/etaoin/issues/517[#517]: Properly cleanup after failed webdriver launch
15+
* https://github.com/clj-commons/etaoin/issues/604[#604]: Add support for shadow DOM
16+
(https://github.com/dgr[@dgr])
17+
* https://github.com/clj-commons/etaoin/issues/603[#603]: Add :fn/index as alias for :index in map syntax
18+
(https://github.com/dgr[@dgr])
1519
* bump all deps to current versions
1620
* tests
1721
** https://github.com/clj-commons/etaoin/issues/572[#572]: stop using chrome `--no-sandbox` option, it has become problematic on Windows (and we did not need it anyway)
1822
* docs
1923
** https://github.com/clj-commons/etaoin/issues/534[#534]: better describe `etaoin.api/select` and its alternatives
2024
** https://github.com/clj-commons/etaoin/issues/536[#536]: user guide examples are now all os agnostic and CI tested via test-doc-blocks on all supported OSes
25+
** https://github.com/clj-commons/etaoin/issues/602[#602]: Document all `:fn/*` query pseudo-functions in a definitive list
26+
(https://github.com/dgr[@dgr])
2127

2228
== v1.0.40
2329

README.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ Can be `alpha`, `beta`, `rc1`, etc.
102102
* https://github.com/verma[Uday Verma]
103103
* https://github.com/mjmeintjes[Matt Meintjes]
104104
* https://github.com/tupini07[Andrea Tupini]
105+
* https://github.com/dgr[Dave Roberts]
105106

106107
=== Current Maintainers
107108

doc/01-user-guide.adoc

Lines changed: 150 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -564,12 +564,35 @@ The rules are:
564564

565565
* A `:tag` key represents a tag's name.
566566
Defaults to `*`.
567-
* An `:index` key expands into the trailing XPath `[x]` clause.
568-
Useful when you need to select a third row from a table, for example.
569567
* Any non-special key represents an attribute and its value.
570568
* `:fn/` is a prefix followed by a supported query function.
571569

572-
Examples:
570+
There are several query functions of the form `:fn/*`.
571+
Each query function takes a parameter which is the value associated with the query function keyword in the map.
572+
573+
* `:fn/index`: Takes an positive integer parameter.
574+
This expands into a trailing XPath `[x]` clause and is useful when you need to select a specific row in a table, for example.
575+
* `:fn/text`: Takes a string parameter. Matches if the element has the exact text specified.
576+
* `:fn/has-text`: Takes a string parameter.
577+
Matches if the element includes the specified text.
578+
* `:fn/has-string`: Takes a string parameter.
579+
Matches if the element string contains the specified string.
580+
The difference between `:fn/has-text` and `:fn/has-string` is the difference between the XPath `text()` and `string()` functions (`text()` is the text within a given element and `string()` is the text of all descendant elements concatenated together in document order).
581+
Generally, if you're targeting an element at the top of the hierarchy, you probably want `:fn/has-string`, and if you're targeting a single element at the bottom of the hierarchy, you probably want to use `:fn/has-text`.
582+
* `:fn/has-class`: Takes a string parameter.
583+
Matches if the element's `class` attribute includes the string. Unlike using a `:class` key in the map, `:fn/has-class` can match single classes, whereas `:class` is an exact match of the whole class string.
584+
* `:fn/has-classes`: Takes a vector of strings parameter.
585+
Matches if the element's `class` attribute includes _all_ of the specified class strings.
586+
* `:fn/link`: Takes a string parameter.
587+
Matches if the element's `href` attribute contains the specified string.
588+
* `:fn/enabled`: Takes a boolean (`true` or `false`) parameter.
589+
If the parameter is `true`, matches if the element is enabled.
590+
If the parameter is `false`, matches if the element is disabled.
591+
* `:fn/disabled`: Takes a boolean (`true` or `false`) parameter.
592+
If the parameter is `true`, matches if the element is disabled.
593+
If the parameter is `true`, matches if the element is enabled.
594+
595+
Here are some examples of the map syntax:
573596

574597
* find the first `div` tag
575598
+
@@ -585,7 +608,7 @@ Examples:
585608
+
586609
[source,clojure]
587610
----
588-
(= (e/query driver {:tag :div :index 1})
611+
(= (e/query driver {:tag :div :fn/index 1})
589612
;; equivalent via xpath:
590613
(e/query driver ".//div[1]"))
591614
;; => true
@@ -622,7 +645,7 @@ Examples:
622645
+
623646
[source,clojure]
624647
----
625-
(e/get-element-text driver {:fn/has-text "blarg" :index 3})
648+
(e/get-element-text driver {:fn/has-text "blarg" :fn/index 3})
626649
;; => "blarg in a p"
627650
628651
;; equivalent in xpath:
@@ -742,11 +765,11 @@ Maybe you want to click on the second link within:
742765
</ul>
743766
----
744767

745-
You can use the `:index` like so:
768+
You can use the `:fn/index` like so:
746769

747770
[source,clojure]
748771
----
749-
(e/click driver [{:tag :li :class :search-result :index 2} {:tag :a}])
772+
(e/click driver [{:tag :li :class :search-result :fn/index 2} {:tag :a}])
750773
;; check click tracker from our sample page:
751774
(e/get-element-text driver :clicked)
752775
;; => "b"
@@ -822,6 +845,125 @@ The following query will find a vector of `div` tags, then return a set of all `
822845
;; => ("a1" "a2" "a3" "a4" "a5" "a6" "a7" "a8" "a9")
823846
----
824847

848+
[#shadow-dom]
849+
=== Querying the Shadow DOM
850+
851+
The shadow DOM provides a way to attach another DOM tree to a specified element in the normal DOM and have the internals of that tree hidden from JavaScript and CSS on the same page.
852+
When the browser renders the DOM, the elements from the shadow DOM appear at the location where the tree is rooted in the normal DOM.
853+
This provides a level of encapsulation, allowing "components" in the shadow DOM to be styled differently than the rest of the page and preventing conflicts between the normal page CSS and the component CSS.
854+
The shadow DOM is also hidden from normal Web Driver queries (`query`) and thus requires a separate set of API calls to query it. For more details about the shadow DOM, see this article at https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM#shadow_dom_and_custom_elements[Mozilla Developer Network (MDN)].
855+
856+
There are a few terms that are important to understand when dealing with the Shadow DOM.
857+
The "shadow root host" is the element in the standard DOM to which a shadow root is attached as a property.
858+
The "shadow root" is the top of the shadow DOM tree rooted at the shadow root host.
859+
860+
The following examples use this HTML fragment in the User Guide sample HTML that has a bit of shadow DOM in it.
861+
862+
[source,html]
863+
----
864+
<span id="not-in-shadow">I'm not in the shadow DOM</span>
865+
<div id="shadow-root-host">
866+
<template shadowrootmode="open">
867+
<span id="in-shadow">I'm in the shadow DOM</span>
868+
<span id="also-in-shadow">I'm also in the shadow DOM</span>
869+
</template>
870+
</div>
871+
----
872+
873+
Everthing in the `template` element is part of the shadow DOM.
874+
The `div` with the `id` of `shadow-root-host` is, as the ID suggests, the shadow root host element.
875+
876+
Given this HTML, you can run a standard `query` to find the shadow root host and then use `get-element-property-el` to return to the `"shadowRoot"` property.
877+
Note that the element IDs returned in the following examples will be unique to the specific Etaoin driver and driver session and you will not see the same IDs.
878+
879+
[source,clojure]
880+
----
881+
(e/query driver {:id "shadow-root-host"})
882+
;; an element ID similar to (but not the same as)
883+
;; "78344155-7a53-46fb-a46e-e864210e501d"
884+
885+
(e/get-element-property-el driver (e/query driver {:id "shadow-root-host"}) "shadowRoot")
886+
;; something similar to
887+
;; {:shadow-6066-11e4-a52e-4f735466cecf "ac5ab914-7f93-427f-a0bf-f7e91098fd37"}
888+
889+
(e/get-element-property driver {:id "shadow-root-host"} "shadowRoot")
890+
;; something similar to
891+
;; {:shadow-6066-11e4-a52e-4f735466cecf "ac5ab914-7f93-427f-a0bf-f7e91098fd37"}
892+
----
893+
894+
If you go this route, you're going to have to pick apart the return
895+
values.
896+
The element-id of the shadow root is the string value of the first map key.
897+
898+
You can get the shadow root element ID more directly using Etaoin's `get-element-shadow-root` API.
899+
The query parameter looks for a matching element in the standard DOM and returns its shadow root property.
900+
901+
[source,clojure]
902+
----
903+
(e/get-element-shadow-root driver {:id "shadow-root-host"})
904+
;; something similar to
905+
;; "ac5ab914-7f93-427f-a0bf-f7e91098fd37"
906+
----
907+
908+
If you already have the shadow root host element, you can return its corresponding shadow root element ID using `get-element-shadow-root-el`.
909+
910+
[source,clojure]
911+
----
912+
(def host (e/query driver {:id "shadow-root-host"}))
913+
(e/get-element-shadow-root-el driver host)
914+
;; something similar to
915+
;; "ac5ab914-7f93-427f-a0bf-f7e91098fd37"
916+
----
917+
918+
You can test whether an element is a shadow root host using `has-shadow-root?` and `has-shadow-root-el?`.
919+
920+
[source,clojure]
921+
----
922+
(e/has-shadow-root? driver {:id "shadow-root-host"})
923+
;; => true
924+
(e/has-shadow-root-el? driver host)
925+
;; => true
926+
(e/has-shadow-root? driver {:id "not-in-shadow"})
927+
;; => false
928+
----
929+
930+
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`.
931+
932+
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.
933+
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.
934+
935+
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.
936+
937+
[source,clojure]
938+
----
939+
(def in-shadow (e/query-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"}))
940+
(e/get-element-text-el driver in-shadow)
941+
;; => "I'm in the shadow DOM"
942+
943+
(->> (e/query-all-shadow-root driver {:id "shadow-root-host"} {:css "span"})
944+
(map #(e/get-element-text-el driver %)))
945+
;; => ("I'm in the shadow DOM" "I'm also in the shadow DOM")
946+
947+
(def shadow-root (e/get-element-shadow-root-el driver host))
948+
(e/get-element-text-el driver (e/query-shadow-root-el driver shadow-root {:css "#in-shadow"}))
949+
;; => "I'm in the shadow DOM"
950+
951+
(->> (e/query-all-shadow-root-el driver shadow-root {:css "span"})
952+
(map #(e/get-element-text-el driver %)))
953+
;; > ("I'm in the shadow DOM" "I'm also in the shadow DOM")
954+
----
955+
956+
[#shadow-root-browser-limitations]
957+
[NOTE]
958+
====
959+
In the previous shadow root queries, you should note that we used CSS selectors for the `shadow-q` argument in each case.
960+
This was done because current browsers do not support XPath, which is what the Etaoin map syntax is typically translated into under the hood.
961+
While it is expected that browsers will support XPath queries of the shadow DOM in the future, it is unclear when this support might appear.
962+
For now, use CSS.
963+
964+
For more information, see the https://wpt.fyi/results/webdriver/tests/classic/find_element_from_shadow_root/find.py?label=experimental&label=master&aligned[Web Platforms Test Dashobard].
965+
====
966+
825967
=== Interacting with Queried Elements
826968

827969
To interact with elements found via a `query` or `query-all` function call you have to pass the query result to either `click-el` or `fill-el` (note the `-el` suffix):
@@ -1110,7 +1252,7 @@ The most important one, `scroll-query` jumps the the first element found with th
11101252
----
11111253
(e/go driver sample-page)
11121254
;; scroll to the 5th h2 heading
1113-
(e/scroll-query driver {:tag :h2} {:index 5})
1255+
(e/scroll-query driver {:tag :h2} {:fn/index 5})
11141256
11151257
;; and back up to first h1
11161258
(e/scroll-query driver {:tag :h1})

doc/user-guide-sample.html

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,15 @@ <h2>Query Tree Example</h2>
140140
</div>
141141
</div>
142142

143+
<h2>Shadow DOM Example</h2>
144+
<span id="not-in-shadow">I'm not in the shadow DOM</span>
145+
<div id="shadow-root-host">
146+
<template shadowrootmode="open">
147+
<span id="in-shadow">I'm in the shadow DOM</span>
148+
<span id="also-in-shadow">I'm also in the shadow DOM</span>
149+
</template>
150+
</div>
151+
143152
<h2>Frames</h2>
144153
<p id="in-main-page">In main page paragraph</p>
145154
<iframe id="frame1" src="user-guide-sample-frame1.html"></iframe>

env/test/resources/static/test.html

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,16 @@ <h3>Find multiple elements nested</h3>
208208
</div>
209209
</div>
210210

211+
<h3>Find elements with index</h3>
212+
213+
<ol id="indexed-elements">
214+
<li class="indexed">One</li>
215+
<li class="indexed">Two</li>
216+
<li class="indexed">Three</li>
217+
<li class="indexed">Four</li>
218+
<li class="indexed">Five</li>
219+
</ol>
220+
211221
<h3>Operate on multiple elements</h3>
212222
<div id="operate-multiple-elements">
213223
<div>
@@ -238,6 +248,15 @@ <h3>Find text with quote</h3>
238248
</p>
239249
</div>
240250

251+
<h3>Shadow DOM</h3>
252+
<span id="not-in-shadow">I'm not in the shadow DOM</span>
253+
<div id="shadow-root-host">
254+
<template shadowrootmode="open">
255+
<span id="in-shadow">I'm in the shadow DOM</span>
256+
<span id="also-in-shadow">I'm also in the shadow DOM</span>
257+
</template>
258+
</div>
259+
241260
<h3 id="document-end">Document end</h3>
242261

243262
</body>

0 commit comments

Comments
 (0)