summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/generic-sensor
diff options
context:
space:
mode:
Diffstat (limited to 'testing/web-platform/tests/generic-sensor')
-rw-r--r--testing/web-platform/tests/generic-sensor/META.yml5
-rw-r--r--testing/web-platform/tests/generic-sensor/README.md40
-rw-r--r--testing/web-platform/tests/generic-sensor/SensorErrorEvent-constructor.https.html18
-rw-r--r--testing/web-platform/tests/generic-sensor/generic-sensor-feature-policy-test.sub.js173
-rw-r--r--testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js324
-rw-r--r--testing/web-platform/tests/generic-sensor/generic-sensor-permission.https.html32
-rw-r--r--testing/web-platform/tests/generic-sensor/generic-sensor-tests.js700
-rw-r--r--testing/web-platform/tests/generic-sensor/idlharness.https.window.js23
-rw-r--r--testing/web-platform/tests/generic-sensor/resources/generic-sensor-helpers.js179
-rw-r--r--testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html91
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>