├── .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 |
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 |
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 + "" + val.localName + ">";
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(/Details
" +
2474 | "
Result
Test Name
" +
2475 | (assertions ? "
Assertion
" : "") +
2476 | "
Message
" +
2477 | "";
2478 | for (var i = 0; i < tests.length; i++) {
2479 | html += '