tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

orientation-event-helpers.js (10946B)


      1 'use strict';
      2 
      3 // @class SensorTestHelper
      4 //
      5 // SensorTestHelper is a helper utilities for orientation event tests.
      6 //
      7 // Usage example with device orientation:
      8 //   const helper = new SensorTestHelper(t, 'deviceorientation');
      9 //   await helper.grantSensorsPermissions();
     10 //   await helper.initializeSensors();
     11 //   const generatedData = generateOrientationData(1, 2, 3, false);
     12 //   await helper.setData(generatedData);
     13 //   await waitForEvent(getExpectedOrientationEvent(generatedData));
     14 class SensorTestHelper {
     15  #eventName;
     16  #sensorsEnabledByDefault;
     17  #enabledSensors;
     18  #disabledSensors;
     19  #testObject;
     20 
     21  // @param {object} t - A testharness.js subtest instance.
     22  // @param {string} eventName - A name of event. Accepted values are
     23  //                             devicemotion, deviceorientation or
     24  //                             deviceorientationabsolute.
     25  constructor(t, eventName) {
     26    this.#eventName = eventName;
     27    this.#testObject = t;
     28    this.#testObject.add_cleanup(() => this.reset());
     29 
     30    switch (this.#eventName) {
     31      case 'devicemotion':
     32        this.#sensorsEnabledByDefault =
     33            new Set(['accelerometer', 'gyroscope', 'linear-acceleration']);
     34        break;
     35      case 'deviceorientation':
     36        this.#sensorsEnabledByDefault = new Set(['relative-orientation']);
     37        break;
     38      case 'deviceorientationabsolute':
     39        this.#sensorsEnabledByDefault = new Set(['absolute-orientation']);
     40        break;
     41      default:
     42        throw new Error(`Invalid event name ${this.#eventName}`);
     43    }
     44  }
     45 
     46  // Creates virtual sensors that will be used in tests.
     47  //
     48  // This function must be called before event listeners are added or calls
     49  // to setData() or waitForEvent() are made.
     50  //
     51  // The |options| parameter is an object that accepts the following entries:
     52  // - enabledSensors: A list of virtual sensor names that will be created
     53  //                   instead of the default ones for a given event type.
     54  // - disabledSensors: A list of virtual sensor names that will be created
     55  //                    in a disabled state, so that creating a sensor of
     56  //                    a given type is guaranteed to fail.
     57  // An Error is thrown if the same name is passed to both options.
     58  //
     59  // A default list of virtual sensors based on the |eventName| parameter passed
     60  // to the constructor is used if |options| is not specified.
     61  //
     62  // Usage examples
     63  // Use default sensors for the given event type:
     64  //   await helper.initializeSensors()
     65  // Enable specific sensors:
     66  //   await helper.initializeSensors({
     67  //     enabledSensors: ['accelerometer', 'gyroscope']
     68  //   })
     69  // Disable some sensors, make some report as not available:
     70  //   await helper.initializeSensors({
     71  //     disabledSensors: ['gyroscope']
     72  //   })
     73  // Enable some sensors, make some report as not available:
     74  //   await helper.initializeSensors({
     75  //     enabledSensors: ['accelerometer'],
     76  //     disabledSensors: ['gyroscope']
     77  //   })
     78  async initializeSensors(options = {}) {
     79    this.#disabledSensors = new Set(options.disabledSensors || []);
     80    // Check that a sensor name is not in both |options.enabledSensors| and
     81    // |options.disabledSensors|.
     82    for (const sensor of (options.enabledSensors || [])) {
     83      if (this.#disabledSensors.has(sensor)) {
     84        throw new Error(`${sensor} can be defined only as enabledSensors or disabledSensors`);
     85      }
     86    }
     87 
     88    this.#enabledSensors = new Set(options.enabledSensors || this.#sensorsEnabledByDefault);
     89    // Remove sensors from enabledSensors that are in disabledSensors
     90    for (const sensor of this.#disabledSensors) {
     91      this.#enabledSensors.delete(sensor);
     92    }
     93 
     94    const createVirtualSensorPromises = [];
     95    for (const sensor of this.#enabledSensors) {
     96      createVirtualSensorPromises.push(
     97          test_driver.create_virtual_sensor(sensor));
     98    }
     99    for (const sensor of this.#disabledSensors) {
    100      createVirtualSensorPromises.push(
    101          test_driver.create_virtual_sensor(sensor, {connected: false}));
    102    }
    103    await Promise.all(createVirtualSensorPromises);
    104  }
    105 
    106  // Updates virtual sensor with given data.
    107  // @param {object} data - Generated data by generateMotionData or
    108  //                        generateOrientationData which is passed to
    109  //                        test_driver.update_virtual_sensor().
    110  async setData(data) {
    111    // WebDriver expects numbers for all values in the readings it receives. We
    112    // convert null to zero here, but any other numeric value would work, as it
    113    // is the presence of one or more sensors in initializeSensors()'
    114    // options.disabledSensors that cause null to be reported in one or more
    115    // event attributes.
    116    const nullToZero = x => (x === null ? 0 : x);
    117    if (this.#eventName === 'devicemotion') {
    118      const degToRad = Math.PI / 180;
    119      await Promise.all([
    120        test_driver.update_virtual_sensor('accelerometer', {
    121          'x': nullToZero(data.accelerationIncludingGravityX),
    122          'y': nullToZero(data.accelerationIncludingGravityY),
    123          'z': nullToZero(data.accelerationIncludingGravityZ),
    124        }),
    125        test_driver.update_virtual_sensor('linear-acceleration', {
    126          'x': nullToZero(data.accelerationX),
    127          'y': nullToZero(data.accelerationY),
    128          'z': nullToZero(data.accelerationZ),
    129        }),
    130        test_driver.update_virtual_sensor('gyroscope', {
    131          'x': nullToZero(data.rotationRateAlpha) * degToRad,
    132          'y': nullToZero(data.rotationRateBeta) * degToRad,
    133          'z': nullToZero(data.rotationRateGamma) * degToRad,
    134        }),
    135      ]);
    136    } else {
    137      const sensorType =
    138          data.absolute ? 'absolute-orientation' : 'relative-orientation';
    139      await test_driver.update_virtual_sensor(sensorType, {
    140        alpha: nullToZero(data.alpha),
    141        beta: nullToZero(data.beta),
    142        gamma: nullToZero(data.gamma),
    143      });
    144    }
    145  }
    146 
    147  // Grants permissions to sensors. Depending on |eventName|, requests
    148  // permission to use either the DeviceMotionEvent or the
    149  // DeviceOrientationEvent API.
    150  async grantSensorsPermissions() {
    151    // Required by all event types.
    152    await test_driver.set_permission({name: 'accelerometer'}, 'granted');
    153    await test_driver.set_permission({name: 'gyroscope'}, 'granted');
    154    if (this.#eventName == 'deviceorientationabsolute') {
    155      await test_driver.set_permission({name: 'magnetometer'}, 'granted');
    156    }
    157 
    158    const interfaceName = this.#eventName == 'devicemotion' ?
    159        DeviceMotionEvent :
    160        DeviceOrientationEvent;
    161    await test_driver.bless('enable user activation', async () => {
    162      const permission = await interfaceName.requestPermission();
    163      assert_equals(permission, 'granted');
    164    });
    165  }
    166 
    167  // Resets SensorTestHelper to default state. Removes all created virtual
    168  // sensors.
    169  async reset() {
    170    const createdVirtualSensors =
    171      new Set([...this.#enabledSensors, ...this.#disabledSensors]);
    172 
    173    const sensorRemovalPromises = [];
    174    for (const sensor of createdVirtualSensors) {
    175      sensorRemovalPromises.push(test_driver.remove_virtual_sensor(sensor));
    176    }
    177    await Promise.all(sensorRemovalPromises);
    178  }
    179 }
    180 
    181 function generateMotionData(
    182    accelerationX, accelerationY, accelerationZ, accelerationIncludingGravityX,
    183    accelerationIncludingGravityY, accelerationIncludingGravityZ,
    184    rotationRateAlpha, rotationRateBeta, rotationRateGamma, interval = 16) {
    185  const motionData = {
    186    accelerationX: accelerationX,
    187    accelerationY: accelerationY,
    188    accelerationZ: accelerationZ,
    189    accelerationIncludingGravityX: accelerationIncludingGravityX,
    190    accelerationIncludingGravityY: accelerationIncludingGravityY,
    191    accelerationIncludingGravityZ: accelerationIncludingGravityZ,
    192    rotationRateAlpha: rotationRateAlpha,
    193    rotationRateBeta: rotationRateBeta,
    194    rotationRateGamma: rotationRateGamma,
    195    interval: interval
    196  };
    197  return motionData;
    198 }
    199 
    200 function generateOrientationData(alpha, beta, gamma, absolute) {
    201  const orientationData =
    202      {alpha: alpha, beta: beta, gamma: gamma, absolute: absolute};
    203  return orientationData;
    204 }
    205 
    206 function assertValueIsCoarsened(value) {
    207  // Checks that the precision of the value is at most 0.1.
    208  // https://www.w3.org/TR/orientation-event/ specification defines that all
    209  // measurements are required to be coarsened to 0.1 degrees, 0.1 m/s^2 or
    210  // 0.1 deg/s.
    211  const resolution = 0.1;
    212  const coarsenedValue = Math.round(value / resolution) * resolution;
    213  assert_approx_equals(value, coarsenedValue, Number.EPSILON,
    214                       `Expected ${value}'s precision to be at most ${resolution}`);
    215 }
    216 
    217 function assertEventEquals(actualEvent, expectedEvent) {
    218  // If two doubles differ by less than this amount, we can consider them
    219  // to be effectively equal.
    220  const EPSILON = 1e-8;
    221 
    222  for (let key1 of Object.keys(Object.getPrototypeOf(expectedEvent))) {
    223    if (typeof expectedEvent[key1] === 'object' &&
    224        expectedEvent[key1] !== null) {
    225      assertEventEquals(actualEvent[key1], expectedEvent[key1]);
    226    } else if (typeof expectedEvent[key1] === 'number') {
    227      assert_approx_equals(
    228          actualEvent[key1], expectedEvent[key1], EPSILON, key1);
    229    } else {
    230      assert_equals(actualEvent[key1], expectedEvent[key1], key1);
    231    }
    232  }
    233 }
    234 
    235 function getExpectedOrientationEvent(expectedOrientationData) {
    236  return new DeviceOrientationEvent('deviceorientation', {
    237    alpha: expectedOrientationData.alpha,
    238    beta: expectedOrientationData.beta,
    239    gamma: expectedOrientationData.gamma,
    240    absolute: expectedOrientationData.absolute,
    241  });
    242 }
    243 
    244 function getExpectedAbsoluteOrientationEvent(expectedOrientationData) {
    245  return new DeviceOrientationEvent('deviceorientationabsolute', {
    246    alpha: expectedOrientationData.alpha,
    247    beta: expectedOrientationData.beta,
    248    gamma: expectedOrientationData.gamma,
    249    absolute: expectedOrientationData.absolute,
    250  });
    251 }
    252 
    253 function getExpectedMotionEvent(expectedMotionData) {
    254  return new DeviceMotionEvent('devicemotion', {
    255    acceleration: {
    256      x: expectedMotionData.accelerationX,
    257      y: expectedMotionData.accelerationY,
    258      z: expectedMotionData.accelerationZ,
    259    },
    260    accelerationIncludingGravity: {
    261      x: expectedMotionData.accelerationIncludingGravityX,
    262      y: expectedMotionData.accelerationIncludingGravityY,
    263      z: expectedMotionData.accelerationIncludingGravityZ,
    264    },
    265    rotationRate: {
    266      alpha: expectedMotionData.rotationRateAlpha,
    267      beta: expectedMotionData.rotationRateBeta,
    268      gamma: expectedMotionData.rotationRateGamma,
    269    },
    270    interval: expectedMotionData.interval,
    271  });
    272 }
    273 
    274 function waitForEvent(expected_event) {
    275  return new Promise((resolve, reject) => {
    276    window.addEventListener(expected_event.type, (event) => {
    277      try {
    278        assertEventEquals(event, expected_event);
    279        resolve();
    280      } catch (e) {
    281        reject(e);
    282      }
    283    }, {once: true});
    284  });
    285 }