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