generic-sensor-iframe-tests.sub.js (14198B)
1 function send_message_to_iframe(iframe, message) { 2 return new Promise((resolve, reject) => { 3 window.addEventListener('message', (e) => { 4 // The usage of test_driver.set_test_context() in 5 // iframe_sensor_handler.html causes unrelated messages to be sent as 6 // well. We just need to ignore them here. 7 if (!e.data.command) { 8 return; 9 } 10 11 if (e.data.command !== message.command) { 12 reject(`Expected reply with command '${message.command}', got '${ 13 e.data.command}' instead`); 14 return; 15 } 16 if (e.data.error) { 17 reject(e.data.error); 18 return; 19 } 20 resolve(e.data.result); 21 }); 22 iframe.contentWindow.postMessage(message, '*'); 23 }); 24 } 25 26 function run_generic_sensor_iframe_tests(sensorData, readingData) { 27 validate_sensor_data(sensorData); 28 validate_reading_data(readingData); 29 30 const {sensorName, permissionName, testDriverName} = sensorData; 31 const sensorType = self[sensorName]; 32 const featurePolicies = get_feature_policies_for_sensor(sensorName); 33 34 // When comparing timestamps in the tests below, we need to account for small 35 // deviations coming from the way time is coarsened according to the High 36 // Resolution Time specification, even more so when we need to translate 37 // timestamps from different documents with different time origins. 38 // 0.5 is 500 microseconds, which is acceptable enough given that even a high 39 // sensor frequency beyond what is usually allowed like 100Hz has a period 40 // much larger than 0.5ms. 41 const ALLOWED_JITTER_IN_MS = 0.5; 42 43 function sensor_test(func, name, properties) { 44 promise_test(async t => { 45 assert_implements(sensorName in self, `${sensorName} is not supported.`); 46 const readings = new RingBuffer(readingData.readings); 47 return func(t, readings); 48 }, name, properties); 49 } 50 51 promise_setup(async () => { 52 // Ensure window's document starts with focus so that it can receive data. 53 await test_driver.click(document.documentElement); 54 }); 55 56 sensor_test(async (t, readings) => { 57 // This is a specialized EventWatcher that works with a sensor inside a 58 // cross-origin iframe. We cannot manipulate the sensor object there 59 // directly from this frame, so we need the iframe to send us a message 60 // when the "reading" event is fired, and we decide whether we were 61 // expecting for it or not. This should be instantiated early in the test 62 // to catch as many unexpected events as possible. 63 class IframeSensorReadingEventWatcher { 64 constructor(test_obj) { 65 this.resolve_ = null; 66 67 window.onmessage = test_obj.step_func((ev) => { 68 // Unrelated message, ignore. 69 if (!ev.data.eventName) { 70 return; 71 } 72 73 assert_equals( 74 ev.data.eventName, 'reading', 'Expecting a "reading" event'); 75 assert_true( 76 !!this.resolve_, 77 'Received "reading" event from iframe but was not expecting one'); 78 const resolveFunc = this.resolve_; 79 this.resolve_ = null; 80 resolveFunc(ev.data.serializedSensor); 81 }); 82 } 83 84 wait_for_reading() { 85 return new Promise(resolve => { 86 this.resolve_ = resolve; 87 }); 88 } 89 }; 90 91 // Create main frame sensor. 92 await test_driver.bidi.permissions.set_permission( 93 {descriptor: {name: permissionName}, state: 'granted'}); 94 await test_driver.create_virtual_sensor(testDriverName); 95 const sensor = new sensorType(); 96 t.add_cleanup(async () => { 97 sensor.stop(); 98 await test_driver.remove_virtual_sensor(testDriverName); 99 }); 100 const sensorWatcher = 101 new EventWatcher(t, sensor, ['activate', 'reading', 'error']); 102 103 // Create cross-origin iframe and a sensor inside it. 104 const iframe = document.createElement('iframe'); 105 iframe.allow = featurePolicies.join(';') + '; focus-without-user-activation;'; 106 iframe.src = 107 'https://{{domains[www1]}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; 108 const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); 109 document.body.appendChild(iframe); 110 t.add_cleanup(async () => { 111 await send_message_to_iframe(iframe, {command: 'stop_sensor'}); 112 iframe.parentNode.removeChild(iframe); 113 }); 114 await iframeLoadWatcher.wait_for('load'); 115 const iframeSensorWatcher = new IframeSensorReadingEventWatcher(t); 116 await send_message_to_iframe( 117 iframe, {command: 'create_sensor', sensorData}); 118 119 // Start the test by focusing the main frame. It is already focused by 120 // default, but this makes the test easier to follow. 121 // When the main frame is focused, it sensor is expected to fire "reading" 122 // events and provide access to new reading values while the sensor in the 123 // cross-origin iframe is not. 124 window.focus(); 125 126 // Start both sensors. They should both have the same state: active, but no 127 // readings have been provided to them yet. 128 await send_message_to_iframe(iframe, {command: 'start_sensor'}); 129 sensor.start(); 130 await sensorWatcher.wait_for('activate'); 131 assert_false( 132 await send_message_to_iframe(iframe, {command: 'has_reading'})); 133 assert_false(sensor.hasReading); 134 135 // We store `reading` here because we want to make sure the very same 136 // value is accepted later. 137 const reading = readings.next().value; 138 await Promise.all([ 139 sensorWatcher.wait_for('reading'), 140 test_driver.update_virtual_sensor(testDriverName, reading), 141 // Since we do not wait for the iframe sensor's "reading" event, it could 142 // arguably be delivered later. There are enough async calls happening 143 // that IframeSensorReadingEventWatcher would end up catching it and 144 // throwing an error. 145 ]); 146 assert_true(sensor.hasReading); 147 assert_false( 148 await send_message_to_iframe(iframe, {command: 'has_reading'})); 149 150 // Save sensor data for later before the sensor is stopped. 151 const savedMainFrameSensorReadings = serialize_sensor_data(sensor); 152 153 sensor.stop(); 154 await send_message_to_iframe(iframe, {command: 'stop_sensor'}); 155 156 // The sensors are stopped; queue the same reading. The virtual sensor 157 // would send it anyway, but this update changes its timestamp. 158 await test_driver.update_virtual_sensor(testDriverName, reading); 159 160 // Now focus the cross-origin iframe. The situation should be the opposite: 161 // the sensor in the main frame should not fire any "reading" events or 162 // provide access to updated readings, but the sensor in the iframe should. 163 iframe.contentWindow.focus(); 164 165 // Start both sensors. Only the iframe sensor should receive a reading 166 // event and contain readings. 167 sensor.start(); 168 await sensorWatcher.wait_for('activate'); 169 await send_message_to_iframe(iframe, {command: 'start_sensor'}); 170 const serializedIframeSensor = await iframeSensorWatcher.wait_for_reading(); 171 assert_true(await send_message_to_iframe(iframe, {command: 'has_reading'})); 172 assert_false(sensor.hasReading); 173 174 assert_sensor_reading_is_null(sensor); 175 176 assert_sensor_reading_equals( 177 savedMainFrameSensorReadings, serializedIframeSensor, 178 {ignoreTimestamps: true}); 179 180 // We could check that serializedIframeSensor.timestamp (adjusted to this 181 // frame by adding the iframe's timeOrigin and substracting 182 // performance.timeOrigin) is greater than 183 // savedMainFrameSensorReadings.timestamp (or other timestamps prior to the 184 // last test_driver.update_virtual_sensor() call), but this is surprisingly 185 // tricky and flaky due to the fact that we are using timestamps from 186 // cross-origin frames. 187 // 188 // On Chrome on Windows (M120 at the time of writing), for example, the 189 // difference between timeOrigin values is sometimes off by more than 10ms 190 // from the real difference, and allowing for this much jitter makes the 191 // test not test something meaningful. 192 }, `${sensorName}: unfocused sensors in cross-origin frames are not updated`); 193 194 sensor_test(async (t, readings) => { 195 // Create main frame sensor. 196 await test_driver.bidi.permissions.set_permission( 197 {descriptor: {name: permissionName}, state: 'granted'}); 198 await test_driver.create_virtual_sensor(testDriverName); 199 const sensor = new sensorType(); 200 t.add_cleanup(async () => { 201 sensor.stop(); 202 await test_driver.remove_virtual_sensor(testDriverName); 203 }); 204 const sensorWatcher = 205 new EventWatcher(t, sensor, ['activate', 'reading', 'error']); 206 207 // Create same-origin iframe and a sensor inside it. 208 const iframe = document.createElement('iframe'); 209 iframe.allow = featurePolicies.join(';') + ';'; 210 iframe.src = 'https://{{host}}:{{ports[https][0]}}/resources/blank.html'; 211 // Create sensor inside same-origin nested browsing context. 212 const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); 213 document.body.appendChild(iframe); 214 t.add_cleanup(() => { 215 if (iframeSensor) { 216 iframeSensor.stop(); 217 } 218 iframe.parentNode.removeChild(iframe); 219 }); 220 await iframeLoadWatcher.wait_for('load'); 221 // We deliberately create the sensor here instead of using 222 // send_messge_to_iframe() because this is a same-origin iframe, and we can 223 // therefore use EventWatcher() to wait for "reading" events a lot more 224 // easily. 225 const iframeSensor = new iframe.contentWindow[sensorName](); 226 const iframeSensorWatcher = 227 new EventWatcher(t, iframeSensor, ['activate', 'error', 'reading']); 228 229 // Focus a different same-origin window each time and check that everything 230 // works the same. 231 for (const windowObject of [window, iframe.contentWindow]) { 232 await test_driver.update_virtual_sensor( 233 testDriverName, readings.next().value); 234 235 windowObject.focus(); 236 237 iframeSensor.start(); 238 sensor.start(); 239 240 await Promise.all([ 241 iframeSensorWatcher.wait_for(['activate', 'reading']), 242 sensorWatcher.wait_for(['activate', 'reading']) 243 ]); 244 245 assert_greater_than( 246 iframe.contentWindow.performance.timeOrigin, performance.timeOrigin, 247 'iframe\'s time origin must be higher than the main window\'s'); 248 249 // Check that the timestamps are similar enough to indicate that this is 250 // the same reading that was delivered to both sensors. 251 // The values are not identical due to how high resolution time is 252 // coarsened. 253 const translatedIframeSensorTimestamp = iframeSensor.timestamp + 254 iframe.contentWindow.performance.timeOrigin - performance.timeOrigin; 255 assert_approx_equals( 256 translatedIframeSensorTimestamp, sensor.timestamp, 257 ALLOWED_JITTER_IN_MS); 258 259 // Do not compare timestamps here because of the reasons above. 260 assert_sensor_reading_equals( 261 sensor, iframeSensor, {ignoreTimestamps: true}); 262 263 // Stop all sensors so we can use the same value in `reading` on every 264 // loop iteration. 265 iframeSensor.stop(); 266 sensor.stop(); 267 } 268 }, `${sensorName}: sensors in same-origin frames are updated if one of the frames is focused`); 269 270 promise_test(async t => { 271 assert_implements(sensorName in self, `${sensorName} is not supported.`); 272 const iframe = document.createElement('iframe'); 273 iframe.allow = featurePolicies.join(';') + ';'; 274 iframe.src = 275 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; 276 277 const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); 278 document.body.appendChild(iframe); 279 await iframeLoadWatcher.wait_for('load'); 280 281 // Create sensor in the iframe. 282 await test_driver.bidi.permissions.set_permission( 283 {descriptor: {name: permissionName}, state: 'granted'}); 284 await test_driver.create_virtual_sensor(testDriverName); 285 iframe.contentWindow.focus(); 286 const iframeSensor = new iframe.contentWindow[sensorName](); 287 t.add_cleanup(async () => { 288 iframeSensor.stop(); 289 await test_driver.remove_virtual_sensor(testDriverName); 290 }); 291 const sensorWatcher = new EventWatcher(t, iframeSensor, ['activate']); 292 iframeSensor.start(); 293 await sensorWatcher.wait_for('activate'); 294 295 // Remove iframe from main document and change focus. When focus changes, 296 // we need to determine whether a sensor must have its execution suspended 297 // or resumed (section 4.2.3, "Focused Area" of the Generic Sensor API 298 // spec). In Blink, this involves querying a frame, which might no longer 299 // exist at the time of the check. 300 iframe.parentNode.removeChild(iframe); 301 window.focus(); 302 }, `${sensorName}: losing a document's frame with an active sensor does not crash`); 303 304 promise_test(async t => { 305 assert_implements(sensorName in self, `${sensorName} is not supported.`); 306 const iframe = document.createElement('iframe'); 307 iframe.allow = featurePolicies.join(';') + ';'; 308 iframe.src = 309 'https://{{host}}:{{ports[https][0]}}/generic-sensor/resources/iframe_sensor_handler.html'; 310 311 const iframeLoadWatcher = new EventWatcher(t, iframe, 'load'); 312 document.body.appendChild(iframe); 313 await iframeLoadWatcher.wait_for('load'); 314 315 // Create sensor in the iframe. 316 await test_driver.bidi.permissions.set_permission( 317 {descriptor: {name: permissionName}, state: 'granted'}); 318 await test_driver.create_virtual_sensor(testDriverName); 319 const iframeSensor = new iframe.contentWindow[sensorName](); 320 t.add_cleanup(async () => { 321 iframeSensor.stop(); 322 await test_driver.remove_virtual_sensor(testDriverName); 323 }); 324 assert_not_equals(iframeSensor, null); 325 326 // Remove iframe from main document. |iframeSensor| no longer has a 327 // non-null browsing context. Calling start() should probably throw an 328 // error when called from a non-fully active document, but that depends on 329 // https://github.com/w3c/sensors/issues/415 330 iframe.parentNode.removeChild(iframe); 331 iframeSensor.start(); 332 }, `${sensorName}: calling start() in a non-fully active document does not crash`); 333 }