Bug 1568157 - Part 5: Move the NodePicker initialization into a getter. r=yulia
[gecko.git] / testing / marionette / sync.js
blobdf8f993da48b629f2819e4810c41f2838b97b1d3
1 /* This Source Code Form is subject to the terms of the Mozilla Public
2  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
3  * You can obtain one at http://mozilla.org/MPL/2.0/. */
5 "use strict";
7 const { AppConstants } = ChromeUtils.import(
8   "resource://gre/modules/AppConstants.jsm"
9 );
10 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
11 const { XPCOMUtils } = ChromeUtils.import(
12   "resource://gre/modules/XPCOMUtils.jsm"
15 const { error, stack, TimeoutError } = ChromeUtils.import(
16   "chrome://marionette/content/error.js"
18 const { truncate } = ChromeUtils.import(
19   "chrome://marionette/content/format.js"
21 const { Log } = ChromeUtils.import("chrome://marionette/content/log.js");
23 XPCOMUtils.defineLazyGetter(this, "log", Log.get);
25 this.EXPORTED_SYMBOLS = [
26   "executeSoon",
27   "DebounceCallback",
28   "IdlePromise",
29   "MessageManagerDestroyedPromise",
30   "PollPromise",
31   "Sleep",
32   "TimedPromise",
33   "waitForEvent",
34   "waitForMessage",
35   "waitForObserverTopic",
38 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
40 const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
42 /**
43  * Dispatch a function to be executed on the main thread.
44  *
45  * @param {function} func
46  *     Function to be executed.
47  */
48 function executeSoon(func) {
49   if (typeof func != "function") {
50     throw new TypeError();
51   }
53   Services.tm.dispatchToMainThread(func);
56 /**
57  * @callback Condition
58  *
59  * @param {function(*)} resolve
60  *     To be called when the condition has been met.  Will return the
61  *     resolved value.
62  * @param {function} reject
63  *     To be called when the condition has not been met.  Will cause
64  *     the condition to be revaluated or time out.
65  *
66  * @return {*}
67  *     The value from calling ``resolve``.
68  */
70 /**
71  * Runs a Promise-like function off the main thread until it is resolved
72  * through ``resolve`` or ``rejected`` callbacks.  The function is
73  * guaranteed to be run at least once, irregardless of the timeout.
74  *
75  * The ``func`` is evaluated every ``interval`` for as long as its
76  * runtime duration does not exceed ``interval``.  Evaluations occur
77  * sequentially, meaning that evaluations of ``func`` are queued if
78  * the runtime evaluation duration of ``func`` is greater than ``interval``.
79  *
80  * ``func`` is given two arguments, ``resolve`` and ``reject``,
81  * of which one must be called for the evaluation to complete.
82  * Calling ``resolve`` with an argument indicates that the expected
83  * wait condition was met and will return the passed value to the
84  * caller.  Conversely, calling ``reject`` will evaluate ``func``
85  * again until the ``timeout`` duration has elapsed or ``func`` throws.
86  * The passed value to ``reject`` will also be returned to the caller
87  * once the wait has expired.
88  *
89  * Usage::
90  *
91  *     let els = new PollPromise((resolve, reject) => {
92  *       let res = document.querySelectorAll("p");
93  *       if (res.length > 0) {
94  *         resolve(Array.from(res));
95  *       } else {
96  *         reject([]);
97  *       }
98  *     }, {timeout: 1000});
99  *
100  * @param {Condition} func
101  *     Function to run off the main thread.
102  * @param {number=} [timeout] timeout
103  *     Desired timeout if wanted.  If 0 or less than the runtime evaluation
104  *     time of ``func``, ``func`` is guaranteed to run at least once.
105  *     Defaults to using no timeout.
106  * @param {number=} [interval=10] interval
107  *     Duration between each poll of ``func`` in milliseconds.
108  *     Defaults to 10 milliseconds.
110  * @return {Promise.<*>}
111  *     Yields the value passed to ``func``'s
112  *     ``resolve`` or ``reject`` callbacks.
114  * @throws {*}
115  *     If ``func`` throws, its error is propagated.
116  * @throws {TypeError}
117  *     If `timeout` or `interval`` are not numbers.
118  * @throws {RangeError}
119  *     If `timeout` or `interval` are not unsigned integers.
120  */
121 function PollPromise(func, { timeout = null, interval = 10 } = {}) {
122   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
124   if (typeof func != "function") {
125     throw new TypeError();
126   }
127   if (timeout != null && typeof timeout != "number") {
128     throw new TypeError();
129   }
130   if (typeof interval != "number") {
131     throw new TypeError();
132   }
133   if (
134     (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
135     (!Number.isInteger(interval) || interval < 0)
136   ) {
137     throw new RangeError();
138   }
140   return new Promise((resolve, reject) => {
141     let start, end;
143     if (Number.isInteger(timeout)) {
144       start = new Date().getTime();
145       end = start + timeout;
146     }
148     let evalFn = () => {
149       new Promise(func)
150         .then(resolve, rejected => {
151           if (error.isError(rejected)) {
152             throw rejected;
153           }
155           // return if there is a timeout and set to 0,
156           // allowing |func| to be evaluated at least once
157           if (
158             typeof end != "undefined" &&
159             (start == end || new Date().getTime() >= end)
160           ) {
161             resolve(rejected);
162           }
163         })
164         .catch(reject);
165     };
167     // the repeating slack timer waits |interval|
168     // before invoking |evalFn|
169     evalFn();
171     timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
172   }).then(
173     res => {
174       timer.cancel();
175       return res;
176     },
177     err => {
178       timer.cancel();
179       throw err;
180     }
181   );
185  * Represents the timed, eventual completion (or failure) of an
186  * asynchronous operation, and its resulting value.
188  * In contrast to a regular Promise, it times out after ``timeout``.
190  * @param {Condition} func
191  *     Function to run, which will have its ``reject``
192  *     callback invoked after the ``timeout`` duration is reached.
193  *     It is given two callbacks: ``resolve(value)`` and
194  *     ``reject(error)``.
195  * @param {timeout=} timeout
196  *     ``condition``'s ``reject`` callback will be called
197  *     after this timeout, given in milliseconds.
198  *     By default 1500 ms in an optimised build and 4500 ms in
199  *     debug builds.
200  * @param {Error=} [throws=TimeoutError] throws
201  *     When the ``timeout`` is hit, this error class will be
202  *     thrown.  If it is null, no error is thrown and the promise is
203  *     instead resolved on timeout.
205  * @return {Promise.<*>}
206  *     Timed promise.
208  * @throws {TypeError}
209  *     If `timeout` is not a number.
210  * @throws {RangeError}
211  *     If `timeout` is not an unsigned integer.
212  */
213 function TimedPromise(
214   fn,
215   { timeout = PROMISE_TIMEOUT, throws = TimeoutError } = {}
216 ) {
217   const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
219   if (typeof fn != "function") {
220     throw new TypeError();
221   }
222   if (typeof timeout != "number") {
223     throw new TypeError();
224   }
225   if (!Number.isInteger(timeout) || timeout < 0) {
226     throw new RangeError();
227   }
229   return new Promise((resolve, reject) => {
230     // Reject only if |throws| is given.  Otherwise it is assumed that
231     // the user is OK with the promise timing out.
232     let bail = () => {
233       if (throws !== null) {
234         let err = new throws();
235         reject(err);
236       } else {
237         log.warn(`TimedPromise timed out after ${timeout} ms`, stack());
238         resolve();
239       }
240     };
242     timer.initWithCallback({ notify: bail }, timeout, TYPE_ONE_SHOT);
244     try {
245       fn(resolve, reject);
246     } catch (e) {
247       reject(e);
248     }
249   }).then(
250     res => {
251       timer.cancel();
252       return res;
253     },
254     err => {
255       timer.cancel();
256       throw err;
257     }
258   );
262  * Pauses for the given duration.
264  * @param {number} timeout
265  *     Duration to wait before fulfilling promise in milliseconds.
267  * @return {Promise}
268  *     Promise that fulfills when the `timeout` is elapsed.
270  * @throws {TypeError}
271  *     If `timeout` is not a number.
272  * @throws {RangeError}
273  *     If `timeout` is not an unsigned integer.
274  */
275 function Sleep(timeout) {
276   if (typeof timeout != "number") {
277     throw new TypeError();
278   }
279   return new TimedPromise(() => {}, { timeout, throws: null });
283  * Detects when the specified message manager has been destroyed.
285  * One can observe the removal and detachment of a content browser
286  * (`<xul:browser>`) or a chrome window by its message manager
287  * disconnecting.
289  * When a browser is associated with a tab, this is safer than only
290  * relying on the event `TabClose` which signalises the _intent to_
291  * remove a tab and consequently would lead to the destruction of
292  * the content browser and its browser message manager.
294  * When closing a chrome window it is safer than only relying on
295  * the event 'unload' which signalises the _intent to_ close the
296  * chrome window and consequently would lead to the destruction of
297  * the window and its window message manager.
299  * @param {MessageListenerManager} messageManager
300  *     The message manager to observe for its disconnect state.
301  *     Use the browser message manager when closing a content browser,
302  *     and the window message manager when closing a chrome window.
304  * @return {Promise}
305  *     A promise that resolves when the message manager has been destroyed.
306  */
307 function MessageManagerDestroyedPromise(messageManager) {
308   return new Promise(resolve => {
309     function observe(subject, topic) {
310       log.trace(`Received observer notification ${topic}`);
312       if (subject == messageManager) {
313         Services.obs.removeObserver(this, "message-manager-disconnect");
314         resolve();
315       }
316     }
318     Services.obs.addObserver(observe, "message-manager-disconnect");
319   });
323  * Throttle until the main thread is idle and `window` has performed
324  * an animation frame (in that order).
326  * @param {ChromeWindow} win
327  *     Window to request the animation frame from.
329  * @return Promise
330  */
331 function IdlePromise(win) {
332   return new Promise(resolve => {
333     Services.tm.idleDispatchToMainThread(() => {
334       win.requestAnimationFrame(resolve);
335     });
336   });
340  * Wraps a callback function, that, as long as it continues to be
341  * invoked, will not be triggered.  The given function will be
342  * called after the timeout duration is reached, after no more
343  * events fire.
345  * This class implements the {@link EventListener} interface,
346  * which means it can be used interchangably with `addEventHandler`.
348  * Debouncing events can be useful when dealing with e.g. DOM events
349  * that fire at a high rate.  It is generally advisable to avoid
350  * computationally expensive operations such as DOM modifications
351  * under these circumstances.
353  * One such high frequenecy event is `resize` that can fire multiple
354  * times before the window reaches its final dimensions.  In order
355  * to delay an operation until the window has completed resizing,
356  * it is possible to use this technique to only invoke the callback
357  * after the last event has fired::
359  *     let cb = new DebounceCallback(event => {
360  *       // fires after the final resize event
361  *       console.log("resize", event);
362  *     });
363  *     window.addEventListener("resize", cb);
365  * Note that it is not possible to use this synchronisation primitive
366  * with `addEventListener(..., {once: true})`.
368  * @param {function(Event)} fn
369  *     Callback function that is guaranteed to be invoked once only,
370  *     after `timeout`.
371  * @param {number=} [timeout = 250] timeout
372  *     Time since last event firing, before `fn` will be invoked.
373  */
374 class DebounceCallback {
375   constructor(fn, { timeout = 250 } = {}) {
376     if (typeof fn != "function" || typeof timeout != "number") {
377       throw new TypeError();
378     }
379     if (!Number.isInteger(timeout) || timeout < 0) {
380       throw new RangeError();
381     }
383     this.fn = fn;
384     this.timeout = timeout;
385     this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
386   }
388   handleEvent(ev) {
389     this.timer.cancel();
390     this.timer.initWithCallback(
391       () => {
392         this.timer.cancel();
393         this.fn(ev);
394       },
395       this.timeout,
396       TYPE_ONE_SHOT
397     );
398   }
400 this.DebounceCallback = DebounceCallback;
403  * Wait for an event to be fired on a specified element.
405  * This method has been duplicated from BrowserTestUtils.jsm.
407  * Because this function is intended for testing, any error in checkFn
408  * will cause the returned promise to be rejected instead of waiting for
409  * the next event, since this is probably a bug in the test.
411  * Usage::
413  *    let promiseEvent = waitForEvent(element, "eventName");
414  *    // Do some processing here that will cause the event to be fired
415  *    // ...
416  *    // Now wait until the Promise is fulfilled
417  *    let receivedEvent = await promiseEvent;
419  * The promise resolution/rejection handler for the returned promise is
420  * guaranteed not to be called until the next event tick after the event
421  * listener gets called, so that all other event listeners for the element
422  * are executed before the handler is executed::
424  *    let promiseEvent = waitForEvent(element, "eventName");
425  *    // Same event tick here.
426  *    await promiseEvent;
427  *    // Next event tick here.
429  * If some code, such like adding yet another event listener, needs to be
430  * executed in the same event tick, use raw addEventListener instead and
431  * place the code inside the event listener::
433  *    element.addEventListener("load", () => {
434  *      // Add yet another event listener in the same event tick as the load
435  *      // event listener.
436  *      p = waitForEvent(element, "ready");
437  *    }, { once: true });
439  * @param {Element} subject
440  *     The element that should receive the event.
441  * @param {string} eventName
442  *     Name of the event to listen to.
443  * @param {Object=} options
444  *     Extra options.
445  * @param {boolean=} options.capture
446  *     True to use a capturing listener.
447  * @param {function(Event)=} options.checkFn
448  *     Called with the ``Event`` object as argument, should return ``true``
449  *     if the event is the expected one, or ``false`` if it should be
450  *     ignored and listening should continue. If not specified, the first
451  *     event with the specified name resolves the returned promise.
452  * @param {boolean=} options.wantsUntrusted
453  *     True to receive synthetic events dispatched by web content.
455  * @return {Promise.<Event>}
456  *     Promise which resolves to the received ``Event`` object, or rejects
457  *     in case of a failure.
458  */
459 function waitForEvent(
460   subject,
461   eventName,
462   { capture = false, checkFn = null, wantsUntrusted = false } = {}
463 ) {
464   if (subject == null || !("addEventListener" in subject)) {
465     throw new TypeError();
466   }
467   if (typeof eventName != "string") {
468     throw new TypeError();
469   }
470   if (capture != null && typeof capture != "boolean") {
471     throw new TypeError();
472   }
473   if (checkFn != null && typeof checkFn != "function") {
474     throw new TypeError();
475   }
476   if (wantsUntrusted != null && typeof wantsUntrusted != "boolean") {
477     throw new TypeError();
478   }
480   return new Promise((resolve, reject) => {
481     subject.addEventListener(
482       eventName,
483       function listener(event) {
484         log.trace(`Received DOM event ${event.type} for ${event.target}`);
485         try {
486           if (checkFn && !checkFn(event)) {
487             return;
488           }
489           subject.removeEventListener(eventName, listener, capture);
490           executeSoon(() => resolve(event));
491         } catch (ex) {
492           try {
493             subject.removeEventListener(eventName, listener, capture);
494           } catch (ex2) {
495             // Maybe the provided object does not support removeEventListener.
496           }
497           executeSoon(() => reject(ex));
498         }
499       },
500       capture,
501       wantsUntrusted
502     );
503   });
507  * Wait for a message to be fired from a particular message manager.
509  * This method has been duplicated from BrowserTestUtils.jsm.
511  * @param {nsIMessageManager} messageManager
512  *     The message manager that should be used.
513  * @param {string} messageName
514  *     The message to wait for.
515  * @param {Object=} options
516  *     Extra options.
517  * @param {function(Message)=} options.checkFn
518  *     Called with the ``Message`` object as argument, should return ``true``
519  *     if the message is the expected one, or ``false`` if it should be
520  *     ignored and listening should continue. If not specified, the first
521  *     message with the specified name resolves the returned promise.
523  * @return {Promise.<Object>}
524  *     Promise which resolves to the data property of the received
525  *     ``Message``.
526  */
527 function waitForMessage(
528   messageManager,
529   messageName,
530   { checkFn = undefined } = {}
531 ) {
532   if (messageManager == null || !("addMessageListener" in messageManager)) {
533     throw new TypeError();
534   }
535   if (typeof messageName != "string") {
536     throw new TypeError();
537   }
538   if (checkFn && typeof checkFn != "function") {
539     throw new TypeError();
540   }
542   return new Promise(resolve => {
543     messageManager.addMessageListener(messageName, function onMessage(msg) {
544       log.trace(`Received ${messageName} for ${msg.target}`);
545       if (checkFn && !checkFn(msg)) {
546         return;
547       }
548       messageManager.removeMessageListener(messageName, onMessage);
549       resolve(msg.data);
550     });
551   });
555  * Wait for the specified observer topic to be observed.
557  * This method has been duplicated from TestUtils.jsm.
559  * Because this function is intended for testing, any error in checkFn
560  * will cause the returned promise to be rejected instead of waiting for
561  * the next notification, since this is probably a bug in the test.
563  * @param {string} topic
564  *     The topic to observe.
565  * @param {Object=} options
566  *     Extra options.
567  * @param {function(String,Object)=} options.checkFn
568  *     Called with ``subject``, and ``data`` as arguments, should return true
569  *     if the notification is the expected one, or false if it should be
570  *     ignored and listening should continue. If not specified, the first
571  *     notification for the specified topic resolves the returned promise.
573  * @return {Promise.<Array<String, Object>>}
574  *     Promise which resolves to an array of ``subject``, and ``data`` from
575  *     the observed notification.
576  */
577 function waitForObserverTopic(topic, { checkFn = null } = {}) {
578   if (typeof topic != "string") {
579     throw new TypeError();
580   }
581   if (checkFn != null && typeof checkFn != "function") {
582     throw new TypeError();
583   }
585   return new Promise((resolve, reject) => {
586     Services.obs.addObserver(function observer(subject, topic, data) {
587       log.trace(`Received observer notification ${topic}`);
588       try {
589         if (checkFn && !checkFn(subject, data)) {
590           return;
591         }
592         Services.obs.removeObserver(observer, topic);
593         resolve({ subject, data });
594       } catch (ex) {
595         Services.obs.removeObserver(observer, topic);
596         reject(ex);
597       }
598     }, topic);
599   });