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 }