Source code

Revision control

Copy as Markdown

Other Tools

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(';') + '; focus-without-user-activation;';
iframe.src =
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(';') + ';';
// 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 =
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 =
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
iframe.parentNode.removeChild(iframe);
iframeSensor.start();
}, `${sensorName}: calling start() in a non-fully active document does not crash`);
}