| function send_message_to_iframe(iframe, message) { |
| return new Promise((resolve, reject) => { |
| window.addEventListener('message', (e) => { |
| // The usage of test_driver.set_test_context() in |
| // iframe_sensor_handler.html causes unrelated messages to be sent as |
| // well. We just need to ignore them here. |
| if (!e.data.command) { |
| return; |
| } |
| |
| if (e.data.command !== message.command) { |
| reject(`Expected reply with command '${message.command}', got '${ |
| e.data.command}' instead`); |
| return; |
| } |
| if (e.data.error) { |
| reject(e.data.error); |
| return; |
| } |
| resolve(e.data.result); |
| }); |
| iframe.contentWindow.postMessage(message, '*'); |
| }); |
| } |
| |
| function run_generic_sensor_iframe_tests(sensorData, readingData) { |
| validate_sensor_data(sensorData); |
| validate_reading_data(readingData); |
| |
| const {sensorName, permissionName, testDriverName} = sensorData; |
| const sensorType = self[sensorName]; |
| const featurePolicies = get_feature_policies_for_sensor(sensorName); |
| |
| // When comparing timestamps in the tests below, we need to account for small |
| // deviations coming from the way time is coarsened according to the High |
| // Resolution Time specification, even more so when we need to translate |
| // timestamps from different documents with different time origins. |
| // 0.5 is 500 microseconds, which is acceptable enough given that even a high |
| // sensor frequency beyond what is usually allowed like 100Hz has a period |
| // much larger than 0.5ms. |
| const ALLOWED_JITTER_IN_MS = 0.5; |
| |
| function sensor_test(func, name, properties) { |
| promise_test(async t => { |
| assert_implements(sensorName in self, `${sensorName} is not supported.`); |
| const readings = new RingBuffer(readingData.readings); |
| return func(t, readings); |
| }, name, properties); |
| } |
| |
| sensor_test(async (t, readings) => { |
| // This is a specialized EventWatcher that works with a sensor inside a |
| // cross-origin iframe. We cannot manipulate the sensor object there |
| // directly from this frame, so we need the iframe to send us a message |
| // when the "reading" event is fired, and we decide whether we were |
| // expecting for it or not. This should be instantiated early in the test |
| // to catch as many unexpected events as possible. |
| class IframeSensorReadingEventWatcher { |
| constructor(test_obj) { |
| this.resolve_ = null; |
| |
| window.onmessage = test_obj.step_func((ev) => { |
| // Unrelated message, ignore. |
| if (!ev.data.eventName) { |
| return; |
| } |
| |
| assert_equals( |
| ev.data.eventName, 'reading', 'Expecting a "reading" event'); |
| assert_true( |
| !!this.resolve_, |
| 'Received "reading" event from iframe but was not expecting one'); |
| const resolveFunc = this.resolve_; |
| this.resolve_ = null; |
| resolveFunc(ev.data.serializedSensor); |
| }); |
| } |
| |
| wait_for_reading() { |
| return new Promise(resolve => { |
| this.resolve_ = resolve; |
| }); |
| } |
| }; |
| |
| // Create main frame sensor. |
| await test_driver.set_permission({name: permissionName}, 'granted'); |
| await test_driver.create_virtual_sensor(testDriverName); |
| const sensor = new sensorType(); |
| t.add_cleanup(async () => { |
| sensor.stop(); |
| await test_driver.remove_virtual_sensor(testDriverName); |
| }); |
| const sensorWatcher = |
| new EventWatcher(t, sensor, ['activate', 'reading', 'error']); |
| |
| // Create cross-origin iframe and a sensor inside it. |
| const iframe = document.createElement('iframe'); |
| iframe.allow = featurePolicies.join(';') + ';'; |
| iframe.src = |
| 'https://{{domains[www1]}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; |
| const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); |
| document.body.appendChild(iframe); |
| t.add_cleanup(async () => { |
| await send_message_to_iframe(iframe, {command: 'stop_sensor'}); |
| iframe.parentNode.removeChild(iframe); |
| }); |
| await iframeLoadWatcher.wait_for('load'); |
| const iframeSensorWatcher = new IframeSensorReadingEventWatcher(t); |
| await send_message_to_iframe( |
| iframe, {command: 'create_sensor', sensorData}); |
| |
| // Start the test by focusing the main frame. It is already focused by |
| // default, but this makes the test easier to follow. |
| // When the main frame is focused, it sensor is expected to fire "reading" |
| // events and provide access to new reading values while the sensor in the |
| // cross-origin iframe is not. |
| window.focus(); |
| |
| // Start both sensors. They should both have the same state: active, but no |
| // readings have been provided to them yet. |
| await send_message_to_iframe(iframe, {command: 'start_sensor'}); |
| sensor.start(); |
| await sensorWatcher.wait_for('activate'); |
| assert_false( |
| await send_message_to_iframe(iframe, {command: 'has_reading'})); |
| assert_false(sensor.hasReading); |
| |
| // We store `reading` here because we want to make sure the very same |
| // value is accepted later. |
| const reading = readings.next().value; |
| await Promise.all([ |
| sensorWatcher.wait_for('reading'), |
| test_driver.update_virtual_sensor(testDriverName, reading), |
| // Since we do not wait for the iframe sensor's "reading" event, it could |
| // arguably be delivered later. There are enough async calls happening |
| // that IframeSensorReadingEventWatcher would end up catching it and |
| // throwing an error. |
| ]); |
| assert_true(sensor.hasReading); |
| assert_false( |
| await send_message_to_iframe(iframe, {command: 'has_reading'})); |
| |
| // Save sensor data for later before the sensor is stopped. |
| const savedMainFrameSensorReadings = serialize_sensor_data(sensor); |
| |
| sensor.stop(); |
| await send_message_to_iframe(iframe, {command: 'stop_sensor'}); |
| |
| // The sensors are stopped; queue the same reading. The virtual sensor |
| // would send it anyway, but this update changes its timestamp. |
| await test_driver.update_virtual_sensor(testDriverName, reading); |
| |
| // Now focus the cross-origin iframe. The situation should be the opposite: |
| // the sensor in the main frame should not fire any "reading" events or |
| // provide access to updated readings, but the sensor in the iframe should. |
| iframe.contentWindow.focus(); |
| |
| // Start both sensors. Only the iframe sensor should receive a reading |
| // event and contain readings. |
| sensor.start(); |
| await sensorWatcher.wait_for('activate'); |
| await send_message_to_iframe(iframe, {command: 'start_sensor'}); |
| const serializedIframeSensor = await iframeSensorWatcher.wait_for_reading(); |
| assert_true(await send_message_to_iframe(iframe, {command: 'has_reading'})); |
| assert_false(sensor.hasReading); |
| |
| assert_sensor_reading_is_null(sensor); |
| |
| assert_sensor_reading_equals( |
| savedMainFrameSensorReadings, serializedIframeSensor, |
| {ignoreTimestamps: true}); |
| |
| // We could check that serializedIframeSensor.timestamp (adjusted to this |
| // frame by adding the iframe's timeOrigin and substracting |
| // performance.timeOrigin) is greater than |
| // savedMainFrameSensorReadings.timestamp (or other timestamps prior to the |
| // last test_driver.update_virtual_sensor() call), but this is surprisingly |
| // tricky and flaky due to the fact that we are using timestamps from |
| // cross-origin frames. |
| // |
| // On Chrome on Windows (M120 at the time of writing), for example, the |
| // difference between timeOrigin values is sometimes off by more than 10ms |
| // from the real difference, and allowing for this much jitter makes the |
| // test not test something meaningful. |
| }, `${sensorName}: unfocused sensors in cross-origin frames are not updated`); |
| |
| sensor_test(async (t, readings) => { |
| // Create main frame sensor. |
| await test_driver.set_permission({name: permissionName}, 'granted'); |
| await test_driver.create_virtual_sensor(testDriverName); |
| const sensor = new sensorType(); |
| t.add_cleanup(async () => { |
| sensor.stop(); |
| await test_driver.remove_virtual_sensor(testDriverName); |
| }); |
| const sensorWatcher = |
| new EventWatcher(t, sensor, ['activate', 'reading', 'error']); |
| |
| // Create same-origin iframe and a sensor inside it. |
| const iframe = document.createElement('iframe'); |
| iframe.allow = featurePolicies.join(';') + ';'; |
| iframe.src = 'https://{{host}}:{{ports[https][0]}}/resources/blank.html'; |
| // Create sensor inside same-origin nested browsing context. |
| const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); |
| document.body.appendChild(iframe); |
| t.add_cleanup(() => { |
| if (iframeSensor) { |
| iframeSensor.stop(); |
| } |
| iframe.parentNode.removeChild(iframe); |
| }); |
| await iframeLoadWatcher.wait_for('load'); |
| // We deliberately create the sensor here instead of using |
| // send_messge_to_iframe() because this is a same-origin iframe, and we can |
| // therefore use EventWatcher() to wait for "reading" events a lot more |
| // easily. |
| const iframeSensor = new iframe.contentWindow[sensorName](); |
| const iframeSensorWatcher = |
| new EventWatcher(t, iframeSensor, ['activate', 'error', 'reading']); |
| |
| // Focus a different same-origin window each time and check that everything |
| // works the same. |
| for (const windowObject of [window, iframe.contentWindow]) { |
| await test_driver.update_virtual_sensor( |
| testDriverName, readings.next().value); |
| |
| windowObject.focus(); |
| |
| iframeSensor.start(); |
| sensor.start(); |
| |
| await Promise.all([ |
| iframeSensorWatcher.wait_for(['activate', 'reading']), |
| sensorWatcher.wait_for(['activate', 'reading']) |
| ]); |
| |
| assert_greater_than( |
| iframe.contentWindow.performance.timeOrigin, performance.timeOrigin, |
| 'iframe\'s time origin must be higher than the main window\'s'); |
| |
| // Check that the timestamps are similar enough to indicate that this is |
| // the same reading that was delivered to both sensors. |
| // The values are not identical due to how high resolution time is |
| // coarsened. |
| const translatedIframeSensorTimestamp = iframeSensor.timestamp + |
| iframe.contentWindow.performance.timeOrigin - performance.timeOrigin; |
| assert_approx_equals( |
| translatedIframeSensorTimestamp, sensor.timestamp, |
| ALLOWED_JITTER_IN_MS); |
| |
| // Do not compare timestamps here because of the reasons above. |
| assert_sensor_reading_equals( |
| sensor, iframeSensor, {ignoreTimestamps: true}); |
| |
| // Stop all sensors so we can use the same value in `reading` on every |
| // loop iteration. |
| iframeSensor.stop(); |
| sensor.stop(); |
| } |
| }, `${sensorName}: sensors in same-origin frames are updated if one of the frames is focused`); |
| |
| promise_test(async t => { |
| assert_implements(sensorName in self, `${sensorName} is not supported.`); |
| const iframe = document.createElement('iframe'); |
| iframe.allow = featurePolicies.join(';') + ';'; |
| iframe.src = |
| 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; |
| |
| const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); |
| document.body.appendChild(iframe); |
| await iframeLoadWatcher.wait_for('load'); |
| |
| // Create sensor in the iframe. |
| await test_driver.set_permission({name: permissionName}, 'granted'); |
| await test_driver.create_virtual_sensor(testDriverName); |
| iframe.contentWindow.focus(); |
| const iframeSensor = new iframe.contentWindow[sensorName](); |
| t.add_cleanup(async () => { |
| iframeSensor.stop(); |
| await test_driver.remove_virtual_sensor(testDriverName); |
| }); |
| const sensorWatcher = new EventWatcher(t, iframeSensor, ['activate']); |
| iframeSensor.start(); |
| await sensorWatcher.wait_for('activate'); |
| |
| // Remove iframe from main document and change focus. When focus changes, |
| // we need to determine whether a sensor must have its execution suspended |
| // or resumed (section 4.2.3, "Focused Area" of the Generic Sensor API |
| // spec). In Blink, this involves querying a frame, which might no longer |
| // exist at the time of the check. |
| iframe.parentNode.removeChild(iframe); |
| window.focus(); |
| }, `${sensorName}: losing a document's frame with an active sensor does not crash`); |
| |
| promise_test(async t => { |
| assert_implements(sensorName in self, `${sensorName} is not supported.`); |
| const iframe = document.createElement('iframe'); |
| iframe.allow = featurePolicies.join(';') + ';'; |
| iframe.src = |
| 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; |
| |
| const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); |
| document.body.appendChild(iframe); |
| await iframeLoadWatcher.wait_for('load'); |
| |
| // Create sensor in the iframe. |
| await test_driver.set_permission({name: permissionName}, 'granted'); |
| await test_driver.create_virtual_sensor(testDriverName); |
| const iframeSensor = new iframe.contentWindow[sensorName](); |
| t.add_cleanup(async () => { |
| iframeSensor.stop(); |
| await test_driver.remove_virtual_sensor(testDriverName); |
| }); |
| assert_not_equals(iframeSensor, null); |
| |
| // Remove iframe from main document. |iframeSensor| no longer has a |
| // non-null browsing context. Calling start() should probably throw an |
| // error when called from a non-fully active document, but that depends on |
| // https://github.com/w3c/sensors/issues/415 |
| iframe.parentNode.removeChild(iframe); |
| iframeSensor.start(); |
| }, `${sensorName}: calling start() in a non-fully active document does not crash`); |
| } |