From fd100e93bf8d8c09849172b36cb628bec906f8da Mon Sep 17 00:00:00 2001 From: Raphael Kubo da Costa Date: Mon, 11 Sep 2023 11:06:37 +0200 Subject: [PATCH] sensors: Add virtual sensor-related commands to testdriver Spec PR: https://github.com/w3c/sensors/pull/470 The Generic Sensor spec used to have an Automation section, but it leaked implementation-specific details and was not implemented anywhere. The changes above have been implemented in Chromium and there are pending patches there to convert the existing Generic Sensor web tests in WPT to the new virtual sensor model, which entirely removes the dependency on Mojo mocks and makes the tests more interoperable. This PR adds the required infrastructure to manipulate virtual sensors from testdriver. The 4 new commands correspond to the 4 WebDriver extension commands added by the spec PR above. This change was co-authored with @JuhaVainio. Related to #9686. --- docs/writing-tests/testdriver.md | 8 + resources/testdriver.js | 159 ++++++++++++++++++ tools/wptrunner/wptrunner/browsers/chrome.py | 3 + .../wptrunner/wptrunner/executors/actions.py | 61 ++++++- .../wptrunner/executors/executormarionette.py | 21 ++- .../wptrunner/executors/executorwebdriver.py | 24 ++- .../wptrunner/wptrunner/executors/protocol.py | 23 +++ tools/wptrunner/wptrunner/testdriver-extra.js | 28 ++- 8 files changed, 318 insertions(+), 9 deletions(-) diff --git a/docs/writing-tests/testdriver.md b/docs/writing-tests/testdriver.md index d5f7f8d3667bed..00bfefc4f2e210 100644 --- a/docs/writing-tests/testdriver.md +++ b/docs/writing-tests/testdriver.md @@ -111,6 +111,14 @@ the global scope. .. js:autofunction:: test_driver.reset_fedcm_cooldown ``` +### Sensors ### +```eval_rst +.. js:autofunction:: test_driver.create_virtual_sensor +.. js:autofunction:: test_driver.update_virtual_sensor +.. js:autofunction:: test_driver.remove_virtual_sensor +.. js:autofunction:: test_driver.get_virtual_sensor_information +``` + ### Using test_driver in other browsing contexts ### Testdriver can be used in browsing contexts (i.e. windows or frames) diff --git a/resources/testdriver.js b/resources/testdriver.js index 4b8656b276e6a6..0327e1759a9efe 100644 --- a/resources/testdriver.js +++ b/resources/testdriver.js @@ -824,6 +824,149 @@ */ reset_fedcm_cooldown: function(context=null) { return window.test_driver_internal.reset_fedcm_cooldown(context); + }, + + /** + * Creates a virtual sensor for use with the Generic Sensors APIs. + * + * Matches the `Create Virtual Sensor + * `_ + * WebDriver command. + * + * Once created, a virtual sensor is available to all navigables under + * the same top-level traversable (i.e. all frames in the same page, + * regardless of origin). + * + * @param {String} sensor_type - A `virtual sensor type + * `_ + * such as "accelerometer". + * @param {Object} [sensor_params={}] - Optional parameters described + * in `Create Virtual Sensor + * `_. + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled when virtual sensor is created. + * Rejected in case the WebDriver command errors out + * (including if a virtual sensor of the same type + * already exists). + */ + create_virtual_sensor: function(sensor_type, sensor_params={}, context=null) { + return window.test_driver_internal.create_virtual_sensor(sensor_type, sensor_params, context); + }, + + /** + * Causes a virtual sensor to report a new reading to any connected + * platform sensor. + * + * Matches the `Update Virtual Sensor Reading + * `_ + * WebDriver command. + * + * Note: The ``Promise`` it returns may fulfill before or after a + * "reading" event is fired. When using + * :js:func:`EventWatcher.wait_for`, it is necessary to take this into + * account: + * + * Note: New values may also be discarded due to the checks in `update + * latest reading + * `_. + * + * @example + * // Avoid races between EventWatcher and update_virtual_sensor(). + * // This assumes you are sure this reading will be processed (see + * // the example below otherwise). + * const reading = { x: 1, y: 2, z: 3 }; + * await Promise.all([ + * test_driver.update_virtual_sensor('gyroscope', reading), + * watcher.wait_for('reading') + * ]); + * + * @example + * // Do not wait forever if you are not sure the reading will be + * // processed. + * const readingPromise = watcher.wait_for('reading'); + * const timeoutPromise = new Promise(resolve => { + * t.step_timeout(() => resolve('TIMEOUT', 3000)) + * }); + * + * const reading = { x: 1, y: 2, z: 3 }; + * await test_driver.update_virtual_sensor('gyroscope', 'reading'); + * + * const value = + * await Promise.race([timeoutPromise, readingPromise]); + * if (value !== 'TIMEOUT') { + * // Do something. The "reading" event was fired. + * } + * + * @param {String} sensor_type - A `virtual sensor type + * `_ + * such as "accelerometer". + * @param {Object} reading - An Object describing a reading in a format + * dependent on ``sensor_type`` (e.g. ``{x: + * 1, y: 2, z: 3}`` or ``{ illuminance: 42 + * }``). + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the reading update reaches the + * virtual sensor. Rejected in case the WebDriver + * command errors out (including if a virtual sensor + * of the given type does not exist). + */ + update_virtual_sensor: function(sensor_type, reading, context=null) { + return window.test_driver_internal.update_virtual_sensor(sensor_type, reading, context); + }, + + /** + * Triggers the removal of a virtual sensor if it exists. + * + * Matches the `Delete Virtual Sensor + * `_ + * WebDriver command. + * + * @param {String} sensor_type - A `virtual sensor type + * `_ + * such as "accelerometer". + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled after the virtual sensor has been + * removed or if a sensor of the given type does not + * exist. Rejected in case the WebDriver command + * errors out. + + */ + remove_virtual_sensor: function(sensor_type, context=null) { + return window.test_driver_internal.remove_virtual_sensor(sensor_type, context); + }, + + /** + * Returns information about a virtual sensor. + * + * Matches the `Get Virtual Sensor Information + * `_ + * WebDriver command. + * + * @param {String} sensor_type - A `virtual sensor type + * `_ + * such as "accelerometer". + * @param {WindowProxy} [context=null] - Browsing context in which to + * run the call, or null for the + * current browsing context. + * + * @returns {Promise} Fulfilled with an Object with the properties + * described in `Get Virtual Sensor Information + * `_. + * Rejected in case the WebDriver command errors out + * (including if a virtual sensor of the given type + * does not exist). + */ + get_virtual_sensor_information: function(sensor_type, context=null) { + return window.test_driver_internal.get_virtual_sensor_information(sensor_type, context); } }; @@ -980,6 +1123,22 @@ async reset_fedcm_cooldown(context=null) { throw new Error("reset_fedcm_cooldown() is not implemented by testdriver-vendor.js"); + }, + + async create_virtual_sensor(sensor_type, sensor_params, context=null) { + throw new Error("create_virtual_sensor() is not implemented by testdriver-vendor.js"); + }, + + async update_virtual_sensor(sensor_type, reading, context=null) { + throw new Error("update_virtual_sensor() is not implemented by testdriver-vendor.js"); + }, + + async remove_virtual_sensor(sensor_type, context=null) { + throw new Error("remove_virtual_sensor() is not implemented by testdriver-vendor.js"); + }, + + async get_virtual_sensor_information(sensor_type, context=null) { + throw new Error("get_virtual_sensor_information() is not implemented by testdriver-vendor.js"); } }; })(); diff --git a/tools/wptrunner/wptrunner/browsers/chrome.py b/tools/wptrunner/wptrunner/browsers/chrome.py index 5e954912544197..889a841b3096c8 100644 --- a/tools/wptrunner/wptrunner/browsers/chrome.py +++ b/tools/wptrunner/wptrunner/browsers/chrome.py @@ -107,6 +107,9 @@ def executor_kwargs(logger, test_type, test_environment, run_info_data, chrome_options["args"].append("--enable-features=SecurePaymentConfirmationBrowser") # For WebTransport tests. chrome_options["args"].append("--webtransport-developer-mode") + # The GenericSensorExtraClasses flag enables the browser-side + # implementation of sensors such as Ambient Light Sensor. + chrome_options["args"].append("--enable-features=GenericSensorExtraClasses") # Classify `http-private`, `http-public` and https variants in the # appropriate IP address spaces. diff --git a/tools/wptrunner/wptrunner/executors/actions.py b/tools/wptrunner/wptrunner/executors/actions.py index e622c6c67d936c..d9875535b253bb 100644 --- a/tools/wptrunner/wptrunner/executors/actions.py +++ b/tools/wptrunner/wptrunner/executors/actions.py @@ -367,6 +367,61 @@ def __call__(self, payload): self.logger.debug("Resetting FedCM cooldown") return self.protocol.fedcm.reset_fedcm_cooldown() + +class CreateVirtualSensorAction: + name = "create_virtual_sensor" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + sensor_type = payload["sensor_type"] + sensor_params = payload["sensor_params"] + self.logger.debug("Creating %s sensor with %s values" % (sensor_type, sensor_params)) + return self.protocol.virtual_sensor.create_virtual_sensor(sensor_type, sensor_params) + + +class UpdateVirtualSensorAction: + name = "update_virtual_sensor" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + sensor_type = payload["sensor_type"] + reading = payload["reading"] + self.logger.debug("Updating %s sensor with new readings: %s" % (sensor_type, reading)) + return self.protocol.virtual_sensor.update_virtual_sensor(sensor_type, reading) + + +class RemoveVirtualSensorAction: + name = "remove_virtual_sensor" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + sensor_type = payload["sensor_type"] + self.logger.debug("Removing %s sensor" % sensor_type) + return self.protocol.virtual_sensor.remove_virtual_sensor(sensor_type) + + +class GetVirtualSensorInformationAction: + name = "get_virtual_sensor_information" + + def __init__(self, logger, protocol): + self.logger = logger + self.protocol = protocol + + def __call__(self, payload): + sensor_type = payload["sensor_type"] + self.logger.debug("Requesting information from %s sensor" % sensor_type) + return self.protocol.virtual_sensor.get_virtual_sensor_information(sensor_type) + + actions = [ClickAction, DeleteAllCookiesAction, GetAllCookiesAction, @@ -394,4 +449,8 @@ def __call__(self, payload): GetFedCMDialogTitleAction, GetFedCMDialogTypeAction, SetFedCMDelayEnabledAction, - ResetFedCMCooldownAction] + ResetFedCMCooldownAction, + CreateVirtualSensorAction, + UpdateVirtualSensorAction, + RemoveVirtualSensorAction, + GetVirtualSensorInformationAction] diff --git a/tools/wptrunner/wptrunner/executors/executormarionette.py b/tools/wptrunner/wptrunner/executors/executormarionette.py index eaeaddf412303e..05567a0b99d56a 100644 --- a/tools/wptrunner/wptrunner/executors/executormarionette.py +++ b/tools/wptrunner/wptrunner/executors/executormarionette.py @@ -44,6 +44,7 @@ SetPermissionProtocolPart, PrintProtocolPart, DebugProtocolPart, + VirtualSensorProtocolPart, merge_dicts) @@ -720,6 +721,23 @@ def get_computed_role(self, element): return element.computed_role +class MarionetteVirtualSensorProtocolPart(VirtualSensorProtocolPart): + def setup(self): + self.marionette = self.parent.marionette + + def create_virtual_sensor(self, sensor_type, sensor_params): + raise NotImplementedError("create_virtual_sensor not yet implemented") + + def update_virtual_sensor(self, sensor_type, reading): + raise NotImplementedError("update_virtual_sensor not yet implemented") + + def remove_virtual_sensor(self, remove_parameters): + raise NotImplementedError("remove_virtual_sensor not yet implemented") + + def get_virtual_sensor_information(self, information_parameters): + raise NotImplementedError("get_virtual_sensor_information not yet implemented") + + class MarionetteProtocol(Protocol): implements = [MarionetteBaseProtocolPart, MarionetteTestharnessProtocolPart, @@ -739,7 +757,8 @@ class MarionetteProtocol(Protocol): MarionetteSetPermissionProtocolPart, MarionettePrintProtocolPart, MarionetteDebugProtocolPart, - MarionetteAccessibilityProtocolPart] + MarionetteAccessibilityProtocolPart, + MarionetteVirtualSensorProtocolPart] def __init__(self, executor, browser, capabilities=None, timeout_multiplier=1, e10s=True, ccov=False): do_delayed_imports() diff --git a/tools/wptrunner/wptrunner/executors/executorwebdriver.py b/tools/wptrunner/wptrunner/executors/executorwebdriver.py index ff174a4e0ca44e..e3760b8d5f8974 100644 --- a/tools/wptrunner/wptrunner/executors/executorwebdriver.py +++ b/tools/wptrunner/wptrunner/executors/executorwebdriver.py @@ -33,6 +33,7 @@ DebugProtocolPart, SPCTransactionsProtocolPart, FedCMProtocolPart, + VirtualSensorProtocolPart, merge_dicts) from webdriver.client import Session @@ -396,6 +397,26 @@ def load_devtools(self): raise NotImplementedError() +class WebDriverVirtualSensorPart(VirtualSensorProtocolPart): + def setup(self): + self.webdriver = self.parent.webdriver + + def create_virtual_sensor(self, sensor_type, sensor_params): + body = {"type": sensor_type} + body.update(sensor_params) + return self.webdriver.send_session_command("POST", "sensor", body) + + def update_virtual_sensor(self, sensor_type, reading): + body = {"reading": reading} + return self.webdriver.send_session_command("POST", "sensor/%s" % sensor_type, body) + + def remove_virtual_sensor(self, sensor_type): + return self.webdriver.send_session_command("DELETE", "sensor/%s" % sensor_type) + + def get_virtual_sensor_information(self, sensor_type): + return self.webdriver.send_session_command("GET", "sensor/%s" % sensor_type) + + class WebDriverProtocol(Protocol): implements = [WebDriverBaseProtocolPart, WebDriverTestharnessProtocolPart, @@ -412,7 +433,8 @@ class WebDriverProtocol(Protocol): WebDriverVirtualAuthenticatorProtocolPart, WebDriverSPCTransactionsProtocolPart, WebDriverFedCMProtocolPart, - WebDriverDebugProtocolPart] + WebDriverDebugProtocolPart, + WebDriverVirtualSensorPart] def __init__(self, executor, browser, capabilities, **kwargs): super().__init__(executor, browser) diff --git a/tools/wptrunner/wptrunner/executors/protocol.py b/tools/wptrunner/wptrunner/executors/protocol.py index abf3191cd2f57c..cac25e4e67219b 100644 --- a/tools/wptrunner/wptrunner/executors/protocol.py +++ b/tools/wptrunner/wptrunner/executors/protocol.py @@ -760,3 +760,26 @@ def is_alive(self): conn.request("HEAD", "/invalid") res = conn.getresponse() return res.status == 404 + + +class VirtualSensorProtocolPart(ProtocolPart): + """Protocol part for Sensors""" + __metaclass__ = ABCMeta + + name = "virtual_sensor" + + @abstractmethod + def create_virtual_sensor(self, sensor_type, sensor_params): + pass + + @abstractmethod + def update_virtual_sensor(self, sensor_type, reading): + pass + + @abstractmethod + def remove_virtual_sensor(self, sensor_type): + pass + + @abstractmethod + def get_virtual_sensor_information(self, sensor_type): + pass diff --git a/tools/wptrunner/wptrunner/testdriver-extra.js b/tools/wptrunner/wptrunner/testdriver-extra.js index 8e44800d6cb7ec..29c661512da37b 100644 --- a/tools/wptrunner/wptrunner/testdriver-extra.js +++ b/tools/wptrunner/wptrunner/testdriver-extra.js @@ -268,11 +268,11 @@ window.test_driver_internal.set_spc_transaction_mode = function(mode, context = null) { return create_action("set_spc_transaction_mode", {mode, context}); }; - + window.test_driver_internal.cancel_fedcm_dialog = function(context = null) { return create_action("cancel_fedcm_dialog", {context}); }; - + window.test_driver_internal.confirm_idp_login = function(context = null) { return create_action("confirm_idp_login", {context}); }; @@ -280,19 +280,19 @@ window.test_driver_internal.select_fedcm_account = function(account_index, context = null) { return create_action("select_fedcm_account", {account_index, context}); }; - + window.test_driver_internal.get_fedcm_account_list = function(context = null) { return create_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}); }; - + window.test_driver_internal.get_fedcm_dialog_type = function(context = null) { return create_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}); }; @@ -300,4 +300,20 @@ window.test_driver_internal.reset_fedcm_cooldown = function(context = null) { return create_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}); + }; + + window.test_driver_internal.update_virtual_sensor = function(sensor_type, reading, context=null) { + return create_action("update_virtual_sensor", {sensor_type, reading, context}); + }; + + window.test_driver_internal.remove_virtual_sensor = function(sensor_type, context=null) { + return create_action("remove_virtual_sensor", {sensor_type, context}); + }; + + window.test_driver_internal.get_virtual_sensor_information = function(sensor_type, context=null) { + return create_action("get_virtual_sensor_information", {sensor_type, context}); + }; })();