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);
  }

  promise_setup(async () => {
    // Ensure window's document starts with focus so that it can receive data.
    await test_driver.click(document.documentElement);
  });

  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.bidi.permissions.set_permission(
        {descriptor: {name: permissionName}, state: '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 =
        '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.bidi.permissions.set_permission(
        {descriptor: {name: permissionName}, state: '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.bidi.permissions.set_permission(
        {descriptor: {name: permissionName}, state: '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.bidi.permissions.set_permission(
        {descriptor: {name: permissionName}, state: '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`);
}
