Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Shadow dom #728

Open
wants to merge 11 commits into
base: main
Choose a base branch
from
Open

Shadow dom #728

wants to merge 11 commits into from

Conversation

superchris
Copy link

@mhanberg this is what we ended up with. We added a shadow_root/1 function on Wallaby.Browser that takes an element and narrows the scope to the shadow root of the element. This seems to line up really nicely with what webdriver wants and pretty much lets everything "just work" in what seems like a nice way IMO. Curious to hear your thoughts. If you think it's ok I'll add docs and get it mergable

@mhanberg
Copy link
Member

for posterity and myself, would you mind commenting with a lay person's explanation of what the shadow root is and how wallaby has to talk with the webdriver in order for the user to interact with them?

@superchris
Copy link
Author

for posterity and myself, would you mind commenting with a lay person's explanation of what the shadow root is and how wallaby has to talk with the webdriver in order for the user to interact with them?

Sure! Here is my attempt: Shadow DOM is a sub-specification of the Web Components group of specs that allow a developer to create an encapsulated DOM within an element. This encapsulated DOM, or shadow DOM, can contain CSS, markup, and anything else a DOM can contain but is isolated from the rest of the document such that styles from the containing DOM do not apply to the shadow DOM (with specific exceptions) and vice versa. This allows a developer of a custom element to have complete control over the precise rendering of their element while avoiding collisions with or dependencies on the containing document's CSS.

Because the Shadow DOM is isolated from the containing DOM, queries within the containing document will not find elements in the shadow DOM of a custom element. In order to overcome this limitation, several new API calls have been added to the WebDriver protocol to "pierce" a shadow DOM and query elements within it. The way we interact with these in wallaby is to scope our query to a shadow DOM of a specific element by using the shadow_root function, eg:

    element =
      session
      |> find(Query.css("shadow-test"))
      |> shadow_root()
      |> find(Query.css("#in-shadow"))

This will query for an element with id in-shadow that is within the shadow DOM for a <shadow-test> element. The name shadow_root corresponds the DOM property shadowRoot which is the accessor property for a shadow DOM within an element.

@mhanberg
Copy link
Member

Excellent explanation!!

I have a question about the API.

When we query an element that contains shadow dom, is it possible to know that before we call the shadow_dom function?

If we can mark an element has containing shadow dom, we can potentially hide the shadow_dom function from the user and they can just pretend that they are querying inside of it like normal.

@superchris
Copy link
Author

superchris commented Mar 16, 2023

Thanks! I don't know of a way to detect if an element has a shadow root, other than with javascript or doing the api call I am doing already in shadow_root. Doing this implicitly for every time we return or scope within an element seems tricky to me. I would be more in favor of being explicit I think. I also think the use case is fairly specific, my guess is for now the percentage of people using wallaby to interact with shadow DOM will be small so adding another api call all the time seems maybe not the best tradeoff.

@superchris
Copy link
Author

@mhanberg in doing some further experimentation, I came across an example where we really need to know whether the user is intending to query inside the shadow dom or the 'light dom' of a custom elements. Custom elements can also have markup placed inside them in the surrounding document, but then have this content merged into the shadow DOM using a feature called slots. This gets complicated, but the result is the element has both light and shadow DOMs :) To query for things in this "light dom" you do not want to scope using shadow_root. I'll try to come up with a test case that demonstrates this.

Copy link
Member

@mhanberg mhanberg left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is looking good.

I think for another PR, it would be good start create a guide that gives a more in depth example. You can see an example of how to add a guide here

|> find(Query.css("shadow-test"))
|> shadow_root()

assert shadow_root
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this be an Element struct? if yes let's assert on that

assert shadow_root
end

test "can find stuff within da shadow dom", %{session: session} do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make these test names slightly more formal

|> shadow_root()
|> find(Query.css("#in-shadow"))

assert element
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's actually assert on the contents of these values rather than just on truthiness

Comment on lines 1016 to 1018
Finds the shadow DOM for the specified element and returns an element corresponding
to this shadow DOM. Subsequent queries made within element retured by calling `shadow_root`
will return elements within the shadow DOM, e. g.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Finds the shadow DOM for the specified element and returns an element corresponding
to this shadow DOM. Subsequent queries made within element retured by calling `shadow_root`
will return elements within the shadow DOM, e. g.
Finds and returns the shadow root for the given element.
Queries executed on the returned shadow root will be scoped to the root's shadow DOM.

Comment on lines 1020 to 1026
```
element =
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> find(Query.css("#in-shadow"))
```
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
```
element =
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> find(Query.css("#in-shadow"))
```
```elixir
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> find(Query.css("#in-shadow"))

@@ -48,6 +49,18 @@ defmodule Wallaby.WebdriverClient do
do: {:ok, elements}
end

@doc """
Finds the shadow root for a given element.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
Finds the shadow root for a given element.
Finds the shadow root for the given element.

Comment on lines 56 to 62
def shadow_root(element) do
with {:ok, resp} <- request(:get, element.url <> "/shadow"),
{:ok, shadow_root} <- Map.fetch(resp, "value"),
element <- cast_as_element(element, shadow_root) do
{:ok, element}
end
end
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
def shadow_root(element) do
with {:ok, resp} <- request(:get, element.url <> "/shadow"),
{:ok, shadow_root} <- Map.fetch(resp, "value"),
element <- cast_as_element(element, shadow_root) do
{:ok, element}
end
end
def shadow_root(element) do
with {:ok, resp} <- request(:get, element.url <> "/shadow"),
{:ok, shadow_root} <- Map.fetch(resp, "value") do
{:ok, cast_as_element(element, shadow_root)}
end
end

@@ -55,6 +55,22 @@ defmodule Wallaby.HTTPClientTest do
assert {:error, :stale_reference} = Client.request(:post, bypass_url(bypass, "/my_url"))
end

test "with a non-existant shadow root response", %{bypass: bypass} do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
test "with a non-existant shadow root response", %{bypass: bypass} do
test "with a non-existent shadow root", %{bypass: bypass} do

@@ -55,6 +55,22 @@ defmodule Wallaby.HTTPClientTest do
assert {:error, :stale_reference} = Client.request(:post, bypass_url(bypass, "/my_url"))
end

test "with a non-existant shadow root response", %{bypass: bypass} do
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add a success case test

@@ -410,6 +410,9 @@ defmodule Wallaby.Chrome do
defdelegate accept_prompt(session, input, fun), to: WebdriverClient
@doc false
defdelegate dismiss_prompt(session, fun), to: WebdriverClient
@doc false
defdelegate shadow_root(element), to: WebdriverClient

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

4 participants