├── .gitignore ├── LICENSE.md ├── README.md ├── index.html ├── package.json ├── run-geolocation.html ├── run-tests.html ├── src ├── geolocation-sensor.js ├── motion-sensors.js └── sensor.js └── tests ├── EventTarget-tests.js ├── generic-sensor-tests.js ├── testharness.css ├── testharness.js └── testharnessreport.js /.gitignore: -------------------------------------------------------------------------------- 1 | motion-sensors.js 2 | sensor.js 3 | geolocation-sensor.js 4 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Intel Corporation 2 | 3 | Redistribution and use in source and binary forms, with or without 4 | modification, are permitted provided that the following conditions are met: 5 | 6 | * Redistributions of source code must retain the above copyright notice, 7 | this list of conditions and the following disclaimer. 8 | * Redistributions in binary form must reproduce the above copyright 9 | notice, this list of conditions and the following disclaimer in the 10 | documentation and/or other materials provided with the distribution. 11 | * Neither the name of Intel Corporation nor the names of its contributors 12 | may be used to endorse or promote products derived from this software 13 | without specific prior written permission. 14 | 15 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 16 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 17 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 18 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 19 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 20 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 21 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 22 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 23 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 24 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | W3C Generic Sensor API polyfills 2 | === 3 | 4 | This is a polyfill for [Generic Sensor](https://w3c.github.io/sensors/)-based [motions sensors](https://w3c.github.io/motion-sensors/) to make migration from the old [DeviceOrientationEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#deviceorientation)/[DeviceMotionEvent](https://w3c.github.io/deviceorientation/spec-source-orientation.html#devicemotion) to the new APIs a smoother experience. 5 | 6 | In particular, this polyfill will allow the users of modern browsers to get a feel of the new API shape before it ships more broadly. 7 | 8 | `src/motion-sensors.js` implements the following interfaces: 9 | 10 | - [`Sensor`](https://w3c.github.io/sensors/#the-sensor-interface) 11 | - [`Accelerometer`](https://w3c.github.io/accelerometer/#accelerometer-interface) 12 | - [`LinearAccelerationSensor`](https://w3c.github.io/accelerometer/#linearaccelerationsensor-interface) 13 | - [`GravitySensor`](https://w3c.github.io/accelerometer/#gravitysensor-interface) 14 | - [`Gyroscope`](https://w3c.github.io/gyroscope/#gyroscope-interface) 15 | - [`RelativeOrientationSensor`](https://w3c.github.io/orientation-sensor/#relativeorientationsensor-interface) 16 | - [`AbsoluteOrientationSensor`](https://w3c.github.io/orientation-sensor/#absoluteorientationsensor-interface) 17 | 18 | `src/geolocation.js` implements the following interface: 19 | 20 | - [`GeolocationSensor`](https://w3c.github.io/geolocation-sensor/#geolocationsensor-interface) 21 | 22 | How to use the polyfill 23 | === 24 | 25 | - Copy [`src/motion-sensors.js`](https://raw.githubusercontent.com/kenchris/sensor-polyfills/master/src/motion-sensors.js) ([source](https://github.com/kenchris/sensor-polyfills/blob/master/src/motion-sensors.js)) into your project, or install via [npm](https://www.npmjs.com/package/motion-sensors-polyfill ) (`$ npm i motion-sensors-polyfill`). 26 | - Import the motion sensor objects in your HTML (see [` 39 | ``` 40 | - That's it. See [AbsoluteOrientationSensor demo](https://intel.github.io/generic-sensor-demos/orientation-phone/) and [RelativeOrientationSensor demo](https://intel.github.io/generic-sensor-demos/orientation-phone/?relative=1) ([code](https://github.com/intel/generic-sensor-demos/blob/master/orientation-phone/index.html)) for examples. 41 | 42 | How to enable the native implementation in Chrome 43 | === 44 | 45 | *Chrome 67 or later:* the native implementation is enabled by default. 46 | 47 | The Generic Sensor Extra Classes (`chrome://flags/#enable-generic-sensor-extra-classes`) feature flag can be activated to enable a few additional sensor types: 48 | - `AmbientLightSensor` 49 | - `Magnetometer` 50 | 51 | Test suite 52 | === 53 | 54 | Run [web-platform-tests](https://github.com/w3c/web-platform-tests/) with this polyfill enabled [here](https://kenchris.github.io/sensor-polyfills/run-tests.html). 55 | 56 | Known issues 57 | === 58 | 59 | - `AbsoluteOrientationSensor` on iOS uses non-standard [`webkitCompassHeading`](https://developer.apple.com/documentation/webkitjs/deviceorientationevent/1804777-webkitcompassheading) that reports wrong readings if the device is held in its [`portrait-secondary`](https://w3c.github.io/screen-orientation/#dom-orientationtype-portrait-secondary) orientation. Specifically, the `webkitCompassHeading` flips by 180 degrees when tilted only slightly. 60 | 61 | Learn more 62 | === 63 | 64 | - [Sensors For The Web article on Google's Web Fundaments](https://developers.google.com/web/updates/2017/09/sensors-for-the-web) - a web developer-oriented article explaining how to use the Generic Sensor-based APIs. 65 | 66 | Reporting a security issue 67 | === 68 | If you have information about a security issue or vulnerability with an Intel-maintained open source project on https://github.com/intel, please send an e-mail to secure@intel.com. Encrypt sensitive information using our PGP public key. For issues related to Intel products, please visit https://security-center.intel.com. 69 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generic Sensor API polyfills example 5 | 20 | 21 | 26 | 27 | 100 | 101 | 102 |
103 |
104 |
105 |
106 | 107 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "motion-sensors-polyfill", 3 | "version": "0.3.7", 4 | "description": "A polyfill for the motion sensors based on the W3C Generic Sensor API", 5 | "main": "motion-sensors.js", 6 | "module": "motion-sensors.js", 7 | "files": [ 8 | "motion-sensors.js", 9 | "sensor.js", 10 | "geolocation-sensor.js", 11 | "/src/" 12 | ], 13 | "directories": { 14 | "test": "tests" 15 | }, 16 | "scripts": { 17 | "build": "cp src/*.js .", 18 | "test": "npm run build", 19 | "checksize": "uglifyjs motion-sensors.js -mc --toplevel | gzip -9 | wc -c" 20 | }, 21 | "author": "Kenneth Rohde Christiansen ", 22 | "homepage": "https://github.com/kenchris/lit-element", 23 | "license": "BSD-3-Clause", 24 | "repository": { 25 | "type": "git", 26 | "url": "git+https://github.com/kenchris/sensor-polyfills.git" 27 | }, 28 | "devDependencies": { 29 | "eslint": "^5.3.0", 30 | "eslint-config-google": "^0.9.1" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /run-geolocation.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Realtime Positioning 6 | 7 | 8 | 9 | 40 | 134 | 195 | 196 | 197 |
198 |

199 | We're using the navigator.permissions.query() API to find out 200 | whether you allow this site to access your location. Use your 201 | browser UI to grant location access to this site if you wish 202 | to experience this example. 203 |

204 |

205 | Chrome: Secure badge > Location > Always allow on this site 206 |

207 |
208 |
209 |

210 | refresh 211 | 212 | 213 |

214 |
215 | 216 | -------------------------------------------------------------------------------- /run-tests.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Generic Sensor API polyfills tests 5 | 6 | 7 | 8 | 9 | 10 | 11 | 53 | 54 | 55 |
56 | 57 | -------------------------------------------------------------------------------- /src/geolocation-sensor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | __sensor__, 4 | Sensor, 5 | SensorErrorEvent, 6 | activateCallback, 7 | deactivateCallback, 8 | notifyActivatedState, 9 | notifyError, 10 | // AbortController, 11 | // AbortSignal, 12 | } from './sensor.js'; 13 | 14 | const slot = __sensor__; 15 | 16 | async function obtainPermission() { 17 | let state = 'prompt'; // Default for geolocation. 18 | // @ts-ignore 19 | if (navigator.permissions) { 20 | // @ts-ignore 21 | const permission = await navigator.permissions.query({name: 'geolocation'}); 22 | state = permission.state; 23 | } 24 | 25 | return new Promise((resolve) => { 26 | if (state === 'granted') { 27 | return resolve(state); 28 | } 29 | 30 | const successFn = (_) => { 31 | resolve('granted'); 32 | }; 33 | 34 | const errorFn = (err) => { 35 | if (err.code === err.PERMISSION_DENIED) { 36 | resolve('denied'); 37 | } else { 38 | resolve(state); 39 | } 40 | }; 41 | 42 | const options = {maximumAge: Infinity, timeout: 0}; 43 | navigator.geolocation.getCurrentPosition(successFn, errorFn, options); 44 | }); 45 | } 46 | 47 | function register(options, onreading, onerror, onactivated) { 48 | const handleEvent = (position) => { 49 | const timestamp = position.timestamp - performance.timing.navigationStart; 50 | const coords = position.coords; 51 | 52 | onreading(timestamp, coords); 53 | }; 54 | 55 | const handleError = (error) => { 56 | let type; 57 | switch (error.code) { 58 | case error.TIMEOUT: 59 | type = 'TimeoutError'; 60 | break; 61 | case error.PERMISSION_DENIED: 62 | type = 'NotAllowedError'; 63 | break; 64 | case error.POSITION_UNAVAILABLE: 65 | type = 'NotReadableError'; 66 | break; 67 | default: 68 | type = 'UnknownError'; 69 | } 70 | onerror(error.message, type); 71 | }; 72 | 73 | const watchOptions = { 74 | enableHighAccuracy: false, 75 | maximumAge: 0, 76 | timeout: Infinity, 77 | }; 78 | 79 | const watchId = navigator.geolocation.watchPosition( 80 | handleEvent, handleError, watchOptions 81 | ); 82 | 83 | return watchId; 84 | } 85 | 86 | function deregister(watchId) { 87 | navigator.geolocation.clearWatch(watchId); 88 | } 89 | 90 | // Old geolocation API is FILO; on Chrome at least. 91 | class FIFOGeolocationEvents { 92 | constructor() { 93 | if (!this.constructor.instance) { 94 | this.constructor.instance = this; 95 | } 96 | 97 | // You can iterate through the elements of a map in insertion order. 98 | this.subscribers = new Map(); 99 | this.options = {}; 100 | this.watchId = null; 101 | 102 | this.lastReading = null; 103 | 104 | return this.constructor.instance; 105 | } 106 | 107 | unsubscribe(obj) { 108 | this.subscribers.delete(obj); 109 | if (!this.subscribers.size && this.watchId) { 110 | deregister(this.watchId); 111 | this.watchId = null; 112 | } 113 | } 114 | 115 | subscribe(obj, options, onreading, onerror) { 116 | const fifoOnReading = (...args) => { 117 | this.lastReading = args; 118 | for ({onreading} of this.subscribers.values()) { 119 | if (typeof onreading === 'function') { 120 | onreading(...args); 121 | } 122 | } 123 | }; 124 | 125 | const fifoOnError = (...args) => { 126 | for ({onerror} of this.subscribers.values()) { 127 | if (typeof onerror === 'function') { 128 | onerror(...args); 129 | } 130 | } 131 | }; 132 | 133 | // TODO(spec): Generate the most precise options here 134 | // ie. lowest maximum-age and highest precision. 135 | this.options = options; 136 | 137 | if (this.watchId) { 138 | deregister(this.watchId); 139 | } 140 | // TODO(spec): Ensure lastReading is still valid. 141 | if (this.lastReading && typeof onreading === 'function') { 142 | onreading(...this.lastReading); 143 | } 144 | this.subscribers.set(obj, {onreading, onerror}); 145 | this.watchId = register(this.options, 146 | fifoOnReading, fifoOnError 147 | ); 148 | } 149 | } 150 | 151 | // @ts-ignore 152 | export const GeolocationSensor = window.GeolocationSensor || 153 | class GeolocationSensor extends Sensor { 154 | static async read(options = {}) { 155 | return new Promise(async (resolve, reject) => { 156 | const onerror = (message, name) => { 157 | let error = new SensorErrorEvent('error', { 158 | error: new DOMException(message, name), 159 | }); 160 | deregister(watchId); 161 | reject(error); 162 | }; 163 | 164 | const onreading = (timestamp, coords) => { 165 | const reading = { 166 | timestamp, 167 | accuracy: coords.accuracy, 168 | altitude: coords.altitude, 169 | altitudeAccuracy: coords.altitudeAccuracy, 170 | heading: coords.heading, 171 | latitude: coords.latitude, 172 | longitude: coords.longitude, 173 | speed: coords.speed, 174 | }; 175 | deregister(watchId); 176 | resolve(reading); 177 | }; 178 | 179 | const signal = options.signal; 180 | if (signal && signal.aborted) { 181 | return reject(new DOMException('Read was cancelled', 'AbortError')); 182 | } 183 | 184 | const permission = await obtainPermission(); 185 | if (permission !== 'granted') { 186 | onerror('Permission denied.', 'NowAllowedError'); 187 | return; 188 | } 189 | const watchId = register(options, onreading, onerror); 190 | 191 | if (signal) { 192 | signal.addEventListener('abort', () => { 193 | deregister(watchId); 194 | reject(new DOMException('Read was cancelled', 'AbortError')); 195 | }); 196 | } 197 | }); 198 | }; 199 | 200 | constructor(options = {}) { 201 | super(options); 202 | 203 | this[slot].options = options; 204 | this[slot].fifo = new FIFOGeolocationEvents(); 205 | 206 | const props = { 207 | latitude: null, 208 | longitude: null, 209 | altitude: null, 210 | accuracy: null, 211 | altitudeAccuracy: null, 212 | heading: null, 213 | speed: null, 214 | }; 215 | 216 | const propertyBag = this[slot]; 217 | /* eslint-disable-next-line guard-for-in */ 218 | for (const propName in props) { 219 | propertyBag[propName] = props[propName]; 220 | Object.defineProperty(this, propName, { 221 | get: () => propertyBag[propName], 222 | }); 223 | } 224 | } 225 | 226 | async [activateCallback]() { 227 | const onreading = (timestamp, coords) => { 228 | this[slot].timestamp = timestamp; 229 | 230 | this[slot].accuracy = coords.accuracy; 231 | this[slot].altitude = coords.altitude; 232 | this[slot].altitudeAccuracy = coords.altitudeAccuracy; 233 | this[slot].heading = coords.heading; 234 | this[slot].latitude = coords.latitude; 235 | this[slot].longitude = coords.longitude; 236 | this[slot].speed = coords.speed; 237 | 238 | this[slot].hasReading = true; 239 | this.dispatchEvent(new Event('reading')); 240 | }; 241 | 242 | const onerror = (message, type) => { 243 | this[notifyError](message, type); 244 | }; 245 | 246 | const permission = await obtainPermission(); 247 | if (permission !== 'granted') { 248 | onerror('Permission denied.', 'NowAllowedError'); 249 | return; 250 | } 251 | 252 | this[slot].fifo.subscribe( 253 | this, this[slot].options, 254 | onreading, onerror 255 | ); 256 | 257 | if (!this[slot].activated) { 258 | this[notifyActivatedState](); 259 | } 260 | } 261 | 262 | [deactivateCallback]() { 263 | this[slot].fifo.unsubscribe(this); 264 | this[slot].timestamp = null; 265 | 266 | this[slot].accuracy = null; 267 | this[slot].altitude = null; 268 | this[slot].altitudeAccuracy = null; 269 | this[slot].heading = null; 270 | this[slot].latitude = null; 271 | this[slot].longitude = null; 272 | this[slot].speed = null; 273 | 274 | this[slot].hasReading = false; 275 | } 276 | }; 277 | -------------------------------------------------------------------------------- /src/motion-sensors.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { 3 | Sensor, 4 | defineReadonlyProperties, 5 | __sensor__, 6 | notifyError, 7 | notifyActivatedState, 8 | activateCallback, 9 | deactivateCallback, 10 | } from './sensor.js'; 11 | 12 | const slot = __sensor__; 13 | const handleEventCallback = Symbol('handleEvent'); 14 | 15 | let orientation; 16 | 17 | // @ts-ignore 18 | if (screen.orientation) { 19 | // @ts-ignore 20 | orientation = screen.orientation; 21 | } else if (screen.msOrientation) { 22 | orientation = screen.msOrientation; 23 | } else { 24 | orientation = {}; 25 | Object.defineProperty(orientation, 'angle', { 26 | get: () => { 27 | return (window.orientation || 0); 28 | }, 29 | }); 30 | } 31 | 32 | const rotationToRadian = (function() { 33 | // Returns Chrome version, or null if not Chrome. 34 | const match = navigator.userAgent.match(/.*Chrome\/([0-9]+)/); 35 | const chromeVersion = match ? parseInt(match[1], 10) : null; 36 | 37 | // DeviceMotion/Orientation API return deg/s (except Chrome, 38 | // but fixing in M66). Gyroscope needs rad/s. 39 | const returnsDegrees = chromeVersion === null || chromeVersion >= 66; 40 | const conversion = returnsDegrees ? Math.PI / 180 : 1.0; 41 | return function(value) { 42 | return value * conversion; 43 | }; 44 | })(); 45 | 46 | const DeviceOrientationMixin = (superclass, ...eventNames) => class extends superclass { 47 | constructor(...args) { 48 | // @ts-ignore 49 | super(args); 50 | 51 | for (const eventName of eventNames) { 52 | if (`on${eventName}` in window) { 53 | this[slot].eventName = eventName; 54 | break; 55 | } 56 | } 57 | } 58 | 59 | [activateCallback]() { 60 | window.addEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); 61 | } 62 | 63 | [deactivateCallback]() { 64 | window.removeEventListener(this[slot].eventName, this[handleEventCallback].bind(this), {capture: true}); 65 | } 66 | }; 67 | 68 | function toQuaternionFromEuler(alpha, beta, gamma) { 69 | const degToRad = Math.PI / 180; 70 | 71 | const x = (beta || 0) * degToRad; 72 | const y = (gamma || 0) * degToRad; 73 | const z = (alpha || 0) * degToRad; 74 | 75 | const cZ = Math.cos(z * 0.5); 76 | const sZ = Math.sin(z * 0.5); 77 | const cY = Math.cos(y * 0.5); 78 | const sY = Math.sin(y * 0.5); 79 | const cX = Math.cos(x * 0.5); 80 | const sX = Math.sin(x * 0.5); 81 | 82 | const qx = sX * cY * cZ - cX * sY * sZ; 83 | const qy = cX * sY * cZ + sX * cY * sZ; 84 | const qz = cX * cY * sZ + sX * sY * cZ; 85 | const qw = cX * cY * cZ - sX * sY * sZ; 86 | 87 | return [qx, qy, qz, qw]; 88 | } 89 | 90 | function rotateQuaternionByAxisAngle(quat, axis, angle) { 91 | const sHalfAngle = Math.sin(angle / 2); 92 | const cHalfAngle = Math.cos(angle / 2); 93 | 94 | const transformQuat = [ 95 | axis[0] * sHalfAngle, 96 | axis[1] * sHalfAngle, 97 | axis[2] * sHalfAngle, 98 | cHalfAngle, 99 | ]; 100 | 101 | function multiplyQuaternion(a, b) { 102 | const qx = a[0] * b[3] + a[3] * b[0] + a[1] * b[2] - a[2] * b[1]; 103 | const qy = a[1] * b[3] + a[3] * b[1] + a[2] * b[0] - a[0] * b[2]; 104 | const qz = a[2] * b[3] + a[3] * b[2] + a[0] * b[1] - a[1] * b[0]; 105 | const qw = a[3] * b[3] - a[0] * b[0] - a[1] * b[1] - a[2] * b[2]; 106 | 107 | return [qx, qy, qz, qw]; 108 | } 109 | 110 | function normalizeQuaternion(quat) { 111 | const length = Math.sqrt(quat[0] ** 2 + quat[1] ** 2 + quat[2] ** 2 + quat[3] ** 2); 112 | if (length === 0) { 113 | return [0, 0, 0, 1]; 114 | } 115 | 116 | return quat.map((v) => v / length); 117 | } 118 | 119 | return normalizeQuaternion(multiplyQuaternion(quat, transformQuat)); 120 | } 121 | 122 | function toMat4FromQuat(mat, q) { 123 | const typed = mat instanceof Float32Array || mat instanceof Float64Array; 124 | 125 | if (typed && mat.length >= 16) { 126 | mat[0] = 1 - 2 * (q[1] ** 2 + q[2] ** 2); 127 | mat[1] = 2 * (q[0] * q[1] - q[2] * q[3]); 128 | mat[2] = 2 * (q[0] * q[2] + q[1] * q[3]); 129 | mat[3] = 0; 130 | 131 | mat[4] = 2 * (q[0] * q[1] + q[2] * q[3]); 132 | mat[5] = 1 - 2 * (q[0] ** 2 + q[2] ** 2); 133 | mat[6] = 2 * (q[1] * q[2] - q[0] * q[3]); 134 | mat[7] = 0; 135 | 136 | mat[8] = 2 * (q[0] * q[2] - q[1] * q[3]); 137 | mat[9] = 2 * (q[1] * q[2] + q[0] * q[3]); 138 | mat[10] = 1 - 2 * (q[0] ** 2 + q[1] ** 2); 139 | mat[11] = 0; 140 | 141 | mat[12] = 0; 142 | mat[13] = 0; 143 | mat[14] = 0; 144 | mat[15] = 1; 145 | } 146 | 147 | return mat; 148 | } 149 | 150 | function deviceToScreen(quaternion) { 151 | return !quaternion ? null : 152 | rotateQuaternionByAxisAngle( 153 | quaternion, 154 | [0, 0, 1], 155 | - orientation.angle * Math.PI / 180 156 | ); 157 | } 158 | 159 | // @ts-ignore 160 | export const RelativeOrientationSensor = window.RelativeOrientationSensor || 161 | class RelativeOrientationSensor extends DeviceOrientationMixin(Sensor, 'deviceorientation') { 162 | constructor(options = {}) { 163 | super(options); 164 | 165 | switch (options.referenceFrame || 'device') { 166 | case 'screen': 167 | Object.defineProperty(this, 'quaternion', { 168 | get: () => deviceToScreen(this[slot].quaternion), 169 | }); 170 | break; 171 | default: // incl. case 'device' 172 | Object.defineProperty(this, 'quaternion', { 173 | get: () => this[slot].quaternion, 174 | }); 175 | } 176 | } 177 | 178 | [handleEventCallback](event) { 179 | // If there is no sensor we will get values equal to null. 180 | if (event.absolute || event.alpha === null) { 181 | // Spec: The implementation can still decide to provide 182 | // absolute orientation if relative is not available or 183 | // the resulting data is more accurate. In either case, 184 | // the absolute property must be set accordingly to reflect 185 | // the choice. 186 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 187 | return; 188 | } 189 | 190 | if (!this[slot].activated) { 191 | this[notifyActivatedState](); 192 | } 193 | 194 | this[slot].timestamp = performance.now(); 195 | 196 | this[slot].quaternion = toQuaternionFromEuler( 197 | event.alpha, 198 | event.beta, 199 | event.gamma 200 | ); 201 | 202 | this[slot].hasReading = true; 203 | this.dispatchEvent(new Event('reading')); 204 | } 205 | 206 | [deactivateCallback]() { 207 | super[deactivateCallback](); 208 | this[slot].quaternion = null; 209 | } 210 | 211 | populateMatrix(mat) { 212 | toMat4FromQuat(mat, this.quaternion); 213 | } 214 | }; 215 | 216 | // @ts-ignore 217 | export const AbsoluteOrientationSensor = window.AbsoluteOrientationSensor || 218 | class AbsoluteOrientationSensor extends DeviceOrientationMixin( 219 | Sensor, 'deviceorientationabsolute', 'deviceorientation') { 220 | constructor(options = {}) { 221 | super(options); 222 | 223 | switch (options.referenceFrame || 'device') { 224 | case 'screen': 225 | Object.defineProperty(this, 'quaternion', { 226 | get: () => deviceToScreen(this[slot].quaternion), 227 | }); 228 | break; 229 | default: // incl. case 'device' 230 | Object.defineProperty(this, 'quaternion', { 231 | get: () => this[slot].quaternion, 232 | }); 233 | } 234 | } 235 | 236 | [handleEventCallback](event) { 237 | // If absolute is set, or webkitCompassHeading exists, 238 | // absolute values should be available. 239 | const isAbsolute = event.absolute === true || 'webkitCompassHeading' in event; 240 | const hasValue = event.alpha !== null || event.webkitCompassHeading !== undefined; 241 | 242 | if (!isAbsolute || !hasValue) { 243 | // Spec: If an implementation can never provide absolute 244 | // orientation information, the event should be fired with 245 | // the alpha, beta and gamma attributes set to null. 246 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 247 | return; 248 | } 249 | 250 | if (!this[slot].activated) { 251 | this[notifyActivatedState](); 252 | } 253 | 254 | this[slot].hasReading = true; 255 | this[slot].timestamp = performance.now(); 256 | 257 | const heading = event.webkitCompassHeading != null ? 360 - event.webkitCompassHeading : event.alpha; 258 | 259 | this[slot].quaternion = toQuaternionFromEuler( 260 | heading, 261 | event.beta, 262 | event.gamma 263 | ); 264 | 265 | this.dispatchEvent(new Event('reading')); 266 | } 267 | 268 | [deactivateCallback]() { 269 | super[deactivateCallback](); 270 | this[slot].quaternion = null; 271 | } 272 | 273 | populateMatrix(mat) { 274 | toMat4FromQuat(mat, this.quaternion); 275 | } 276 | }; 277 | 278 | // @ts-ignore 279 | export const Gyroscope = window.Gyroscope || 280 | class Gyroscope extends DeviceOrientationMixin(Sensor, 'devicemotion') { 281 | constructor(options) { 282 | super(options); 283 | defineReadonlyProperties(this, slot, { 284 | x: null, 285 | y: null, 286 | z: null, 287 | }); 288 | } 289 | 290 | [handleEventCallback](event) { 291 | // If there is no sensor we will get values equal to null. 292 | if (event.rotationRate.alpha === null) { 293 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 294 | return; 295 | } 296 | 297 | if (!this[slot].activated) { 298 | this[notifyActivatedState](); 299 | } 300 | 301 | this[slot].timestamp = performance.now(); 302 | 303 | this[slot].x = rotationToRadian(event.rotationRate.alpha); 304 | this[slot].y = rotationToRadian(event.rotationRate.beta); 305 | this[slot].z = rotationToRadian(event.rotationRate.gamma); 306 | 307 | this[slot].hasReading = true; 308 | this.dispatchEvent(new Event('reading')); 309 | } 310 | 311 | [deactivateCallback]() { 312 | super[deactivateCallback](); 313 | this[slot].x = null; 314 | this[slot].y = null; 315 | this[slot].z = null; 316 | } 317 | }; 318 | 319 | // @ts-ignore 320 | export const Accelerometer = window.Accelerometer || 321 | class Accelerometer extends DeviceOrientationMixin(Sensor, 'devicemotion') { 322 | constructor(options) { 323 | super(options); 324 | defineReadonlyProperties(this, slot, { 325 | x: null, 326 | y: null, 327 | z: null, 328 | }); 329 | } 330 | 331 | [handleEventCallback](event) { 332 | // If there is no sensor we will get values equal to null. 333 | if (event.accelerationIncludingGravity.x === null) { 334 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 335 | return; 336 | } 337 | 338 | if (!this[slot].activated) { 339 | this[notifyActivatedState](); 340 | } 341 | 342 | this[slot].timestamp = performance.now(); 343 | 344 | this[slot].x = event.accelerationIncludingGravity.x; 345 | this[slot].y = event.accelerationIncludingGravity.y; 346 | this[slot].z = event.accelerationIncludingGravity.z; 347 | 348 | this[slot].hasReading = true; 349 | this.dispatchEvent(new Event('reading')); 350 | } 351 | 352 | [deactivateCallback]() { 353 | super[deactivateCallback](); 354 | this[slot].x = null; 355 | this[slot].y = null; 356 | this[slot].z = null; 357 | } 358 | }; 359 | 360 | // @ts-ignore 361 | export const LinearAccelerationSensor = window.LinearAccelerationSensor || 362 | class LinearAccelerationSensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { 363 | constructor(options) { 364 | super(options); 365 | defineReadonlyProperties(this, slot, { 366 | x: null, 367 | y: null, 368 | z: null, 369 | }); 370 | } 371 | 372 | [handleEventCallback](event) { 373 | // If there is no sensor we will get values equal to null. 374 | if (event.acceleration.x === null) { 375 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 376 | return; 377 | } 378 | 379 | if (!this[slot].activated) { 380 | this[notifyActivatedState](); 381 | } 382 | 383 | this[slot].timestamp = performance.now(); 384 | 385 | this[slot].x = event.acceleration.x; 386 | this[slot].y = event.acceleration.y; 387 | this[slot].z = event.acceleration.z; 388 | 389 | this[slot].hasReading = true; 390 | this.dispatchEvent(new Event('reading')); 391 | } 392 | 393 | [deactivateCallback]() { 394 | super[deactivateCallback](); 395 | this[slot].x = null; 396 | this[slot].y = null; 397 | this[slot].z = null; 398 | } 399 | }; 400 | 401 | // @ts-ignore 402 | export const GravitySensor = window.GravitySensor || 403 | class GravitySensor extends DeviceOrientationMixin(Sensor, 'devicemotion') { 404 | constructor(options) { 405 | super(options); 406 | defineReadonlyProperties(this, slot, { 407 | x: null, 408 | y: null, 409 | z: null, 410 | }); 411 | } 412 | 413 | [handleEventCallback](event) { 414 | // If there is no sensor we will get values equal to null. 415 | if (event.acceleration.x === null || event.accelerationIncludingGravity.x === null) { 416 | this[notifyError]('Could not connect to a sensor', 'NotReadableError'); 417 | return; 418 | } 419 | 420 | if (!this[slot].activated) { 421 | this[notifyActivatedState](); 422 | } 423 | 424 | this[slot].timestamp = performance.now(); 425 | 426 | this[slot].x = event.accelerationIncludingGravity.x - event.acceleration.x; 427 | this[slot].y = event.accelerationIncludingGravity.y - event.acceleration.y; 428 | this[slot].z = event.accelerationIncludingGravity.z - event.acceleration.z; 429 | 430 | this[slot].hasReading = true; 431 | this.dispatchEvent(new Event('reading')); 432 | } 433 | 434 | [deactivateCallback]() { 435 | super[deactivateCallback](); 436 | this[slot].x = null; 437 | this[slot].y = null; 438 | this[slot].z = null; 439 | } 440 | }; 441 | -------------------------------------------------------------------------------- /src/sensor.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | function defineProperties(target, descriptions) { 4 | /* eslint-disable-next-line guard-for-in */ 5 | for (const property in descriptions) { 6 | Object.defineProperty(target, property, { 7 | configurable: true, 8 | value: descriptions[property], 9 | }); 10 | } 11 | } 12 | 13 | const privates = new WeakMap(); 14 | 15 | export const EventTargetMixin = (superclass, ...eventNames) => class extends superclass { 16 | constructor(...args) { 17 | // @ts-ignore 18 | super(args); 19 | const eventTarget = document.createDocumentFragment(); 20 | privates.set(this, eventTarget); 21 | } 22 | 23 | addEventListener(type, ...args) { 24 | const eventTarget = privates.get(this); 25 | return eventTarget.addEventListener(type, ...args); 26 | } 27 | 28 | removeEventListener(...args) { 29 | const eventTarget = privates.get(this); 30 | // @ts-ignore 31 | return eventTarget.removeEventListener(...args); 32 | } 33 | 34 | dispatchEvent(event) { 35 | defineProperties(event, {currentTarget: this}); 36 | if (!event.target) { 37 | defineProperties(event, {target: this}); 38 | } 39 | 40 | const eventTarget = privates.get(this); 41 | const retValue = eventTarget.dispatchEvent(event); 42 | 43 | if (retValue && this.parentNode) { 44 | this.parentNode.dispatchEvent(event); 45 | } 46 | 47 | defineProperties(event, {currentTarget: null, target: null}); 48 | 49 | return retValue; 50 | } 51 | }; 52 | 53 | export class EventTarget extends EventTargetMixin(Object) {} 54 | 55 | const __abort__ = Symbol('__abort__'); 56 | 57 | export class AbortSignal extends EventTarget { 58 | constructor() { 59 | super(); 60 | 61 | this[__abort__] = { 62 | aborted: false, 63 | }; 64 | 65 | defineOnEventListener(this, 'abort'); 66 | Object.defineProperty(this, 'aborted', { 67 | get: () => this[__abort__].aborted, 68 | }); 69 | } 70 | 71 | dispatchEvent(event) { 72 | if (event.type === 'abort') { 73 | this[__abort__].aborted = true; 74 | 75 | const methodName = `on${event.type}`; 76 | if (typeof this[methodName] == 'function') { 77 | this[methodName](event); 78 | } 79 | } 80 | super.dispatchEvent(event); 81 | } 82 | 83 | toString() { 84 | return '[object AbortSignal]'; 85 | } 86 | } 87 | 88 | export class AbortController { 89 | constructor() { 90 | const signal = new AbortSignal(); 91 | Object.defineProperty(this, 'signal', { 92 | get: () => signal, 93 | }); 94 | } 95 | 96 | abort() { 97 | let abort = new Event('abort'); 98 | this.signal.dispatchEvent(abort); 99 | } 100 | 101 | toString() { 102 | return '[object AbortController]'; 103 | } 104 | } 105 | 106 | function defineOnEventListener(target, name) { 107 | Object.defineProperty(target, `on${name}`, { 108 | enumerable: true, 109 | configurable: false, 110 | writable: true, 111 | value: null, 112 | }); 113 | } 114 | 115 | export function defineReadonlyProperties(target, slot, descriptions) { 116 | const propertyBag = target[slot]; 117 | /* eslint-disable-next-line guard-for-in */ 118 | for (const property in descriptions) { 119 | propertyBag[property] = descriptions[property]; 120 | Object.defineProperty(target, property, { 121 | get: () => propertyBag[property], 122 | }); 123 | } 124 | } 125 | 126 | export class SensorErrorEvent extends Event { 127 | constructor(type, errorEventInitDict) { 128 | super(type, errorEventInitDict); 129 | 130 | if (!errorEventInitDict || !(errorEventInitDict.error instanceof DOMException)) { 131 | throw TypeError( 132 | 'Failed to construct \'SensorErrorEvent\':' + 133 | '2nd argument much contain \'error\' property' 134 | ); 135 | } 136 | 137 | Object.defineProperty(this, 'error', { 138 | configurable: false, 139 | writable: false, 140 | value: errorEventInitDict.error, 141 | }); 142 | } 143 | } 144 | 145 | const SensorState = { 146 | IDLE: 1, 147 | ACTIVATING: 2, 148 | ACTIVE: 3, 149 | }; 150 | 151 | export const __sensor__ = Symbol('__sensor__'); 152 | const slot = __sensor__; 153 | 154 | export const notifyError = Symbol('Sensor.notifyError'); 155 | export const notifyActivatedState = Symbol('Sensor.notifyActivatedState'); 156 | 157 | export const activateCallback = Symbol('Sensor.activateCallback'); 158 | export const deactivateCallback = Symbol('Sensor.deactivateCallback'); 159 | 160 | export class Sensor extends EventTarget { 161 | [activateCallback]() {} 162 | [deactivateCallback]() {} 163 | 164 | [notifyError](message, name) { 165 | let error = new SensorErrorEvent('error', { 166 | error: new DOMException(message, name), 167 | }); 168 | this.dispatchEvent(error); 169 | this.stop(); 170 | } 171 | 172 | [notifyActivatedState]() { 173 | let activate = new Event('activate'); 174 | this[slot].activated = true; 175 | this.dispatchEvent(activate); 176 | this[slot].state = SensorState.ACTIVE; 177 | } 178 | 179 | constructor(options) { 180 | super(); 181 | 182 | this[__sensor__] = { 183 | // Internal slots 184 | state: SensorState.IDLE, 185 | frequency: null, 186 | 187 | // Property backing 188 | activated: false, 189 | hasReading: false, 190 | timestamp: null, 191 | }; 192 | 193 | defineOnEventListener(this, 'reading'); 194 | defineOnEventListener(this, 'activate'); 195 | defineOnEventListener(this, 'error'); 196 | 197 | Object.defineProperty(this, 'activated', { 198 | get: () => this[slot].activated, 199 | }); 200 | Object.defineProperty(this, 'hasReading', { 201 | get: () => this[slot].hasReading, 202 | }); 203 | Object.defineProperty(this, 'timestamp', { 204 | get: () => this[slot].timestamp, 205 | }); 206 | 207 | if (window && window.parent != window.top) { 208 | throw new DOMException( 209 | 'Only instantiable in a top-level browsing context', 210 | 'SecurityError' 211 | ); 212 | } 213 | 214 | if (options && typeof(options.frequency) == 'number') { 215 | if (options.frequency > 60) { 216 | this.frequency = options.frequency; 217 | } 218 | } 219 | } 220 | 221 | dispatchEvent(event) { 222 | switch (event.type) { 223 | case 'reading': 224 | case 'error': 225 | case 'activate': 226 | { 227 | const methodName = `on${event.type}`; 228 | if (typeof this[methodName] == 'function') { 229 | this[methodName](event); 230 | } 231 | super.dispatchEvent(event); 232 | break; 233 | } 234 | default: 235 | super.dispatchEvent(event); 236 | } 237 | } 238 | 239 | start() { 240 | if (this[slot].state === SensorState.ACTIVATING || 241 | this[slot].state === SensorState.ACTIVE) { 242 | return; 243 | } 244 | this[slot].state = SensorState.ACTIVATING; 245 | this[activateCallback](); 246 | } 247 | 248 | stop() { 249 | if (this[slot].state === SensorState.IDLE) { 250 | return; 251 | } 252 | this[slot].activated = false; 253 | this[slot].hasReading = false; 254 | this[slot].timestamp = null; 255 | this[deactivateCallback](); 256 | 257 | this[slot].state = SensorState.IDLE; 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /tests/EventTarget-tests.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | function runEventTarget_addEventListener(eventTarget) { 4 | test(() => { 5 | const target = new eventTarget(); 6 | 7 | assert_equals(target.addEventListener("x", null, false), undefined); 8 | assert_equals(target.addEventListener("x", null, true), undefined); 9 | assert_equals(target.addEventListener("x", null), undefined); 10 | }, "Adding a null event listener should succeed"); 11 | } 12 | 13 | function runEventTarget_removeEventListener(eventTarget) { 14 | test(() => { 15 | const target = new eventTarget(); 16 | 17 | assert_equals(target.removeEventListener("x", null, false), undefined); 18 | assert_equals(target.removeEventListener("x", null, true), undefined); 19 | assert_equals(target.removeEventListener("x", null), undefined); 20 | }, "removing a null event listener should succeed"); 21 | } 22 | 23 | setup({ 24 | "allow_uncaught_exception": true, 25 | }) 26 | 27 | function runEventTarget_dispatchEvent(testElement) { 28 | customElements.define('test-b', testElement); 29 | 30 | test(function() { 31 | assert_throws(new TypeError(), function() { document.dispatchEvent(null) }) 32 | }, "Calling dispatchEvent(null).") 33 | 34 | var dispatch_dispatch = async_test("If the event's dispatch flag is set, an InvalidStateError must be thrown.") 35 | dispatch_dispatch.step(function() { 36 | var e = document.createEvent("Event") 37 | e.initEvent("type", false, false) 38 | var target = document.createElement("test-b"); 39 | target.addEventListener("type", dispatch_dispatch.step_func(function() { 40 | assert_throws("InvalidStateError", function() { 41 | target.dispatchEvent(e) 42 | }) 43 | assert_throws("InvalidStateError", function() { 44 | document.dispatchEvent(e) 45 | }) 46 | }), false) 47 | assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true") 48 | dispatch_dispatch.done() 49 | }) 50 | 51 | test(function() { 52 | // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17713 53 | // https://www.w3.org/Bugs/Public/show_bug.cgi?id=17714 54 | var e = document.createEvent("Event") 55 | e.initEvent("type", false, false) 56 | var called = [] 57 | var target = document.createElement("test-b"); 58 | target.addEventListener("type", function() { 59 | called.push("First") 60 | throw new Error() 61 | }, false) 62 | target.addEventListener("type", function() { 63 | called.push("Second") 64 | }, false) 65 | assert_equals(target.dispatchEvent(e), true, "dispatchEvent must return true") 66 | assert_array_equals(called, ["First", "Second"], 67 | "Should have continued to call other event listeners") 68 | }, "Exceptions from event listeners must not be propagated.") 69 | 70 | async_test(function() { 71 | var results = [] 72 | var outerb = document.createElement("test-b") 73 | var middleb = outerb.appendChild(document.createElement("test-b")) 74 | var innerb = middleb.appendChild(document.createElement("test-b")) 75 | outerb.addEventListener("x", this.step_func(function() { 76 | middleb.addEventListener("x", this.step_func(function() { 77 | results.push("middle") 78 | }), true) 79 | results.push("outer") 80 | }), true) 81 | innerb.dispatchEvent(new Event("x")) 82 | console.log(results); 83 | assert_array_equals(results, ["outer", "middle"]) 84 | this.done() 85 | }, "Event listeners added during dispatch should be called"); 86 | 87 | async_test(function() { 88 | var results = [] 89 | var b = document.createElement("test-b") 90 | b.addEventListener("x", this.step_func(function() { 91 | results.push(1) 92 | }), true) 93 | b.addEventListener("x", this.step_func(function() { 94 | results.push(2) 95 | }), false) 96 | b.addEventListener("x", this.step_func(function() { 97 | results.push(3) 98 | }), true) 99 | b.dispatchEvent(new Event("x")) 100 | assert_array_equals(results, [1, 2, 3]) 101 | this.done() 102 | }, "Event listeners should be called in order of addition") 103 | 104 | test(function() { 105 | var event_type = "foo"; 106 | var target = document.createElement("test-b"); 107 | var parent = document.createElement("test-b"); 108 | parent.appendChild(target); 109 | 110 | var default_prevented; 111 | parent.addEventListener(event_type, function(e) {}, true); 112 | target.addEventListener(event_type, function(e) { 113 | evt.preventDefault(); 114 | default_prevented = evt.defaultPrevented; 115 | }, true); 116 | target.addEventListener(event_type, function(e) {}, true); 117 | var evt = document.createEvent("Event"); 118 | evt.initEvent(event_type, true, true); 119 | assert_true(parent.dispatchEvent(evt)); 120 | assert_false(target.dispatchEvent(evt)); 121 | assert_true(default_prevented); 122 | }, "Return value of EventTarget.dispatchEvent."); 123 | } 124 | 125 | function runEventTarget_constructible(eventTarget) { 126 | test(() => { 127 | const target = new eventTarget(); 128 | const event = new Event("foo", { bubbles: true, cancelable: false }); 129 | let callCount = 0; 130 | 131 | function listener(e) { 132 | assert_equals(e, event); 133 | ++callCount; 134 | } 135 | 136 | target.addEventListener("foo", listener); 137 | 138 | target.dispatchEvent(event); 139 | assert_equals(callCount, 1); 140 | 141 | target.dispatchEvent(event); 142 | assert_equals(callCount, 2); 143 | 144 | target.removeEventListener("foo", listener); 145 | target.dispatchEvent(event); 146 | assert_equals(callCount, 2); 147 | }, "A constructed EventTarget can be used as expected"); 148 | 149 | test(() => { 150 | class NicerEventTarget extends eventTarget { 151 | on(...args) { 152 | this.addEventListener(...args); 153 | } 154 | 155 | off(...args) { 156 | this.removeEventListener(...args); 157 | } 158 | 159 | dispatch(type, detail) { 160 | this.dispatchEvent(new CustomEvent(type, { detail })); 161 | } 162 | } 163 | 164 | const target = new NicerEventTarget(); 165 | const event = new Event("foo", { bubbles: true, cancelable: false }); 166 | const detail = "some data"; 167 | let callCount = 0; 168 | 169 | function listener(e) { 170 | assert_equals(e.detail, detail); 171 | ++callCount; 172 | } 173 | 174 | target.on("foo", listener); 175 | 176 | target.dispatch("foo", detail); 177 | assert_equals(callCount, 1); 178 | 179 | target.dispatch("foo", detail); 180 | assert_equals(callCount, 2); 181 | 182 | target.off("foo", listener); 183 | target.dispatch("foo", detail); 184 | assert_equals(callCount, 2); 185 | }, "EventTarget can be subclassed"); 186 | } -------------------------------------------------------------------------------- /tests/generic-sensor-tests.js: -------------------------------------------------------------------------------- 1 | const properties = { 2 | 'Accelerometer' : ['timestamp', 'x', 'y', 'z'], 3 | 'LinearAccelerationSensor' : ['timestamp', 'x', 'y', 'z'], 4 | "GravitySensor" : ['timestamp', 'x', 'y', 'z'], 5 | 'Gyroscope' : ['timestamp', 'x', 'y', 'z'], 6 | 'Magnetometer' : ['timestamp', 'x', 'y', 'z'], 7 | 'AbsoluteOrientationSensor' : ['timestamp', 'quaternion'], 8 | 'RelativeOrientationSensor' : ['timestamp', 'quaternion'], 9 | 'GeolocationSensor' : [ 10 | 'timestamp', 11 | 'latitude', 12 | 'longitude' 13 | ] 14 | }; 15 | 16 | function assert_reading_not_null(sensor) { 17 | for (let property in properties[sensor.constructor.name]) { 18 | let propertyName = properties[sensor.constructor.name][property]; 19 | assert_not_equals(sensor[propertyName], null); 20 | } 21 | } 22 | 23 | function assert_reading_null(sensor) { 24 | for (let property in properties[sensor.constructor.name]) { 25 | let propertyName = properties[sensor.constructor.name][property]; 26 | assert_equals(sensor[propertyName], null); 27 | } 28 | } 29 | 30 | function reading_to_array(sensor) { 31 | const arr = new Array(); 32 | for (let property in properties[sensor.constructor.name]) { 33 | let propertyName = properties[sensor.constructor.name][property]; 34 | let value = sensor[propertyName]; 35 | // Quaternions are arrays, compare as strings. 36 | if (Array.isArray(value)) { 37 | value = value.toString(); 38 | } 39 | arr[property] = value; 40 | } 41 | return arr; 42 | } 43 | 44 | function runGenericSensorTests(sensorType) { 45 | promise_test(async t => { 46 | const sensor = new sensorType(); 47 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 48 | sensor.start(); 49 | 50 | await sensorWatcher.wait_for("reading"); 51 | assert_reading_not_null(sensor); 52 | assert_true(sensor.hasReading); 53 | 54 | sensor.stop(); 55 | assert_reading_null(sensor); 56 | assert_false(sensor.hasReading); 57 | }, `${sensorType.name}: Test that 'onreading' is called and sensor reading is valid`); 58 | 59 | promise_test(async t => { 60 | const sensor1 = new sensorType(); 61 | const sensor2 = new sensorType(); 62 | const sensorWatcher = new EventWatcher(t, sensor1, ["reading", "error"]); 63 | sensor2.start(); 64 | sensor1.start(); 65 | await sensorWatcher.wait_for("reading"); 66 | 67 | // Reading values are correct for both sensors. 68 | assert_reading_not_null(sensor1); 69 | assert_reading_not_null(sensor2); 70 | 71 | //After first sensor stops its reading values are null, 72 | //reading values for the second sensor remains 73 | sensor1.stop(); 74 | assert_reading_null(sensor1); 75 | assert_reading_not_null(sensor2); 76 | sensor2.stop(); 77 | assert_reading_null(sensor2); 78 | }, `${sensorType.name}: sensor reading is correct`); 79 | 80 | promise_test(async t => { 81 | const sensor = new sensorType(); 82 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 83 | sensor.start(); 84 | 85 | await sensorWatcher.wait_for("reading"); 86 | const cachedTimeStamp1 = sensor.timestamp; 87 | 88 | await sensorWatcher.wait_for("reading"); 89 | const cachedTimeStamp2 = sensor.timestamp; 90 | 91 | assert_greater_than(cachedTimeStamp2, cachedTimeStamp1); 92 | sensor.stop(); 93 | }, `${sensorType.name}: sensor timestamp is updated when time passes`); 94 | 95 | promise_test(async t => { 96 | const sensor = new sensorType(); 97 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 98 | assert_false(sensor.activated); 99 | sensor.start(); 100 | assert_false(sensor.activated); 101 | 102 | await sensorWatcher.wait_for("activate"); 103 | assert_true(sensor.activated); 104 | 105 | sensor.stop(); 106 | assert_false(sensor.activated); 107 | }, `${sensorType.name}: Test that sensor can be successfully created and its states are correct.`); 108 | 109 | promise_test(async t => { 110 | const sensor = new sensorType(); 111 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 112 | const start_return = sensor.start(); 113 | 114 | await sensorWatcher.wait_for("activate"); 115 | assert_equals(start_return, undefined); 116 | sensor.stop(); 117 | }, `${sensorType.name}: sensor.start() returns undefined`); 118 | 119 | promise_test(async t => { 120 | const sensor = new sensorType(); 121 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 122 | sensor.start(); 123 | sensor.start(); 124 | 125 | await sensorWatcher.wait_for("activate"); 126 | assert_true(sensor.activated); 127 | sensor.stop(); 128 | }, `${sensorType.name}: no exception is thrown when calling start() on already started sensor`); 129 | 130 | promise_test(async t => { 131 | const sensor = new sensorType(); 132 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 133 | sensor.start(); 134 | 135 | await sensorWatcher.wait_for("activate"); 136 | const stop_return = sensor.stop(); 137 | assert_equals(stop_return, undefined); 138 | }, `${sensorType.name}: sensor.stop() returns undefined`); 139 | 140 | promise_test(async t => { 141 | const sensor = new sensorType(); 142 | const sensorWatcher = new EventWatcher(t, sensor, ["activate", "error"]); 143 | sensor.start(); 144 | 145 | await sensorWatcher.wait_for("activate"); 146 | sensor.stop(); 147 | sensor.stop(); 148 | assert_false(sensor.activated); 149 | }, `${sensorType.name}: no exception is thrown when calling stop() on already stopped sensor`); 150 | 151 | promise_test(async t => { 152 | const sensor = new sensorType(); 153 | const sensorWatcher = new EventWatcher(t, sensor, ["reading", "error"]); 154 | const visibilityChangeWatcher = new EventWatcher(t, document, "visibilitychange"); 155 | sensor.start(); 156 | 157 | await sensorWatcher.wait_for("reading"); 158 | assert_reading_not_null(sensor); 159 | const cachedSensor1 = reading_to_array(sensor); 160 | 161 | const win = window.open('', '_blank'); 162 | await visibilityChangeWatcher.wait_for("visibilitychange"); 163 | const cachedSensor2 = reading_to_array(sensor); 164 | 165 | win.close(); 166 | sensor.stop(); 167 | assert_array_equals(cachedSensor1, cachedSensor2); 168 | }, `${sensorType.name}: sensor readings can not be fired on the background tab`); 169 | } 170 | 171 | function runGenericSensorInsecureContext(sensorType) { 172 | test(() => { 173 | assert_false(sensorType in window, `${sensorType} must not be exposed`); 174 | }, `${sensorType} is not exposed in an insecure context`); 175 | } 176 | 177 | function runGenericSensorOnerror(sensorType) { 178 | promise_test(async t => { 179 | const sensor = new sensorType(); 180 | const sensorWatcher = new EventWatcher(t, sensor, ["error", "activate"]); 181 | sensor.start(); 182 | 183 | const event = await sensorWatcher.wait_for("error"); 184 | assert_false(sensor.activated); 185 | assert_equals(event.error.name, 'NotReadableError'); 186 | }, `${sensorType.name}: 'onerror' event is fired when sensor is not supported`); 187 | } -------------------------------------------------------------------------------- /tests/testharness.css: -------------------------------------------------------------------------------- 1 | html { 2 | font-family:DejaVu Sans, Bitstream Vera Sans, Arial, Sans; 3 | } 4 | 5 | #log .warning, 6 | #log .warning a { 7 | color: black; 8 | background: yellow; 9 | } 10 | 11 | #log .error, 12 | #log .error a { 13 | color: white; 14 | background: red; 15 | } 16 | 17 | section#summary { 18 | margin-bottom:1em; 19 | } 20 | 21 | table#results { 22 | border-collapse:collapse; 23 | table-layout:fixed; 24 | width:100%; 25 | } 26 | 27 | table#results th:first-child, 28 | table#results td:first-child { 29 | width:4em; 30 | } 31 | 32 | table#results th:last-child, 33 | table#results td:last-child { 34 | width:50%; 35 | } 36 | 37 | table#results.assertions th:last-child, 38 | table#results.assertions td:last-child { 39 | width:35%; 40 | } 41 | 42 | table#results th { 43 | padding:0; 44 | padding-bottom:0.5em; 45 | border-bottom:medium solid black; 46 | } 47 | 48 | table#results td { 49 | padding:1em; 50 | padding-bottom:0.5em; 51 | border-bottom:thin solid black; 52 | } 53 | 54 | tr.pass > td:first-child { 55 | color:green; 56 | } 57 | 58 | tr.fail > td:first-child { 59 | color:red; 60 | } 61 | 62 | tr.timeout > td:first-child { 63 | color:red; 64 | } 65 | 66 | tr.notrun > td:first-child { 67 | color:blue; 68 | } 69 | 70 | .pass > td:first-child, .fail > td:first-child, .timeout > td:first-child, .notrun > td:first-child { 71 | font-variant:small-caps; 72 | } 73 | 74 | table#results span { 75 | display:block; 76 | } 77 | 78 | table#results span.expected { 79 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 80 | white-space:pre; 81 | } 82 | 83 | table#results span.actual { 84 | font-family:DejaVu Sans Mono, Bitstream Vera Sans Mono, Monospace; 85 | white-space:pre; 86 | } 87 | 88 | span.ok { 89 | color:green; 90 | } 91 | 92 | tr.error { 93 | color:red; 94 | } 95 | 96 | span.timeout { 97 | color:red; 98 | } 99 | 100 | span.ok, span.timeout, span.error { 101 | font-variant:small-caps; 102 | } -------------------------------------------------------------------------------- /tests/testharness.js: -------------------------------------------------------------------------------- 1 | /*global self*/ 2 | /*jshint latedef: nofunc*/ 3 | /* 4 | Distributed under both the W3C Test Suite License [1] and the W3C 5 | 3-clause BSD License [2]. To contribute to a W3C Test Suite, see the 6 | policies and contribution forms [3]. 7 | 8 | [1] http://www.w3.org/Consortium/Legal/2008/04-testsuite-license 9 | [2] http://www.w3.org/Consortium/Legal/2008/03-bsd-license 10 | [3] http://www.w3.org/2004/10/27-testcases 11 | */ 12 | 13 | /* Documentation: http://web-platform-tests.org/writing-tests/testharness-api.html 14 | * (../docs/_writing-tests/testharness-api.md) */ 15 | 16 | (function () 17 | { 18 | var debug = false; 19 | // default timeout is 10 seconds, test can override if needed 20 | var settings = { 21 | output:true, 22 | harness_timeout:{ 23 | "normal":10000, 24 | "long":60000 25 | }, 26 | test_timeout:null, 27 | message_events: ["start", "test_state", "result", "completion"] 28 | }; 29 | 30 | var xhtml_ns = "http://www.w3.org/1999/xhtml"; 31 | 32 | /* 33 | * TestEnvironment is an abstraction for the environment in which the test 34 | * harness is used. Each implementation of a test environment has to provide 35 | * the following interface: 36 | * 37 | * interface TestEnvironment { 38 | * // Invoked after the global 'tests' object has been created and it's 39 | * // safe to call add_*_callback() to register event handlers. 40 | * void on_tests_ready(); 41 | * 42 | * // Invoked after setup() has been called to notify the test environment 43 | * // of changes to the test harness properties. 44 | * void on_new_harness_properties(object properties); 45 | * 46 | * // Should return a new unique default test name. 47 | * DOMString next_default_test_name(); 48 | * 49 | * // Should return the test harness timeout duration in milliseconds. 50 | * float test_timeout(); 51 | * 52 | * // Should return the global scope object. 53 | * object global_scope(); 54 | * }; 55 | */ 56 | 57 | /* 58 | * A test environment with a DOM. The global object is 'window'. By default 59 | * test results are displayed in a table. Any parent windows receive 60 | * callbacks or messages via postMessage() when test events occur. See 61 | * apisample11.html and apisample12.html. 62 | */ 63 | function WindowTestEnvironment() { 64 | this.name_counter = 0; 65 | this.window_cache = null; 66 | this.output_handler = null; 67 | this.all_loaded = false; 68 | var this_obj = this; 69 | this.message_events = []; 70 | this.dispatched_messages = []; 71 | 72 | this.message_functions = { 73 | start: [add_start_callback, remove_start_callback, 74 | function (properties) { 75 | this_obj._dispatch("start_callback", [properties], 76 | {type: "start", properties: properties}); 77 | }], 78 | 79 | test_state: [add_test_state_callback, remove_test_state_callback, 80 | function(test) { 81 | this_obj._dispatch("test_state_callback", [test], 82 | {type: "test_state", 83 | test: test.structured_clone()}); 84 | }], 85 | result: [add_result_callback, remove_result_callback, 86 | function (test) { 87 | this_obj.output_handler.show_status(); 88 | this_obj._dispatch("result_callback", [test], 89 | {type: "result", 90 | test: test.structured_clone()}); 91 | }], 92 | completion: [add_completion_callback, remove_completion_callback, 93 | function (tests, harness_status) { 94 | var cloned_tests = map(tests, function(test) { 95 | return test.structured_clone(); 96 | }); 97 | this_obj._dispatch("completion_callback", [tests, harness_status], 98 | {type: "complete", 99 | tests: cloned_tests, 100 | status: harness_status.structured_clone()}); 101 | }] 102 | } 103 | 104 | on_event(window, 'load', function() { 105 | this_obj.all_loaded = true; 106 | }); 107 | 108 | on_event(window, 'message', function(event) { 109 | if (event.data && event.data.type === "getmessages" && event.source) { 110 | // A window can post "getmessages" to receive a duplicate of every 111 | // message posted by this environment so far. This allows subscribers 112 | // from fetch_tests_from_window to 'catch up' to the current state of 113 | // this environment. 114 | for (var i = 0; i < this_obj.dispatched_messages.length; ++i) 115 | { 116 | event.source.postMessage(this_obj.dispatched_messages[i], "*"); 117 | } 118 | } 119 | }); 120 | } 121 | 122 | WindowTestEnvironment.prototype._dispatch = function(selector, callback_args, message_arg) { 123 | this.dispatched_messages.push(message_arg); 124 | this._forEach_windows( 125 | function(w, same_origin) { 126 | if (same_origin) { 127 | try { 128 | var has_selector = selector in w; 129 | } catch(e) { 130 | // If document.domain was set at some point same_origin can be 131 | // wrong and the above will fail. 132 | has_selector = false; 133 | } 134 | if (has_selector) { 135 | try { 136 | w[selector].apply(undefined, callback_args); 137 | } catch (e) { 138 | if (debug) { 139 | throw e; 140 | } 141 | } 142 | } 143 | } 144 | if (supports_post_message(w) && w !== self) { 145 | w.postMessage(message_arg, "*"); 146 | } 147 | }); 148 | }; 149 | 150 | WindowTestEnvironment.prototype._forEach_windows = function(callback) { 151 | // Iterate of the the windows [self ... top, opener]. The callback is passed 152 | // two objects, the first one is the windows object itself, the second one 153 | // is a boolean indicating whether or not its on the same origin as the 154 | // current window. 155 | var cache = this.window_cache; 156 | if (!cache) { 157 | cache = [[self, true]]; 158 | var w = self; 159 | var i = 0; 160 | var so; 161 | while (w != w.parent) { 162 | w = w.parent; 163 | so = is_same_origin(w); 164 | cache.push([w, so]); 165 | i++; 166 | } 167 | w = window.opener; 168 | if (w) { 169 | cache.push([w, is_same_origin(w)]); 170 | } 171 | this.window_cache = cache; 172 | } 173 | 174 | forEach(cache, 175 | function(a) { 176 | callback.apply(null, a); 177 | }); 178 | }; 179 | 180 | WindowTestEnvironment.prototype.on_tests_ready = function() { 181 | var output = new Output(); 182 | this.output_handler = output; 183 | 184 | var this_obj = this; 185 | 186 | add_start_callback(function (properties) { 187 | this_obj.output_handler.init(properties); 188 | }); 189 | 190 | add_test_state_callback(function(test) { 191 | this_obj.output_handler.show_status(); 192 | }); 193 | 194 | add_result_callback(function (test) { 195 | this_obj.output_handler.show_status(); 196 | }); 197 | 198 | add_completion_callback(function (tests, harness_status) { 199 | this_obj.output_handler.show_results(tests, harness_status); 200 | }); 201 | this.setup_messages(settings.message_events); 202 | }; 203 | 204 | WindowTestEnvironment.prototype.setup_messages = function(new_events) { 205 | var this_obj = this; 206 | forEach(settings.message_events, function(x) { 207 | var current_dispatch = this_obj.message_events.indexOf(x) !== -1; 208 | var new_dispatch = new_events.indexOf(x) !== -1; 209 | if (!current_dispatch && new_dispatch) { 210 | this_obj.message_functions[x][0](this_obj.message_functions[x][2]); 211 | } else if (current_dispatch && !new_dispatch) { 212 | this_obj.message_functions[x][1](this_obj.message_functions[x][2]); 213 | } 214 | }); 215 | this.message_events = new_events; 216 | } 217 | 218 | WindowTestEnvironment.prototype.next_default_test_name = function() { 219 | //Don't use document.title to work around an Opera bug in XHTML documents 220 | var title = document.getElementsByTagName("title")[0]; 221 | var prefix = (title && title.firstChild && title.firstChild.data) || "Untitled"; 222 | var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; 223 | this.name_counter++; 224 | return prefix + suffix; 225 | }; 226 | 227 | WindowTestEnvironment.prototype.on_new_harness_properties = function(properties) { 228 | this.output_handler.setup(properties); 229 | if (properties.hasOwnProperty("message_events")) { 230 | this.setup_messages(properties.message_events); 231 | } 232 | }; 233 | 234 | WindowTestEnvironment.prototype.add_on_loaded_callback = function(callback) { 235 | on_event(window, 'load', callback); 236 | }; 237 | 238 | WindowTestEnvironment.prototype.test_timeout = function() { 239 | var metas = document.getElementsByTagName("meta"); 240 | for (var i = 0; i < metas.length; i++) { 241 | if (metas[i].name == "timeout") { 242 | if (metas[i].content == "long") { 243 | return settings.harness_timeout.long; 244 | } 245 | break; 246 | } 247 | } 248 | return settings.harness_timeout.normal; 249 | }; 250 | 251 | WindowTestEnvironment.prototype.global_scope = function() { 252 | return window; 253 | }; 254 | 255 | /* 256 | * Base TestEnvironment implementation for a generic web worker. 257 | * 258 | * Workers accumulate test results. One or more clients can connect and 259 | * retrieve results from a worker at any time. 260 | * 261 | * WorkerTestEnvironment supports communicating with a client via a 262 | * MessagePort. The mechanism for determining the appropriate MessagePort 263 | * for communicating with a client depends on the type of worker and is 264 | * implemented by the various specializations of WorkerTestEnvironment 265 | * below. 266 | * 267 | * A client document using testharness can use fetch_tests_from_worker() to 268 | * retrieve results from a worker. See apisample16.html. 269 | */ 270 | function WorkerTestEnvironment() { 271 | this.name_counter = 0; 272 | this.all_loaded = true; 273 | this.message_list = []; 274 | this.message_ports = []; 275 | } 276 | 277 | WorkerTestEnvironment.prototype._dispatch = function(message) { 278 | this.message_list.push(message); 279 | for (var i = 0; i < this.message_ports.length; ++i) 280 | { 281 | this.message_ports[i].postMessage(message); 282 | } 283 | }; 284 | 285 | // The only requirement is that port has a postMessage() method. It doesn't 286 | // have to be an instance of a MessagePort, and often isn't. 287 | WorkerTestEnvironment.prototype._add_message_port = function(port) { 288 | this.message_ports.push(port); 289 | for (var i = 0; i < this.message_list.length; ++i) 290 | { 291 | port.postMessage(this.message_list[i]); 292 | } 293 | }; 294 | 295 | WorkerTestEnvironment.prototype.next_default_test_name = function() { 296 | var suffix = this.name_counter > 0 ? " " + this.name_counter : ""; 297 | this.name_counter++; 298 | return "Untitled" + suffix; 299 | }; 300 | 301 | WorkerTestEnvironment.prototype.on_new_harness_properties = function() {}; 302 | 303 | WorkerTestEnvironment.prototype.on_tests_ready = function() { 304 | var this_obj = this; 305 | add_start_callback( 306 | function(properties) { 307 | this_obj._dispatch({ 308 | type: "start", 309 | properties: properties, 310 | }); 311 | }); 312 | add_test_state_callback( 313 | function(test) { 314 | this_obj._dispatch({ 315 | type: "test_state", 316 | test: test.structured_clone() 317 | }); 318 | }); 319 | add_result_callback( 320 | function(test) { 321 | this_obj._dispatch({ 322 | type: "result", 323 | test: test.structured_clone() 324 | }); 325 | }); 326 | add_completion_callback( 327 | function(tests, harness_status) { 328 | this_obj._dispatch({ 329 | type: "complete", 330 | tests: map(tests, 331 | function(test) { 332 | return test.structured_clone(); 333 | }), 334 | status: harness_status.structured_clone() 335 | }); 336 | }); 337 | }; 338 | 339 | WorkerTestEnvironment.prototype.add_on_loaded_callback = function() {}; 340 | 341 | WorkerTestEnvironment.prototype.test_timeout = function() { 342 | // Tests running in a worker don't have a default timeout. I.e. all 343 | // worker tests behave as if settings.explicit_timeout is true. 344 | return null; 345 | }; 346 | 347 | WorkerTestEnvironment.prototype.global_scope = function() { 348 | return self; 349 | }; 350 | 351 | /* 352 | * Dedicated web workers. 353 | * https://html.spec.whatwg.org/multipage/workers.html#dedicatedworkerglobalscope 354 | * 355 | * This class is used as the test_environment when testharness is running 356 | * inside a dedicated worker. 357 | */ 358 | function DedicatedWorkerTestEnvironment() { 359 | WorkerTestEnvironment.call(this); 360 | // self is an instance of DedicatedWorkerGlobalScope which exposes 361 | // a postMessage() method for communicating via the message channel 362 | // established when the worker is created. 363 | this._add_message_port(self); 364 | } 365 | DedicatedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 366 | 367 | DedicatedWorkerTestEnvironment.prototype.on_tests_ready = function() { 368 | WorkerTestEnvironment.prototype.on_tests_ready.call(this); 369 | // In the absence of an onload notification, we a require dedicated 370 | // workers to explicitly signal when the tests are done. 371 | tests.wait_for_finish = true; 372 | }; 373 | 374 | /* 375 | * Shared web workers. 376 | * https://html.spec.whatwg.org/multipage/workers.html#sharedworkerglobalscope 377 | * 378 | * This class is used as the test_environment when testharness is running 379 | * inside a shared web worker. 380 | */ 381 | function SharedWorkerTestEnvironment() { 382 | WorkerTestEnvironment.call(this); 383 | var this_obj = this; 384 | // Shared workers receive message ports via the 'onconnect' event for 385 | // each connection. 386 | self.addEventListener("connect", 387 | function(message_event) { 388 | this_obj._add_message_port(message_event.source); 389 | }, false); 390 | } 391 | SharedWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 392 | 393 | SharedWorkerTestEnvironment.prototype.on_tests_ready = function() { 394 | WorkerTestEnvironment.prototype.on_tests_ready.call(this); 395 | // In the absence of an onload notification, we a require shared 396 | // workers to explicitly signal when the tests are done. 397 | tests.wait_for_finish = true; 398 | }; 399 | 400 | /* 401 | * Service workers. 402 | * http://www.w3.org/TR/service-workers/ 403 | * 404 | * This class is used as the test_environment when testharness is running 405 | * inside a service worker. 406 | */ 407 | function ServiceWorkerTestEnvironment() { 408 | WorkerTestEnvironment.call(this); 409 | this.all_loaded = false; 410 | this.on_loaded_callback = null; 411 | var this_obj = this; 412 | self.addEventListener("message", 413 | function(event) { 414 | if (event.data && event.data.type && event.data.type === "connect") { 415 | if (event.ports && event.ports[0]) { 416 | // If a MessageChannel was passed, then use it to 417 | // send results back to the main window. This 418 | // allows the tests to work even if the browser 419 | // does not fully support MessageEvent.source in 420 | // ServiceWorkers yet. 421 | this_obj._add_message_port(event.ports[0]); 422 | event.ports[0].start(); 423 | } else { 424 | // If there is no MessageChannel, then attempt to 425 | // use the MessageEvent.source to send results 426 | // back to the main window. 427 | this_obj._add_message_port(event.source); 428 | } 429 | } 430 | }, false); 431 | 432 | // The oninstall event is received after the service worker script and 433 | // all imported scripts have been fetched and executed. It's the 434 | // equivalent of an onload event for a document. All tests should have 435 | // been added by the time this event is received, thus it's not 436 | // necessary to wait until the onactivate event. However, tests for 437 | // installed service workers need another event which is equivalent to 438 | // the onload event because oninstall is fired only on installation. The 439 | // onmessage event is used for that purpose since tests using 440 | // testharness.js should ask the result to its service worker by 441 | // PostMessage. If the onmessage event is triggered on the service 442 | // worker's context, that means the worker's script has been evaluated. 443 | on_event(self, "install", on_all_loaded); 444 | on_event(self, "message", on_all_loaded); 445 | function on_all_loaded() { 446 | if (this_obj.all_loaded) 447 | return; 448 | this_obj.all_loaded = true; 449 | if (this_obj.on_loaded_callback) { 450 | this_obj.on_loaded_callback(); 451 | } 452 | } 453 | } 454 | 455 | ServiceWorkerTestEnvironment.prototype = Object.create(WorkerTestEnvironment.prototype); 456 | 457 | ServiceWorkerTestEnvironment.prototype.add_on_loaded_callback = function(callback) { 458 | if (this.all_loaded) { 459 | callback(); 460 | } else { 461 | this.on_loaded_callback = callback; 462 | } 463 | }; 464 | 465 | function create_test_environment() { 466 | if ('document' in self) { 467 | return new WindowTestEnvironment(); 468 | } 469 | if ('DedicatedWorkerGlobalScope' in self && 470 | self instanceof DedicatedWorkerGlobalScope) { 471 | return new DedicatedWorkerTestEnvironment(); 472 | } 473 | if ('SharedWorkerGlobalScope' in self && 474 | self instanceof SharedWorkerGlobalScope) { 475 | return new SharedWorkerTestEnvironment(); 476 | } 477 | if ('ServiceWorkerGlobalScope' in self && 478 | self instanceof ServiceWorkerGlobalScope) { 479 | return new ServiceWorkerTestEnvironment(); 480 | } 481 | if ('WorkerGlobalScope' in self && 482 | self instanceof WorkerGlobalScope) { 483 | return new DedicatedWorkerTestEnvironment(); 484 | } 485 | 486 | throw new Error("Unsupported test environment"); 487 | } 488 | 489 | var test_environment = create_test_environment(); 490 | 491 | function is_shared_worker(worker) { 492 | return 'SharedWorker' in self && worker instanceof SharedWorker; 493 | } 494 | 495 | function is_service_worker(worker) { 496 | // The worker object may be from another execution context, 497 | // so do not use instanceof here. 498 | return 'ServiceWorker' in self && 499 | Object.prototype.toString.call(worker) == '[object ServiceWorker]'; 500 | } 501 | 502 | /* 503 | * API functions 504 | */ 505 | 506 | function test(func, name, properties) 507 | { 508 | var test_name = name ? name : test_environment.next_default_test_name(); 509 | properties = properties ? properties : {}; 510 | var test_obj = new Test(test_name, properties); 511 | test_obj.step(func, test_obj, test_obj); 512 | if (test_obj.phase === test_obj.phases.STARTED) { 513 | test_obj.done(); 514 | } 515 | } 516 | 517 | function async_test(func, name, properties) 518 | { 519 | if (typeof func !== "function") { 520 | properties = name; 521 | name = func; 522 | func = null; 523 | } 524 | var test_name = name ? name : test_environment.next_default_test_name(); 525 | properties = properties ? properties : {}; 526 | var test_obj = new Test(test_name, properties); 527 | if (func) { 528 | test_obj.step(func, test_obj, test_obj); 529 | } 530 | return test_obj; 531 | } 532 | 533 | function promise_test(func, name, properties) { 534 | var test = async_test(name, properties); 535 | // If there is no promise tests queue make one. 536 | if (!tests.promise_tests) { 537 | tests.promise_tests = Promise.resolve(); 538 | } 539 | tests.promise_tests = tests.promise_tests.then(function() { 540 | var donePromise = new Promise(function(resolve) { 541 | test._add_cleanup(resolve); 542 | }); 543 | var promise = test.step(func, test, test); 544 | test.step(function() { 545 | assert_not_equals(promise, undefined); 546 | }); 547 | Promise.resolve(promise).then( 548 | function() { 549 | test.done(); 550 | }) 551 | .catch(test.step_func( 552 | function(value) { 553 | if (value instanceof AssertionError) { 554 | throw value; 555 | } 556 | assert(false, "promise_test", null, 557 | "Unhandled rejection with value: ${value}", {value:value}); 558 | })); 559 | return donePromise; 560 | }); 561 | } 562 | 563 | function promise_rejects(test, expected, promise, description) { 564 | return promise.then(test.unreached_func("Should have rejected: " + description)).catch(function(e) { 565 | assert_throws(expected, function() { throw e }, description); 566 | }); 567 | } 568 | 569 | /** 570 | * This constructor helper allows DOM events to be handled using Promises, 571 | * which can make it a lot easier to test a very specific series of events, 572 | * including ensuring that unexpected events are not fired at any point. 573 | */ 574 | function EventWatcher(test, watchedNode, eventTypes) 575 | { 576 | if (typeof eventTypes == 'string') { 577 | eventTypes = [eventTypes]; 578 | } 579 | 580 | var waitingFor = null; 581 | 582 | // This is null unless we are recording all events, in which case it 583 | // will be an Array object. 584 | var recordedEvents = null; 585 | 586 | var eventHandler = test.step_func(function(evt) { 587 | assert_true(!!waitingFor, 588 | 'Not expecting event, but got ' + evt.type + ' event'); 589 | assert_equals(evt.type, waitingFor.types[0], 590 | 'Expected ' + waitingFor.types[0] + ' event, but got ' + 591 | evt.type + ' event instead'); 592 | 593 | if (Array.isArray(recordedEvents)) { 594 | recordedEvents.push(evt); 595 | } 596 | 597 | if (waitingFor.types.length > 1) { 598 | // Pop first event from array 599 | waitingFor.types.shift(); 600 | return; 601 | } 602 | // We need to null out waitingFor before calling the resolve function 603 | // since the Promise's resolve handlers may call wait_for() which will 604 | // need to set waitingFor. 605 | var resolveFunc = waitingFor.resolve; 606 | waitingFor = null; 607 | // Likewise, we should reset the state of recordedEvents. 608 | var result = recordedEvents || evt; 609 | recordedEvents = null; 610 | resolveFunc(result); 611 | }); 612 | 613 | for (var i = 0; i < eventTypes.length; i++) { 614 | watchedNode.addEventListener(eventTypes[i], eventHandler, false); 615 | } 616 | 617 | /** 618 | * Returns a Promise that will resolve after the specified event or 619 | * series of events has occured. 620 | * 621 | * @param options An optional options object. If the 'record' property 622 | * on this object has the value 'all', when the Promise 623 | * returned by this function is resolved, *all* Event 624 | * objects that were waited for will be returned as an 625 | * array. 626 | * 627 | * For example, 628 | * 629 | * ```js 630 | * const watcher = new EventWatcher(t, div, [ 'animationstart', 631 | * 'animationiteration', 632 | * 'animationend' ]); 633 | * return watcher.wait_for([ 'animationstart', 'animationend' ], 634 | * { record: 'all' }).then(evts => { 635 | * assert_equals(evts[0].elapsedTime, 0.0); 636 | * assert_equals(evts[1].elapsedTime, 2.0); 637 | * }); 638 | * ``` 639 | */ 640 | this.wait_for = function(types, options) { 641 | if (waitingFor) { 642 | return Promise.reject('Already waiting for an event or events'); 643 | } 644 | if (typeof types == 'string') { 645 | types = [types]; 646 | } 647 | if (options && options.record && options.record === 'all') { 648 | recordedEvents = []; 649 | } 650 | return new Promise(function(resolve, reject) { 651 | waitingFor = { 652 | types: types, 653 | resolve: resolve, 654 | reject: reject 655 | }; 656 | }); 657 | }; 658 | 659 | function stop_watching() { 660 | for (var i = 0; i < eventTypes.length; i++) { 661 | watchedNode.removeEventListener(eventTypes[i], eventHandler, false); 662 | } 663 | }; 664 | 665 | test._add_cleanup(stop_watching); 666 | 667 | return this; 668 | } 669 | expose(EventWatcher, 'EventWatcher'); 670 | 671 | function setup(func_or_properties, maybe_properties) 672 | { 673 | var func = null; 674 | var properties = {}; 675 | if (arguments.length === 2) { 676 | func = func_or_properties; 677 | properties = maybe_properties; 678 | } else if (func_or_properties instanceof Function) { 679 | func = func_or_properties; 680 | } else { 681 | properties = func_or_properties; 682 | } 683 | tests.setup(func, properties); 684 | test_environment.on_new_harness_properties(properties); 685 | } 686 | 687 | function done() { 688 | if (tests.tests.length === 0) { 689 | tests.set_file_is_test(); 690 | } 691 | if (tests.file_is_test) { 692 | tests.tests[0].done(); 693 | } 694 | tests.end_wait(); 695 | } 696 | 697 | function generate_tests(func, args, properties) { 698 | forEach(args, function(x, i) 699 | { 700 | var name = x[0]; 701 | test(function() 702 | { 703 | func.apply(this, x.slice(1)); 704 | }, 705 | name, 706 | Array.isArray(properties) ? properties[i] : properties); 707 | }); 708 | } 709 | 710 | function on_event(object, event, callback) 711 | { 712 | object.addEventListener(event, callback, false); 713 | } 714 | 715 | function step_timeout(f, t) { 716 | var outer_this = this; 717 | var args = Array.prototype.slice.call(arguments, 2); 718 | return setTimeout(function() { 719 | f.apply(outer_this, args); 720 | }, t * tests.timeout_multiplier); 721 | } 722 | 723 | expose(test, 'test'); 724 | expose(async_test, 'async_test'); 725 | expose(promise_test, 'promise_test'); 726 | expose(promise_rejects, 'promise_rejects'); 727 | expose(generate_tests, 'generate_tests'); 728 | expose(setup, 'setup'); 729 | expose(done, 'done'); 730 | expose(on_event, 'on_event'); 731 | expose(step_timeout, 'step_timeout'); 732 | 733 | /* 734 | * Return a string truncated to the given length, with ... added at the end 735 | * if it was longer. 736 | */ 737 | function truncate(s, len) 738 | { 739 | if (s.length > len) { 740 | return s.substring(0, len - 3) + "..."; 741 | } 742 | return s; 743 | } 744 | 745 | /* 746 | * Return true if object is probably a Node object. 747 | */ 748 | function is_node(object) 749 | { 750 | // I use duck-typing instead of instanceof, because 751 | // instanceof doesn't work if the node is from another window (like an 752 | // iframe's contentWindow): 753 | // http://www.w3.org/Bugs/Public/show_bug.cgi?id=12295 754 | try { 755 | var has_node_properties = ("nodeType" in object && 756 | "nodeName" in object && 757 | "nodeValue" in object && 758 | "childNodes" in object); 759 | } catch (e) { 760 | // We're probably cross-origin, which means we aren't a node 761 | return false; 762 | } 763 | 764 | if (has_node_properties) { 765 | try { 766 | object.nodeType; 767 | } catch (e) { 768 | // The object is probably Node.prototype or another prototype 769 | // object that inherits from it, and not a Node instance. 770 | return false; 771 | } 772 | return true; 773 | } 774 | return false; 775 | } 776 | 777 | var replacements = { 778 | "0": "0", 779 | "1": "x01", 780 | "2": "x02", 781 | "3": "x03", 782 | "4": "x04", 783 | "5": "x05", 784 | "6": "x06", 785 | "7": "x07", 786 | "8": "b", 787 | "9": "t", 788 | "10": "n", 789 | "11": "v", 790 | "12": "f", 791 | "13": "r", 792 | "14": "x0e", 793 | "15": "x0f", 794 | "16": "x10", 795 | "17": "x11", 796 | "18": "x12", 797 | "19": "x13", 798 | "20": "x14", 799 | "21": "x15", 800 | "22": "x16", 801 | "23": "x17", 802 | "24": "x18", 803 | "25": "x19", 804 | "26": "x1a", 805 | "27": "x1b", 806 | "28": "x1c", 807 | "29": "x1d", 808 | "30": "x1e", 809 | "31": "x1f", 810 | "0xfffd": "ufffd", 811 | "0xfffe": "ufffe", 812 | "0xffff": "uffff", 813 | }; 814 | 815 | /* 816 | * Convert a value to a nice, human-readable string 817 | */ 818 | function format_value(val, seen) 819 | { 820 | if (!seen) { 821 | seen = []; 822 | } 823 | if (typeof val === "object" && val !== null) { 824 | if (seen.indexOf(val) >= 0) { 825 | return "[...]"; 826 | } 827 | seen.push(val); 828 | } 829 | if (Array.isArray(val)) { 830 | return "[" + val.map(function(x) {return format_value(x, seen);}).join(", ") + "]"; 831 | } 832 | 833 | switch (typeof val) { 834 | case "string": 835 | val = val.replace("\\", "\\\\"); 836 | for (var p in replacements) { 837 | var replace = "\\" + replacements[p]; 838 | val = val.replace(RegExp(String.fromCharCode(p), "g"), replace); 839 | } 840 | return '"' + val.replace(/"/g, '\\"') + '"'; 841 | case "boolean": 842 | case "undefined": 843 | return String(val); 844 | case "number": 845 | // In JavaScript, -0 === 0 and String(-0) == "0", so we have to 846 | // special-case. 847 | if (val === -0 && 1/val === -Infinity) { 848 | return "-0"; 849 | } 850 | return String(val); 851 | case "object": 852 | if (val === null) { 853 | return "null"; 854 | } 855 | 856 | // Special-case Node objects, since those come up a lot in my tests. I 857 | // ignore namespaces. 858 | if (is_node(val)) { 859 | switch (val.nodeType) { 860 | case Node.ELEMENT_NODE: 861 | var ret = "<" + val.localName; 862 | for (var i = 0; i < val.attributes.length; i++) { 863 | ret += " " + val.attributes[i].name + '="' + val.attributes[i].value + '"'; 864 | } 865 | ret += ">" + val.innerHTML + ""; 866 | return "Element node " + truncate(ret, 60); 867 | case Node.TEXT_NODE: 868 | return 'Text node "' + truncate(val.data, 60) + '"'; 869 | case Node.PROCESSING_INSTRUCTION_NODE: 870 | return "ProcessingInstruction node with target " + format_value(truncate(val.target, 60)) + " and data " + format_value(truncate(val.data, 60)); 871 | case Node.COMMENT_NODE: 872 | return "Comment node "; 873 | case Node.DOCUMENT_NODE: 874 | return "Document node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 875 | case Node.DOCUMENT_TYPE_NODE: 876 | return "DocumentType node"; 877 | case Node.DOCUMENT_FRAGMENT_NODE: 878 | return "DocumentFragment node with " + val.childNodes.length + (val.childNodes.length == 1 ? " child" : " children"); 879 | default: 880 | return "Node object of unknown type"; 881 | } 882 | } 883 | 884 | /* falls through */ 885 | default: 886 | try { 887 | return typeof val + ' "' + truncate(String(val), 1000) + '"'; 888 | } catch(e) { 889 | return ("[stringifying object threw " + String(e) + 890 | " with type " + String(typeof e) + "]"); 891 | } 892 | } 893 | } 894 | expose(format_value, "format_value"); 895 | 896 | /* 897 | * Assertions 898 | */ 899 | 900 | function assert_true(actual, description) 901 | { 902 | assert(actual === true, "assert_true", description, 903 | "expected true got ${actual}", {actual:actual}); 904 | } 905 | expose(assert_true, "assert_true"); 906 | 907 | function assert_false(actual, description) 908 | { 909 | assert(actual === false, "assert_false", description, 910 | "expected false got ${actual}", {actual:actual}); 911 | } 912 | expose(assert_false, "assert_false"); 913 | 914 | function same_value(x, y) { 915 | if (y !== y) { 916 | //NaN case 917 | return x !== x; 918 | } 919 | if (x === 0 && y === 0) { 920 | //Distinguish +0 and -0 921 | return 1/x === 1/y; 922 | } 923 | return x === y; 924 | } 925 | 926 | function assert_equals(actual, expected, description) 927 | { 928 | /* 929 | * Test if two primitives are equal or two objects 930 | * are the same object 931 | */ 932 | if (typeof actual != typeof expected) { 933 | assert(false, "assert_equals", description, 934 | "expected (" + typeof expected + ") ${expected} but got (" + typeof actual + ") ${actual}", 935 | {expected:expected, actual:actual}); 936 | return; 937 | } 938 | assert(same_value(actual, expected), "assert_equals", description, 939 | "expected ${expected} but got ${actual}", 940 | {expected:expected, actual:actual}); 941 | } 942 | expose(assert_equals, "assert_equals"); 943 | 944 | function assert_not_equals(actual, expected, description) 945 | { 946 | /* 947 | * Test if two primitives are unequal or two objects 948 | * are different objects 949 | */ 950 | assert(!same_value(actual, expected), "assert_not_equals", description, 951 | "got disallowed value ${actual}", 952 | {actual:actual}); 953 | } 954 | expose(assert_not_equals, "assert_not_equals"); 955 | 956 | function assert_in_array(actual, expected, description) 957 | { 958 | assert(expected.indexOf(actual) != -1, "assert_in_array", description, 959 | "value ${actual} not in array ${expected}", 960 | {actual:actual, expected:expected}); 961 | } 962 | expose(assert_in_array, "assert_in_array"); 963 | 964 | function assert_object_equals(actual, expected, description) 965 | { 966 | //This needs to be improved a great deal 967 | function check_equal(actual, expected, stack) 968 | { 969 | stack.push(actual); 970 | 971 | var p; 972 | for (p in actual) { 973 | assert(expected.hasOwnProperty(p), "assert_object_equals", description, 974 | "unexpected property ${p}", {p:p}); 975 | 976 | if (typeof actual[p] === "object" && actual[p] !== null) { 977 | if (stack.indexOf(actual[p]) === -1) { 978 | check_equal(actual[p], expected[p], stack); 979 | } 980 | } else { 981 | assert(same_value(actual[p], expected[p]), "assert_object_equals", description, 982 | "property ${p} expected ${expected} got ${actual}", 983 | {p:p, expected:expected, actual:actual}); 984 | } 985 | } 986 | for (p in expected) { 987 | assert(actual.hasOwnProperty(p), 988 | "assert_object_equals", description, 989 | "expected property ${p} missing", {p:p}); 990 | } 991 | stack.pop(); 992 | } 993 | check_equal(actual, expected, []); 994 | } 995 | expose(assert_object_equals, "assert_object_equals"); 996 | 997 | function assert_array_equals(actual, expected, description) 998 | { 999 | assert(typeof actual === "object" && actual !== null && "length" in actual, 1000 | "assert_array_equals", description, 1001 | "value is ${actual}, expected array", 1002 | {actual:actual}); 1003 | assert(actual.length === expected.length, 1004 | "assert_array_equals", description, 1005 | "lengths differ, expected ${expected} got ${actual}", 1006 | {expected:expected.length, actual:actual.length}); 1007 | 1008 | for (var i = 0; i < actual.length; i++) { 1009 | assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), 1010 | "assert_array_equals", description, 1011 | "property ${i}, property expected to be ${expected} but was ${actual}", 1012 | {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", 1013 | actual:actual.hasOwnProperty(i) ? "present" : "missing"}); 1014 | assert(same_value(expected[i], actual[i]), 1015 | "assert_array_equals", description, 1016 | "property ${i}, expected ${expected} but got ${actual}", 1017 | {i:i, expected:expected[i], actual:actual[i]}); 1018 | } 1019 | } 1020 | expose(assert_array_equals, "assert_array_equals"); 1021 | 1022 | function assert_array_approx_equals(actual, expected, epsilon, description) 1023 | { 1024 | /* 1025 | * Test if two primitive arrays are equal withing +/- epsilon 1026 | */ 1027 | assert(actual.length === expected.length, 1028 | "assert_array_approx_equals", description, 1029 | "lengths differ, expected ${expected} got ${actual}", 1030 | {expected:expected.length, actual:actual.length}); 1031 | 1032 | for (var i = 0; i < actual.length; i++) { 1033 | assert(actual.hasOwnProperty(i) === expected.hasOwnProperty(i), 1034 | "assert_array_approx_equals", description, 1035 | "property ${i}, property expected to be ${expected} but was ${actual}", 1036 | {i:i, expected:expected.hasOwnProperty(i) ? "present" : "missing", 1037 | actual:actual.hasOwnProperty(i) ? "present" : "missing"}); 1038 | assert(typeof actual[i] === "number", 1039 | "assert_array_approx_equals", description, 1040 | "property ${i}, expected a number but got a ${type_actual}", 1041 | {i:i, type_actual:typeof actual[i]}); 1042 | assert(Math.abs(actual[i] - expected[i]) <= epsilon, 1043 | "assert_array_approx_equals", description, 1044 | "property ${i}, expected ${expected} +/- ${epsilon}, expected ${expected} but got ${actual}", 1045 | {i:i, expected:expected[i], actual:actual[i]}); 1046 | } 1047 | } 1048 | expose(assert_array_approx_equals, "assert_array_approx_equals"); 1049 | 1050 | function assert_approx_equals(actual, expected, epsilon, description) 1051 | { 1052 | /* 1053 | * Test if two primitive numbers are equal withing +/- epsilon 1054 | */ 1055 | assert(typeof actual === "number", 1056 | "assert_approx_equals", description, 1057 | "expected a number but got a ${type_actual}", 1058 | {type_actual:typeof actual}); 1059 | 1060 | assert(Math.abs(actual - expected) <= epsilon, 1061 | "assert_approx_equals", description, 1062 | "expected ${expected} +/- ${epsilon} but got ${actual}", 1063 | {expected:expected, actual:actual, epsilon:epsilon}); 1064 | } 1065 | expose(assert_approx_equals, "assert_approx_equals"); 1066 | 1067 | function assert_less_than(actual, expected, description) 1068 | { 1069 | /* 1070 | * Test if a primitive number is less than another 1071 | */ 1072 | assert(typeof actual === "number", 1073 | "assert_less_than", description, 1074 | "expected a number but got a ${type_actual}", 1075 | {type_actual:typeof actual}); 1076 | 1077 | assert(actual < expected, 1078 | "assert_less_than", description, 1079 | "expected a number less than ${expected} but got ${actual}", 1080 | {expected:expected, actual:actual}); 1081 | } 1082 | expose(assert_less_than, "assert_less_than"); 1083 | 1084 | function assert_greater_than(actual, expected, description) 1085 | { 1086 | /* 1087 | * Test if a primitive number is greater than another 1088 | */ 1089 | assert(typeof actual === "number", 1090 | "assert_greater_than", description, 1091 | "expected a number but got a ${type_actual}", 1092 | {type_actual:typeof actual}); 1093 | 1094 | assert(actual > expected, 1095 | "assert_greater_than", description, 1096 | "expected a number greater than ${expected} but got ${actual}", 1097 | {expected:expected, actual:actual}); 1098 | } 1099 | expose(assert_greater_than, "assert_greater_than"); 1100 | 1101 | function assert_between_exclusive(actual, lower, upper, description) 1102 | { 1103 | /* 1104 | * Test if a primitive number is between two others 1105 | */ 1106 | assert(typeof actual === "number", 1107 | "assert_between_exclusive", description, 1108 | "expected a number but got a ${type_actual}", 1109 | {type_actual:typeof actual}); 1110 | 1111 | assert(actual > lower && actual < upper, 1112 | "assert_between_exclusive", description, 1113 | "expected a number greater than ${lower} " + 1114 | "and less than ${upper} but got ${actual}", 1115 | {lower:lower, upper:upper, actual:actual}); 1116 | } 1117 | expose(assert_between_exclusive, "assert_between_exclusive"); 1118 | 1119 | function assert_less_than_equal(actual, expected, description) 1120 | { 1121 | /* 1122 | * Test if a primitive number is less than or equal to another 1123 | */ 1124 | assert(typeof actual === "number", 1125 | "assert_less_than_equal", description, 1126 | "expected a number but got a ${type_actual}", 1127 | {type_actual:typeof actual}); 1128 | 1129 | assert(actual <= expected, 1130 | "assert_less_than_equal", description, 1131 | "expected a number less than or equal to ${expected} but got ${actual}", 1132 | {expected:expected, actual:actual}); 1133 | } 1134 | expose(assert_less_than_equal, "assert_less_than_equal"); 1135 | 1136 | function assert_greater_than_equal(actual, expected, description) 1137 | { 1138 | /* 1139 | * Test if a primitive number is greater than or equal to another 1140 | */ 1141 | assert(typeof actual === "number", 1142 | "assert_greater_than_equal", description, 1143 | "expected a number but got a ${type_actual}", 1144 | {type_actual:typeof actual}); 1145 | 1146 | assert(actual >= expected, 1147 | "assert_greater_than_equal", description, 1148 | "expected a number greater than or equal to ${expected} but got ${actual}", 1149 | {expected:expected, actual:actual}); 1150 | } 1151 | expose(assert_greater_than_equal, "assert_greater_than_equal"); 1152 | 1153 | function assert_between_inclusive(actual, lower, upper, description) 1154 | { 1155 | /* 1156 | * Test if a primitive number is between to two others or equal to either of them 1157 | */ 1158 | assert(typeof actual === "number", 1159 | "assert_between_inclusive", description, 1160 | "expected a number but got a ${type_actual}", 1161 | {type_actual:typeof actual}); 1162 | 1163 | assert(actual >= lower && actual <= upper, 1164 | "assert_between_inclusive", description, 1165 | "expected a number greater than or equal to ${lower} " + 1166 | "and less than or equal to ${upper} but got ${actual}", 1167 | {lower:lower, upper:upper, actual:actual}); 1168 | } 1169 | expose(assert_between_inclusive, "assert_between_inclusive"); 1170 | 1171 | function assert_regexp_match(actual, expected, description) { 1172 | /* 1173 | * Test if a string (actual) matches a regexp (expected) 1174 | */ 1175 | assert(expected.test(actual), 1176 | "assert_regexp_match", description, 1177 | "expected ${expected} but got ${actual}", 1178 | {expected:expected, actual:actual}); 1179 | } 1180 | expose(assert_regexp_match, "assert_regexp_match"); 1181 | 1182 | function assert_class_string(object, class_string, description) { 1183 | assert_equals({}.toString.call(object), "[object " + class_string + "]", 1184 | description); 1185 | } 1186 | expose(assert_class_string, "assert_class_string"); 1187 | 1188 | 1189 | function _assert_own_property(name) { 1190 | return function(object, property_name, description) 1191 | { 1192 | assert(object.hasOwnProperty(property_name), 1193 | name, description, 1194 | "expected property ${p} missing", {p:property_name}); 1195 | }; 1196 | } 1197 | expose(_assert_own_property("assert_exists"), "assert_exists"); 1198 | expose(_assert_own_property("assert_own_property"), "assert_own_property"); 1199 | 1200 | function assert_not_exists(object, property_name, description) 1201 | { 1202 | assert(!object.hasOwnProperty(property_name), 1203 | "assert_not_exists", description, 1204 | "unexpected property ${p} found", {p:property_name}); 1205 | } 1206 | expose(assert_not_exists, "assert_not_exists"); 1207 | 1208 | function _assert_inherits(name) { 1209 | return function (object, property_name, description) 1210 | { 1211 | assert(typeof object === "object" || typeof object === "function", 1212 | name, description, 1213 | "provided value is not an object"); 1214 | 1215 | assert("hasOwnProperty" in object, 1216 | name, description, 1217 | "provided value is an object but has no hasOwnProperty method"); 1218 | 1219 | assert(!object.hasOwnProperty(property_name), 1220 | name, description, 1221 | "property ${p} found on object expected in prototype chain", 1222 | {p:property_name}); 1223 | 1224 | assert(property_name in object, 1225 | name, description, 1226 | "property ${p} not found in prototype chain", 1227 | {p:property_name}); 1228 | }; 1229 | } 1230 | expose(_assert_inherits("assert_inherits"), "assert_inherits"); 1231 | expose(_assert_inherits("assert_idl_attribute"), "assert_idl_attribute"); 1232 | 1233 | function assert_readonly(object, property_name, description) 1234 | { 1235 | var initial_value = object[property_name]; 1236 | try { 1237 | //Note that this can have side effects in the case where 1238 | //the property has PutForwards 1239 | object[property_name] = initial_value + "a"; //XXX use some other value here? 1240 | assert(same_value(object[property_name], initial_value), 1241 | "assert_readonly", description, 1242 | "changing property ${p} succeeded", 1243 | {p:property_name}); 1244 | } finally { 1245 | object[property_name] = initial_value; 1246 | } 1247 | } 1248 | expose(assert_readonly, "assert_readonly"); 1249 | 1250 | function assert_throws(code, func, description) 1251 | { 1252 | try { 1253 | func.call(this); 1254 | assert(false, "assert_throws", description, 1255 | "${func} did not throw", {func:func}); 1256 | } catch (e) { 1257 | if (e instanceof AssertionError) { 1258 | throw e; 1259 | } 1260 | if (code === null) { 1261 | throw new AssertionError('Test bug: need to pass exception to assert_throws()'); 1262 | } 1263 | if (typeof code === "object") { 1264 | assert(typeof e == "object" && "name" in e && e.name == code.name, 1265 | "assert_throws", description, 1266 | "${func} threw ${actual} (${actual_name}) expected ${expected} (${expected_name})", 1267 | {func:func, actual:e, actual_name:e.name, 1268 | expected:code, 1269 | expected_name:code.name}); 1270 | return; 1271 | } 1272 | 1273 | var code_name_map = { 1274 | INDEX_SIZE_ERR: 'IndexSizeError', 1275 | HIERARCHY_REQUEST_ERR: 'HierarchyRequestError', 1276 | WRONG_DOCUMENT_ERR: 'WrongDocumentError', 1277 | INVALID_CHARACTER_ERR: 'InvalidCharacterError', 1278 | NO_MODIFICATION_ALLOWED_ERR: 'NoModificationAllowedError', 1279 | NOT_FOUND_ERR: 'NotFoundError', 1280 | NOT_SUPPORTED_ERR: 'NotSupportedError', 1281 | INUSE_ATTRIBUTE_ERR: 'InUseAttributeError', 1282 | INVALID_STATE_ERR: 'InvalidStateError', 1283 | SYNTAX_ERR: 'SyntaxError', 1284 | INVALID_MODIFICATION_ERR: 'InvalidModificationError', 1285 | NAMESPACE_ERR: 'NamespaceError', 1286 | INVALID_ACCESS_ERR: 'InvalidAccessError', 1287 | TYPE_MISMATCH_ERR: 'TypeMismatchError', 1288 | SECURITY_ERR: 'SecurityError', 1289 | NETWORK_ERR: 'NetworkError', 1290 | ABORT_ERR: 'AbortError', 1291 | URL_MISMATCH_ERR: 'URLMismatchError', 1292 | QUOTA_EXCEEDED_ERR: 'QuotaExceededError', 1293 | TIMEOUT_ERR: 'TimeoutError', 1294 | INVALID_NODE_TYPE_ERR: 'InvalidNodeTypeError', 1295 | DATA_CLONE_ERR: 'DataCloneError' 1296 | }; 1297 | 1298 | var name = code in code_name_map ? code_name_map[code] : code; 1299 | 1300 | var name_code_map = { 1301 | IndexSizeError: 1, 1302 | HierarchyRequestError: 3, 1303 | WrongDocumentError: 4, 1304 | InvalidCharacterError: 5, 1305 | NoModificationAllowedError: 7, 1306 | NotFoundError: 8, 1307 | NotSupportedError: 9, 1308 | InUseAttributeError: 10, 1309 | InvalidStateError: 11, 1310 | SyntaxError: 12, 1311 | InvalidModificationError: 13, 1312 | NamespaceError: 14, 1313 | InvalidAccessError: 15, 1314 | TypeMismatchError: 17, 1315 | SecurityError: 18, 1316 | NetworkError: 19, 1317 | AbortError: 20, 1318 | URLMismatchError: 21, 1319 | QuotaExceededError: 22, 1320 | TimeoutError: 23, 1321 | InvalidNodeTypeError: 24, 1322 | DataCloneError: 25, 1323 | 1324 | EncodingError: 0, 1325 | NotReadableError: 0, 1326 | UnknownError: 0, 1327 | ConstraintError: 0, 1328 | DataError: 0, 1329 | TransactionInactiveError: 0, 1330 | ReadOnlyError: 0, 1331 | VersionError: 0, 1332 | OperationError: 0, 1333 | NotAllowedError: 0 1334 | }; 1335 | 1336 | if (!(name in name_code_map)) { 1337 | throw new AssertionError('Test bug: unrecognized DOMException code "' + code + '" passed to assert_throws()'); 1338 | } 1339 | 1340 | var required_props = { code: name_code_map[name] }; 1341 | 1342 | if (required_props.code === 0 || 1343 | (typeof e == "object" && 1344 | "name" in e && 1345 | e.name !== e.name.toUpperCase() && 1346 | e.name !== "DOMException")) { 1347 | // New style exception: also test the name property. 1348 | required_props.name = name; 1349 | } 1350 | 1351 | //We'd like to test that e instanceof the appropriate interface, 1352 | //but we can't, because we don't know what window it was created 1353 | //in. It might be an instanceof the appropriate interface on some 1354 | //unknown other window. TODO: Work around this somehow? 1355 | 1356 | assert(typeof e == "object", 1357 | "assert_throws", description, 1358 | "${func} threw ${e} with type ${type}, not an object", 1359 | {func:func, e:e, type:typeof e}); 1360 | 1361 | for (var prop in required_props) { 1362 | assert(typeof e == "object" && prop in e && e[prop] == required_props[prop], 1363 | "assert_throws", description, 1364 | "${func} threw ${e} that is not a DOMException " + code + ": property ${prop} is equal to ${actual}, expected ${expected}", 1365 | {func:func, e:e, prop:prop, actual:e[prop], expected:required_props[prop]}); 1366 | } 1367 | } 1368 | } 1369 | expose(assert_throws, "assert_throws"); 1370 | 1371 | function assert_unreached(description) { 1372 | assert(false, "assert_unreached", description, 1373 | "Reached unreachable code"); 1374 | } 1375 | expose(assert_unreached, "assert_unreached"); 1376 | 1377 | function assert_any(assert_func, actual, expected_array) 1378 | { 1379 | var args = [].slice.call(arguments, 3); 1380 | var errors = []; 1381 | var passed = false; 1382 | forEach(expected_array, 1383 | function(expected) 1384 | { 1385 | try { 1386 | assert_func.apply(this, [actual, expected].concat(args)); 1387 | passed = true; 1388 | } catch (e) { 1389 | errors.push(e.message); 1390 | } 1391 | }); 1392 | if (!passed) { 1393 | throw new AssertionError(errors.join("\n\n")); 1394 | } 1395 | } 1396 | expose(assert_any, "assert_any"); 1397 | 1398 | function Test(name, properties) 1399 | { 1400 | if (tests.file_is_test && tests.tests.length) { 1401 | throw new Error("Tried to create a test with file_is_test"); 1402 | } 1403 | this.name = name; 1404 | 1405 | this.phase = tests.phase === tests.phases.ABORTED ? 1406 | this.phases.COMPLETE : this.phases.INITIAL; 1407 | 1408 | this.status = this.NOTRUN; 1409 | this.timeout_id = null; 1410 | this.index = null; 1411 | 1412 | this.properties = properties; 1413 | var timeout = properties.timeout ? properties.timeout : settings.test_timeout; 1414 | if (timeout !== null) { 1415 | this.timeout_length = timeout * tests.timeout_multiplier; 1416 | } else { 1417 | this.timeout_length = null; 1418 | } 1419 | 1420 | this.message = null; 1421 | this.stack = null; 1422 | 1423 | this.steps = []; 1424 | 1425 | this.cleanup_callbacks = []; 1426 | this._user_defined_cleanup_count = 0; 1427 | 1428 | tests.push(this); 1429 | } 1430 | 1431 | Test.statuses = { 1432 | PASS:0, 1433 | FAIL:1, 1434 | TIMEOUT:2, 1435 | NOTRUN:3 1436 | }; 1437 | 1438 | Test.prototype = merge({}, Test.statuses); 1439 | 1440 | Test.prototype.phases = { 1441 | INITIAL:0, 1442 | STARTED:1, 1443 | HAS_RESULT:2, 1444 | COMPLETE:3 1445 | }; 1446 | 1447 | Test.prototype.structured_clone = function() 1448 | { 1449 | if (!this._structured_clone) { 1450 | var msg = this.message; 1451 | msg = msg ? String(msg) : msg; 1452 | this._structured_clone = merge({ 1453 | name:String(this.name), 1454 | properties:merge({}, this.properties), 1455 | phases:merge({}, this.phases) 1456 | }, Test.statuses); 1457 | } 1458 | this._structured_clone.status = this.status; 1459 | this._structured_clone.message = this.message; 1460 | this._structured_clone.stack = this.stack; 1461 | this._structured_clone.index = this.index; 1462 | this._structured_clone.phase = this.phase; 1463 | return this._structured_clone; 1464 | }; 1465 | 1466 | Test.prototype.step = function(func, this_obj) 1467 | { 1468 | if (this.phase > this.phases.STARTED) { 1469 | return; 1470 | } 1471 | this.phase = this.phases.STARTED; 1472 | //If we don't get a result before the harness times out that will be a test timout 1473 | this.set_status(this.TIMEOUT, "Test timed out"); 1474 | 1475 | tests.started = true; 1476 | tests.notify_test_state(this); 1477 | 1478 | if (this.timeout_id === null) { 1479 | this.set_timeout(); 1480 | } 1481 | 1482 | this.steps.push(func); 1483 | 1484 | if (arguments.length === 1) { 1485 | this_obj = this; 1486 | } 1487 | 1488 | try { 1489 | return func.apply(this_obj, Array.prototype.slice.call(arguments, 2)); 1490 | } catch (e) { 1491 | if (this.phase >= this.phases.HAS_RESULT) { 1492 | return; 1493 | } 1494 | var message = String((typeof e === "object" && e !== null) ? e.message : e); 1495 | var stack = e.stack ? e.stack : null; 1496 | 1497 | this.set_status(this.FAIL, message, stack); 1498 | this.phase = this.phases.HAS_RESULT; 1499 | this.done(); 1500 | } 1501 | }; 1502 | 1503 | Test.prototype.step_func = function(func, this_obj) 1504 | { 1505 | var test_this = this; 1506 | 1507 | if (arguments.length === 1) { 1508 | this_obj = test_this; 1509 | } 1510 | 1511 | return function() 1512 | { 1513 | return test_this.step.apply(test_this, [func, this_obj].concat( 1514 | Array.prototype.slice.call(arguments))); 1515 | }; 1516 | }; 1517 | 1518 | Test.prototype.step_func_done = function(func, this_obj) 1519 | { 1520 | var test_this = this; 1521 | 1522 | if (arguments.length === 1) { 1523 | this_obj = test_this; 1524 | } 1525 | 1526 | return function() 1527 | { 1528 | if (func) { 1529 | test_this.step.apply(test_this, [func, this_obj].concat( 1530 | Array.prototype.slice.call(arguments))); 1531 | } 1532 | test_this.done(); 1533 | }; 1534 | }; 1535 | 1536 | Test.prototype.unreached_func = function(description) 1537 | { 1538 | return this.step_func(function() { 1539 | assert_unreached(description); 1540 | }); 1541 | }; 1542 | 1543 | Test.prototype.step_timeout = function(f, timeout) { 1544 | var test_this = this; 1545 | var args = Array.prototype.slice.call(arguments, 2); 1546 | return setTimeout(this.step_func(function() { 1547 | return f.apply(test_this, args); 1548 | }), timeout * tests.timeout_multiplier); 1549 | } 1550 | 1551 | /* 1552 | * Private method for registering cleanup functions. `testharness.js` 1553 | * internals should use this method instead of the public `add_cleanup` 1554 | * method in order to hide implementation details from the harness status 1555 | * message in the case errors. 1556 | */ 1557 | Test.prototype._add_cleanup = function(callback) { 1558 | this.cleanup_callbacks.push(callback); 1559 | }; 1560 | 1561 | /* 1562 | * Schedule a function to be run after the test result is known, regardless 1563 | * of passing or failing state. The behavior of this function will not 1564 | * influence the result of the test, but if an exception is thrown, the 1565 | * test harness will report an error. 1566 | */ 1567 | Test.prototype.add_cleanup = function(callback) { 1568 | this._user_defined_cleanup_count += 1; 1569 | this._add_cleanup(callback); 1570 | }; 1571 | 1572 | Test.prototype.set_timeout = function() 1573 | { 1574 | if (this.timeout_length !== null) { 1575 | var this_obj = this; 1576 | this.timeout_id = setTimeout(function() 1577 | { 1578 | this_obj.timeout(); 1579 | }, this.timeout_length); 1580 | } 1581 | }; 1582 | 1583 | Test.prototype.set_status = function(status, message, stack) 1584 | { 1585 | this.status = status; 1586 | this.message = message; 1587 | this.stack = stack ? stack : null; 1588 | }; 1589 | 1590 | Test.prototype.timeout = function() 1591 | { 1592 | this.timeout_id = null; 1593 | this.set_status(this.TIMEOUT, "Test timed out"); 1594 | this.phase = this.phases.HAS_RESULT; 1595 | this.done(); 1596 | }; 1597 | 1598 | Test.prototype.force_timeout = Test.prototype.timeout; 1599 | 1600 | Test.prototype.done = function() 1601 | { 1602 | if (this.phase == this.phases.COMPLETE) { 1603 | return; 1604 | } 1605 | 1606 | if (this.phase <= this.phases.STARTED) { 1607 | this.set_status(this.PASS, null); 1608 | } 1609 | 1610 | this.phase = this.phases.COMPLETE; 1611 | 1612 | clearTimeout(this.timeout_id); 1613 | tests.result(this); 1614 | this.cleanup(); 1615 | }; 1616 | 1617 | /* 1618 | * Invoke all specified cleanup functions. If one or more produce an error, 1619 | * the context is in an unpredictable state, so all further testing should 1620 | * be cancelled. 1621 | */ 1622 | Test.prototype.cleanup = function() { 1623 | var error_count = 0; 1624 | var total; 1625 | 1626 | forEach(this.cleanup_callbacks, 1627 | function(cleanup_callback) { 1628 | try { 1629 | cleanup_callback(); 1630 | } catch (e) { 1631 | // Set test phase immediately so that tests declared 1632 | // within subsequent cleanup functions are not run. 1633 | tests.phase = tests.phases.ABORTED; 1634 | error_count += 1; 1635 | } 1636 | }); 1637 | 1638 | if (error_count > 0) { 1639 | total = this._user_defined_cleanup_count; 1640 | tests.status.status = tests.status.ERROR; 1641 | tests.status.message = "Test named '" + this.name + 1642 | "' specified " + total + " 'cleanup' function" + 1643 | (total > 1 ? "s" : "") + ", and " + error_count + " failed."; 1644 | tests.status.stack = null; 1645 | } 1646 | }; 1647 | 1648 | /* 1649 | * A RemoteTest object mirrors a Test object on a remote worker. The 1650 | * associated RemoteWorker updates the RemoteTest object in response to 1651 | * received events. In turn, the RemoteTest object replicates these events 1652 | * on the local document. This allows listeners (test result reporting 1653 | * etc..) to transparently handle local and remote events. 1654 | */ 1655 | function RemoteTest(clone) { 1656 | var this_obj = this; 1657 | Object.keys(clone).forEach( 1658 | function(key) { 1659 | this_obj[key] = clone[key]; 1660 | }); 1661 | this.index = null; 1662 | this.phase = this.phases.INITIAL; 1663 | this.update_state_from(clone); 1664 | tests.push(this); 1665 | } 1666 | 1667 | RemoteTest.prototype.structured_clone = function() { 1668 | var clone = {}; 1669 | Object.keys(this).forEach( 1670 | (function(key) { 1671 | var value = this[key]; 1672 | 1673 | if (typeof value === "object" && value !== null) { 1674 | clone[key] = merge({}, value); 1675 | } else { 1676 | clone[key] = value; 1677 | } 1678 | }).bind(this)); 1679 | clone.phases = merge({}, this.phases); 1680 | return clone; 1681 | }; 1682 | 1683 | RemoteTest.prototype.cleanup = function() {}; 1684 | RemoteTest.prototype.phases = Test.prototype.phases; 1685 | RemoteTest.prototype.update_state_from = function(clone) { 1686 | this.status = clone.status; 1687 | this.message = clone.message; 1688 | this.stack = clone.stack; 1689 | if (this.phase === this.phases.INITIAL) { 1690 | this.phase = this.phases.STARTED; 1691 | } 1692 | }; 1693 | RemoteTest.prototype.done = function() { 1694 | this.phase = this.phases.COMPLETE; 1695 | } 1696 | 1697 | /* 1698 | * A RemoteContext listens for test events from a remote test context, such 1699 | * as another window or a worker. These events are then used to construct 1700 | * and maintain RemoteTest objects that mirror the tests running in the 1701 | * remote context. 1702 | * 1703 | * An optional third parameter can be used as a predicate to filter incoming 1704 | * MessageEvents. 1705 | */ 1706 | function RemoteContext(remote, message_target, message_filter) { 1707 | this.running = true; 1708 | this.tests = new Array(); 1709 | 1710 | var this_obj = this; 1711 | // If remote context is cross origin assigning to onerror is not 1712 | // possible, so silently catch those errors. 1713 | try { 1714 | remote.onerror = function(error) { this_obj.remote_error(error); }; 1715 | } catch (e) { 1716 | // Ignore. 1717 | } 1718 | 1719 | // Keeping a reference to the remote object and the message handler until 1720 | // remote_done() is seen prevents the remote object and its message channel 1721 | // from going away before all the messages are dispatched. 1722 | this.remote = remote; 1723 | this.message_target = message_target; 1724 | this.message_handler = function(message) { 1725 | var passesFilter = !message_filter || message_filter(message); 1726 | if (this_obj.running && message.data && passesFilter && 1727 | (message.data.type in this_obj.message_handlers)) { 1728 | this_obj.message_handlers[message.data.type].call(this_obj, message.data); 1729 | } 1730 | }; 1731 | 1732 | this.message_target.addEventListener("message", this.message_handler); 1733 | } 1734 | 1735 | RemoteContext.prototype.remote_error = function(error) { 1736 | var message = error.message || String(error); 1737 | var filename = (error.filename ? " " + error.filename: ""); 1738 | // FIXME: Display remote error states separately from main document 1739 | // error state. 1740 | this.remote_done({ 1741 | status: { 1742 | status: tests.status.ERROR, 1743 | message: "Error in remote" + filename + ": " + message, 1744 | stack: error.stack 1745 | } 1746 | }); 1747 | 1748 | if (error.preventDefault) { 1749 | error.preventDefault(); 1750 | } 1751 | }; 1752 | 1753 | RemoteContext.prototype.test_state = function(data) { 1754 | var remote_test = this.tests[data.test.index]; 1755 | if (!remote_test) { 1756 | remote_test = new RemoteTest(data.test); 1757 | this.tests[data.test.index] = remote_test; 1758 | } 1759 | remote_test.update_state_from(data.test); 1760 | tests.notify_test_state(remote_test); 1761 | }; 1762 | 1763 | RemoteContext.prototype.test_done = function(data) { 1764 | var remote_test = this.tests[data.test.index]; 1765 | remote_test.update_state_from(data.test); 1766 | remote_test.done(); 1767 | tests.result(remote_test); 1768 | }; 1769 | 1770 | RemoteContext.prototype.remote_done = function(data) { 1771 | if (tests.status.status === null && 1772 | data.status.status !== data.status.OK) { 1773 | tests.status.status = data.status.status; 1774 | tests.status.message = data.status.message; 1775 | tests.status.stack = data.status.stack; 1776 | } 1777 | this.message_target.removeEventListener("message", this.message_handler); 1778 | this.running = false; 1779 | this.remote = null; 1780 | this.message_target = null; 1781 | if (tests.all_done()) { 1782 | tests.complete(); 1783 | } 1784 | }; 1785 | 1786 | RemoteContext.prototype.message_handlers = { 1787 | test_state: RemoteContext.prototype.test_state, 1788 | result: RemoteContext.prototype.test_done, 1789 | complete: RemoteContext.prototype.remote_done 1790 | }; 1791 | 1792 | /* 1793 | * Harness 1794 | */ 1795 | 1796 | function TestsStatus() 1797 | { 1798 | this.status = null; 1799 | this.message = null; 1800 | this.stack = null; 1801 | } 1802 | 1803 | TestsStatus.statuses = { 1804 | OK:0, 1805 | ERROR:1, 1806 | TIMEOUT:2 1807 | }; 1808 | 1809 | TestsStatus.prototype = merge({}, TestsStatus.statuses); 1810 | 1811 | TestsStatus.prototype.structured_clone = function() 1812 | { 1813 | if (!this._structured_clone) { 1814 | var msg = this.message; 1815 | msg = msg ? String(msg) : msg; 1816 | this._structured_clone = merge({ 1817 | status:this.status, 1818 | message:msg, 1819 | stack:this.stack 1820 | }, TestsStatus.statuses); 1821 | } 1822 | return this._structured_clone; 1823 | }; 1824 | 1825 | function Tests() 1826 | { 1827 | this.tests = []; 1828 | this.num_pending = 0; 1829 | 1830 | this.phases = { 1831 | INITIAL:0, 1832 | SETUP:1, 1833 | HAVE_TESTS:2, 1834 | HAVE_RESULTS:3, 1835 | COMPLETE:4, 1836 | ABORTED:5 1837 | }; 1838 | this.phase = this.phases.INITIAL; 1839 | 1840 | this.properties = {}; 1841 | 1842 | this.wait_for_finish = false; 1843 | this.processing_callbacks = false; 1844 | 1845 | this.allow_uncaught_exception = false; 1846 | 1847 | this.file_is_test = false; 1848 | 1849 | this.timeout_multiplier = 1; 1850 | this.timeout_length = test_environment.test_timeout(); 1851 | this.timeout_id = null; 1852 | 1853 | this.start_callbacks = []; 1854 | this.test_state_callbacks = []; 1855 | this.test_done_callbacks = []; 1856 | this.all_done_callbacks = []; 1857 | 1858 | this.pending_remotes = []; 1859 | 1860 | this.status = new TestsStatus(); 1861 | 1862 | var this_obj = this; 1863 | 1864 | test_environment.add_on_loaded_callback(function() { 1865 | if (this_obj.all_done()) { 1866 | this_obj.complete(); 1867 | } 1868 | }); 1869 | 1870 | this.set_timeout(); 1871 | } 1872 | 1873 | Tests.prototype.setup = function(func, properties) 1874 | { 1875 | if (this.phase >= this.phases.HAVE_RESULTS) { 1876 | return; 1877 | } 1878 | 1879 | if (this.phase < this.phases.SETUP) { 1880 | this.phase = this.phases.SETUP; 1881 | } 1882 | 1883 | this.properties = properties; 1884 | 1885 | for (var p in properties) { 1886 | if (properties.hasOwnProperty(p)) { 1887 | var value = properties[p]; 1888 | if (p == "allow_uncaught_exception") { 1889 | this.allow_uncaught_exception = value; 1890 | } else if (p == "explicit_done" && value) { 1891 | this.wait_for_finish = true; 1892 | } else if (p == "explicit_timeout" && value) { 1893 | this.timeout_length = null; 1894 | if (this.timeout_id) 1895 | { 1896 | clearTimeout(this.timeout_id); 1897 | } 1898 | } else if (p == "timeout_multiplier") { 1899 | this.timeout_multiplier = value; 1900 | } 1901 | } 1902 | } 1903 | 1904 | if (func) { 1905 | try { 1906 | func(); 1907 | } catch (e) { 1908 | this.status.status = this.status.ERROR; 1909 | this.status.message = String(e); 1910 | this.status.stack = e.stack ? e.stack : null; 1911 | } 1912 | } 1913 | this.set_timeout(); 1914 | }; 1915 | 1916 | Tests.prototype.set_file_is_test = function() { 1917 | if (this.tests.length > 0) { 1918 | throw new Error("Tried to set file as test after creating a test"); 1919 | } 1920 | this.wait_for_finish = true; 1921 | this.file_is_test = true; 1922 | // Create the test, which will add it to the list of tests 1923 | async_test(); 1924 | }; 1925 | 1926 | Tests.prototype.set_timeout = function() { 1927 | var this_obj = this; 1928 | clearTimeout(this.timeout_id); 1929 | if (this.timeout_length !== null) { 1930 | this.timeout_id = setTimeout(function() { 1931 | this_obj.timeout(); 1932 | }, this.timeout_length); 1933 | } 1934 | }; 1935 | 1936 | Tests.prototype.timeout = function() { 1937 | if (this.status.status === null) { 1938 | this.status.status = this.status.TIMEOUT; 1939 | } 1940 | this.complete(); 1941 | }; 1942 | 1943 | Tests.prototype.end_wait = function() 1944 | { 1945 | this.wait_for_finish = false; 1946 | if (this.all_done()) { 1947 | this.complete(); 1948 | } 1949 | }; 1950 | 1951 | Tests.prototype.push = function(test) 1952 | { 1953 | if (this.phase < this.phases.HAVE_TESTS) { 1954 | this.start(); 1955 | } 1956 | this.num_pending++; 1957 | test.index = this.tests.push(test); 1958 | this.notify_test_state(test); 1959 | }; 1960 | 1961 | Tests.prototype.notify_test_state = function(test) { 1962 | var this_obj = this; 1963 | forEach(this.test_state_callbacks, 1964 | function(callback) { 1965 | callback(test, this_obj); 1966 | }); 1967 | }; 1968 | 1969 | Tests.prototype.all_done = function() { 1970 | return this.phase === this.phases.ABORTED || 1971 | (this.tests.length > 0 && test_environment.all_loaded && 1972 | this.num_pending === 0 && !this.wait_for_finish && 1973 | !this.processing_callbacks && 1974 | !this.pending_remotes.some(function(w) { return w.running; })); 1975 | }; 1976 | 1977 | Tests.prototype.start = function() { 1978 | this.phase = this.phases.HAVE_TESTS; 1979 | this.notify_start(); 1980 | }; 1981 | 1982 | Tests.prototype.notify_start = function() { 1983 | var this_obj = this; 1984 | forEach (this.start_callbacks, 1985 | function(callback) 1986 | { 1987 | callback(this_obj.properties); 1988 | }); 1989 | }; 1990 | 1991 | Tests.prototype.result = function(test) 1992 | { 1993 | if (this.phase > this.phases.HAVE_RESULTS) { 1994 | return; 1995 | } 1996 | this.phase = this.phases.HAVE_RESULTS; 1997 | this.num_pending--; 1998 | this.notify_result(test); 1999 | }; 2000 | 2001 | Tests.prototype.notify_result = function(test) { 2002 | var this_obj = this; 2003 | this.processing_callbacks = true; 2004 | forEach(this.test_done_callbacks, 2005 | function(callback) 2006 | { 2007 | callback(test, this_obj); 2008 | }); 2009 | this.processing_callbacks = false; 2010 | if (this_obj.all_done()) { 2011 | this_obj.complete(); 2012 | } 2013 | }; 2014 | 2015 | Tests.prototype.complete = function() { 2016 | if (this.phase === this.phases.COMPLETE) { 2017 | return; 2018 | } 2019 | this.phase = this.phases.COMPLETE; 2020 | var this_obj = this; 2021 | this.tests.forEach( 2022 | function(x) 2023 | { 2024 | if (x.phase < x.phases.COMPLETE) { 2025 | this_obj.notify_result(x); 2026 | x.cleanup(); 2027 | x.phase = x.phases.COMPLETE; 2028 | } 2029 | } 2030 | ); 2031 | this.notify_complete(); 2032 | }; 2033 | 2034 | /* 2035 | * Determine if any tests share the same `name` property. Return an array 2036 | * containing the names of any such duplicates. 2037 | */ 2038 | Tests.prototype.find_duplicates = function() { 2039 | var names = Object.create(null); 2040 | var duplicates = []; 2041 | 2042 | forEach (this.tests, 2043 | function(test) 2044 | { 2045 | if (test.name in names && duplicates.indexOf(test.name) === -1) { 2046 | duplicates.push(test.name); 2047 | } 2048 | names[test.name] = true; 2049 | }); 2050 | 2051 | return duplicates; 2052 | }; 2053 | 2054 | Tests.prototype.notify_complete = function() { 2055 | var this_obj = this; 2056 | var duplicates; 2057 | 2058 | if (this.status.status === null) { 2059 | duplicates = this.find_duplicates(); 2060 | 2061 | // Test names are presumed to be unique within test files--this 2062 | // allows consumers to use them for identification purposes. 2063 | // Duplicated names violate this expectation and should therefore 2064 | // be reported as an error. 2065 | if (duplicates.length) { 2066 | this.status.status = this.status.ERROR; 2067 | this.status.message = 2068 | duplicates.length + ' duplicate test name' + 2069 | (duplicates.length > 1 ? 's' : '') + ': "' + 2070 | duplicates.join('", "') + '"'; 2071 | } else { 2072 | this.status.status = this.status.OK; 2073 | } 2074 | } 2075 | 2076 | forEach (this.all_done_callbacks, 2077 | function(callback) 2078 | { 2079 | callback(this_obj.tests, this_obj.status); 2080 | }); 2081 | }; 2082 | 2083 | /* 2084 | * Constructs a RemoteContext that tracks tests from a specific worker. 2085 | */ 2086 | Tests.prototype.create_remote_worker = function(worker) { 2087 | var message_port; 2088 | 2089 | if (is_service_worker(worker)) { 2090 | if (window.MessageChannel) { 2091 | // The ServiceWorker's implicit MessagePort is currently not 2092 | // reliably accessible from the ServiceWorkerGlobalScope due to 2093 | // Blink setting MessageEvent.source to null for messages sent 2094 | // via ServiceWorker.postMessage(). Until that's resolved, 2095 | // create an explicit MessageChannel and pass one end to the 2096 | // worker. 2097 | var message_channel = new MessageChannel(); 2098 | message_port = message_channel.port1; 2099 | message_port.start(); 2100 | worker.postMessage({type: "connect"}, [message_channel.port2]); 2101 | } else { 2102 | // If MessageChannel is not available, then try the 2103 | // ServiceWorker.postMessage() approach using MessageEvent.source 2104 | // on the other end. 2105 | message_port = navigator.serviceWorker; 2106 | worker.postMessage({type: "connect"}); 2107 | } 2108 | } else if (is_shared_worker(worker)) { 2109 | message_port = worker.port; 2110 | message_port.start(); 2111 | } else { 2112 | message_port = worker; 2113 | } 2114 | 2115 | return new RemoteContext(worker, message_port); 2116 | }; 2117 | 2118 | /* 2119 | * Constructs a RemoteContext that tracks tests from a specific window. 2120 | */ 2121 | Tests.prototype.create_remote_window = function(remote) { 2122 | remote.postMessage({type: "getmessages"}, "*"); 2123 | return new RemoteContext( 2124 | remote, 2125 | window, 2126 | function(msg) { 2127 | return msg.source === remote; 2128 | } 2129 | ); 2130 | }; 2131 | 2132 | Tests.prototype.fetch_tests_from_worker = function(worker) { 2133 | if (this.phase >= this.phases.COMPLETE) { 2134 | return; 2135 | } 2136 | 2137 | this.pending_remotes.push(this.create_remote_worker(worker)); 2138 | }; 2139 | 2140 | function fetch_tests_from_worker(port) { 2141 | tests.fetch_tests_from_worker(port); 2142 | } 2143 | expose(fetch_tests_from_worker, 'fetch_tests_from_worker'); 2144 | 2145 | Tests.prototype.fetch_tests_from_window = function(remote) { 2146 | if (this.phase >= this.phases.COMPLETE) { 2147 | return; 2148 | } 2149 | 2150 | this.pending_remotes.push(this.create_remote_window(remote)); 2151 | }; 2152 | 2153 | function fetch_tests_from_window(window) { 2154 | tests.fetch_tests_from_window(window); 2155 | } 2156 | expose(fetch_tests_from_window, 'fetch_tests_from_window'); 2157 | 2158 | function timeout() { 2159 | if (tests.timeout_length === null) { 2160 | tests.timeout(); 2161 | } 2162 | } 2163 | expose(timeout, 'timeout'); 2164 | 2165 | function add_start_callback(callback) { 2166 | tests.start_callbacks.push(callback); 2167 | } 2168 | 2169 | function add_test_state_callback(callback) { 2170 | tests.test_state_callbacks.push(callback); 2171 | } 2172 | 2173 | function add_result_callback(callback) { 2174 | tests.test_done_callbacks.push(callback); 2175 | } 2176 | 2177 | function add_completion_callback(callback) { 2178 | tests.all_done_callbacks.push(callback); 2179 | } 2180 | 2181 | expose(add_start_callback, 'add_start_callback'); 2182 | expose(add_test_state_callback, 'add_test_state_callback'); 2183 | expose(add_result_callback, 'add_result_callback'); 2184 | expose(add_completion_callback, 'add_completion_callback'); 2185 | 2186 | function remove(array, item) { 2187 | var index = array.indexOf(item); 2188 | if (index > -1) { 2189 | array.splice(index, 1); 2190 | } 2191 | } 2192 | 2193 | function remove_start_callback(callback) { 2194 | remove(tests.start_callbacks, callback); 2195 | } 2196 | 2197 | function remove_test_state_callback(callback) { 2198 | remove(tests.test_state_callbacks, callback); 2199 | } 2200 | 2201 | function remove_result_callback(callback) { 2202 | remove(tests.test_done_callbacks, callback); 2203 | } 2204 | 2205 | function remove_completion_callback(callback) { 2206 | remove(tests.all_done_callbacks, callback); 2207 | } 2208 | 2209 | /* 2210 | * Output listener 2211 | */ 2212 | 2213 | function Output() { 2214 | this.output_document = document; 2215 | this.output_node = null; 2216 | this.enabled = settings.output; 2217 | this.phase = this.INITIAL; 2218 | } 2219 | 2220 | Output.prototype.INITIAL = 0; 2221 | Output.prototype.STARTED = 1; 2222 | Output.prototype.HAVE_RESULTS = 2; 2223 | Output.prototype.COMPLETE = 3; 2224 | 2225 | Output.prototype.setup = function(properties) { 2226 | if (this.phase > this.INITIAL) { 2227 | return; 2228 | } 2229 | 2230 | //If output is disabled in testharnessreport.js the test shouldn't be 2231 | //able to override that 2232 | this.enabled = this.enabled && (properties.hasOwnProperty("output") ? 2233 | properties.output : settings.output); 2234 | }; 2235 | 2236 | Output.prototype.init = function(properties) { 2237 | if (this.phase >= this.STARTED) { 2238 | return; 2239 | } 2240 | if (properties.output_document) { 2241 | this.output_document = properties.output_document; 2242 | } else { 2243 | this.output_document = document; 2244 | } 2245 | this.phase = this.STARTED; 2246 | }; 2247 | 2248 | Output.prototype.resolve_log = function() { 2249 | var output_document; 2250 | if (typeof this.output_document === "function") { 2251 | output_document = this.output_document.apply(undefined); 2252 | } else { 2253 | output_document = this.output_document; 2254 | } 2255 | if (!output_document) { 2256 | return; 2257 | } 2258 | var node = output_document.getElementById("log"); 2259 | if (!node) { 2260 | if (!document.readyState == "loading") { 2261 | return; 2262 | } 2263 | node = output_document.createElementNS("http://www.w3.org/1999/xhtml", "div"); 2264 | node.id = "log"; 2265 | if (output_document.body) { 2266 | output_document.body.appendChild(node); 2267 | } else { 2268 | var is_svg = false; 2269 | var output_window = output_document.defaultView; 2270 | if (output_window && "SVGSVGElement" in output_window) { 2271 | is_svg = output_document.documentElement instanceof output_window.SVGSVGElement; 2272 | } 2273 | if (is_svg) { 2274 | var foreignObject = output_document.createElementNS("http://www.w3.org/2000/svg", "foreignObject"); 2275 | foreignObject.setAttribute("width", "100%"); 2276 | foreignObject.setAttribute("height", "100%"); 2277 | output_document.documentElement.appendChild(foreignObject); 2278 | foreignObject.appendChild(node); 2279 | } else { 2280 | output_document.documentElement.appendChild(node); 2281 | } 2282 | } 2283 | } 2284 | this.output_document = output_document; 2285 | this.output_node = node; 2286 | }; 2287 | 2288 | Output.prototype.show_status = function() { 2289 | if (this.phase < this.STARTED) { 2290 | this.init(); 2291 | } 2292 | if (!this.enabled) { 2293 | return; 2294 | } 2295 | if (this.phase < this.HAVE_RESULTS) { 2296 | this.resolve_log(); 2297 | this.phase = this.HAVE_RESULTS; 2298 | } 2299 | var done_count = tests.tests.length - tests.num_pending; 2300 | if (this.output_node) { 2301 | if (done_count < 100 || 2302 | (done_count < 1000 && done_count % 100 === 0) || 2303 | done_count % 1000 === 0) { 2304 | this.output_node.textContent = "Running, " + 2305 | done_count + " complete, " + 2306 | tests.num_pending + " remain"; 2307 | } 2308 | } 2309 | }; 2310 | 2311 | Output.prototype.show_results = function (tests, harness_status) { 2312 | if (this.phase >= this.COMPLETE) { 2313 | return; 2314 | } 2315 | if (!this.enabled) { 2316 | return; 2317 | } 2318 | if (!this.output_node) { 2319 | this.resolve_log(); 2320 | } 2321 | this.phase = this.COMPLETE; 2322 | 2323 | var log = this.output_node; 2324 | if (!log) { 2325 | return; 2326 | } 2327 | var output_document = this.output_document; 2328 | 2329 | while (log.lastChild) { 2330 | log.removeChild(log.lastChild); 2331 | } 2332 | 2333 | var harness_url = get_harness_url(); 2334 | if (harness_url !== undefined) { 2335 | var stylesheet = output_document.createElementNS(xhtml_ns, "link"); 2336 | stylesheet.setAttribute("rel", "stylesheet"); 2337 | stylesheet.setAttribute("href", harness_url + "testharness.css"); 2338 | var heads = output_document.getElementsByTagName("head"); 2339 | if (heads.length) { 2340 | heads[0].appendChild(stylesheet); 2341 | } 2342 | } 2343 | 2344 | var status_text_harness = {}; 2345 | status_text_harness[harness_status.OK] = "OK"; 2346 | status_text_harness[harness_status.ERROR] = "Error"; 2347 | status_text_harness[harness_status.TIMEOUT] = "Timeout"; 2348 | 2349 | var status_text = {}; 2350 | status_text[Test.prototype.PASS] = "Pass"; 2351 | status_text[Test.prototype.FAIL] = "Fail"; 2352 | status_text[Test.prototype.TIMEOUT] = "Timeout"; 2353 | status_text[Test.prototype.NOTRUN] = "Not Run"; 2354 | 2355 | var status_number = {}; 2356 | forEach(tests, 2357 | function(test) { 2358 | var status = status_text[test.status]; 2359 | if (status_number.hasOwnProperty(status)) { 2360 | status_number[status] += 1; 2361 | } else { 2362 | status_number[status] = 1; 2363 | } 2364 | }); 2365 | 2366 | function status_class(status) 2367 | { 2368 | return status.replace(/\s/g, '').toLowerCase(); 2369 | } 2370 | 2371 | var summary_template = ["section", {"id":"summary"}, 2372 | ["h2", {}, "Summary"], 2373 | function() 2374 | { 2375 | 2376 | var status = status_text_harness[harness_status.status]; 2377 | var rv = [["section", {}, 2378 | ["p", {}, 2379 | "Harness status: ", 2380 | ["span", {"class":status_class(status)}, 2381 | status 2382 | ], 2383 | ] 2384 | ]]; 2385 | 2386 | if (harness_status.status === harness_status.ERROR) { 2387 | rv[0].push(["pre", {}, harness_status.message]); 2388 | if (harness_status.stack) { 2389 | rv[0].push(["pre", {}, harness_status.stack]); 2390 | } 2391 | } 2392 | return rv; 2393 | }, 2394 | ["p", {}, "Found ${num_tests} tests"], 2395 | function() { 2396 | var rv = [["div", {}]]; 2397 | var i = 0; 2398 | while (status_text.hasOwnProperty(i)) { 2399 | if (status_number.hasOwnProperty(status_text[i])) { 2400 | var status = status_text[i]; 2401 | rv[0].push(["div", {"class":status_class(status)}, 2402 | ["label", {}, 2403 | ["input", {type:"checkbox", checked:"checked"}], 2404 | status_number[status] + " " + status]]); 2405 | } 2406 | i++; 2407 | } 2408 | return rv; 2409 | }, 2410 | ]; 2411 | 2412 | log.appendChild(render(summary_template, {num_tests:tests.length}, output_document)); 2413 | 2414 | forEach(output_document.querySelectorAll("section#summary label"), 2415 | function(element) 2416 | { 2417 | on_event(element, "click", 2418 | function(e) 2419 | { 2420 | if (output_document.getElementById("results") === null) { 2421 | e.preventDefault(); 2422 | return; 2423 | } 2424 | var result_class = element.parentNode.getAttribute("class"); 2425 | var style_element = output_document.querySelector("style#hide-" + result_class); 2426 | var input_element = element.querySelector("input"); 2427 | if (!style_element && !input_element.checked) { 2428 | style_element = output_document.createElementNS(xhtml_ns, "style"); 2429 | style_element.id = "hide-" + result_class; 2430 | style_element.textContent = "table#results > tbody > tr."+result_class+"{display:none}"; 2431 | output_document.body.appendChild(style_element); 2432 | } else if (style_element && input_element.checked) { 2433 | style_element.parentNode.removeChild(style_element); 2434 | } 2435 | }); 2436 | }); 2437 | 2438 | // This use of innerHTML plus manual escaping is not recommended in 2439 | // general, but is necessary here for performance. Using textContent 2440 | // on each individual adds tens of seconds of execution time for 2441 | // large test suites (tens of thousands of tests). 2442 | function escape_html(s) 2443 | { 2444 | return s.replace(/\&/g, "&") 2445 | .replace(/" + 2474 | "ResultTest Name" + 2475 | (assertions ? "Assertion" : "") + 2476 | "Message" + 2477 | ""; 2478 | for (var i = 0; i < tests.length; i++) { 2479 | html += '' + 2482 | escape_html(status_text[tests[i].status]) + 2483 | "" + 2484 | escape_html(tests[i].name) + 2485 | "" + 2486 | (assertions ? escape_html(get_assertion(tests[i])) + "" : "") + 2487 | escape_html(tests[i].message ? tests[i].message : " ") + 2488 | (tests[i].stack ? "
" +
2489 |                  escape_html(tests[i].stack) +
2490 |                  "
": "") + 2491 | ""; 2492 | } 2493 | html += ""; 2494 | try { 2495 | log.lastChild.innerHTML = html; 2496 | } catch (e) { 2497 | log.appendChild(document.createElementNS(xhtml_ns, "p")) 2498 | .textContent = "Setting innerHTML for the log threw an exception."; 2499 | log.appendChild(document.createElementNS(xhtml_ns, "pre")) 2500 | .textContent = html; 2501 | } 2502 | }; 2503 | 2504 | /* 2505 | * Template code 2506 | * 2507 | * A template is just a javascript structure. An element is represented as: 2508 | * 2509 | * [tag_name, {attr_name:attr_value}, child1, child2] 2510 | * 2511 | * the children can either be strings (which act like text nodes), other templates or 2512 | * functions (see below) 2513 | * 2514 | * A text node is represented as 2515 | * 2516 | * ["{text}", value] 2517 | * 2518 | * String values have a simple substitution syntax; ${foo} represents a variable foo. 2519 | * 2520 | * It is possible to embed logic in templates by using a function in a place where a 2521 | * node would usually go. The function must either return part of a template or null. 2522 | * 2523 | * In cases where a set of nodes are required as output rather than a single node 2524 | * with children it is possible to just use a list 2525 | * [node1, node2, node3] 2526 | * 2527 | * Usage: 2528 | * 2529 | * render(template, substitutions) - take a template and an object mapping 2530 | * variable names to parameters and return either a DOM node or a list of DOM nodes 2531 | * 2532 | * substitute(template, substitutions) - take a template and variable mapping object, 2533 | * make the variable substitutions and return the substituted template 2534 | * 2535 | */ 2536 | 2537 | function is_single_node(template) 2538 | { 2539 | return typeof template[0] === "string"; 2540 | } 2541 | 2542 | function substitute(template, substitutions) 2543 | { 2544 | if (typeof template === "function") { 2545 | var replacement = template(substitutions); 2546 | if (!replacement) { 2547 | return null; 2548 | } 2549 | 2550 | return substitute(replacement, substitutions); 2551 | } 2552 | 2553 | if (is_single_node(template)) { 2554 | return substitute_single(template, substitutions); 2555 | } 2556 | 2557 | return filter(map(template, function(x) { 2558 | return substitute(x, substitutions); 2559 | }), function(x) {return x !== null;}); 2560 | } 2561 | 2562 | function substitute_single(template, substitutions) 2563 | { 2564 | var substitution_re = /\$\{([^ }]*)\}/g; 2565 | 2566 | function do_substitution(input) { 2567 | var components = input.split(substitution_re); 2568 | var rv = []; 2569 | for (var i = 0; i < components.length; i += 2) { 2570 | rv.push(components[i]); 2571 | if (components[i + 1]) { 2572 | rv.push(String(substitutions[components[i + 1]])); 2573 | } 2574 | } 2575 | return rv; 2576 | } 2577 | 2578 | function substitute_attrs(attrs, rv) 2579 | { 2580 | rv[1] = {}; 2581 | for (var name in template[1]) { 2582 | if (attrs.hasOwnProperty(name)) { 2583 | var new_name = do_substitution(name).join(""); 2584 | var new_value = do_substitution(attrs[name]).join(""); 2585 | rv[1][new_name] = new_value; 2586 | } 2587 | } 2588 | } 2589 | 2590 | function substitute_children(children, rv) 2591 | { 2592 | for (var i = 0; i < children.length; i++) { 2593 | if (children[i] instanceof Object) { 2594 | var replacement = substitute(children[i], substitutions); 2595 | if (replacement !== null) { 2596 | if (is_single_node(replacement)) { 2597 | rv.push(replacement); 2598 | } else { 2599 | extend(rv, replacement); 2600 | } 2601 | } 2602 | } else { 2603 | extend(rv, do_substitution(String(children[i]))); 2604 | } 2605 | } 2606 | return rv; 2607 | } 2608 | 2609 | var rv = []; 2610 | rv.push(do_substitution(String(template[0])).join("")); 2611 | 2612 | if (template[0] === "{text}") { 2613 | substitute_children(template.slice(1), rv); 2614 | } else { 2615 | substitute_attrs(template[1], rv); 2616 | substitute_children(template.slice(2), rv); 2617 | } 2618 | 2619 | return rv; 2620 | } 2621 | 2622 | function make_dom_single(template, doc) 2623 | { 2624 | var output_document = doc || document; 2625 | var element; 2626 | if (template[0] === "{text}") { 2627 | element = output_document.createTextNode(""); 2628 | for (var i = 1; i < template.length; i++) { 2629 | element.data += template[i]; 2630 | } 2631 | } else { 2632 | element = output_document.createElementNS(xhtml_ns, template[0]); 2633 | for (var name in template[1]) { 2634 | if (template[1].hasOwnProperty(name)) { 2635 | element.setAttribute(name, template[1][name]); 2636 | } 2637 | } 2638 | for (var i = 2; i < template.length; i++) { 2639 | if (template[i] instanceof Object) { 2640 | var sub_element = make_dom(template[i]); 2641 | element.appendChild(sub_element); 2642 | } else { 2643 | var text_node = output_document.createTextNode(template[i]); 2644 | element.appendChild(text_node); 2645 | } 2646 | } 2647 | } 2648 | 2649 | return element; 2650 | } 2651 | 2652 | function make_dom(template, substitutions, output_document) 2653 | { 2654 | if (is_single_node(template)) { 2655 | return make_dom_single(template, output_document); 2656 | } 2657 | 2658 | return map(template, function(x) { 2659 | return make_dom_single(x, output_document); 2660 | }); 2661 | } 2662 | 2663 | function render(template, substitutions, output_document) 2664 | { 2665 | return make_dom(substitute(template, substitutions), output_document); 2666 | } 2667 | 2668 | /* 2669 | * Utility funcions 2670 | */ 2671 | function assert(expected_true, function_name, description, error, substitutions) 2672 | { 2673 | if (tests.tests.length === 0) { 2674 | tests.set_file_is_test(); 2675 | } 2676 | if (expected_true !== true) { 2677 | var msg = make_message(function_name, description, 2678 | error, substitutions); 2679 | throw new AssertionError(msg); 2680 | } 2681 | } 2682 | 2683 | function AssertionError(message) 2684 | { 2685 | this.message = message; 2686 | this.stack = this.get_stack(); 2687 | } 2688 | expose(AssertionError, "AssertionError"); 2689 | 2690 | AssertionError.prototype = Object.create(Error.prototype); 2691 | 2692 | AssertionError.prototype.get_stack = function() { 2693 | var stack = new Error().stack; 2694 | // IE11 does not initialize 'Error.stack' until the object is thrown. 2695 | if (!stack) { 2696 | try { 2697 | throw new Error(); 2698 | } catch (e) { 2699 | stack = e.stack; 2700 | } 2701 | } 2702 | 2703 | // 'Error.stack' is not supported in all browsers/versions 2704 | if (!stack) { 2705 | return "(Stack trace unavailable)"; 2706 | } 2707 | 2708 | var lines = stack.split("\n"); 2709 | 2710 | // Create a pattern to match stack frames originating within testharness.js. These include the 2711 | // script URL, followed by the line/col (e.g., '/resources/testharness.js:120:21'). 2712 | // Escape the URL per http://stackoverflow.com/questions/3561493/is-there-a-regexp-escape-function-in-javascript 2713 | // in case it contains RegExp characters. 2714 | var script_url = get_script_url(); 2715 | var re_text = script_url ? script_url.replace(/[-\/\\^$*+?.()|[\]{}]/g, '\\$&') : "\\btestharness.js"; 2716 | var re = new RegExp(re_text + ":\\d+:\\d+"); 2717 | 2718 | // Some browsers include a preamble that specifies the type of the error object. Skip this by 2719 | // advancing until we find the first stack frame originating from testharness.js. 2720 | var i = 0; 2721 | while (!re.test(lines[i]) && i < lines.length) { 2722 | i++; 2723 | } 2724 | 2725 | // Then skip the top frames originating from testharness.js to begin the stack at the test code. 2726 | while (re.test(lines[i]) && i < lines.length) { 2727 | i++; 2728 | } 2729 | 2730 | // Paranoid check that we didn't skip all frames. If so, return the original stack unmodified. 2731 | if (i >= lines.length) { 2732 | return stack; 2733 | } 2734 | 2735 | return lines.slice(i).join("\n"); 2736 | } 2737 | 2738 | function make_message(function_name, description, error, substitutions) 2739 | { 2740 | for (var p in substitutions) { 2741 | if (substitutions.hasOwnProperty(p)) { 2742 | substitutions[p] = format_value(substitutions[p]); 2743 | } 2744 | } 2745 | var node_form = substitute(["{text}", "${function_name}: ${description}" + error], 2746 | merge({function_name:function_name, 2747 | description:(description?description + " ":"")}, 2748 | substitutions)); 2749 | return node_form.slice(1).join(""); 2750 | } 2751 | 2752 | function filter(array, callable, thisObj) { 2753 | var rv = []; 2754 | for (var i = 0; i < array.length; i++) { 2755 | if (array.hasOwnProperty(i)) { 2756 | var pass = callable.call(thisObj, array[i], i, array); 2757 | if (pass) { 2758 | rv.push(array[i]); 2759 | } 2760 | } 2761 | } 2762 | return rv; 2763 | } 2764 | 2765 | function map(array, callable, thisObj) 2766 | { 2767 | var rv = []; 2768 | rv.length = array.length; 2769 | for (var i = 0; i < array.length; i++) { 2770 | if (array.hasOwnProperty(i)) { 2771 | rv[i] = callable.call(thisObj, array[i], i, array); 2772 | } 2773 | } 2774 | return rv; 2775 | } 2776 | 2777 | function extend(array, items) 2778 | { 2779 | Array.prototype.push.apply(array, items); 2780 | } 2781 | 2782 | function forEach(array, callback, thisObj) 2783 | { 2784 | for (var i = 0; i < array.length; i++) { 2785 | if (array.hasOwnProperty(i)) { 2786 | callback.call(thisObj, array[i], i, array); 2787 | } 2788 | } 2789 | } 2790 | 2791 | function merge(a,b) 2792 | { 2793 | var rv = {}; 2794 | var p; 2795 | for (p in a) { 2796 | rv[p] = a[p]; 2797 | } 2798 | for (p in b) { 2799 | rv[p] = b[p]; 2800 | } 2801 | return rv; 2802 | } 2803 | 2804 | function expose(object, name) 2805 | { 2806 | var components = name.split("."); 2807 | var target = test_environment.global_scope(); 2808 | for (var i = 0; i < components.length - 1; i++) { 2809 | if (!(components[i] in target)) { 2810 | target[components[i]] = {}; 2811 | } 2812 | target = target[components[i]]; 2813 | } 2814 | target[components[components.length - 1]] = object; 2815 | } 2816 | 2817 | function is_same_origin(w) { 2818 | try { 2819 | 'random_prop' in w; 2820 | return true; 2821 | } catch (e) { 2822 | return false; 2823 | } 2824 | } 2825 | 2826 | /** Returns the 'src' URL of the first