diff --git a/infrastructure/metadata/infrastructure/testdriver/bidi/subscription.html.ini b/infrastructure/metadata/infrastructure/testdriver/bidi/subscription.html.ini index 86b5f6487ba08e..2d828266f1083b 100644 --- a/infrastructure/metadata/infrastructure/testdriver/bidi/subscription.html.ini +++ b/infrastructure/metadata/infrastructure/testdriver/bidi/subscription.html.ini @@ -1,8 +1,3 @@ [subscription.html] expected: if product != "chrome": ERROR - [Assert testdriver can subscribe globally] - expected: - # TODO(https://github.com/web-platform-tests/wpt/issues/56985): - # Investigate root cause - if product == "chrome": FAIL diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index cdb7deb5799747..26201cb88583f8 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -621,54 +621,31 @@ def release(self): class WebDriverTestDriverProtocolPart(TestDriverProtocolPart): def setup(self): self.webdriver = self.parent.webdriver - - def run(self, url, script_resume, test_window=None): - # If protocol implements `bidi_events`, remove all the existing subscriptions. + # Required for detecting relevant `browsingContext.userPromptOpened` events. + self._test_window = self.parent.base.current_window + # Exceptions occurred outside the main loop. Uses to store exceptions happened in async code + # to communicate the failure to the test runner. Reset after each test. + self._unexpected_exceptions = [] if hasattr(self.parent, 'bidi_events'): - # Use protocol loop to run the async cleanup. - self.parent.loop.run_until_complete(self.parent.bidi_events.unsubscribe_all()) + # If protocol implements `bidi_events`, forward all the events to test_driver. This has + # to be done only once on setup to prevent events duplications. + self.parent.bidi_events.add_event_listener(None, self._process_bidi_event) + def run(self, url, script_resume, test_window=None): if test_window is None: - test_window = self.parent.base.current_window + self._test_window = self.parent.base.current_window + else: + self._test_window = test_window - # Exceptions occurred outside the main loop. - unexpected_exceptions = [] + # Reset exceptions list. + self._unexpected_exceptions = [] if hasattr(self.parent, 'bidi_events'): - # If protocol implements `bidi_events`, forward all the events to test_driver. - async def process_bidi_event(method, params): - try: - self.logger.debug(f"Received bidi event: {method}, {params}") - if hasattr(self.parent, 'bidi_browsing_context') and method == "browsingContext.userPromptOpened" and \ - params["context"] == test_window: - # User prompts of the test window are handled separately. In classic - # implementation, this user prompt always causes an exception when - # `protocol.testdriver.get_next_message()` is called. In BiDi it's not the - # case, as the BiDi protocol allows sending commands even with the user - # prompt opened. However, the user prompt can block the testdriver JS - # execution and cause a dead loop. To overcome this issue, the user prompt - # of the test window is always dismissed and the test is failing. - try: - await self.parent.bidi_browsing_context.handle_user_prompt(params["context"]) - except Exception as e: - if "no such alert" in str(e): - # The user prompt is already dismissed by WebDriver BiDi server. Ignore the exception. - pass - else: - # The exception is unexpected. Re-raising it to handle it in the main loop. - raise e - raise Exception("Unexpected user prompt in test window: %s" % params) - else: - self.send_message(-1, "event", method, json.dumps({ - "params": params, - "method": method})) - except Exception as e: - # As the event listener is async, the exceptions should be added to the list to be processed in the - # main loop. - self.logger.error("BiDi event processing failed: %s" % e) - unexpected_exceptions.append(e) - - self.parent.bidi_events.add_event_listener(None, process_bidi_event) + # Remove all the existing subscriptions. Use protocol loop to run the async cleanup. + self.parent.loop.run_until_complete(self.parent.bidi_events.unsubscribe_all()) + # As long as test runner requires JS execution on the test page, if the alert blocks the + # page, the communication to the test page is blocked. To prevent it, we need to keep + # track of the user prompts. self.parent.loop.run_until_complete(self.parent.bidi_events.subscribe(['browsingContext.userPromptOpened'], None)) # If possible, support async actions. @@ -680,9 +657,9 @@ async def process_bidi_event(method, params): self.webdriver.url = url while True: - if len(unexpected_exceptions) > 0: + if len(self._unexpected_exceptions) > 0: # TODO: what to do if there are more then 1 unexpected exceptions? - raise unexpected_exceptions[0] + raise self._unexpected_exceptions[0] test_driver_message = self.get_next_message(url, script_resume, test_window) self.logger.debug("Receive message from testdriver: %s" % test_driver_message) @@ -717,9 +694,9 @@ async def process_bidi_event(method, params): # Use protocol loop to run the async cleanup. self.parent.loop.run_until_complete(self.parent.bidi_events.unsubscribe_all()) - if len(unexpected_exceptions) > 0: + if len(self._unexpected_exceptions) > 0: # TODO: what to do if there are more then 1 unexpected exceptions? - raise unexpected_exceptions[0] + raise self._unexpected_exceptions[0] return rv @@ -770,6 +747,44 @@ def _get_next_message_bidi(self, url, script_resume, test_window): deserialized_message = bidi_deserialize(message) return deserialized_message + async def _process_bidi_event(self, method, params): + """ + Forwards WebDriver BiDi session's events to testdriver.js. Also automatically handles user + prompts to prevent deadlocks. Any exceptions are added to `self._unexpected_exceptions`. + """ + try: + self.logger.debug(f"Received bidi event: {method}, {params}") + if hasattr(self.parent, 'bidi_browsing_context') and \ + method == "browsingContext.userPromptOpened" and \ + params["context"] == self._test_window: + # Handle user prompts in the test window. In the classic implementation, an open + # user prompt always causes an exception when + # `protocol.testdriver.get_next_message()` is called. In WebDriver BiDi, this is not + # the case, as the protocol allows sending commands even when a user prompt is open. + # However, the prompt can block `testdriver.js` execution, causing a deadlock. To + # prevent this, we automatically dismiss the prompt in the test window and fail the + # test. + try: + await self.parent.bidi_browsing_context.handle_user_prompt(params["context"]) + except Exception as e: + if "no such alert" in str(e): + # The user prompt is already dismissed by WebDriver BiDi server. Ignore the + # exception. + pass + else: + # The exception is unexpected. Re-raising it to handle it in the main loop. + raise e + raise Exception("Unexpected user prompt in test window: %s" % params) + else: + self.send_message(-1, "event", method, json.dumps({ + "params": params, + "method": method})) + except Exception as e: + # As the event listener is async, the exceptions should be added to the list to be + # processed in the main loop. + self.logger.error("BiDi event processing failed: %s" % e) + self._unexpected_exceptions.append(e) + def send_message(self, cmd_id, message_type, status, message=None): self.webdriver.execute_script( self._format_send_message_script(cmd_id, message_type, status, message))