summaryrefslogtreecommitdiffstats
path: root/testing/web-platform/tests/generic-sensor/generic-sensor-iframe-tests.sub.js
blob: ed3415e66ec799da22277dcd3f81dbbfb956145b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
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`);
}