From 00af1093a7930beb382274142e5fd305a3dc4fe0 Mon Sep 17 00:00:00 2001
From: Dave Roberts
Date: Sat, 13 Jul 2024 22:26:52 -0500
Subject: [PATCH] dev: Implement shadow DOM functions (#604)
---
doc/01-user-guide.adoc | 116 +++++++++++++++++++++++++++
env/test/resources/static/test.html | 9 +++
src/etaoin/api.clj | 117 ++++++++++++++++++++++++++++
test/etaoin/api_test.clj | 39 ++++++++++
4 files changed, 281 insertions(+)
diff --git a/doc/01-user-guide.adoc b/doc/01-user-guide.adoc
index 4dc35970..84df10af 100644
--- a/doc/01-user-guide.adoc
+++ b/doc/01-user-guide.adoc
@@ -845,6 +845,122 @@ The following query will find a vector of `div` tags, then return a set of all `
;; => ("a1" "a2" "a3" "a4" "a5" "a6" "a7" "a8" "a9")
----
+=== Querying the Shadow DOM
+
+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.
+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.
+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.
+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)].
+
+There are a few terms that are important to understand when dealing with the Shadow DOM.
+The "shadow root host" is the element in the standard DOM to which a shadow root is attached as a property.
+The "shadow root" is the top of the shadow DOM tree rooted at the shadow root host.
+
+The following examples use this HTML fragment that has a bit of shadow DOM in it.
+
+[source,html]
+----
+I'm not in the shadow DOM
+
+
+ I'm in the shadow DOM
+ I'm also in the shadow DOM
+
+
+----
+
+Everthing in the `template` element is part of the shadow DOM.
+The `div` with the `id` of `shadow-root-host` is, as the ID suggests, the shadow root host element.
+
+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.
+
+[source,clojure]
+----
+(e/query driver {:id "shadow-root-host"})
+;; => "78344155-7a53-46fb-a46e-e864210e501d"
+
+(e/get-element-property-el driver (e/query driver {:id "shadow-root-host"}) "shadowRoot")
+;; => {:shadow-6066-11e4-a52e-4f735466cecf
+;; => "ac5ab914-7f93-427f-a0bf-f7e91098fd37"}
+
+(e/get-element-property driver {:id "shadow-root-host"} "shadowRoot")
+;; => {:shadow-6066-11e4-a52e-4f735466cecf
+;; => "ac5ab914-7f93-427f-a0bf-f7e91098fd37"}
+----
+
+If you go this route, you're going to have to pick apart the return
+value.
+The element-id of the shadow root is the string value of the first map key.
+
+You can get the shadow root element ID more directly using Etaoin's `get-element-shadow-root` API.
+The query parameter looks for an element in the standard DOM and returns its shadow root property.
+
+[source,clojure]
+----
+(e/get-element-shadow-root driver {:id "shadow-root-host"})
+;; => "ac5ab914-7f93-427f-a0bf-f7e91098fd37"
+----
+
+If you already have the shadow root host element, you can return its corresponding shadow root element ID using `get-element-shadow-root-el`.
+
+[source,clojure]
+----
+(def host (e/query driver {:id "shadow-root-host"}))
+;; => #'user/host
+(e/get-element-shadow-root-el driver host)
+;; => "ac5ab914-7f93-427f-a0bf-f7e91098fd37"
+----
+
+You can test whether an element is a shadow root host using `has-shadow-root?` and `has-shadow-root-el?`.
+
+[source,clojure]
+----
+(e/has-shadow-root? driver {:id "shadow-root-host"})
+;; => true
+(e/has-shadow-root-el? driver host)
+;; => true
+(e/has-shadow-root? driver {:id "not-in-shadow"})
+;; => 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`.
+
+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.
+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.
+
+[source,clojure]
+----
+(e/query-shadow-root driver {:id "shadow-root-host"} {:css "#in-shadow"})
+;; => "30fca382-6d4a-4f8a-9534-db76a1ed7cba"
+(e/get-element-text-el driver "30fca382-6d4a-4f8a-9534-db76a1ed7cba")
+;; => "I'm in the shadow DOM"
+
+(->> (e/query-all-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 root (e/get-element-shadow-root-el driver host))
+;; => #'user/root
+(e/get-element-text-el driver (e/query-shadow-root-el driver root {:css "#in-shadow"}))
+;; => "I'm in the shadow DOM"
+
+(->> (e/query-all-shadow-root-el driver root {:css "span"})
+ (map #(e/get-element-text-el driver %)))
+;; => ("I'm in the shadow DOM" "I'm also in the shadow DOM")
+----
+
+[NOTE]
+====
+In the previous shadow root queries, you should note that we used CSS selectors for the `shadow-q` argument in each case.
+This was done because current browsers do not support XPath, which is what the Etaoin map syntax is typically translated into under the hood.
+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.
+For now, use CSS.
+
+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].
+====
+
=== Interacting with Queried Elements
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):
diff --git a/env/test/resources/static/test.html b/env/test/resources/static/test.html
index 1e4473e5..9071cef8 100644
--- a/env/test/resources/static/test.html
+++ b/env/test/resources/static/test.html
@@ -238,6 +238,15 @@ Find text with quote
+ Shadow DOM
+ I'm not in the shadow DOM
+
+
+ I'm in the shadow DOM
+ I'm also in the shadow DOM
+
+
+
Document end