diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /testing/web-platform/tests/generic-sensor | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to '')
10 files changed, 1585 insertions, 0 deletions
diff --git a/testing/web-platform/tests/generic-sensor/META.yml b/testing/web-platform/tests/generic-sensor/META.yml new file mode 100644 index 0000000000..916bc8074f --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/META.yml @@ -0,0 +1,5 @@ +spec: https://w3c.github.io/sensors/ +suggested_reviewers: + - riju + - rakuco + - Honry diff --git a/testing/web-platform/tests/generic-sensor/README.md b/testing/web-platform/tests/generic-sensor/README.md new file mode 100644 index 0000000000..250300b51e --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/README.md @@ -0,0 +1,40 @@ +The `resources/generic-sensor-helpers.js` tests require an implementation of +the `GenericSensorTest` interface, which should emulate platform +sensor backends. The `GenericSensorTest` interface is defined as: + +``` + class MockSensor { + // Sets fake data that is used to deliver sensor reading updates. + async setSensorReading(FrozenArray<double> readingData); + setStartShouldFail(boolean shouldFail); // Sets flag that forces sensor to fail. + getSamplingFrequency(); // Return the sampling frequency. + }; + + class MockSensorProvider { + // Sets flag that forces mock SensorProvider to fail when getSensor() is + // invoked. + setGetSensorShouldFail(DOMString sensorType, boolean shouldFail); + // Sets flag that forces mock SensorProvider to permissions denied when + // getSensor() is invoked. + setPermissionsDenied(DOMString sensorType, boolean permissionsDenied); + getCreatedSensor(DOMString sensorType); // Return `MockSensor` interface. + setMaximumSupportedFrequency(double frequency); // Sets the maximum frequency. + setMinimumSupportedFrequency(double frequency); // Sets the minimum frequency. + } + + class GenericSensorTest { + initialize(); // Sets up the testing environment. + async reset(); // Frees the resources. + getSensorProvider(); // Returns `MockSensorProvider` interface. + }; +``` + +The Chromium implementation of the `GenericSensorTest` interface is located in +[generic_sensor_mocks.js](../resources/chromium/generic_sensor_mocks.js). + +Other browser vendors should provide their own implementations of +the `GenericSensorTest` interface. + +[Known issue](https://github.com/web-platform-tests/wpt/issues/9686): a +WebDriver extension is a better approach for the Generic Sensor tests +automation. diff --git a/testing/web-platform/tests/generic-sensor/SensorErrorEvent-constructor.https.html b/testing/web-platform/tests/generic-sensor/SensorErrorEvent-constructor.https.html new file mode 100644 index 0000000000..2d68dec56d --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/SensorErrorEvent-constructor.https.html @@ -0,0 +1,18 @@ +<!DOCTYPE html> +<title>SensorErrorEvent constructor</title> +<link rel="help" href="https://w3c.github.io/sensors/#the-sensor-error-event-interface"> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script> + test(() => { + assert_equals(SensorErrorEvent.length, 2); + assert_throws_js(TypeError, () => new SensorErrorEvent('error')); + }, 'SensorErrorEvent constructor without init dict'); + + test(() => { + const error = new DOMException; + const event = new SensorErrorEvent('type', { error: error }); + assert_equals(event.type, 'type', 'type'); + assert_equals(event.error, error, 'error'); + }, 'SensorErrorEvent constructor with init dict'); +</script> diff --git a/testing/web-platform/tests/generic-sensor/generic-sensor-feature-policy-test.sub.js b/testing/web-platform/tests/generic-sensor/generic-sensor-feature-policy-test.sub.js new file mode 100644 index 0000000000..9bc46ae936 --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/generic-sensor-feature-policy-test.sub.js @@ -0,0 +1,173 @@ +const feature_policies = { + "AmbientLightSensor" : ["ambient-light-sensor"], + "Accelerometer" : ["accelerometer"], + "LinearAccelerationSensor" : ["accelerometer"], + "GravitySensor" : ["accelerometer"], + "Gyroscope" : ["gyroscope"], + "GeolocationSensor" : ["geolocation"], + "Magnetometer" : ["magnetometer"], + "UncalibratedMagnetometer" : ["magnetometer"], + "AbsoluteOrientationSensor" : ["accelerometer", "gyroscope", "magnetometer"], + "RelativeOrientationSensor" : ["accelerometer", "gyroscope"] +}; + +const same_origin_src = + "/feature-policy/resources/feature-policy-generic-sensor.html#"; +const cross_origin_src = + "https://{{domains[www]}}:{{ports[https][0]}}" + same_origin_src; +const base_src = "/feature-policy/resources/redirect-on-load.html#"; + +function get_feature_policies_for_sensor(sensorType) { + return feature_policies[sensorType]; +} + +function run_fp_tests_disabled(sensorName) { + const sensorType = self[sensorName]; + const featureNameList = feature_policies[sensorName]; + const header = "Feature-Policy header " + featureNameList.join(" 'none';") + " 'none'"; + const desc = "'new " + sensorName + "()'"; + + test(() => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + assert_throws_dom("SecurityError", () => {new sensorType()}); + }, `${sensorName}: ${header} disallows the top-level document.`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + same_origin_src + sensorName, + expect_feature_unavailable_default + ); + }, `${sensorName}: ${header} disallows same-origin iframes.`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + cross_origin_src + sensorName, + expect_feature_unavailable_default + ); + }, `${sensorName}: ${header} disallows cross-origin iframes.`); +} + +function run_fp_tests_enabled(sensorName) { + const featureNameList = feature_policies[sensorName]; + const header = "Feature-Policy header " + featureNameList.join(" *;") + " *"; + const desc = "'new " + sensorName + "()'"; + + test(() => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + }, `${sensorName}: ${header} allows the top-level document.`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + same_origin_src + sensorName, + expect_feature_available_default + ); + }, `${sensorName}: ${header} allows same-origin iframes.`); + + // Set allow="feature;feature;..." on iframe element to delegate features + // under test to cross origin subframe. + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + cross_origin_src + sensorName, + expect_feature_available_default, + feature_policies[sensorName].join(";") + ); + }, `${sensorName}: ${header} allows cross-origin iframes.`); +} + +function run_fp_tests_enabled_by_attribute(sensorName) { + const featureNameList = feature_policies[sensorName]; + const header = "Feature-Policy allow='" + featureNameList.join(" ") + "' attribute"; + const desc = "'new " + sensorName + "()'"; + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + same_origin_src + sensorName, + expect_feature_available_default, + featureNameList.join(";") + ); + }, `${sensorName}: ${header} allows same-origin iframe`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + cross_origin_src + sensorName, + expect_feature_available_default, + featureNameList.join(";") + ); + }, `${sensorName}: ${header} allows cross-origin iframe`); +} + +function run_fp_tests_enabled_by_attribute_redirect_on_load(sensorName) { + const featureNameList = feature_policies[sensorName]; + const header = "Feature-Policy allow='" + featureNameList.join(" ") + "' attribute"; + const desc = "'new " + sensorName + "()'"; + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + base_src + same_origin_src + sensorName, + expect_feature_available_default, + featureNameList.join(";") + ); + }, `${sensorName}: ${header} allows same-origin relocation`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + base_src + cross_origin_src + sensorName, + expect_feature_unavailable_default, + featureNameList.join(";") + ); + }, `${sensorName}: ${header} disallows cross-origin relocation`); +} + +function run_fp_tests_enabled_on_self_origin(sensorName) { + const featureNameList = feature_policies[sensorName]; + const header = "Feature-Policy header " + featureNameList.join(" 'self';") + " 'self'"; + const desc = "'new " + sensorName + "()'"; + + test(() => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + }, `${sensorName}: ${header} allows the top-level document.`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + same_origin_src + sensorName, + expect_feature_available_default + ); + }, `${sensorName}: ${header} allows same-origin iframes.`); + + async_test(t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + test_feature_availability( + desc, + t, + cross_origin_src + sensorName, + expect_feature_unavailable_default + ); + }, `${sensorName}: ${header} disallows cross-origin iframes.`); +} diff --git a/testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js b/testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js new file mode 100644 index 0000000000..ed3415e66e --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js @@ -0,0 +1,324 @@ +function send_message_to_iframe(iframe, message) { + return new Promise((resolve, reject) => { + window.addEventListener('message', (e) => { + // The usage of test_driver.set_test_context() in + // iframe_sensor_handler.html causes unrelated messages to be sent as + // well. We just need to ignore them here. + if (!e.data.command) { + return; + } + + if (e.data.command !== message.command) { + reject(`Expected reply with command '${message.command}', got '${ + e.data.command}' instead`); + return; + } + if (e.data.error) { + reject(e.data.error); + return; + } + resolve(e.data.result); + }); + iframe.contentWindow.postMessage(message, '*'); + }); +} + +function run_generic_sensor_iframe_tests(sensorData, readingData) { + validate_sensor_data(sensorData); + validate_reading_data(readingData); + + const {sensorName, permissionName, testDriverName} = sensorData; + const sensorType = self[sensorName]; + const featurePolicies = get_feature_policies_for_sensor(sensorName); + + // When comparing timestamps in the tests below, we need to account for small + // deviations coming from the way time is coarsened according to the High + // Resolution Time specification, even more so when we need to translate + // timestamps from different documents with different time origins. + // 0.5 is 500 microseconds, which is acceptable enough given that even a high + // sensor frequency beyond what is usually allowed like 100Hz has a period + // much larger than 0.5ms. + const ALLOWED_JITTER_IN_MS = 0.5; + + function sensor_test(func, name, properties) { + promise_test(async t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + const readings = new RingBuffer(readingData.readings); + return func(t, readings); + }, name, properties); + } + + sensor_test(async (t, readings) => { + // This is a specialized EventWatcher that works with a sensor inside a + // cross-origin iframe. We cannot manipulate the sensor object there + // directly from this frame, so we need the iframe to send us a message + // when the "reading" event is fired, and we decide whether we were + // expecting for it or not. This should be instantiated early in the test + // to catch as many unexpected events as possible. + class IframeSensorReadingEventWatcher { + constructor(test_obj) { + this.resolve_ = null; + + window.onmessage = test_obj.step_func((ev) => { + // Unrelated message, ignore. + if (!ev.data.eventName) { + return; + } + + assert_equals( + ev.data.eventName, 'reading', 'Expecting a "reading" event'); + assert_true( + !!this.resolve_, + 'Received "reading" event from iframe but was not expecting one'); + const resolveFunc = this.resolve_; + this.resolve_ = null; + resolveFunc(ev.data.serializedSensor); + }); + } + + wait_for_reading() { + return new Promise(resolve => { + this.resolve_ = resolve; + }); + } + }; + + // Create main frame sensor. + await test_driver.set_permission({name: permissionName}, 'granted'); + await test_driver.create_virtual_sensor(testDriverName); + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + + // Create cross-origin iframe and a sensor inside it. + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicies.join(';') + ';'; + iframe.src = + 'https://{{domains[www1]}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; + const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + t.add_cleanup(async () => { + await send_message_to_iframe(iframe, {command: 'stop_sensor'}); + iframe.parentNode.removeChild(iframe); + }); + await iframeLoadWatcher.wait_for('load'); + const iframeSensorWatcher = new IframeSensorReadingEventWatcher(t); + await send_message_to_iframe( + iframe, {command: 'create_sensor', sensorData}); + + // Start the test by focusing the main frame. It is already focused by + // default, but this makes the test easier to follow. + // When the main frame is focused, it sensor is expected to fire "reading" + // events and provide access to new reading values while the sensor in the + // cross-origin iframe is not. + window.focus(); + + // Start both sensors. They should both have the same state: active, but no + // readings have been provided to them yet. + await send_message_to_iframe(iframe, {command: 'start_sensor'}); + sensor.start(); + await sensorWatcher.wait_for('activate'); + assert_false( + await send_message_to_iframe(iframe, {command: 'has_reading'})); + assert_false(sensor.hasReading); + + // We store `reading` here because we want to make sure the very same + // value is accepted later. + const reading = readings.next().value; + await Promise.all([ + sensorWatcher.wait_for('reading'), + test_driver.update_virtual_sensor(testDriverName, reading), + // Since we do not wait for the iframe sensor's "reading" event, it could + // arguably be delivered later. There are enough async calls happening + // that IframeSensorReadingEventWatcher would end up catching it and + // throwing an error. + ]); + assert_true(sensor.hasReading); + assert_false( + await send_message_to_iframe(iframe, {command: 'has_reading'})); + + // Save sensor data for later before the sensor is stopped. + const savedMainFrameSensorReadings = serialize_sensor_data(sensor); + + sensor.stop(); + await send_message_to_iframe(iframe, {command: 'stop_sensor'}); + + // The sensors are stopped; queue the same reading. The virtual sensor + // would send it anyway, but this update changes its timestamp. + await test_driver.update_virtual_sensor(testDriverName, reading); + + // Now focus the cross-origin iframe. The situation should be the opposite: + // the sensor in the main frame should not fire any "reading" events or + // provide access to updated readings, but the sensor in the iframe should. + iframe.contentWindow.focus(); + + // Start both sensors. Only the iframe sensor should receive a reading + // event and contain readings. + sensor.start(); + await sensorWatcher.wait_for('activate'); + await send_message_to_iframe(iframe, {command: 'start_sensor'}); + const serializedIframeSensor = await iframeSensorWatcher.wait_for_reading(); + assert_true(await send_message_to_iframe(iframe, {command: 'has_reading'})); + assert_false(sensor.hasReading); + + assert_sensor_reading_is_null(sensor); + + assert_sensor_reading_equals( + savedMainFrameSensorReadings, serializedIframeSensor, + {ignoreTimestamps: true}); + + // We could check that serializedIframeSensor.timestamp (adjusted to this + // frame by adding the iframe's timeOrigin and substracting + // performance.timeOrigin) is greater than + // savedMainFrameSensorReadings.timestamp (or other timestamps prior to the + // last test_driver.update_virtual_sensor() call), but this is surprisingly + // tricky and flaky due to the fact that we are using timestamps from + // cross-origin frames. + // + // On Chrome on Windows (M120 at the time of writing), for example, the + // difference between timeOrigin values is sometimes off by more than 10ms + // from the real difference, and allowing for this much jitter makes the + // test not test something meaningful. + }, `${sensorName}: unfocused sensors in cross-origin frames are not updated`); + + sensor_test(async (t, readings) => { + // Create main frame sensor. + await test_driver.set_permission({name: permissionName}, 'granted'); + await test_driver.create_virtual_sensor(testDriverName); + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + + // Create same-origin iframe and a sensor inside it. + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicies.join(';') + ';'; + iframe.src = 'https://{{host}}:{{ports[https][0]}}/resources/blank.html'; + // Create sensor inside same-origin nested browsing context. + const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + t.add_cleanup(() => { + if (iframeSensor) { + iframeSensor.stop(); + } + iframe.parentNode.removeChild(iframe); + }); + await iframeLoadWatcher.wait_for('load'); + // We deliberately create the sensor here instead of using + // send_messge_to_iframe() because this is a same-origin iframe, and we can + // therefore use EventWatcher() to wait for "reading" events a lot more + // easily. + const iframeSensor = new iframe.contentWindow[sensorName](); + const iframeSensorWatcher = + new EventWatcher(t, iframeSensor, ['activate', 'error', 'reading']); + + // Focus a different same-origin window each time and check that everything + // works the same. + for (const windowObject of [window, iframe.contentWindow]) { + await test_driver.update_virtual_sensor( + testDriverName, readings.next().value); + + windowObject.focus(); + + iframeSensor.start(); + sensor.start(); + + await Promise.all([ + iframeSensorWatcher.wait_for(['activate', 'reading']), + sensorWatcher.wait_for(['activate', 'reading']) + ]); + + assert_greater_than( + iframe.contentWindow.performance.timeOrigin, performance.timeOrigin, + 'iframe\'s time origin must be higher than the main window\'s'); + + // Check that the timestamps are similar enough to indicate that this is + // the same reading that was delivered to both sensors. + // The values are not identical due to how high resolution time is + // coarsened. + const translatedIframeSensorTimestamp = iframeSensor.timestamp + + iframe.contentWindow.performance.timeOrigin - performance.timeOrigin; + assert_approx_equals( + translatedIframeSensorTimestamp, sensor.timestamp, + ALLOWED_JITTER_IN_MS); + + // Do not compare timestamps here because of the reasons above. + assert_sensor_reading_equals( + sensor, iframeSensor, {ignoreTimestamps: true}); + + // Stop all sensors so we can use the same value in `reading` on every + // loop iteration. + iframeSensor.stop(); + sensor.stop(); + } + }, `${sensorName}: sensors in same-origin frames are updated if one of the frames is focused`); + + promise_test(async t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicies.join(';') + ';'; + iframe.src = + 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; + + const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + await iframeLoadWatcher.wait_for('load'); + + // Create sensor in the iframe. + await test_driver.set_permission({name: permissionName}, 'granted'); + await test_driver.create_virtual_sensor(testDriverName); + iframe.contentWindow.focus(); + const iframeSensor = new iframe.contentWindow[sensorName](); + t.add_cleanup(async () => { + iframeSensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, iframeSensor, ['activate']); + iframeSensor.start(); + await sensorWatcher.wait_for('activate'); + + // Remove iframe from main document and change focus. When focus changes, + // we need to determine whether a sensor must have its execution suspended + // or resumed (section 4.2.3, "Focused Area" of the Generic Sensor API + // spec). In Blink, this involves querying a frame, which might no longer + // exist at the time of the check. + iframe.parentNode.removeChild(iframe); + window.focus(); + }, `${sensorName}: losing a document's frame with an active sensor does not crash`); + + promise_test(async t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicies.join(';') + ';'; + iframe.src = + 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; + + const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + await iframeLoadWatcher.wait_for('load'); + + // Create sensor in the iframe. + await test_driver.set_permission({name: permissionName}, 'granted'); + await test_driver.create_virtual_sensor(testDriverName); + const iframeSensor = new iframe.contentWindow[sensorName](); + t.add_cleanup(async () => { + iframeSensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + assert_not_equals(iframeSensor, null); + + // Remove iframe from main document. |iframeSensor| no longer has a + // non-null browsing context. Calling start() should probably throw an + // error when called from a non-fully active document, but that depends on + // https://github.com/w3c/sensors/issues/415 + iframe.parentNode.removeChild(iframe); + iframeSensor.start(); + }, `${sensorName}: calling start() in a non-fully active document does not crash`); +} diff --git a/testing/web-platform/tests/generic-sensor/generic-sensor-permission.https.html b/testing/web-platform/tests/generic-sensor/generic-sensor-permission.https.html new file mode 100644 index 0000000000..5edc80026d --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/generic-sensor-permission.https.html @@ -0,0 +1,32 @@ +<!DOCTYPE html> +<meta charset=utf-8> +<title>sensor: permission</title> +<link rel="help" href="https://w3c.github.io/sensors/"/> +<script src="/resources/testharness.js"></script> +<script src="/resources/testharnessreport.js"></script> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script> + +"use strict"; + +for (const entry of ['accelerometer', 'gyroscope', + 'magnetometer', 'ambient-light-sensor']) { + promise_test(async t => { + await test_driver.set_permission({ name: entry }, 'denied'); + + const status = await navigator.permissions.query({ name: entry }); + assert_class_string(status, "PermissionStatus"); + assert_equals(status.state, "denied"); + }, `Deny ${entry} permission should work.`); + + promise_test(async t => { + await test_driver.set_permission({ name: entry }, 'granted'); + + const status = await navigator.permissions.query({ name: entry }); + assert_class_string(status, "PermissionStatus"); + assert_equals(status.state, "granted"); + }, `Grant ${entry} permission should work.`); +}; + +</script>
\ No newline at end of file diff --git a/testing/web-platform/tests/generic-sensor/generic-sensor-tests.js b/testing/web-platform/tests/generic-sensor/generic-sensor-tests.js new file mode 100644 index 0000000000..b4ed22554a --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/generic-sensor-tests.js @@ -0,0 +1,700 @@ +'use strict'; + +// Run a set of tests for a given |sensorName|. +// |readingData| is an object with 3 keys, all of which are arrays of arrays: +// 1. "readings". Each value corresponds to one raw reading that will be +// processed by a sensor. +// 2. "expectedReadings". Each value corresponds to the processed value a +// sensor will make available to users (i.e. a capped or rounded value). +// Its length must match |readings|'. +// 3. "expectedRemappedReadings" (optional). Similar to |expectedReadings|, but +// used only by spatial sensors, whose reference frame can change the values +// returned by a sensor. +// Its length should match |readings|'. +// |verificationFunction| is called to verify that a given reading matches a +// value in |expectedReadings|. +// |featurePolicies| represents |sensorName|'s associated sensor feature name. +function runGenericSensorTests(sensorData, readingData) { + validate_sensor_data(sensorData); + validate_reading_data(readingData); + + const {sensorName, permissionName, testDriverName, featurePolicyNames} = + sensorData; + const sensorType = self[sensorName]; + + function sensor_test(func, name, properties) { + promise_test(async t => { + assert_implements(sensorName in self, `${sensorName} is not supported.`); + + const readings = new RingBuffer(readingData.readings); + const expectedReadings = new RingBuffer(readingData.expectedReadings); + const expectedRemappedReadings = readingData.expectedRemappedReadings ? + new RingBuffer(readingData.expectedRemappedReadings) : + undefined; + + return func(t, readings, expectedReadings, expectedRemappedReadings); + }, name, properties); + } + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'denied'); + + await test_driver.create_virtual_sensor(testDriverName); + const sensor = new sensorType; + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['reading', 'error']); + sensor.start(); + + const event = await sensorWatcher.wait_for('error'); + + assert_false(sensor.activated); + assert_equals(event.error.name, 'NotAllowedError'); + }, `${sensorName}: Test that onerror is sent when permissions are not\ + granted.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName, {connected: false}); + const sensor = new sensorType; + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['reading', 'error']); + + sensor.start(); + + const event = await sensorWatcher.wait_for('error'); + + assert_false(sensor.activated); + assert_equals(event.error.name, 'NotReadableError'); + }, `${sensorName}: Test that onerror is send when start() call has failed.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType({frequency: 560}); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + const mockSensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + + assert_less_than_equal(mockSensorInfo.requestedSamplingFrequency, 60); + }, `${sensorName}: Test that frequency is capped to allowed maximum.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + const maxSupportedFrequency = 5; + await test_driver.create_virtual_sensor( + testDriverName, {maxSamplingFrequency: maxSupportedFrequency}); + + const sensor = new sensorType({frequency: 50}); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + const mockSensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + + assert_equals( + mockSensorInfo.requestedSamplingFrequency, maxSupportedFrequency); + }, `${sensorName}: Test that frequency is capped to the maximum supported\ + frequency.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + const minSupportedFrequency = 2; + await test_driver.create_virtual_sensor( + testDriverName, {minSamplingFrequency: minSupportedFrequency}); + + const sensor = new sensorType({frequency: -1}); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + const mockSensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + + assert_equals( + mockSensorInfo.requestedSamplingFrequency, minSupportedFrequency); + }, `${sensorName}: Test that frequency is limited to the minimum supported\ + frequency.`); + + sensor_test(async t => { + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicyNames.join(' \'none\'; ') + ' \'none\';'; + iframe.srcdoc = '<script>' + + ' window.onmessage = message => {' + + ' if (message.data === "LOADED") {' + + ' try {' + + ' new ' + sensorName + '();' + + ' parent.postMessage("FAIL", "*");' + + ' } catch (e) {' + + ' parent.postMessage(`PASS: got ${e.name}`, "*");' + + ' }' + + ' }' + + ' };' + + '<\/script>'; + const iframeWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + await iframeWatcher.wait_for('load'); + iframe.contentWindow.postMessage('LOADED', '*'); + + const windowWatcher = new EventWatcher(t, window, 'message'); + const message = await windowWatcher.wait_for('message'); + assert_equals(message.data, 'PASS: got SecurityError'); + }, `${sensorName}: Test that sensor cannot be constructed within iframe\ + disallowed to use feature policy.`); + + sensor_test(async t => { + const iframe = document.createElement('iframe'); + iframe.allow = featurePolicyNames.join(';') + ';'; + iframe.srcdoc = '<script>' + + ' window.onmessage = message => {' + + ' if (message.data === "LOADED") {' + + ' try {' + + ' new ' + sensorName + '();' + + ' parent.postMessage("PASS", "*");' + + ' } catch (e) {' + + ' parent.postMessage("FAIL", "*");' + + ' }' + + ' }' + + ' };' + + '<\/script>'; + const iframeWatcher = new EventWatcher(t, iframe, 'load'); + document.body.appendChild(iframe); + await iframeWatcher.wait_for('load'); + iframe.contentWindow.postMessage('LOADED', '*'); + + const windowWatcher = new EventWatcher(t, window, 'message'); + const message = await windowWatcher.wait_for('message'); + assert_equals(message.data, 'PASS'); + }, `${sensorName}: Test that sensor can be constructed within an iframe\ + allowed to use feature policy.`); + + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType; + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + + sensor.start(); + assert_false(sensor.hasReading); + await sensorWatcher.wait_for('activate'); + + await Promise.all([ + test_driver.update_virtual_sensor(testDriverName, readings.next().value), + sensorWatcher.wait_for('reading') + ]); + + assert_sensor_reading_equals(sensor, expectedReadings.next().value); + + assert_true(sensor.hasReading); + + sensor.stop(); + + assert_sensor_reading_is_null(sensor); + assert_false(sensor.hasReading); + }, `${sensorName}: Test that 'onreading' is called and sensor reading is\ + valid.`); + + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor1 = new sensorType(); + const sensor2 = new sensorType(); + t.add_cleanup(async () => { + sensor1.stop(); + sensor2.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher1 = + new EventWatcher(t, sensor1, ['activate', 'reading', 'error']); + const sensorWatcher2 = + new EventWatcher(t, sensor2, ['activate', 'reading', 'error']); + sensor1.start(); + sensor2.start(); + + await Promise.all([ + sensorWatcher1.wait_for('activate'), sensorWatcher2.wait_for('activate') + ]); + + await Promise.all([ + test_driver.update_virtual_sensor(testDriverName, readings.next().value), + sensorWatcher1.wait_for('reading'), sensorWatcher2.wait_for('reading') + ]); + + // Reading values are correct for both sensors. + const expected = expectedReadings.next().value; + assert_sensor_reading_equals(sensor1, expected); + assert_sensor_reading_equals(sensor2, expected); + + // After first sensor stops its reading values are null, + // reading values for the second sensor sensor remain. + sensor1.stop(); + assert_sensor_reading_is_null(sensor1); + assert_sensor_reading_equals(sensor2, expected); + + sensor2.stop(); + assert_sensor_reading_is_null(sensor2); + }, `${sensorName}: sensor reading is correct.`); + + // Tests that readings maps to expectedReadings correctly. Due to threshold + // check and rounding some values might be discarded or changed. + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + + const sensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + const sensorPeriodInMs = (1 / sensorInfo.requestedSamplingFrequency) * 1000; + + for (let expectedReading of expectedReadings.data) { + await update_virtual_sensor_until_reading( + t, readings, sensorWatcher.wait_for('reading'), testDriverName, + sensorPeriodInMs * 3); + assert_true(sensor.hasReading, 'hasReading'); + assert_sensor_reading_equals(sensor, expectedReading); + } + }, `${sensorName}: Test that readings are all mapped to expectedReadings\ + correctly.`); + + sensor_test(async (t, readings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + + const sensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + const sensorPeriodInMs = (1 / sensorInfo.requestedSamplingFrequency) * 1000; + + await Promise.all([ + test_driver.update_virtual_sensor(testDriverName, readings.next().value), + sensorWatcher.wait_for('reading') + ]); + const cachedTimeStamp1 = sensor.timestamp; + + await update_virtual_sensor_until_reading( + t, readings, sensorWatcher.wait_for('reading'), testDriverName, + sensorPeriodInMs * 3); + const cachedTimeStamp2 = sensor.timestamp; + + assert_greater_than(cachedTimeStamp2, cachedTimeStamp1); + }, `${sensorName}: sensor timestamp is updated when time passes.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + assert_false(sensor.activated); + sensor.start(); + assert_false(sensor.activated); + + await sensorWatcher.wait_for('activate'); + assert_true(sensor.activated); + + sensor.stop(); + assert_false(sensor.activated); + }, `${sensorName}: Test that sensor can be successfully created and its\ + states are correct.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + sensor.start(); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + assert_true(sensor.activated); + }, `${sensorName}: no exception is thrown when calling start() on already\ + started sensor.`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + sensor.stop(); + sensor.stop(); + assert_false(sensor.activated); + }, `${sensorName}: no exception is thrown when calling stop() on already\ + stopped sensor.`); + + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = + new EventWatcher(t, sensor, ['activate', 'reading', 'error']); + sensor.start(); + + await sensorWatcher.wait_for('activate'); + + await Promise.all([ + test_driver.update_virtual_sensor(testDriverName, readings.next().value), + sensorWatcher.wait_for('reading') + ]); + + assert_true(sensor.hasReading); + + const expected = expectedReadings.next().value; + assert_sensor_reading_equals(sensor, expected); + + const timestamp = sensor.timestamp; + sensor.stop(); + assert_false(sensor.hasReading); + assert_false(sensor.activated); + + readings.reset(); + await test_driver.update_virtual_sensor( + testDriverName, readings.next().value); + + sensor.start(); + + // Starting |sensor| again will cause the backing virtual sensor to report + // the previous reading automatically. + await sensorWatcher.wait_for('activate'); + await sensorWatcher.wait_for('reading'); + + assert_sensor_reading_equals(sensor, expected); + // Make sure that 'timestamp' is already initialized. + assert_greater_than(timestamp, 0); + // Check that the reading is updated. + assert_greater_than(sensor.timestamp, timestamp); + }, `${sensorName}: Test that fresh reading is fetched on start().`); + + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor = new sensorType(); + t.add_cleanup(async () => { + sensor.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + const sensorWatcher = new EventWatcher(t, sensor, ['activate', 'error']); + + sensor.start(); + await sensorWatcher.wait_for('activate'); + + assert_false(sensor.hasReading); + assert_sensor_reading_is_null(sensor); + + const {minimize, restore} = window_state_context(t); + + await minimize(); + assert_true(document.hidden); + assert_true(sensor.activated); + assert_false(sensor.hasReading); + assert_sensor_reading_is_null(sensor); + + const hiddenEventPromise = new Promise(resolve => { + sensor.addEventListener('reading', t.step_func((event) => { + assert_false(document.hidden); + resolve(event); + }, {once: true})); + }); + + const reading = readings.next().value; + await test_driver.update_virtual_sensor(testDriverName, reading); + + const visibilityChangeEventPromise = + new EventWatcher(t, document, 'visibilitychange') + .wait_for('visibilitychange'); + + const preRestoreTimestamp = performance.now(); + await restore(); + + const readingEvent = await hiddenEventPromise; + + assert_false(document.hidden); + assert_true(sensor.activated); + assert_true(sensor.hasReading); + assert_sensor_reading_equals(sensor, expectedReadings.next().value); + + // Check that a reading sent while the page is hidden is stashed and + // triggers an update only when it is visible again: the original timestamp + // remains, but the event is emitted only after the "visibilitychange" + // event is fired. + assert_less_than( + sensor.timestamp, preRestoreTimestamp, + 'Original sensor timestamp is used even if the update is delayed'); + assert_greater_than( + readingEvent.timeStamp, (await visibilityChangeEventPromise).timeStamp, + 'Sensor "reading" event is always emitted after page visibility is restored'); + }, `${sensorName}: Readings are not delivered when the page has no visibility`); + + sensor_test(async t => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const fastSensor = new sensorType({frequency: 60}); + t.add_cleanup(() => { + fastSensor.stop(); + }); + let eventWatcher = new EventWatcher(t, fastSensor, ['activate']); + fastSensor.start(); + + // Wait for |fastSensor| to be activated so that the call to + // getSamplingFrequency() below works. + await eventWatcher.wait_for('activate'); + + let mockSensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + + // We need |fastSensorFrequency| because 60Hz might be higher than a sensor + // type's maximum allowed frequency. + const fastSensorFrequency = mockSensorInfo.requestedSamplingFrequency; + const slowSensorFrequency = fastSensorFrequency * 0.25; + + const slowSensor = new sensorType({frequency: slowSensorFrequency}); + t.add_cleanup(() => { + slowSensor.stop(); + }); + t.add_cleanup(async () => { + // Remove the virtual sensor only after calling stop() on both sensors. + await test_driver.remove_virtual_sensor(testDriverName); + }); + eventWatcher = new EventWatcher(t, slowSensor, 'activate'); + slowSensor.start(); + + // Wait for |slowSensor| to be activated before we check if the mock + // platform sensor's sampling frequency has changed. + await eventWatcher.wait_for('activate'); + mockSensorInfo = + await test_driver.get_virtual_sensor_information(testDriverName); + assert_equals( + mockSensorInfo.requestedSamplingFrequency, fastSensorFrequency); + + // Now stop |fastSensor| and verify that the sampling frequency has dropped + // to the one |slowSensor| had requested. + fastSensor.stop(); + await wait_for_virtual_sensor_state(testDriverName, (info) => { + return info.requestedSamplingFrequency === slowSensorFrequency; + }); + }, `${sensorName}: frequency hint works.`); + + sensor_test(async (t, readings, expectedReadings) => { + await test_driver.set_permission({name: permissionName}, 'granted'); + + await test_driver.create_virtual_sensor(testDriverName); + + const sensor1 = new sensorType(); + const sensor2 = new sensorType(); + + t.add_cleanup(async () => { + sensor1.stop(); + sensor2.stop(); + await test_driver.remove_virtual_sensor(testDriverName); + }); + + return new Promise(async (resolve, reject) => { + sensor1.addEventListener('reading', () => { + sensor2.addEventListener('activate', () => { + try { + assert_true(sensor1.activated); + assert_true(sensor1.hasReading); + + const expected = expectedReadings.next().value; + assert_sensor_reading_equals(sensor1, expected); + + assert_true(sensor2.activated); + assert_sensor_reading_equals(sensor2, expected); + } catch (e) { + reject(e); + } + }, {once: true}); + sensor2.addEventListener('reading', () => { + try { + assert_true(sensor2.activated); + assert_true(sensor2.hasReading); + assert_sensor_reading_equals(sensor1, sensor2); + assert_equals(sensor1.timestamp, sensor2.timestamp); + resolve(); + } catch (e) { + reject(e); + } + }, {once: true}); + sensor2.start(); + }, {once: true}); + + const eventWatcher = new EventWatcher(t, sensor1, ['activate']); + sensor1.start(); + await eventWatcher.wait_for('activate'); + await test_driver.update_virtual_sensor( + testDriverName, readings.next().value); + }); + }, `${sensorName}: Readings delivered by shared platform sensor are\ + immediately accessible to all sensors.`); + + // Re-enable after https://github.com/w3c/sensors/issues/361 is fixed. + // test(() => { + // assert_throws_dom("NotSupportedError", + // () => { new sensorType({invalid: 1}) }); + // assert_throws_dom("NotSupportedError", + // () => { new sensorType({frequency: 60, invalid: 1}) }); + // if (!expectedRemappedReadings) { + // assert_throws_dom("NotSupportedError", + // () => { new sensorType({referenceFrame: "screen"}) }); + // } + // }, `${sensorName}: throw 'NotSupportedError' for an unsupported sensor\ + // option.`); + + test(() => { + const invalidFreqs = ['invalid', NaN, Infinity, -Infinity, {}]; + invalidFreqs.map(freq => { + assert_throws_js( + TypeError, () => {new sensorType({frequency: freq})}, + `when freq is ${freq}`); + }); + }, `${sensorName}: throw 'TypeError' if frequency is invalid.`); + + if (!readingData.expectedRemappedReadings) { + // The sensorType does not represent a spatial sensor. + return; + } + + // TODO(https://github.com/web-platform-tests/wpt/issues/42724): Re-enable + // when there is a cross-platform way to set an orientation angle. + // sensor_test( + // async (t, readings, expectedReadings, expectedRemappedReadings) => { + // assert_implements_optional(screen.orientation.angle == 270, + // 'Remapped values expect a specific screen rotation.'); + // await test_driver.set_permission({name: permissionName}, 'granted'); + + // await test_driver.create_virtual_sensor(testDriverName); + + // const sensor1 = new sensorType({frequency: 60}); + // const sensor2 = + // new sensorType({frequency: 60, referenceFrame: 'screen'}); + // t.add_cleanup(async () => { + // sensor1.stop(); + // sensor2.stop(); + // await test_driver.remove_virtual_sensor(testDriverName); + // }); + // const sensorWatcher1 = + // new EventWatcher(t, sensor1, ['activate', 'reading', 'error']); + // const sensorWatcher2 = + // new EventWatcher(t, sensor1, ['activate', 'reading', 'error']); + + // sensor1.start(); + // sensor2.start(); + + // await Promise.all([ + // sensorWatcher1.wait_for('activate'), + // sensorWatcher2.wait_for('activate') + // ]); + + // await Promise.all([ + // test_driver.update_virtual_sensor(testDriverName, + // readings.next().value), sensorWatcher1.wait_for('reading'), + // sensorWatcher2.wait_for('reading') + // ]); + + // const expected = expectedReadings.next().value; + // const expectedRemapped = expectedRemappedReadings.next().value; + // assert_sensor_reading_equals(sensor1, expected); + // assert_sensor_reading_equals(sensor2, expectedRemapped); + + // sensor1.stop(); + // assert_sensor_reading_is_null(sensor1); + // assert_sensor_reading_equals(sensor2, expectedRemapped); + + // sensor2.stop(); + // assert_sensor_reading_is_null(sensor2); + // }, + // `${sensorName}: sensor reading is correct when options.referenceFrame\ + // is 'screen'.`); +} + +function runGenericSensorInsecureContext(sensorName) { + test(() => { + assert_false(sensorName in window, `${sensorName} must not be exposed`); + }, `${sensorName} is not exposed in an insecure context.`); +} diff --git a/testing/web-platform/tests/generic-sensor/idlharness.https.window.js b/testing/web-platform/tests/generic-sensor/idlharness.https.window.js new file mode 100644 index 0000000000..db932f35c0 --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/idlharness.https.window.js @@ -0,0 +1,23 @@ +// META: script=/resources/WebIDLParser.js +// META: script=/resources/idlharness.js + +// https://w3c.github.io/sensors/ + +'use strict'; + +function cast(i, t) { + return Object.assign(Object.create(t.prototype), i); +} + +idl_test( + ['generic-sensor'], + ['dom', 'html', 'webidl'], + idl_array => { + idl_array.add_objects({ + Sensor: ['cast(new Accelerometer(), Sensor)'], + SensorErrorEvent: [ + 'new SensorErrorEvent("error", { error: new DOMException });' + ], + }); + } +); diff --git a/testing/web-platform/tests/generic-sensor/resources/generic-sensor-helpers.js b/testing/web-platform/tests/generic-sensor/resources/generic-sensor-helpers.js new file mode 100644 index 0000000000..146f4292ad --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/resources/generic-sensor-helpers.js @@ -0,0 +1,179 @@ +'use strict'; + +// If two doubles differ by less than this amount, we can consider them +// to be effectively equal. +const kEpsilon = 1e-8; + +class RingBuffer { + constructor(data) { + if (!Array.isArray(data)) { + throw new TypeError('`data` must be an array.'); + } + + this.bufferPosition_ = 0; + this.data_ = Array.from(data); + } + + get data() { + return Array.from(this.data_); + } + + next() { + const value = this.data_[this.bufferPosition_]; + this.bufferPosition_ = (this.bufferPosition_ + 1) % this.data_.length; + return {done: false, value: value}; + } + + value() { + return this.data_[this.bufferPosition_]; + } + + [Symbol.iterator]() { + return this; + } + + reset() { + this.bufferPosition_ = 0; + } +}; + +// Calls test_driver.update_virtual_sensor() until it results in a "reading" +// event. It waits |timeoutInMs| before considering that an event has not been +// delivered. +async function update_virtual_sensor_until_reading( + t, readings, readingPromise, testDriverName, timeoutInMs) { + while (true) { + await test_driver.update_virtual_sensor( + testDriverName, readings.next().value); + const value = await Promise.race([ + new Promise( + resolve => {t.step_timeout(() => resolve('TIMEOUT'), timeoutInMs)}), + readingPromise, + ]); + if (value !== 'TIMEOUT') { + break; + } + } +} + +// This could be turned into a t.step_wait() call once +// https://github.com/web-platform-tests/wpt/pull/34289 is merged. +async function wait_for_virtual_sensor_state(testDriverName, predicate) { + const result = + await test_driver.get_virtual_sensor_information(testDriverName); + if (!predicate(result)) { + await wait_for_virtual_sensor_state(testDriverName, predicate); + } +} + +function validate_sensor_data(sensorData) { + if (!('sensorName' in sensorData)) { + throw new TypeError('sensorData.sensorName is missing'); + } + if (!('permissionName' in sensorData)) { + throw new TypeError('sensorData.permissionName is missing'); + } + if (!('testDriverName' in sensorData)) { + throw new TypeError('sensorData.testDriverName is missing'); + } + if (sensorData.featurePolicyNames !== undefined && + !Array.isArray(sensorData.featurePolicyNames)) { + throw new TypeError('sensorData.featurePolicyNames must be an array'); + } +} + +function validate_reading_data(readingData) { + if (!Array.isArray(readingData.readings)) { + throw new TypeError('readingData.readings must be an array.'); + } + if (!Array.isArray(readingData.expectedReadings)) { + throw new TypeError('readingData.expectedReadings must be an array.'); + } + if (readingData.readings.length < readingData.expectedReadings.length) { + throw new TypeError( + 'readingData.readings\' length must be bigger than ' + + 'or equal to readingData.expectedReadings\' length.'); + } + if (readingData.expectedRemappedReadings && + !Array.isArray(readingData.expectedRemappedReadings)) { + throw new TypeError( + 'readingData.expectedRemappedReadings must be an ' + + 'array.'); + } + if (readingData.expectedRemappedReadings && + readingData.expectedReadings.length != + readingData.expectedRemappedReadings.length) { + throw new TypeError( + 'readingData.expectedReadings and ' + + 'readingData.expectedRemappedReadings must have the same ' + + 'length.'); + } +} + +function get_sensor_reading_properties(sensor) { + const className = sensor[Symbol.toStringTag]; + if ([ + 'Accelerometer', 'GravitySensor', 'Gyroscope', + 'LinearAccelerationSensor', 'Magnetometer', 'ProximitySensor' + ].includes(className)) { + return ['x', 'y', 'z']; + } else if (className == 'AmbientLightSensor') { + return ['illuminance']; + } else if ([ + 'AbsoluteOrientationSensor', 'RelativeOrientationSensor' + ].includes(className)) { + return ['quaternion']; + } else { + throw new TypeError(`Unexpected sensor '${className}'`); + } +} + +// Checks that `sensor` and `expectedSensorLike` have the same properties +// (except for timestamp) and they have the same values. +// +// Options allows configuring some aspects of the comparison: +// - ignoreTimestamps (boolean): If true, `sensor` and `expectedSensorLike`'s +// "timestamp" attribute will not be compared. If `expectedSensorLike` does +// not have a "timestamp" attribute, the values will not be compared either. +// This is particularly useful when comparing sensor objects from different +// origins (and consequently different time origins). +function assert_sensor_reading_equals( + sensor, expectedSensorLike, options = {}) { + for (const prop of get_sensor_reading_properties(sensor)) { + assert_true( + prop in expectedSensorLike, + `expectedSensorLike must have a property called '${prop}'`); + if (Array.isArray(sensor[prop])) + assert_array_approx_equals( + sensor[prop], expectedSensorLike[prop], kEpsilon); + else + assert_approx_equals(sensor[prop], expectedSensorLike[prop], kEpsilon); + } + assert_not_equals(sensor.timestamp, null); + + if ('timestamp' in expectedSensorLike && !options.ignoreTimestamps) { + assert_equals( + sensor.timestamp, expectedSensorLike.timestamp, + 'Sensor timestamps must be equal'); + } +} + +function assert_sensor_reading_is_null(sensor) { + for (const prop of get_sensor_reading_properties(sensor)) { + assert_equals(sensor[prop], null); + } + assert_equals(sensor.timestamp, null); +} + +function serialize_sensor_data(sensor) { + const sensorData = {}; + for (const property of get_sensor_reading_properties(sensor)) { + sensorData[property] = sensor[property]; + } + sensorData['timestamp'] = sensor.timestamp; + + // Note that this is not serialized by postMessage(). + sensorData[Symbol.toStringTag] = sensor[Symbol.toStringTag]; + + return sensorData; +} diff --git a/testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html b/testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html new file mode 100644 index 0000000000..80cdcf7c0d --- /dev/null +++ b/testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html @@ -0,0 +1,91 @@ +<!DOCTYPE html> +<meta charset="utf-8"> +<title>cross-origin iframe sensor tester</title> +<script src="/resources/testdriver.js"></script> +<script src="/resources/testdriver-vendor.js"></script> +<script src="/generic-sensor/resources/generic-sensor-helpers.js"></script> +<script> + let sensor = null; + + test_driver.set_test_context(window.parent); + + // This function is defined separately so that it is added only once + // regardless of how many times the 'start_sensor' command is received. + function sensorReadingEventHandler() { + window.parent.postMessage( + { + eventName: 'reading', + serializedSensor: serialize_sensor_data(sensor), + }, '*'); + } + + async function messageHandler(e) { + switch (e.data.command) { + case 'create_sensor': + if (!sensor) { + const { sensorName, permissionName } = e.data.sensorData; + // TODO(https://github.com/w3c/permissions/issues/419): This does not + // work as expected: due to the set_test_context() call above, this + // call goes through the top-level frame, which has a different + // origin in cross-origin tests, meaning that cross-origin tests only + // really work when permissions are granted by default. This can only + // be fixed by testdriver.js allowing set_permission() to specify a + // different origin. + await test_driver.set_permission({ name: permissionName }, 'granted'); + sensor = new self[sensorName](); + } + return Promise.resolve(); + + case 'start_sensor': + return new Promise((resolve, reject) => { + // This event listener is different from the ones below, as it is + // supposed to be used together with IframeSensorReadingEventWatcher. + // It sends a message whenever there is an event, and window.parent + // decides whether it was expected or not. It is the only way to have + // something akin to EventWatcher in a cross-origin iframe. + sensor.addEventListener('reading', sensorReadingEventHandler); + + sensor.addEventListener('activate', () => { + resolve(); + }, { once: true }); + sensor.addEventListener('error', e => { + reject(`${e.error.name}: ${e.error.message}`); + }, { once: true }); + sensor.start(); + }); + + case 'has_reading': + return Promise.resolve(sensor.hasReading); + + case 'stop_sensor': + if (sensor) { + sensor.stop(); + } + return Promise.resolve(); + + default: + return Promise.reject(`unknown command "${e.data.command}"`); + } + } + + window.onmessage = async (e) => { + // The call to test_driver.set_context() above makes messages other than + // those we are specifically waiting for to be delivered too. Ignore those + // here. + if (!e.data.command) { + return; + } + + try { + test_driver.message_test({ + command: e.data.command, + result: await messageHandler(e), + }); + } catch (error) { + test_driver.message_test({ + command: e.data.command, + error, + }); + } + } +</script> |