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.js186
-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.js553
-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.js142
-rw-r--r--testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html63
10 files changed, 1235 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..1d1a012380
--- /dev/null
+++ b/testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js
@@ -0,0 +1,186 @@
+function send_message_to_iframe(iframe, message, reply) {
+ if (reply === undefined) {
+ reply = 'success';
+ }
+
+ return new Promise((resolve, reject) => {
+ window.addEventListener('message', (e) => {
+ if (e.data.command !== message.command) {
+ reject(`Expected reply with command '${message.command}', got '${e.data.command}' instead`);
+ return;
+ }
+ if (e.data.result === reply) {
+ resolve();
+ } else {
+ reject(`Got unexpected reply '${e.data.result}' to command '${message.command}', expected '${reply}'`);
+ }
+ }, { once: true });
+ iframe.contentWindow.postMessage(message, '*');
+ });
+}
+
+function run_generic_sensor_iframe_tests(sensorName) {
+ const sensorType = self[sensorName];
+ const featurePolicies = get_feature_policies_for_sensor(sensorName);
+
+ sensor_test(async t => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const iframe = document.createElement('iframe');
+ iframe.allow = featurePolicies.join(';') + ';';
+ iframe.src = 'https://{{domains[www1]}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html';
+
+ // Create sensor inside cross-origin nested browsing context.
+ const iframeLoadWatcher = new EventWatcher(t, iframe, 'load');
+ document.body.appendChild(iframe);
+ t.add_cleanup(async () => {
+ await send_message_to_iframe(iframe, { command: 'reset_sensor_backend' });
+ iframe.parentNode.removeChild(iframe);
+ });
+ await iframeLoadWatcher.wait_for('load');
+ await send_message_to_iframe(iframe, {command: 'create_sensor',
+ type: sensorName});
+
+ // Focus on the main frame and test that sensor receives readings.
+ window.focus();
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ['reading', 'error']);
+ sensor.start();
+
+ await sensorWatcher.wait_for('reading');
+ const cachedTimeStamp = sensor.timestamp;
+
+ // Focus on the cross-origin frame and verify that sensor reading updates in
+ // the top level browsing context are suspended.
+ iframe.contentWindow.focus();
+ await send_message_to_iframe(iframe, {command: 'start_sensor'});
+
+ // Focus on the main frame, verify that sensor reading updates are resumed.
+ window.focus();
+ await sensorWatcher.wait_for('reading');
+ assert_greater_than(sensor.timestamp, cachedTimeStamp);
+ sensor.stop();
+
+ // Verify that sensor in cross-origin frame is suspended.
+ await send_message_to_iframe(iframe, {command: 'is_sensor_suspended'}, true);
+ }, `${sensorName}: sensor is suspended and resumed when focus traverses from\
+ to cross-origin frame`);
+
+ sensor_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';
+
+ // Create sensor inside same-origin nested browsing context.
+ const iframeLoadWatcher = new EventWatcher(t, iframe, 'load');
+ document.body.appendChild(iframe);
+ t.add_cleanup(async () => {
+ await send_message_to_iframe(iframe, { command: 'reset_sensor_backend' });
+ iframe.parentNode.removeChild(iframe);
+ });
+ await iframeLoadWatcher.wait_for('load');
+ await send_message_to_iframe(iframe, {command: 'create_sensor',
+ type: sensorName});
+
+ // Focus on main frame and test that sensor receives readings.
+ window.focus();
+ const sensor = new sensorType({
+ // generic_sensor_mocks.js uses a default frequency of 5Hz for sensors.
+ // We deliberately use a higher frequency here to make it easier to spot
+ // spurious, unexpected 'reading' events caused by the main frame's
+ // sensor not stopping early enough.
+ // TODO(rakuco): Create a constant with the 5Hz default frequency instead
+ // of using magic numbers.
+ frequency: 15
+ });
+ const sensorWatcher = new EventWatcher(t, sensor, ['reading', 'error']);
+ sensor.start();
+ await sensorWatcher.wait_for('reading');
+ let cachedTimeStamp = sensor.timestamp;
+
+ // Stop sensor in main frame, so that sensorWatcher would not receive
+ // readings while sensor in iframe is started. Sensors that are active and
+ // belong to the same-origin context are not suspended automatically when
+ // focus changes to another same-origin iframe, so if we do not explicitly
+ // stop them we may receive extra 'reading' events that cause the test to
+ // fail (see e.g. https://crbug.com/857520).
+ sensor.stop();
+
+ iframe.contentWindow.focus();
+ await send_message_to_iframe(iframe, {command: 'start_sensor'});
+
+ // Start sensor on main frame, verify that readings are updated.
+ window.focus();
+ sensor.start();
+ await sensorWatcher.wait_for('reading');
+ assert_greater_than(sensor.timestamp, cachedTimeStamp);
+ cachedTimeStamp = sensor.timestamp;
+ sensor.stop();
+
+ // Verify that sensor in nested browsing context is not suspended.
+ await send_message_to_iframe(iframe, {command: 'is_sensor_suspended'}, false);
+
+ // Verify that sensor in top level browsing context is receiving readings.
+ iframe.contentWindow.focus();
+ sensor.start();
+ await sensorWatcher.wait_for('reading');
+ assert_greater_than(sensor.timestamp, cachedTimeStamp);
+ sensor.stop();
+ }, `${sensorName}: sensor is not suspended when focus traverses from\
+ to same-origin frame`);
+
+ sensor_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';
+
+ // Create sensor in the iframe (we do not care whether this is a
+ // cross-origin nested context in this test).
+ const iframeLoadWatcher = new EventWatcher(t, iframe, 'load');
+ document.body.appendChild(iframe);
+ await iframeLoadWatcher.wait_for('load');
+ await send_message_to_iframe(iframe, {command: 'create_sensor',
+ type: sensorName});
+ iframe.contentWindow.focus();
+ await send_message_to_iframe(iframe, {command: 'start_sensor'});
+
+ // 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.
+ // Note that we cannot send the "reset_sensor_backend" command because the
+ // iframe is discarded with the removeChild call.
+ iframe.parentNode.removeChild(iframe);
+ window.focus();
+ }, `${sensorName}: losing a document's frame with an active sensor does not crash`);
+
+ sensor_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';
+
+ // Create sensor in the iframe (we do not care whether this is a
+ // cross-origin nested context in this test).
+ const iframeLoadWatcher = new EventWatcher(t, iframe, 'load');
+ document.body.appendChild(iframe);
+ await iframeLoadWatcher.wait_for('load');
+
+ // The purpose of this message is to initialize the mock backend in the
+ // iframe. We are not going to use the sensor created there.
+ await send_message_to_iframe(iframe, {command: 'create_sensor',
+ type: sensorName});
+
+ const iframeSensor = new iframe.contentWindow[sensorName]();
+ 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..3c8f478c54
--- /dev/null
+++ b/testing/web-platform/tests/generic-sensor/generic-sensor-tests.js
@@ -0,0 +1,553 @@
+'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(sensorName,
+ readingData,
+ verificationFunction,
+ featurePolicies) {
+ const sensorType = self[sensorName];
+
+ function validateReadingFormat(data) {
+ return Array.isArray(data) && data.every(element => Array.isArray(element));
+ }
+
+ const { readings, expectedReadings, expectedRemappedReadings } = readingData;
+ if (!validateReadingFormat(readings)) {
+ throw new TypeError('readingData.readings must be an array of arrays.');
+ }
+ if (!validateReadingFormat(expectedReadings)) {
+ throw new TypeError('readingData.expectedReadings must be an array of ' +
+ 'arrays.');
+ }
+ if (readings.length < expectedReadings.length) {
+ throw new TypeError('readingData.readings\' length must be bigger than ' +
+ 'or equal to readingData.expectedReadings\' length.');
+ }
+ if (expectedRemappedReadings &&
+ !validateReadingFormat(expectedRemappedReadings)) {
+ throw new TypeError('readingData.expectedRemappedReadings must be an ' +
+ 'array of arrays.');
+ }
+ if (expectedRemappedReadings &&
+ expectedReadings.length != expectedRemappedReadings.length) {
+ throw new TypeError('readingData.expectedReadings and ' +
+ 'readingData.expectedRemappedReadings must have the same ' +
+ 'length.');
+ }
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ sensorProvider.setGetSensorShouldFail(sensorName, true);
+ const sensor = new sensorType;
+ 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 sent when sensor is not supported.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ sensorProvider.setPermissionsDenied(sensorName, true);
+ const sensor = new sensorType;
+ 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, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType({frequency: 560});
+ const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setStartShouldFail(true);
+
+ 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, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType({frequency: 560});
+ const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+
+ await sensorWatcher.wait_for("activate");
+
+ assert_less_than_equal(mockSensor.getSamplingFrequency(), 60);
+ sensor.stop();
+ assert_false(sensor.activated);
+ }, `${sensorName}: Test that frequency is capped to allowed maximum.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const maxSupportedFrequency = 5;
+ sensorProvider.setMaximumSupportedFrequency(maxSupportedFrequency);
+ const sensor = new sensorType({frequency: 50});
+ const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+
+ await sensorWatcher.wait_for("activate");
+
+ assert_equals(mockSensor.getSamplingFrequency(), maxSupportedFrequency);
+ sensor.stop();
+ assert_false(sensor.activated);
+ }, `${sensorName}: Test that frequency is capped to the maximum supported\
+ frequency.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const minSupportedFrequency = 2;
+ sensorProvider.setMinimumSupportedFrequency(minSupportedFrequency);
+ const sensor = new sensorType({frequency: -1});
+ const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+
+ await sensorWatcher.wait_for("activate");
+
+ assert_equals(mockSensor.getSamplingFrequency(), minSupportedFrequency);
+ sensor.stop();
+ assert_false(sensor.activated);
+ }, `${sensorName}: Test that frequency is limited to the minimum supported\
+ frequency.`);
+
+ promise_test(async t => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const iframe = document.createElement('iframe');
+ iframe.allow = featurePolicies.join(' \'none\'; ') + ' \'none\';';
+ iframe.srcdoc = '<script>' +
+ ' window.onmessage = message => {' +
+ ' if (message.data === "LOADED") {' +
+ ' try {' +
+ ' new ' + sensorName + '();' +
+ ' parent.postMessage("FAIL", "*");' +
+ ' } catch (e) {' +
+ ' parent.postMessage("PASS", "*");' +
+ ' }' +
+ ' }' +
+ ' };' +
+ '<\/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 cannot be constructed within iframe\
+ disallowed to use feature policy.`);
+
+ promise_test(async t => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const iframe = document.createElement('iframe');
+ iframe.allow = featurePolicies.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, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
+ sensor.start();
+ assert_false(sensor.hasReading);
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ await sensorWatcher.wait_for("reading");
+ const expected = new RingBuffer(expectedReadings).next().value;
+ assert_true(verificationFunction(expected, sensor));
+ assert_true(sensor.hasReading);
+
+ sensor.stop();
+ assert_true(verificationFunction(expected, sensor, /*isNull=*/true));
+ assert_false(sensor.hasReading);
+ }, `${sensorName}: Test that 'onreading' is called and sensor reading is\
+ valid.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor1 = new sensorType();
+ const sensorWatcher1 = new EventWatcher(t, sensor1, ["reading", "error"]);
+ sensor1.start();
+
+ const sensor2 = new sensorType();
+ const sensorWatcher2 = new EventWatcher(t, sensor2, ["reading", "error"]);
+ sensor2.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ await Promise.all([sensorWatcher1.wait_for("reading"),
+ sensorWatcher2.wait_for("reading")]);
+ const expected = new RingBuffer(expectedReadings).next().value;
+ // Reading values are correct for both sensors.
+ assert_true(verificationFunction(expected, sensor1));
+ assert_true(verificationFunction(expected, sensor2));
+
+ // After first sensor stops its reading values are null,
+ // reading values for the second sensor sensor remain.
+ sensor1.stop();
+ assert_true(verificationFunction(expected, sensor1, /*isNull=*/true));
+ assert_true(verificationFunction(expected, sensor2));
+
+ sensor2.stop();
+ assert_true(verificationFunction(expected, sensor2, /*isNull=*/true));
+ }, `${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, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ await mockSensor.setSensorReading(readings);
+
+ for (let expectedReading of expectedReadings) {
+ await sensorWatcher.wait_for("reading");
+ assert_true(sensor.hasReading, "hasReading");
+ assert_true(verificationFunction(expectedReading, sensor),
+ "verification");
+ }
+
+ sensor.stop();
+ }, `${sensorName}: Test that readings are all mapped to expectedReadings\
+ correctly.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ await sensorWatcher.wait_for("reading");
+ const cachedTimeStamp1 = sensor.timestamp;
+
+ await sensorWatcher.wait_for("reading");
+ const cachedTimeStamp2 = sensor.timestamp;
+
+ assert_greater_than(cachedTimeStamp2, cachedTimeStamp1);
+ sensor.stop();
+ }, `${sensorName}: sensor timestamp is updated when time passes.`);
+
+ sensor_test(async t => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ 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 => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]);
+ sensor.start();
+ sensor.start();
+
+ await sensorWatcher.wait_for("activate");
+ assert_true(sensor.activated);
+ sensor.stop();
+ }, `${sensorName}: no exception is thrown when calling start() on already\
+ started sensor.`);
+
+ sensor_test(async t => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ 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, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ const expectedBuffer = new RingBuffer(expectedReadings);
+ await sensorWatcher.wait_for("reading");
+ const expected1 = expectedBuffer.next().value;
+ assert_true(sensor.hasReading);
+ assert_true(verificationFunction(expected1, sensor));
+ const timestamp = sensor.timestamp;
+ sensor.stop();
+ assert_false(sensor.hasReading);
+
+ sensor.start();
+ await sensorWatcher.wait_for("reading");
+ assert_true(sensor.hasReading);
+ // |readingData| may have a single reading/expectation value, and this
+ // is the second reading we are getting. For that case, make sure we
+ // also wrap around as if we had the same RingBuffer used in
+ // generic_sensor_mocks.js.
+ const expected2 = expectedBuffer.next().value;
+ assert_true(verificationFunction(expected2, sensor));
+ // Make sure that 'timestamp' is already initialized.
+ assert_greater_than(timestamp, 0);
+ // Check that the reading is updated.
+ assert_greater_than(sensor.timestamp, timestamp);
+ sensor.stop();
+ }, `${sensorName}: Test that fresh reading is fetched on start().`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor = new sensorType();
+ t.add_cleanup(() => {
+ sensor.stop();
+ });
+ const sensorWatcher = new EventWatcher(t, sensor, ['reading', 'error']);
+ sensor.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ const expectedBuffer = new RingBuffer(expectedReadings);
+ await sensorWatcher.wait_for('reading');
+ const expected1 = expectedBuffer.next().value;
+ assert_true(verificationFunction(expected1, sensor));
+ assert_true(mockSensor.isReadingData());
+ const cachedTimestamp1 = sensor.timestamp;
+
+ const {minimize, restore} = window_state_context(t);
+
+ await minimize();
+ assert_true(document.hidden);
+ await t.step_wait(
+ () => !mockSensor.isReadingData(), 'readings must be suspended');
+ const cachedTimestamp2 = sensor.timestamp;
+ assert_equals(cachedTimestamp1, cachedTimestamp2);
+
+ await restore();
+ assert_false(document.hidden);
+ await t.step_wait(
+ () => mockSensor.isReadingData(), 'readings must be restored');
+ await sensorWatcher.wait_for('reading');
+ const expected2 = expectedBuffer.next().value;
+ assert_true(verificationFunction(expected2, sensor));
+ assert_greater_than(sensor.timestamp, cachedTimestamp2);
+ }, `${sensorName}: Losing visibility must cause readings to be suspended.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+
+ 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");
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ // We need |fastSensorFrequency| because 60Hz might be higher than a sensor
+ // type's maximum allowed frequency.
+ const fastSensorFrequency = mockSensor.getSamplingFrequency();
+ const slowSensorFrequency = fastSensorFrequency * 0.25;
+
+ const slowSensor = new sensorType({ frequency: slowSensorFrequency });
+ t.add_cleanup(() => { slowSensor.stop(); });
+ 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");
+ assert_equals(mockSensor.getSamplingFrequency(), fastSensorFrequency);
+
+ // Now stop |fastSensor| and verify that the sampling frequency has dropped
+ // to the one |slowSensor| had requested.
+ fastSensor.stop();
+ return t.step_wait(() => {
+ return mockSensor.getSamplingFrequency() === slowSensorFrequency;
+ }, "Sampling frequency has dropped to slowSensor's requested frequency");
+ }, `${sensorName}: frequency hint works.`);
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+
+ const sensor1 = new sensorType();
+ const sensor2 = new sensorType();
+
+ return new Promise((resolve, reject) => {
+ sensor1.addEventListener('reading', () => {
+ sensor2.addEventListener('activate', () => {
+ try {
+ assert_true(sensor1.activated);
+ assert_true(sensor1.hasReading);
+ assert_false(verificationFunction(null, sensor1, /*isNull=*/true));
+ assert_not_equals(sensor1.timestamp, null);
+
+ assert_true(sensor2.activated);
+ assert_false(verificationFunction(null, sensor2, /*isNull=*/true));
+ assert_not_equals(sensor2.timestamp, null);
+ } catch (e) {
+ reject(e);
+ }
+ }, { once: true });
+ sensor2.addEventListener('reading', () => {
+ try {
+ assert_true(sensor2.activated);
+ assert_true(sensor2.hasReading);
+ assert_sensor_equals(sensor1, sensor2);
+ resolve();
+ } catch (e) {
+ reject(e);
+ }
+ }, { once: true });
+ sensor2.start();
+ }, { once: true });
+ sensor1.start();
+ });
+ }, `${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(() => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ 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 (!expectedRemappedReadings) {
+ // The sensorType does not represent a spatial sensor.
+ return;
+ }
+
+ sensor_test(async (t, sensorProvider) => {
+ assert_implements(sensorName in self, `${sensorName} is not supported.`);
+ const sensor1 = new sensorType({frequency: 60});
+ const sensor2 = new sensorType({frequency: 60, referenceFrame: "screen"});
+ const sensorWatcher1 = new EventWatcher(t, sensor1, ["reading", "error"]);
+ const sensorWatcher2 = new EventWatcher(t, sensor1, ["reading", "error"]);
+
+ sensor1.start();
+ sensor2.start();
+
+ const mockSensor = await sensorProvider.getCreatedSensor(sensorName);
+ mockSensor.setSensorReading(readings);
+
+ await Promise.all([sensorWatcher1.wait_for("reading"),
+ sensorWatcher2.wait_for("reading")]);
+
+ const expected = new RingBuffer(expectedReadings).next().value;
+ const expectedRemapped =
+ new RingBuffer(expectedRemappedReadings).next().value;
+ assert_true(verificationFunction(expected, sensor1));
+ assert_true(verificationFunction(expectedRemapped, sensor2));
+
+ sensor1.stop();
+ assert_true(verificationFunction(expected, sensor1, /*isNull=*/true));
+ assert_true(verificationFunction(expectedRemapped, sensor2));
+
+ sensor2.stop();
+ assert_true(verificationFunction(expectedRemapped, sensor2,
+ /*isNull=*/true));
+ }, `${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..9a51a591ce
--- /dev/null
+++ b/testing/web-platform/tests/generic-sensor/resources/generic-sensor-helpers.js
@@ -0,0 +1,142 @@
+'use strict';
+
+// These tests rely on the User Agent providing an implementation of
+// platform sensor backends.
+//
+// In Chromium-based browsers this implementation is provided by a polyfill
+// in order to reduce the amount of test-only code shipped to users. To enable
+// these tests the browser must be run with these options:
+//
+// --enable-blink-features=MojoJS,MojoJSTest
+async function loadChromiumResources() {
+ await loadScript('/resources/testdriver.js');
+ await loadScript('/resources/testdriver-vendor.js');
+ await loadScript('/page-visibility/resources/window_state_context.js');
+ await import('/resources/chromium/generic_sensor_mocks.js');
+}
+
+async function initialize_generic_sensor_tests() {
+ if (typeof GenericSensorTest === 'undefined') {
+ const script = document.createElement('script');
+ script.src = '/resources/test-only-api.js';
+ script.async = false;
+ const p = new Promise((resolve, reject) => {
+ script.onload = () => { resolve(); };
+ script.onerror = e => { reject(e); };
+ })
+ document.head.appendChild(script);
+ await p;
+
+ if (isChromiumBased) {
+ await loadChromiumResources();
+ }
+ }
+
+ let sensorTest = new GenericSensorTest();
+ await sensorTest.initialize();
+ return sensorTest;
+}
+
+function sensor_test(func, name, properties) {
+ promise_test(async (t) => {
+ t.add_cleanup(() => {
+ if (sensorTest)
+ return sensorTest.reset();
+ });
+
+ let sensorTest = await initialize_generic_sensor_tests();
+ return func(t, sensorTest.getSensorProvider());
+ }, name, properties);
+}
+
+function verifySensorReading(pattern, values, timestamp, isNull) {
+ // If |val| cannot be converted to a float, we return the original value.
+ // This can happen when a value in |pattern| is not a number.
+ function round(val) {
+ const res = Number.parseFloat(val).toPrecision(6);
+ return res === "NaN" ? val : res;
+ }
+
+ if (isNull) {
+ return (values === null || values.every(r => r === null)) &&
+ timestamp === null;
+ }
+
+ return values.every((r, i) => round(r) === round(pattern[i])) &&
+ timestamp !== null;
+}
+
+function verifyXyzSensorReading(pattern, {x, y, z, timestamp}, isNull) {
+ return verifySensorReading(pattern, [x, y, z], timestamp, isNull);
+}
+
+function verifyQuatSensorReading(pattern, {quaternion, timestamp}, isNull) {
+ return verifySensorReading(pattern, quaternion, timestamp, isNull);
+}
+
+function verifyAlsSensorReading(pattern, {illuminance, timestamp}, isNull) {
+ return verifySensorReading(pattern, [illuminance], timestamp, isNull);
+}
+
+function verifyGeoSensorReading(pattern, {latitude, longitude, altitude,
+ accuracy, altitudeAccuracy, heading, speed, timestamp}, isNull) {
+ return verifySensorReading(pattern, [latitude, longitude, altitude,
+ accuracy, altitudeAccuracy, heading, speed], timestamp, isNull);
+}
+
+function verifyProximitySensorReading(pattern, {distance, max, near, timestamp}, isNull) {
+ return verifySensorReading(pattern, [distance, max, near], timestamp, isNull);
+}
+
+// Assert that two Sensor objects have the same properties and values.
+//
+// Verifies that ``actual`` and ``expected`` have the same sensor properties
+// and, if so, that their values are the same.
+//
+// @param {Sensor} actual - Test value.
+// @param {Sensor} expected - Expected value.
+function assert_sensor_equals(actual, expected) {
+ assert_true(
+ actual instanceof Sensor,
+ 'assert_sensor_equals: actual must be a Sensor');
+ assert_true(
+ expected instanceof Sensor,
+ 'assert_sensor_equals: expected must be a Sensor');
+
+ // These properties vary per sensor type.
+ const CUSTOM_PROPERTIES = [
+ ['illuminance'], ['quaternion'], ['x', 'y', 'z'],
+ [
+ 'latitude', 'longitude', 'altitude', 'accuracy', 'altitudeAccuracy',
+ 'heading', 'speed'
+ ]
+ ];
+
+ // These properties are present on all objects derived from Sensor.
+ const GENERAL_PROPERTIES = ['timestamp'];
+
+ for (let customProperties of CUSTOM_PROPERTIES) {
+ if (customProperties.every(p => p in actual) &&
+ customProperties.every(p => p in expected)) {
+ customProperties.forEach(p => {
+ if (customProperties == 'quaternion') {
+ assert_array_equals(
+ actual[p], expected[p],
+ `assert_sensor_equals: property '${p}' does not match`);
+ } else {
+ assert_equals(
+ actual[p], expected[p],
+ `assert_sensor_equals: property '${p}' does not match`);
+ }
+ });
+ GENERAL_PROPERTIES.forEach(p => {
+ assert_equals(
+ actual[p], expected[p],
+ `assert_sensor_equals: property '${p}' does not match`);
+ });
+ return;
+ }
+ }
+
+ assert_true(false, 'assert_sensor_equals: sensors have different attributes');
+}
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..4528c57a6b
--- /dev/null
+++ b/testing/web-platform/tests/generic-sensor/resources/iframe_sensor_handler.html
@@ -0,0 +1,63 @@
+<!DOCTYPE html>
+<meta charset="utf-8">
+<title>iframe sensor tester</title>
+<script src="/generic-sensor/resources/generic-sensor-helpers.js"></script>
+<script>
+ let mockBackend = null;
+ let sensor = null;
+ let sensorType = null;
+
+ async function messageHandler(e) {
+ if (e.data.command === 'create_sensor') {
+ if (!sensor) {
+ mockBackend = await initialize_generic_sensor_tests();
+ sensor = new self[e.data.type]();
+ sensorType = e.data.type;
+ }
+
+ return Promise.resolve('success');
+ } else if (e.data.command === 'start_sensor') {
+ if (!sensor) {
+ return Promise.reject('"create_sensor" must be called first');
+ }
+
+ return new Promise((resolve, reject) => {
+ sensor.addEventListener('reading', () => {
+ resolve('success');
+ }, { once: true });
+ sensor.addEventListener('error', e => {
+ reject(`${e.error.name}: ${e.error.message}`);
+ }, { once: true });
+ sensor.start();
+ });
+ } else if (e.data.command === 'is_sensor_suspended') {
+ if (!mockBackend) {
+ return Promise.reject('"create_sensor" must be called first');
+ }
+
+ const mockPlatformSensor = await mockBackend.getSensorProvider().getCreatedSensor(sensorType);
+ return Promise.resolve(!mockPlatformSensor.isReadingData());
+ } else if (e.data.command === 'reset_sensor_backend') {
+ if (sensor) {
+ sensor.stop();
+ await mockBackend.reset();
+
+ sensor = null;
+ mockBackend = null;
+ }
+ return Promise.resolve('success');
+ } else {
+ return Promise.reject(`unknown command "${e.data.command}"`);
+ }
+ }
+
+ window.onmessage = async (e) => {
+ let reply;
+ try {
+ reply = await messageHandler(e);
+ } catch (error) {
+ reply = error;
+ }
+ e.source.postMessage({ command: e.data.command, result: reply }, '*');
+ }
+</script>