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
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions integration_test/cases/browser/shadow_dom_test.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
defmodule Wallaby.Integration.Browser.ShadowDomTest do
superchris marked this conversation as resolved.
Show resolved Hide resolved
use Wallaby.Integration.SessionCase, async: true

alias Wallaby.Element

setup %{session: session} do
page =
session
|> visit("shadow_dom.html")

{:ok, page: page}
end

test "can find a shadow root", %{session: session} do
shadow_root =
session
|> find(Query.css("shadow-test"))
|> shadow_root()

assert %Element{} = shadow_root
end

test "can find elements within a shadow dom", %{session: session} do
element =
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> find(Query.css("#in-shadow"))

assert Element.text(%Element{} = element) == "I am in shadow"
end

test "can click elements within a shadow dom", %{session: session} do
element =
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> click(Query.css("#option1"))
|> click(Query.css("#option2"))

assert selected?(element, Query.css("#option2"))
end

test "does not return a shadow root when one does not exist", %{session: session} do
shadow_root =
session
|> find(Query.css("#outside-shadow"))
|> shadow_root()

refute shadow_root
end
end
24 changes: 24 additions & 0 deletions integration_test/support/pages/shadow_dom.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
<html>
<head>
<script>
class ShadowTestElement extends HTMLElement {
connectedCallback() {
this.attachShadow({mode: 'open'});
this.shadowRoot.innerHTML = `
<div id="in-shadow">I am in shadow</div>
<form class="form" action="index.html" method="get">
<input id="option1" type="radio" name="radios" value="option1" />
<input id="option2" type="radio" name="radios" value="option2" />
</form>
`;
}
}
window.customElements.define('shadow-test', ShadowTestElement);
</script>
</head>

<body>
<div id="outside-shadow">I am out of shadow</div>
<shadow-test></shadow-test>
</body>
</html>
1 change: 1 addition & 0 deletions integration_test/tests.exs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ Code.require_file("cases/browser/screenshot_test.exs", __DIR__)
Code.require_file("cases/browser/select_test.exs", __DIR__)
Code.require_file("cases/browser/set_value_test.exs", __DIR__)
Code.require_file("cases/browser/send_keys_test.exs", __DIR__)
Code.require_file("cases/browser/shadow_dom_test.exs", __DIR__)
Code.require_file("cases/browser/stale_nodes_test.exs", __DIR__)
Code.require_file("cases/browser/text_test.exs", __DIR__)
Code.require_file("cases/browser/title_test.exs", __DIR__)
Expand Down
19 changes: 19 additions & 0 deletions lib/wallaby/browser.ex
Original file line number Diff line number Diff line change
Expand Up @@ -1012,6 +1012,25 @@ defmodule Wallaby.Browser do
)
end

@doc """
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.

```
session
|> find(Query.css("shadow-test"))
|> shadow_root()
|> find(Query.css("#in-shadow"))
```
"""
def shadow_root(%{driver: driver} = element) 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 an @spec annotation

case driver.shadow_root(element) do
{:ok, element} -> element
{:error, _error} -> nil
end
end

@doc """
Validates that the query returns a result. This can be used to define other
types of matchers.
Expand Down
3 changes: 3 additions & 0 deletions lib/wallaby/chrome.ex
Original file line number Diff line number Diff line change
Expand Up @@ -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

@doc false
defdelegate parse_log(log), to: Wallaby.Chrome.Logger

Expand Down
5 changes: 5 additions & 0 deletions lib/wallaby/driver.ex
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,11 @@ defmodule Wallaby.Driver do
@callback find_elements(Session.t() | Element.t(), Query.compiled()) ::
{:ok, [Element.t()]} | {:error, reason}

@doc """
Invoked to find shadow root of 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
Invoked to find shadow root of given element
Invoked to find shadow root of the given element.

"""
@callback shadow_root(Element.t()) :: {:ok, Element.t()} | {:error, reason}

@doc """
Invoked to execute JavaScript in the browser.
"""
Expand Down
3 changes: 3 additions & 0 deletions lib/wallaby/httpclient.ex
Original file line number Diff line number Diff line change
Expand Up @@ -114,6 +114,9 @@ defmodule Wallaby.HTTPClient do
%{"message" => "stale element reference" <> _} ->
{:error, :stale_reference}

%{"message" => "no such shadow root" <> _} ->
{:error, :shadow_root_not_found}

%{
"message" =>
"An element command failed because the referenced element is no longer available" <> _
Expand Down
3 changes: 3 additions & 0 deletions lib/wallaby/selenium.ex
Original file line number Diff line number Diff line change
Expand Up @@ -190,6 +190,9 @@ defmodule Wallaby.Selenium do
@doc false
defdelegate dismiss_prompt(session, fun), to: WebdriverClient

@doc false
defdelegate shadow_root(element), to: WebdriverClient

@doc false
defdelegate take_screenshot(session_or_element), to: WebdriverClient

Expand Down
22 changes: 22 additions & 0 deletions lib/wallaby/webdriver_client.ex
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ defmodule Wallaby.WebdriverClient do
| Session.t()

@web_element_identifier "element-6066-11e4-a52e-4f735466cecf"
@shadow_root_identifier "shadow-6066-11e4-a52e-4f735466cecf"

@button_mapping %{left: 0, middle: 1, right: 2}

Expand Down Expand Up @@ -48,6 +49,17 @@ defmodule Wallaby.WebdriverClient do
do: {:ok, elements}
end

@doc """
Finds the shadow root for the given element.
"""
@spec shadow_root(Element.t()) :: {:ok, Element.t()}
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

@doc """
Sets the value of an element.
"""
Expand Down Expand Up @@ -699,6 +711,16 @@ defmodule Wallaby.WebdriverClient do
}
end

defp cast_as_element(parent, %{@shadow_root_identifier => id}) do
%Wallaby.Element{
id: id,
session_url: parent.session_url,
url: parent.session_url <> "/shadow/#{id}",
parent: parent,
driver: parent.driver
}
end

# Retrieves the text from an alert, prompt or confirm.
@spec alert_text(Session.t()) :: {:ok, String.t()}
defp alert_text(session) do
Expand Down
34 changes: 34 additions & 0 deletions test/wallaby/http_client_test.exs
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,40 @@ defmodule Wallaby.HTTPClientTest do
assert {:error, :stale_reference} = Client.request(:post, bypass_url(bypass, "/my_url"))
end

test "with a non-existent shadow root", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
send_json_resp(conn, 404, %{
"sessionId" => "abc123",
"status" => 10,
"value" => %{
"message" =>
"no such shadow root\n (Session info: headless chrome=111.0.5563.64)\n (Driver info: chromedriver=110.0.5481.77 (65ed616c6e8ee3fe0ad64fe83796c020644d42af-refs/branch-heads/5481@{#839}),platform=Mac OS X 12.0.1 arm64)"
}
})
end)

assert {:error, :shadow_root_not_found} =
Client.request(:post, bypass_url(bypass, "/my_url"))
end

test "with an existing shadow root", %{bypass: bypass} do
Bypass.expect(bypass, fn conn ->
send_json_resp(conn, 200, %{
"sessionId" => "abc123",
"status" => 0,
"value" => nil
})
end)

{:ok, response} = Client.request(:post, bypass_url(bypass, "/my_url"))

assert response == %{
"sessionId" => "abc123",
"status" => 0,
"value" => nil
}
end

test "with an obscure status code", %{bypass: bypass} do
expected_message = "message from an obscure error"

Expand Down