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

Add tests for proposed WebDriver Shadow DOM support #27132

Merged
merged 24 commits into from
Jan 14, 2021
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
4fcd1b7
Add tests for proposed WebDriver Shadow DOM support
jimevanssfdc Jan 11, 2021
477401e
Correcting HTTP verb for WebDriver command
jimevanssfdc Jan 11, 2021
768ad5c
Adding WebDriver transport commands for Shadow DOM
jimevanssfdc Jan 11, 2021
c4e6a2c
Updating WebDriver client with code review suggestions
jimevanssfdc Jan 12, 2021
94b8624
Fleshing out find element from shadow root tests per review
jimevanssfdc Jan 12, 2021
e246de3
Renaming test_shadow_page to get_shadow_page
jimevanssfdc Jan 12, 2021
694cc74
Fleshing out tests for find elements from shadow root
jimevanssfdc Jan 12, 2021
3855813
Add user prompt tests for find element(s) from shadow root
jimevanssfdc Jan 12, 2021
5a99dfc
Update Get Element Shadow Root tests
jimevanssfdc Jan 12, 2021
9e70d8f
Code review: moving fixtures to conftest.py files
jimevanssfdc Jan 12, 2021
5720604
Code review: Updating string format mechanism
jimevanssfdc Jan 12, 2021
5d3b4c5
Correct string formatting for real
jimevanssfdc Jan 12, 2021
d74e42a
Rearrange tests per code review
jimevanssfdc Jan 13, 2021
c9921ba
Code review: Have get_shadow_page return internal function
jimevanssfdc Jan 13, 2021
6ca0c4f
Revert move of user prompt fixtures to conftest.py
jimevanssfdc Jan 13, 2021
45f8649
Adding forgotten file from previous commit
jimevanssfdc Jan 13, 2021
1cf10f0
Add tests for detached shadow root
jimevanssfdc Jan 13, 2021
8ef520e
Add assertion for returning correct shadow root
jimevanssfdc Jan 13, 2021
edd30ab
Add import statement to conftest.py files
jimevanssfdc Jan 13, 2021
044d4e1
Reverting rename of inner function
jimevanssfdc Jan 13, 2021
55e34fe
Correcting URL variable parsing in get element shadow root
jimevanssfdc Jan 13, 2021
ab9cf01
Add new exceptions to WebDriver error parsing
jimevanssfdc Jan 13, 2021
fb8ab3c
Adding same element assert for find element from shadow root
jimevanssfdc Jan 14, 2021
eda55bd
Code review: changing name of supporing fixture
jimevanssfdc Jan 14, 2021
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
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
jimevans marked this conversation as resolved.
Show resolved Hide resolved
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
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
71 changes: 71 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,71 @@
@pytest.fixture
def get_shadow_page(inline, shadow_content):
return inline("""
Copy link
Contributor

Choose a reason for hiding this comment

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

I actually wonder how this can work with the current implementation. What you return here is a string as produced by inline(). In the tests you call get_shadow_page(...), which has to fail on a string.

So what you want is to return an inner function; the same as what we do for most of the global fixtures. And only the inner method has the shadow_content argument.

This also applies to the other commands.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Acknowledged. Will work on this.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Took a stab at this in c9921ba. I've probably botched it, so do feel free to re-review.

<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))


@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
122 changes: 122 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,122 @@
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_found_element_equivalence(session, get_shadow_page):
jimevans marked this conversation as resolved.
Show resolved Hide resolved
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)


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")


@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)
shadow_root = custom_element.shadow_root
response = find_element(session, shadow_root.id, using, value)
assert_success(response)
jimevans marked this conversation as resolved.
Show resolved Hide resolved

jimevans marked this conversation as resolved.
Show resolved Hide resolved

@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)
shadow_root = custom_element.shadow_root

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


@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)
shadow_root = custom_element.shadow_root

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


@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")
jimevans marked this conversation as resolved.
Show resolved Hide resolved
73 changes: 73 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,73 @@
# 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.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