diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py index 785962062822bf..e3177de19dd12f 100644 --- a/tools/wptrunner/wptrunner/executors/actions.py +++ b/tools/wptrunner/wptrunner/executors/actions.py @@ -8,9 +8,9 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): - selector = payload["selector"] - element = self.protocol.select.element_by_selector(selector) - self.logger.debug("Clicking element: %s" % selector) + selectors = payload["selectors"] + element = self.protocol.select.element_by_selector_array(selectors) + self.logger.debug("Clicking element: %s" % selectors) self.protocol.click.element(element) @@ -46,8 +46,8 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): - selector = payload["selector"] - element = self.protocol.select.element_by_selector(selector) + selectors = payload["selectors"] + element = self.protocol.select.element_by_selector_array(selectors) self.logger.debug("Getting computed label for element: %s" % element) return self.protocol.accessibility.get_computed_label(element) @@ -60,8 +60,8 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): - selector = payload["selector"] - element = self.protocol.select.element_by_selector(selector) + selectors = payload["selectors"] + element = self.protocol.select.element_by_selector_array(selectors) self.logger.debug("Getting computed role for element: %s" % element) return self.protocol.accessibility.get_computed_role(element) @@ -87,10 +87,10 @@ def __init__(self, logger, protocol): self.protocol = protocol def __call__(self, payload): - selector = payload["selector"] + selectors = payload["selectors"] keys = payload["keys"] - element = self.protocol.select.element_by_selector(selector) - self.logger.debug("Sending keys to element: %s" % selector) + element = self.protocol.select.element_by_selector_array(selectors) + self.logger.debug("Sending keys to element: %s" % selectors) self.protocol.send_keys.send_keys(element, keys) @@ -145,11 +145,11 @@ def __call__(self, payload): for action in actionSequence["actions"]: if (action["type"] == "pointerMove" and isinstance(action["origin"], dict)): - action["origin"] = self.get_element(action["origin"]["selector"]) + action["origin"] = self.get_element(action["origin"]["selectors"]) self.protocol.action_sequence.send_actions({"actions": actions}) - def get_element(self, element_selector): - return self.protocol.select.element_by_selector(element_selector) + def get_element(self, element_selectors): + return self.protocol.select.element_by_selector_array(element_selectors) def reset(self): self.protocol.action_sequence.release() diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py index e1b954a7d23da5..6018e3b2447ace 100644 --- a/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -403,6 +403,26 @@ class MarionetteSelectorProtocolPart(SelectorProtocolPart): def setup(self): self.marionette = self.parent.marionette + def elements_by_selector_array(self, selectors): + shadow_roots = [] + selectors = selectors.copy() + selectors.reverse() + + while selectors: + selector = selectors.pop() + intermediate = [] + if not shadow_roots: + intermediate = self.marionette.find_elements("css selector", selector) + else: + for root in shadow_roots: + intermediate.extend(root.find_elements("css selector", selector)) + + if (selectors): + shadow_roots = [element.shadow_root for element in intermediate] + shadow_roots = [root for root in shadow_roots if root is not None] + else: + return intermediate + def elements_by_selector(self, selector): return self.marionette.find_elements("css selector", selector) diff --git a/tools/wptrunner/wptrunner/executors/executorselenium.py b/tools/wptrunner/wptrunner/executors/executorselenium.py index cf5ac2a22f02c5..0080367f6ed194 100644 --- a/tools/wptrunner/wptrunner/executors/executorselenium.py +++ b/tools/wptrunner/wptrunner/executors/executorselenium.py @@ -171,6 +171,12 @@ class SeleniumSelectorProtocolPart(SelectorProtocolPart): def setup(self): self.webdriver = self.parent.webdriver + def elements_by_selector_array(self, selectors): + if len(selectors) == 1: + return self.elements_by_selector(selectors[0]) + + raise NotImplementedError() + def elements_by_selector(self, selector): return self.webdriver.find_elements_by_css_selector(selector) diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index cdb7deb5799747..2f59bfa1098624 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -531,6 +531,12 @@ class WebDriverSelectorProtocolPart(SelectorProtocolPart): def setup(self): self.webdriver = self.parent.webdriver + def elements_by_selector_array(self, selectors): + if len(selectors) == 1: + return self.elements_by_selector(selectors[0]) + + raise NotImplementedError() + def elements_by_selector(self, selector): return self.webdriver.find.css(selector) diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py index d5f9b0bfc4bb89..d5f215d4b9be05 100644 --- a/tools/wptrunner/wptrunner/executors/protocol.py +++ b/tools/wptrunner/wptrunner/executors/protocol.py @@ -285,6 +285,24 @@ class SelectorProtocolPart(ProtocolPart): name = "select" + def element_by_selector_array(self, element_selectors): + elements = self.elements_by_selector_array(element_selectors) + if len(elements) == 0: + raise ValueError(f"Selector array '{element_selectors}' matches no elements") + elif len(elements) > 1: + raise ValueError(f"Selector array '{element_selectors}' matches multiple elements") + return elements[0] + + @abstractmethod + def elements_by_selector_array(self, selectors): + """Select elements matching an array of selectors, such that the first + selector matches an element in the document root, and each successive + selector matches an element inside the shadow root of the previous. + + :param List[str] selectors: The CSS selectors + :returns: A list of protocol-specific handles to elements""" + pass + def element_by_selector(self, element_selector): elements = self.elements_by_selector(element_selector) if len(elements) == 0: diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 6f6b13620782f5..f8b83097c58a71 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -78,7 +78,7 @@ } else { if (bits >= 1 && bits <= 30) { return 0 | ((1 << bits) * Math.random()); - } else { + } else { var high = (0 | ((1 << (bits - 30)) * Math.random())) * (1 << 30); var low = 0 | ((1 << 30) * Math.random()); return high + low; @@ -134,14 +134,18 @@ } else { // push and then reverse to avoid O(n) unshift in the loop let segments = []; - for (let node = element; - node.parentElement; - node = node.parentElement) { - let segment = "*|" + node.localName; - let nth = Array.prototype.indexOf.call(node.parentElement.children, node) + 1; + let el = element; + while (el && el.parentElement) { + let segment = "*|" + el.localName; + let nth = Array.prototype.indexOf.call(el.parentNode.children, el) + 1; segments.push(segment + ":nth-child(" + nth + ")"); + el = el.parentElement; + } + if (element.getRootNode() == element.ownerDocument) { + segments.push(":root"); + } else { + segments.push(":scope"); } - segments.push(":root"); segments.reverse(); selector = segments.join(" > "); @@ -150,6 +154,18 @@ return selector; }; + const get_selector_array = function(element) { + let selectors = []; + let current = element; + + do { + selectors.push(get_selector(current)); + current = current.getRootNode().host; + } while (current); + + return selectors.reverse(); + }; + /** * Create an action and return a promise that resolves when the action is complete. * @param name: The name of the action to create. @@ -460,9 +476,9 @@ }; window.test_driver_internal.click = function(element) { - const selector = get_selector(element); + const selectors = get_selector_array(element); const context = get_context(element); - return create_context_action("click", context, {selector}); + return create_context_action("click", context, {selectors}); }; window.test_driver_internal.delete_all_cookies = function(context=null) { @@ -482,15 +498,15 @@ } window.test_driver_internal.get_computed_label = function(element) { - const selector = get_selector(element); + const selectors = get_selector_array(element); const context = get_context(element); - return create_context_action("get_computed_label", context, {selector}); + return create_context_action("get_computed_label", context, {selectors}); }; window.test_driver_internal.get_computed_role = function(element) { - const selector = get_selector(element); + const selectors = get_selector_array(element); const context = get_context(element); - return create_context_action("get_computed_role", context, {selector}); + return create_context_action("get_computed_role", context, {selectors}); }; window.test_driver_internal.get_named_cookie = function(name, context=null) { @@ -510,9 +526,9 @@ }; window.test_driver_internal.send_keys = function(element, keys) { - const selector = get_selector(element); + const selectors = get_selector_array(element); const context = get_context(element); - return create_context_action("send_keys", context, {selector, keys}); + return create_context_action("send_keys", context, {selectors, keys}); }; window.test_driver_internal.action_sequence = function(actions, context=null) { @@ -522,7 +538,7 @@ // The origin of each action can only be an element or a string of a value "viewport" or "pointer". if (action.type == "pointerMove" && typeof(action.origin) != 'string') { let action_context = get_context(action.origin); - action.origin = {selector: get_selector(action.origin)}; + action.origin = {selectors: get_selector_array(action.origin)}; if (context !== null && action_context !== context) { throw new Error("Actions must be in a single context"); }