diff options
Diffstat (limited to 'testing/web-platform/tests/generic-sensor')
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> |