Skip to content

Commit

Permalink
Add tests for proposed WebDriver Shadow DOM support (#27132)
Browse files Browse the repository at this point in the history
  • Loading branch information
jimevans committed Jan 14, 2021
1 parent f333959 commit a7ccbc6
Show file tree
Hide file tree
Showing 13 changed files with 837 additions and 0 deletions.
1 change: 1 addition & 0 deletions tools/webdriver/webdriver/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
Find,
Frame,
Session,
ShadowRoot,
Timeouts,
Window)
from .error import (
Expand Down
41 changes: 41 additions & 0 deletions tools/webdriver/webdriver/client.py
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,42 @@ def from_json(cls, json, session):
return cls(uuid, session)


class ShadowRoot(object):
identifier = "shadow-075b-4da1-b6ba-e579c2d3230a"

def __init__(self, session, id):
"""
Construct a new shadow root representation.
:param id: Shadow root UUID which must be unique across
all browsing contexts.
:param session: Current ``webdriver.Session``.
"""
self.id = id
self.session = session

@classmethod
def from_json(cls, json, session):
uuid = json[ShadowRoot.identifier]
return cls(uuid, session)

def send_shadow_command(self, method, uri, body=None):
url = "shadow/{}/{}".format(self.id, uri)
return self.session.send_session_command(method, url, body)

@command
def find_element(self, strategy, selector):
body = {"using": strategy,
"value": selector}
return self.send_shadow_command("POST", "element", body)

@command
def find_elements(self, strategy, selector):
body = {"using": strategy,
"value": selector}
return self.send_shadow_command("POST", "elements", body)


class Find(object):
def __init__(self, session):
self.session = session
Expand Down Expand Up @@ -804,6 +840,11 @@ def selected(self):
def screenshot(self):
return self.send_element_command("GET", "screenshot")

@property
@command
def shadow_root(self):
return self.send_element_command("GET", "shadow")

@command
def attribute(self, name):
return self.send_element_command("GET", "attribute/%s" % name)
Expand Down
10 changes: 10 additions & 0 deletions tools/webdriver/webdriver/error.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ def __str__(self):
return message


class DetachedShadowRootException(WebDriverException):
http_status = 404
status_code = "detached shadow root"


class ElementClickInterceptedException(WebDriverException):
http_status = 400
status_code = "element click intercepted"
Expand Down Expand Up @@ -114,6 +119,11 @@ class NoSuchFrameException(WebDriverException):
status_code = "no such frame"


class NoSuchShadowRootException(WebDriverException):
http_status = 404
status_code = "no such shadow root"


class NoSuchWindowException(WebDriverException):
http_status = 404
status_code = "no such window"
Expand Down
4 changes: 4 additions & 0 deletions tools/webdriver/webdriver/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ def default(self, obj):
return {webdriver.Frame.identifier: obj.id}
elif isinstance(obj, webdriver.Window):
return {webdriver.Frame.identifier: obj.id}
elif isinstance(obj, webdriver.ShadowRoot):
return {webdriver.ShadowRoot.identifier: obj.id}
return super(Encoder, self).default(obj)


Expand All @@ -40,6 +42,8 @@ def object_hook(self, payload):
return webdriver.Frame.from_json(payload, self.session)
elif isinstance(payload, dict) and webdriver.Window.identifier in payload:
return webdriver.Window.from_json(payload, self.session)
elif isinstance(payload, dict) and webdriver.ShadowRoot.identifier in payload:
return webdriver.ShadowRoot.from_json(payload, self.session)
elif isinstance(payload, dict):
return {k: self.object_hook(v) for k, v in iteritems(payload)}
return payload
19 changes: 19 additions & 0 deletions webdriver/tests/find_element_from_shadow_root/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import pytest

@pytest.fixture
def get_shadow_page(inline):
def get_shadow_page(shadow_content):
return inline("""
<custom-shadow-element></custom-shadow-element>
<script>
customElements.define('custom-shadow-element',
class extends HTMLElement {{
constructor() {{
super();
this.attachShadow({{mode: 'open'}}).innerHTML = `
{{ {0} }}
`;
}}
}});
</script>""".format(shadow_content))
return get_shadow_page
141 changes: 141 additions & 0 deletions webdriver/tests/find_element_from_shadow_root/find.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import pytest

from webdriver.transport import Response

from tests.support.asserts import assert_error, assert_same_element, assert_success


def find_element(session, shadow_id, using, value):
return session.transport.send(
"POST", "session/{session_id}/shadow/{shadow_id}/element".format(
session_id=session.session_id,
shadow_id=shadow_id),
{"using": using, "value": value})


def test_null_parameter_value(session, http, get_shadow_page):
session.url = get_shadow_page("<div><a href=# id=linkText>full link text</a></div>")
custom_element = session.find.css("custom-shadow-element", all=False)
shadow_root = custom_element.shadow_root

path = "/session/{session_id}/shadow/{shadow_id}/element".format(
session_id=session.session_id, shadow_id=shadow_root.id)
with http.post(path, None) as response:
assert_error(Response.from_http(response), "invalid argument")


def test_no_top_browsing_context(session, closed_window):
response = find_element(session, "notReal", "css selector", "foo")
assert_error(response, "no such window")


def test_no_browsing_context(session, closed_frame):
response = find_element(session, "notReal", "css selector", "foo")
assert_error(response, "no such window")


@pytest.mark.parametrize("using", ["a", True, None, 1, [], {}])
def test_invalid_using_argument(session, using):
# Step 1 - 2
response = find_element(session, "notReal", using, "value")
assert_error(response, "invalid argument")


@pytest.mark.parametrize("value", [None, [], {}])
def test_invalid_selector_argument(session, value):
# Step 3 - 4
response = find_element(session, "notReal", "css selector", value)
assert_error(response, "invalid argument")


def test_detached_shadow_root(session, get_shadow_page):
session.url = get_shadow_page("<div><input type='checkbox'/></div>")
custom_element = session.find.css("custom-shadow-element", all=False)
shadow_root = custom_element.shadow_root
session.refresh()

response = find_element(session, shadow_root.id, "css", "input")
assert_error(response, "detached shadow root")


def test_found_element_equivalence(session, get_shadow_page):
session.url = get_shadow_page("<div><input type='checkbox'/></div>")
custom_element = session.find.css("custom-shadow-element", all=False)
expected = session.execute_script("return arguments[0].shadowRoot.querySelector('input')",
args=(custom_element,))
shadow_root = custom_element.shadow_root
response = find_element(session, shadow_root.id, "css", "input")
value = assert_success(response)
assert_same_element(session, value, expected)


@pytest.mark.parametrize("using,value",
[("css selector", "#linkText"),
("link text", "full link text"),
("partial link text", "link text"),
("tag name", "a"),
("xpath", "//a")])
def test_find_element(session, get_shadow_page, using, value):
# Step 8 - 9
session.url = get_shadow_page("<div><a href=# id=linkText>full link text</a></div>")
custom_element = session.find.css("custom-shadow-element", all=False)
expected = session.execute_script("return arguments[0].shadowRoot.querySelector('#linkText')",
args=(custom_element,))
shadow_root = custom_element.shadow_root
response = find_element(session, shadow_root.id, using, value)
assert_success(response)
assert_same_element(session, value, expected)


@pytest.mark.parametrize("document,value", [
("<a href=#>link text</a>", "link text"),
("<a href=#>&nbsp;link text&nbsp;</a>", "link text"),
("<a href=#>link<br>text</a>", "link\ntext"),
("<a href=#>link&amp;text</a>", "link&text"),
("<a href=#>LINK TEXT</a>", "LINK TEXT"),
("<a href=# style='text-transform: uppercase'>link text</a>", "LINK TEXT"),
])
def test_find_element_link_text(session, get_shadow_page, document, value):
# Step 8 - 9
session.url = get_shadow_page("<div>{0}</div>".format(document))
custom_element = session.find.css("custom-shadow-element", all=False)
expected = session.execute_script("return arguments[0].shadowRoot.querySelectorAll('a')[0]",
args=(custom_element,))
shadow_root = custom_element.shadow_root

response = find_element(session, shadow_root.id, "link text", value)
assert_success(response)
assert_same_element(session, value, expected)


@pytest.mark.parametrize("document,value", [
("<a href=#>partial link text</a>", "link"),
("<a href=#>&nbsp;partial link text&nbsp;</a>", "link"),
("<a href=#>partial link text</a>", "k t"),
("<a href=#>partial link<br>text</a>", "k\nt"),
("<a href=#>partial link&amp;text</a>", "k&t"),
("<a href=#>PARTIAL LINK TEXT</a>", "LINK"),
("<a href=# style='text-transform: uppercase'>partial link text</a>", "LINK"),
])
def test_find_element_partial_link_text(session, get_shadow_page, document, value):
# Step 8 - 9
session.url = get_shadow_page("<div>{0}</div>".format(document))
custom_element = session.find.css("custom-shadow-element", all=False)
expected = session.execute_script("return arguments[0].shadowRoot.querySelectorAll('a')[0]",
args=(custom_element,))
shadow_root = custom_element.shadow_root

response = find_element(session, shadow_root.id, "partial link text", value)
assert_success(response)
assert_same_element(session, value, expected)


@pytest.mark.parametrize("using,value", [("css selector", "#wontExist")])
def test_no_element(session, get_shadow_page, using, value):
# Step 8 - 9
session.url = get_shadow_page("<div></div>")
custom_element = session.find.css("custom-shadow-element", all=False)
shadow_root = custom_element.shadow_root

response = find_element(session, shadow_root.id, using, value)
assert_error(response, "no such element")
129 changes: 129 additions & 0 deletions webdriver/tests/find_element_from_shadow_root/user_prompts.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
# META: timeout=long

import pytest

from tests.support.asserts import (
assert_error,
assert_same_element,
assert_success,
assert_dialog_handled,
)


def find_element(session, shadow_id, using, value):
return session.transport.send(
"POST", "session/{session_id}/shadow/{shadow_id}/element".format(
session_id=session.session_id,
element_id=shadow_id),
{"using": using, "value": value})


@pytest.fixture
def check_user_prompt_closed_without_exception(session, create_dialog, get_shadow_page):
def check_user_prompt_closed_without_exception(dialog_type, retval):
session.url = get_shadow_page("<div><p>bar</p><div>")
outer_element = session.find.css("custom-shadow-element", all=False)
shadow_root = outer_element.shadow_root
inner_element = session.execute_script("return arguments[0].shadowRoot.querySelector('p')",
args=(outer_element,))

create_dialog(dialog_type, text=dialog_type)

response = find_element(session, shadow_root.id, "css selector", "p")
value = assert_success(response)

assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)

assert_same_element(session, value, inner_element)

return check_user_prompt_closed_without_exception


@pytest.fixture
def check_user_prompt_closed_with_exception(session, create_dialog, get_shadow_page):
def check_user_prompt_closed_with_exception(dialog_type, retval):
session.url = get_shadow_page("<div><p>bar</p><div>")
outer_element = session.find.css("custom-shadow-element", all=False)
shadow_root = outer_element.shadow_root

create_dialog(dialog_type, text=dialog_type)

response = find_element(session, shadow_root.id, "css selector", "p")
assert_error(response, "unexpected alert open")

assert_dialog_handled(session, expected_text=dialog_type, expected_retval=retval)

return check_user_prompt_closed_with_exception


@pytest.fixture
def check_user_prompt_not_closed_but_exception(session, create_dialog, get_shadow_page):
def check_user_prompt_not_closed_but_exception(dialog_type):
session.url = get_shadow_page("<div><p>bar</p><div>")
outer_element = session.find.css("custom-shadow-element", all=False)
shadow_root = outer_element.shadow_root

create_dialog(dialog_type, text=dialog_type)

response = find_element(session, shadow_root.id, "css selector", "p")
assert_error(response, "unexpected alert open")

assert session.alert.text == dialog_type
session.alert.dismiss()

return check_user_prompt_not_closed_but_exception


@pytest.mark.capabilities({"unhandledPromptBehavior": "accept"})
@pytest.mark.parametrize("dialog_type, retval", [
("alert", None),
("confirm", True),
("prompt", ""),
])
def test_accept(check_user_prompt_closed_without_exception, dialog_type, retval):
check_user_prompt_closed_without_exception(dialog_type, retval)


@pytest.mark.capabilities({"unhandledPromptBehavior": "accept and notify"})
@pytest.mark.parametrize("dialog_type, retval", [
("alert", None),
("confirm", True),
("prompt", ""),
])
def test_accept_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
check_user_prompt_closed_with_exception(dialog_type, retval)


@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss"})
@pytest.mark.parametrize("dialog_type, retval", [
("alert", None),
("confirm", False),
("prompt", None),
])
def test_dismiss(check_user_prompt_closed_without_exception, dialog_type, retval):
check_user_prompt_closed_without_exception(dialog_type, retval)


@pytest.mark.capabilities({"unhandledPromptBehavior": "dismiss and notify"})
@pytest.mark.parametrize("dialog_type, retval", [
("alert", None),
("confirm", False),
("prompt", None),
])
def test_dismiss_and_notify(check_user_prompt_closed_with_exception, dialog_type, retval):
check_user_prompt_closed_with_exception(dialog_type, retval)


@pytest.mark.capabilities({"unhandledPromptBehavior": "ignore"})
@pytest.mark.parametrize("dialog_type", ["alert", "confirm", "prompt"])
def test_ignore(check_user_prompt_not_closed_but_exception, dialog_type):
check_user_prompt_not_closed_but_exception(dialog_type)


@pytest.mark.parametrize("dialog_type, retval", [
("alert", None),
("confirm", False),
("prompt", None),
])
def test_default(check_user_prompt_closed_with_exception, dialog_type, retval):
check_user_prompt_closed_with_exception(dialog_type, retval)
Loading

0 comments on commit a7ccbc6

Please sign in to comment.