From 55e8b6fdeb8951842847a397e5408fc8da0086df Mon Sep 17 00:00:00 2001 From: Maksim Sadym Date: Mon, 22 Apr 2024 15:14:23 +0200 Subject: [PATCH] Squash https://github.com/web-platform-tests/wpt/pull/44649 --- console/console-log-logged.html | 25 +++ resources/testdriver.js | 53 ++++- .../wptrunner/executors/asyncactions.py | 57 +++++ tools/wptrunner/wptrunner/executors/base.py | 59 +++++ .../wptrunner/executors/executorchrome.py | 6 +- .../wptrunner/executors/executorwebdriver.py | 204 +++++++++++++++++- .../wptrunner/wptrunner/executors/protocol.py | 53 ++++- tools/wptrunner/wptrunner/testdriver-extra.js | 129 +++++++---- 8 files changed, 531 insertions(+), 55 deletions(-) create mode 100644 console/console-log-logged.html create mode 100644 tools/wptrunner/wptrunner/executors/asyncactions.py diff --git a/console/console-log-logged.html b/console/console-log-logged.html new file mode 100644 index 00000000000000..c0588dae924cea --- /dev/null +++ b/console/console-log-logged.html @@ -0,0 +1,25 @@ + + +Test console log are present + + + + + diff --git a/resources/testdriver.js b/resources/testdriver.js index 2d1a89690cc25f..e6c0b300ebf0dd 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -44,11 +44,49 @@ return pointerInteractablePaintTree.indexOf(element) !== -1; } - /** * @namespace {test_driver} */ window.test_driver = { + /** + Represents `WebDriver BiDi `_ protocol. + */ + bidi: { + /** + * `log `_ module. + */ + log: { + /** + * `log.entryAdded `_ event. + */ + entry_added: { + /** + * Subscribe to the `log.entryAdded` event. This does not add actual listeners. To listen to the + * event, use `on` method. + * @param {{contexts?: null | (string | Window)[]}} props - Parameters for the subscription. + * * `contexts`: an array of window proxies or browsing context ids to listen to the event. If not + * provided, the event subscription is done for the current window's browsing context. `null` for + * the global subscription. + * @return {Promise} + */ + subscribe: async function (props = {}) { + return window.test_driver_internal.bidi.log.entry_added.subscribe(props); + }, + /** + * Add an event listener for the `log.entryAdded + * `_ event. Make sure `subscribe` is + * called before using this method. + * + * @param callback {function(event): void} - The callback to be called when the event is fired. + * @returns {function(): void} - A function to call to remove the event listener. + */ + on: function (callback) { + return window.test_driver_internal.bidi.log.entry_added.on(callback); + }, + } + } + }, + /** * Set the context in which testharness.js is loaded * @@ -1078,6 +1116,19 @@ */ in_automation: false, + bidi: { + log: { + entry_added: { + subscribe: function () { + throw new Error("bidi.log.entry_added.subscribe is not implemented by testdriver-vendor.js"); + }, + on: function () { + throw new Error("bidi.log.entry_added.on is not implemented by testdriver-vendor.js"); + } + } + } + }, + async click(element, coords) { if (this.in_automation) { throw new Error("click() is not implemented by testdriver-vendor.js"); diff --git a/tools/wptrunner/wptrunner/executors/asyncactions.py b/tools/wptrunner/wptrunner/executors/asyncactions.py new file mode 100644 index 00000000000000..735aa017ce1611 --- /dev/null +++ b/tools/wptrunner/wptrunner/executors/asyncactions.py @@ -0,0 +1,57 @@ +# mypy: allow-untyped-defs +import sys + +from typing import Dict, List, Literal, Optional, Union + + +# TODO: check if type annotation is supported by all the required versions of Python. +# noinspection PyCompatibility +class WindowProxyProperties(Dict): + context: str + + +# TODO: check if type annotation is supported by all the required versions of Python. +# noinspection PyCompatibility +class WindowProxyRemoteValue(Dict): + """ + WebDriver BiDi browsing context descriptor. + """ + type: Literal["window"] + value: WindowProxyProperties + + +class BidiSessionSubscribeAction: + name = "bidi.session.subscribe" + + # TODO: check if type annotation is supported by all the required versions of Python. + # noinspection PyCompatibility + class Payload(Dict): + """ + Payload for the "bidi.session.subscribe" action. + events: List of event names to subscribe to. + contexts: Optional list of browsing contexts to subscribe to. Each context can be either a BiDi serialized value, + or a string. The latter is considered as a browsing context id. + """ + events: List[str] + contexts: Optional[List[Union[str, WindowProxyRemoteValue]]] + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + async def __call__(self, payload: Payload): + events = payload["events"] + contexts = None + if payload["contexts"] is not None: + contexts = [] + for c in payload["contexts"]: + if isinstance(c, str): + contexts.append(c) + elif isinstance(c, dict) and "type" in c and c["type"] == "window": + contexts.append(c["value"]["context"]) + else: + raise ValueError("Unexpected context type: %s" % c) + return await self.protocol.bidi_events.subscribe(events, contexts) + + +async_actions = [BidiSessionSubscribeAction] diff --git a/tools/wptrunner/wptrunner/executors/base.py b/tools/wptrunner/wptrunner/executors/base.py index 92a782e835c11b..d8c2024a9a38d9 100644 --- a/tools/wptrunner/wptrunner/executors/base.py +++ b/tools/wptrunner/wptrunner/executors/base.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs +import asyncio import base64 import hashlib import io @@ -15,6 +16,7 @@ from . import pytestrunner from .actions import actions +from .asyncactions import async_actions from .protocol import Protocol, WdspecProtocol @@ -786,6 +788,63 @@ def _send_message(self, cmd_id, message_type, status, message=None): self.protocol.testdriver.send_message(cmd_id, message_type, status, message=message) +class AsyncCallbackHandler(CallbackHandler): + """ + Handle synchronous and asynchronous actions. Extends `CallbackHandler` with support of async actions. + """ + + def __init__(self, logger, protocol, test_window, loop): + super().__init__(logger, protocol, test_window) + self.loop = loop + self.async_actions = {cls.name: cls(self.logger, self.protocol) for cls in async_actions} + + def process_action(self, url, payload): + action = payload["action"] + if action in self.async_actions: + # Schedule async action to be processed in the event loop and return immediately. + self.logger.debug(f"Scheduling async action processing: {action}, {payload}") + self.loop.create_task(self._process_async_action(action, payload)) + return False, None + else: + # Fallback to the default action processing, which will fail if the action is not implemented. + self.logger.debug(f"Processing synchronous action: {action}, {payload}") + return super().process_action(url, payload) + + async def _process_async_action(self, action, payload): + """ + Process async action and send the result back to the test driver. + + This method is analogous to `process_action` but is intended to be used with async actions in a task, so it does + not raise unexpected exceptions. + """ + async_action_handler = self.async_actions[action] + cmd_id = payload["id"] + try: + result = await async_action_handler(payload) + except AttributeError as e: + # If we fail to get an attribute from the protocol presumably that's a + # ProtocolPart we don't implement + # AttributeError got an obj property in Python 3.10, for older versions we + # fall back to looking at the error message. + if ((hasattr(e, "obj") and getattr(e, "obj") == self.protocol) or + f"'{self.protocol.__class__.__name__}' object has no attribute" in str(e)): + raise NotImplementedError from e + except self.unimplemented_exc: + self.logger.warning("Action %s not implemented" % action) + self._send_message(cmd_id, "complete", "error", f"Action {action} not implemented") + except self.expected_exc: + self.logger.debug(f"Action {action} failed with an expected exception") + self._send_message(cmd_id, "complete", "error", f"Action {action} failed") + except Exception: + self.logger.warning(f"Action {action} failed") + self._send_message(cmd_id, "complete", "error") + raise + else: + self.logger.debug(f"Action {action} completed with result {result}") + return_message = {"result": result} + self._send_message(cmd_id, "complete", "success", json.dumps(return_message)) + + class ActionContext: def __init__(self, logger, protocol, context): self.logger = logger diff --git a/tools/wptrunner/wptrunner/executors/executorchrome.py b/tools/wptrunner/wptrunner/executors/executorchrome.py index f5641471562b65..a01cb36b54a33a 100644 --- a/tools/wptrunner/wptrunner/executors/executorchrome.py +++ b/tools/wptrunner/wptrunner/executors/executorchrome.py @@ -16,7 +16,7 @@ from .executorwebdriver import ( WebDriverCrashtestExecutor, WebDriverFedCMProtocolPart, - WebDriverProtocol, + WebDriverBidiProtocol, WebDriverRefTestExecutor, WebDriverRun, WebDriverTestharnessExecutor, @@ -202,12 +202,12 @@ def confirm_idp_login(self): self.fedcm_company_prefix + "/fedcm/confirmidplogin") -class ChromeDriverProtocol(WebDriverProtocol): +class ChromeDriverProtocol(WebDriverBidiProtocol): implements = [ ChromeDriverFedCMProtocolPart, ChromeDriverPrintProtocolPart, ChromeDriverTestharnessProtocolPart, - *(part for part in WebDriverProtocol.implements + *(part for part in WebDriverBidiProtocol.implements if part.name != ChromeDriverTestharnessProtocolPart.name and part.name != ChromeDriverFedCMProtocolPart.name) ] diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index 69013e5e796979..331a25e9a3b1f6 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -1,5 +1,6 @@ # mypy: allow-untyped-defs +import asyncio import json import os import socket @@ -9,7 +10,8 @@ import uuid from urllib.parse import urljoin -from .base import (CallbackHandler, +from .base import (AsyncCallbackHandler, + CallbackHandler, CrashtestExecutor, RefTestExecutor, RefTestImplementation, @@ -35,6 +37,8 @@ RPHRegistrationsProtocolPart, FedCMProtocolPart, VirtualSensorProtocolPart, + BidiEventsProtocolPart, + BidiScriptProtocolPart, DevicePostureProtocolPart, merge_dicts) @@ -49,6 +53,11 @@ class WebDriverCallbackHandler(CallbackHandler): expected_exc = (error.WebDriverException,) +class WebDriverAsyncCallbackHandler(AsyncCallbackHandler): + unimplemented_exc = (NotImplementedError, error.UnknownCommandException) + expected_exc = (error.WebDriverException,) + + class WebDriverBaseProtocolPart(BaseProtocolPart): def setup(self): self.webdriver = self.parent.webdriver @@ -101,6 +110,49 @@ def wait(self): return False +class WebDriverBidiEventsProtocolPart(BidiEventsProtocolPart): + _subscriptions = [] + + def __init__(self, parent): + super().__init__(parent) + self.webdriver = None + + def setup(self): + self.webdriver = self.parent.webdriver + + async def subscribe(self, events, contexts): + self.logger.info("Subscribing to events %s in %s" % (events, contexts)) + self._subscriptions.append((events, contexts)) + return await self.webdriver.bidi_session.session.subscribe(events=events, contexts=contexts) + + async def unsubscribe_all(self): + self.logger.info("Unsubscribing from all the events") + while self._subscriptions: + events, contexts = self._subscriptions.pop() + self.logger.info("Unsubscribing from events %s in %s" % (events, contexts)) + await self.webdriver.bidi_session.session.unsubscribe(events=events, contexts=contexts) + + def add_event_listener(self, fn, event=None): + self.logger.info("adding event listener %s" % event) + return self.webdriver.bidi_session.add_event_listener(name=event, fn=fn) + + +class WebDriverBidiScriptProtocolPart(BidiScriptProtocolPart): + def __init__(self, parent): + super().__init__(parent) + self.webdriver = None + + def setup(self): + self.webdriver = self.parent.webdriver + + async def call_function(self, function_declaration, target, arguments=None): + return await self.webdriver.bidi_session.script.call_function( + function_declaration=function_declaration, + arguments=arguments, + target=target, + await_promise=True) + + class WebDriverTestharnessProtocolPart(TestharnessProtocolPart): def setup(self): self.webdriver = self.parent.webdriver @@ -444,6 +496,7 @@ def clear_device_posture(self): return self.webdriver.send_session_command("DELETE", "deviceposture") class WebDriverProtocol(Protocol): + enable_bidi = False implements = [WebDriverBaseProtocolPart, WebDriverTestharnessProtocolPart, WebDriverSelectorProtocolPart, @@ -488,13 +541,13 @@ def __init__(self, executor, browser, capabilities, **kwargs): self.webdriver = None def connect(self): - """Connect to browser via WebDriver.""" + """Connect to browser via WebDriver and crete a WebDriver session.""" self.logger.debug("Connecting to WebDriver on URL: %s" % self.url) host, port = self.url.split(":")[1].strip("/"), self.url.split(':')[-1].strip("/") capabilities = {"alwaysMatch": self.capabilities} - self.webdriver = Session(host, port, capabilities=capabilities) + self.webdriver = Session(host, port, capabilities=capabilities, enable_bidi=self.enable_bidi) self.webdriver.start() def teardown(self): @@ -525,6 +578,34 @@ def after_connect(self): self.testharness.load_runner(self.executor.last_environment["protocol"]) +class WebDriverBidiProtocol(WebDriverProtocol): + enable_bidi = True + implements = [WebDriverBidiEventsProtocolPart, + WebDriverBidiScriptProtocolPart, + *(part for part in WebDriverProtocol.implements) + ] + + def __init__(self, executor, browser, capabilities, **kwargs): + super().__init__(executor, browser, capabilities, **kwargs) + self.loop = asyncio.new_event_loop() + + def connect(self): + super().connect() + self.loop.run_until_complete(self.webdriver.bidi_session.start(self.loop)) + + def teardown(self): + try: + self.loop.run_until_complete(self.webdriver.bidi_session.end()) + except Exception as e: + message = str(getattr(e, "message", "")) + if message: + message += "\n" + message += traceback.format_exc() + self.logger.debug(message) + self.loop.stop() + super().teardown() + + class WebDriverRun(TimedRunner): def set_timeout(self): try: @@ -604,10 +685,17 @@ def do_test(self, test): return (test.make_result(*data), []) def do_testharness(self, protocol, url, timeout): + # TODO: respect timeout. + # The previous test may not have closed its old windows (if something # went wrong or if cleanup_after_test was False), so clean up here. parent_window = protocol.testharness.close_old_windows() + # If protocol implements `bidi_events`, remove all the existing subscriptions. + if protocol.bidi_events: + # Use protocol loop to run the async cleanup. + protocol.loop.run_until_complete(protocol.bidi_events.unsubscribe_all()) + # Now start the test harness protocol.testharness.open_test_window(self.window_id) test_window = protocol.testharness.get_test_window(self.window_id, @@ -618,23 +706,37 @@ def do_testharness(self, protocol, url, timeout): # Wait until about:blank has been loaded protocol.base.execute_script(self.window_loaded_script, asynchronous=True) - handler = WebDriverCallbackHandler(self.logger, protocol, test_window) + if protocol.bidi_events: + # If protocol implements `bidi_events`, forward all the events to test_driver. + async def process_bidi_event(method, params): + print("bidi event received", method, params) + protocol.testdriver.send_message(-1, "event", method, json.dumps({ + "params": params, + "method": method})) + + protocol.bidi_events.add_event_listener(process_bidi_event) + + # If possible, support async actions. + if protocol.loop: + handler = WebDriverAsyncCallbackHandler(self.logger, protocol, test_window, protocol.loop) + else: + handler = WebDriverCallbackHandler(self.logger, protocol, test_window) + protocol.webdriver.url = url while True: - result = protocol.base.execute_script( - self.script_resume, asynchronous=True, args=[strip_server(url)]) + test_driver_message = self._get_next_message(protocol, url, test_window) + self.logger.debug("Receive message from testdriver: %s" % test_driver_message) # As of 2019-03-29, WebDriver does not define expected behavior for # cases where the browser crashes during script execution: # # https://github.com/w3c/webdriver/issues/1308 - if not isinstance(result, list) or len(result) != 3: + if not isinstance(test_driver_message, list) or len(test_driver_message) != 3: try: is_alive = self.is_alive() except error.WebDriverException: is_alive = False - if not is_alive: raise Exception("Browser crashed during script execution.") @@ -645,13 +747,18 @@ def do_testharness(self, protocol, url, timeout): # # [1]: Step 5.3 of https://www.w3.org/TR/webdriver/#execute-async-script # [2]: https://www.w3.org/TR/webdriver/#dfn-execute-a-function-body - if result is None: + if test_driver_message is None: continue - done, rv = handler(result) + done, rv = handler(test_driver_message) if done: break + # If protocol implements `bidi_events`, remove all the existing subscriptions. + if protocol.bidi_events: + # Use protocol loop to run the async cleanup. + protocol.loop.run_until_complete(protocol.bidi_events.unsubscribe_all()) + # Attempt to cleanup any leftover windows, if allowed. This is # preferable as it will blame the correct test if something goes wrong # closing windows, but if the user wants to see the test results we @@ -661,6 +768,83 @@ def do_testharness(self, protocol, url, timeout): return rv + def bidi_deserialize(self, bidi_value): + """ + Deserialize the BiDi value to the Python value, keeping non-common data typs (window) in BiDi format. The result + can have collisions with the classic values. + :param bidi_value: BiDi value to deserialize. + """ + if isinstance(bidi_value, str): + return bidi_value + if isinstance(bidi_value, int): + return bidi_value + if not isinstance(bidi_value, dict): + raise ValueError("Unexpected bidi value: %s" % bidi_value) + if bidi_value["type"] == "null": + return None + if bidi_value["type"] == "boolean": + return bidi_value["value"] + if bidi_value["type"] == "number": + # TODO: extend with edge case values, like `Infinity`. + return bidi_value["value"] + if bidi_value["type"] == "string": + return bidi_value["value"] + if bidi_value["type"] == "array": + result = [] + for item in bidi_value["value"]: + result.append(self.bidi_deserialize(item)) + return result + if bidi_value["type"] == "object": + result = {} + for item in bidi_value["value"]: + result[self.bidi_deserialize(item[0])] = self.bidi_deserialize(item[1]) + return result + if bidi_value["type"] == "window": + return bidi_value + # TODO: probably should return bidi value as-is, like `window` instead of raising exception. Keep for now to + # check any regression in classic values. + raise ValueError("Unexpected bidi value: %s" % bidi_value) + + def _get_next_message(self, protocol, url, test_window): + """ + Get the next message from the test_driver. If the protocol supports bidi scripts, the messages are processed + asynchronously, otherwise the messages are processed synchronously. + """ + if protocol.bidi_script: + # If `bidi_script` is available, use it as async call allows to process the events from the + # test_runner to test_driver while waiting for the next test_driver commands. + + # As long as we want to be able to use scripts both in bidi and in classic mode, the script should + # be wrapped to some harness to emulate the WebDriver Classic async script execution. The script + # will be provided with the `resolve` delegate, which finishes the execution. After that the + # coroutine is finished as well. + wrapped_script = """async function(...args){ + return new Promise((resolve, reject) => { + args.push(resolve); + (async function(){ + %s + }).apply(null, args); + }) + }""" % self.script_resume + + bidi_url_argument = { + "type": "string", + "value": strip_server(url) + } + + # `run_until_complete` allows processing BiDi events in the same loop while waiting for the next message. + message = protocol.loop.run_until_complete(protocol.bidi_script.call_function( + wrapped_script, target={ + "context": test_window + }, + arguments=[bidi_url_argument])) + # The message is in WebDriver BiDi format. Deserialize it. + deserialized_message = self.bidi_deserialize(message) + return deserialized_message + else: + # If `bidi_script` is not available, use the classic WebDriver async script execution. This will + # block the event loop until the test_driver send a message. + return protocol.base.execute_script(self.script_resume, asynchronous=True, args=[strip_server(url)]) class WebDriverRefTestExecutor(RefTestExecutor): protocol_cls = WebDriverProtocol diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py index 3d588738b6e005..fcd949195652b4 100644 --- a/tools/wptrunner/wptrunner/executors/protocol.py +++ b/tools/wptrunner/wptrunner/executors/protocol.py @@ -4,7 +4,7 @@ from http.client import HTTPConnection from abc import ABCMeta, abstractmethod -from typing import ClassVar, List, Type +from typing import Any, Awaitable, Callable, ClassVar, List, Mapping, Optional, Type def merge_dicts(target, source): @@ -19,6 +19,7 @@ def merge_dicts(target, source): else: target[key] = source_value + class Protocol: """Backend for a specific browser-control protocol. @@ -321,6 +322,56 @@ def get_computed_role(self, element): pass +class BidiEventsProtocolPart(ProtocolPart): + """Protocol part for managing BiDi events""" + __metaclass__ = ABCMeta + name = "bidi_events" + + @abstractmethod + async def subscribe(self, events, contexts): + """Subscribe to events. + + :param list events: The list of events names to subscribe to. + :param list|None contexts: The list of contexts ids to subscribe to. None for global subscription.""" + pass + + @abstractmethod + async def unsubscribe_all(self): + """Cleans up the subscription state. Removes all the previously added subscriptions.""" + pass + + @abstractmethod + def add_event_listener( + self, + fn: Callable[[str, Mapping[str, Any]], Awaitable[Any]], + event: Optional[str] = None + ) -> Callable[[], None]: + """Add an event listener. The callback will be called with the event name and the event data. + + :param fn: The function to call when the event is received. + :param event: The name of the event to listen for. If None, the function will be called for all events. + :return: Function to remove the added listener.""" + pass + + +class BidiScriptProtocolPart(ProtocolPart): + """Protocol part for executing BiDi scripts""" + __metaclass__ = ABCMeta + + name = "bidi_script" + + @abstractmethod + async def call_function(self, function_declaration, target, arguments=None): + """ + Executes the provided script in the given target in asynchronous mode. + + :param str function_declaration: The js source of the function to execute. + :param script.Target target: The target in which to execute the script. + :param list[script.LocalValue] arguments: The arguments to pass to the script. + """ + pass + + class CookiesProtocolPart(ProtocolPart): """Protocol part for managing cookies""" __metaclass__ = ABCMeta diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 87d3826bfceb6a..3c98963df27e12 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -2,6 +2,7 @@ (function() { const pending = new Map(); + const event_target = new EventTarget(); let result = null; let ctx_cmd_id = 0; @@ -39,6 +40,12 @@ if (is_test_context()) { window.__wptrunner_process_next_event(); } + } else if (data.type === "testdriver-event") { + const event_data = JSON.parse(data.message); + const event_name = event_data.method; + const event = new Event(event_name); + event.payload = event_data.params; + event_target.dispatchEvent(event); } }); @@ -125,23 +132,23 @@ return selector; }; + /** + * Create an action and return a promise that resolves when the action is complete. + * @param name: The name of the action to create. + * @param props: The properties to pass to the action. + * @return {Promise}: A promise that resolves with the action result when the action is complete. + */ const create_action = function(name, props) { let cmd_id; const action_msg = {type: "action", action: name, ...props}; - if (action_msg.context) { - action_msg.context = get_window_id(action_msg.context); - } if (is_test_context()) { cmd_id = window.__wptrunner_message_queue.push(action_msg); } else { if (testharness_context === null) { throw new Error("Tried to run in a non-testharness window without a call to set_test_context"); } - if (action_msg.context === null) { - action_msg.context = get_window_id(window); - } cmd_id = ctx_cmd_id++; action_msg.cmd_id = cmd_id; window.test_driver.message_test({type: "testdriver-command", @@ -160,8 +167,50 @@ return pending_promise; }; + /** + * Create an action in a specific context and return a promise that resolves when the action is complete. This is + * required for WebDriver Classic actions, as they require a specific context. + * @param name: The name of the action to create. + * @param context: The context in which to run the action. `null` for the current window. + * @param props: The properties to pass to the action. + * @return {Promise}: A promise that resolves with the action result when the action is complete. + */ + const create_context_action = function (name, context, props) { + const context_props = {...props}; + if (context) { + context_props.context = get_window_id(context_props.context); + } + if (context === null && !is_test_context()) { + context_props.context = get_window_id(window); + } + return create_action(name, context_props); + }; + + const subscribe = function (props) { + return create_action("bidi.session.subscribe", { + // Default to subscribing to the window's events. + contexts: [window], + ...props + }); + }; + window.test_driver_internal.in_automation = true; + window.test_driver_internal.bidi.log.entry_added.subscribe = function (props) { + return subscribe({ + ...(props ?? {}), + events: ["log.entryAdded"] + }) + }; + + window.test_driver_internal.bidi.log.entry_added.on = function (callback) { + const on_event = (event)=> { + callback(event.payload); + }; + event_target.addEventListener("log.entryAdded", on_event); + return () => event_target.removeEventListener("log.entryAdded", on_event); + }; + window.test_driver_internal.set_test_context = function(context) { if (window.__wptrunner_message_queue) { throw new Error("Tried to set testharness context in a window containing testharness.js"); @@ -172,49 +221,49 @@ window.test_driver_internal.click = function(element) { const selector = get_selector(element); const context = get_context(element); - return create_action("click", {selector, context}); + return create_context_action("click", context, {selector}); }; window.test_driver_internal.delete_all_cookies = function(context=null) { - return create_action("delete_all_cookies", {context}); + return create_context_action("delete_all_cookies", context, {}); }; window.test_driver_internal.get_all_cookies = function(context=null) { - return create_action("get_all_cookies", {context}); + return create_context_action("get_all_cookies", context, {}); }; window.test_driver_internal.get_computed_label = function(element) { const selector = get_selector(element); const context = get_context(element); - return create_action("get_computed_label", {selector, context}); + return create_context_action("get_computed_label", context, {selector}); }; window.test_driver_internal.get_computed_role = function(element) { const selector = get_selector(element); const context = get_context(element); - return create_action("get_computed_role", {selector, context}); + return create_context_action("get_computed_role", context, {selector}); }; window.test_driver_internal.get_named_cookie = function(name, context=null) { - return create_action("get_named_cookie", {name, context}); + return create_context_action("get_named_cookie", context, {name}); }; window.test_driver_internal.minimize_window = function(context=null) { - return create_action("minimize_window", {context}); + return create_context_action("minimize_window", context, {}); }; window.test_driver_internal.set_window_rect = function(rect, context=null) { - return create_action("set_window_rect", {rect, context}); + return create_context_action("set_window_rect", context, {rect}); }; window.test_driver_internal.get_window_rect = function(context=null) { - return create_action("get_window_rect", {context}); + return create_context_action("get_window_rect", context, {}); }; window.test_driver_internal.send_keys = function(element, keys) { const selector = get_selector(element); const context = get_context(element); - return create_action("send_keys", {selector, keys, context}); + return create_context_action("send_keys", context, {selector, keys}); }; window.test_driver_internal.action_sequence = function(actions, context=null) { @@ -233,99 +282,99 @@ } } } - return create_action("action_sequence", {actions, context}); + return create_context_action("action_sequence", context, {actions}); }; window.test_driver_internal.generate_test_report = function(message, context=null) { - return create_action("generate_test_report", {message, context}); + return create_context_action("generate_test_report", context, {message}); }; window.test_driver_internal.set_permission = function(permission_params, context=null) { - return create_action("set_permission", {permission_params, context}); + return create_context_action("set_permission", context, {permission_params}); }; window.test_driver_internal.add_virtual_authenticator = function(config, context=null) { - return create_action("add_virtual_authenticator", {config, context}); + return create_context_action("add_virtual_authenticator", context, {config}); }; window.test_driver_internal.remove_virtual_authenticator = function(authenticator_id, context=null) { - return create_action("remove_virtual_authenticator", {authenticator_id, context}); + return create_context_action("remove_virtual_authenticator", context, {authenticator_id}); }; window.test_driver_internal.add_credential = function(authenticator_id, credential, context=null) { - return create_action("add_credential", {authenticator_id, credential, context}); + return create_context_action("add_credential", context, {authenticator_id, credential}); }; window.test_driver_internal.get_credentials = function(authenticator_id, context=null) { - return create_action("get_credentials", {authenticator_id, context}); + return create_context_action("get_credentials", context, {authenticator_id}); }; window.test_driver_internal.remove_credential = function(authenticator_id, credential_id, context=null) { - return create_action("remove_credential", {authenticator_id, credential_id, context}); + return create_context_action("remove_credential", context, {authenticator_id, credential_id}); }; window.test_driver_internal.remove_all_credentials = function(authenticator_id, context=null) { - return create_action("remove_all_credentials", {authenticator_id, context}); + return create_context_action("remove_all_credentials", context, {authenticator_id}); }; window.test_driver_internal.set_user_verified = function(authenticator_id, uv, context=null) { - return create_action("set_user_verified", {authenticator_id, uv, context}); + return create_context_action("set_user_verified", context, {authenticator_id, uv}); }; window.test_driver_internal.set_spc_transaction_mode = function(mode, context = null) { - return create_action("set_spc_transaction_mode", {mode, context}); + return create_context_action("set_spc_transaction_mode", context, {mode}); }; window.test_driver_internal.set_rph_registration_mode = function(mode, context = null) { - return create_action("set_rph_registration_mode", {mode, context}); + return create_context_action("set_rph_registration_mode", context, {mode}); }; window.test_driver_internal.cancel_fedcm_dialog = function(context = null) { - return create_action("cancel_fedcm_dialog", {context}); + return create_context_action("cancel_fedcm_dialog", context, {}); }; window.test_driver_internal.click_fedcm_dialog_button = function(dialog_button, context = null) { - return create_action("click_fedcm_dialog_button", {dialog_button, context}); + return create_context_action("click_fedcm_dialog_button", context, {dialog_button}); }; window.test_driver_internal.select_fedcm_account = function(account_index, context = null) { - return create_action("select_fedcm_account", {account_index, context}); + return create_context_action("select_fedcm_account", context, {account_index}); }; window.test_driver_internal.get_fedcm_account_list = function(context = null) { - return create_action("get_fedcm_account_list", {context}); + return create_context_action("get_fedcm_account_list", context, {}); }; window.test_driver_internal.get_fedcm_dialog_title = function(context = null) { - return create_action("get_fedcm_dialog_title", {context}); + return create_context_action("get_fedcm_dialog_title", context, {}); }; window.test_driver_internal.get_fedcm_dialog_type = function(context = null) { - return create_action("get_fedcm_dialog_type", {context}); + return create_context_action("get_fedcm_dialog_type", context, {}); }; window.test_driver_internal.set_fedcm_delay_enabled = function(enabled, context = null) { - return create_action("set_fedcm_delay_enabled", {enabled, context}); + return create_context_action("set_fedcm_delay_enabled", context, {enabled}); }; window.test_driver_internal.reset_fedcm_cooldown = function(context = null) { - return create_action("reset_fedcm_cooldown", {context}); + return create_context_action("reset_fedcm_cooldown", context, {}); }; window.test_driver_internal.create_virtual_sensor = function(sensor_type, sensor_params={}, context=null) { - return create_action("create_virtual_sensor", {sensor_type, sensor_params, context}); + return create_context_action("create_virtual_sensor", context, {sensor_type, sensor_params}); }; window.test_driver_internal.update_virtual_sensor = function(sensor_type, reading, context=null) { - return create_action("update_virtual_sensor", {sensor_type, reading, context}); + return create_context_action("update_virtual_sensor", context, {sensor_type, reading}); }; window.test_driver_internal.remove_virtual_sensor = function(sensor_type, context=null) { - return create_action("remove_virtual_sensor", {sensor_type, context}); + return create_context_action("remove_virtual_sensor", context, {sensor_type}); }; window.test_driver_internal.get_virtual_sensor_information = function(sensor_type, context=null) { - return create_action("get_virtual_sensor_information", {sensor_type, context}); + return create_context_action("get_virtual_sensor_information", context, {sensor_type}); }; window.test_driver_internal.set_device_posture = function(posture, context=null) {