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/. */
7 const { AppConstants } = ChromeUtils.import(
8 "resource://gre/modules/AppConstants.jsm"
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 = [
29 "MessageManagerDestroyedPromise",
35 "waitForObserverTopic",
38 const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
40 const PROMISE_TIMEOUT = AppConstants.DEBUG ? 4500 : 1500;
43 * Dispatch a function to be executed on the main thread.
45 * @param {function} func
46 * Function to be executed.
48 function executeSoon(func) {
49 if (typeof func != "function") {
50 throw new TypeError();
53 Services.tm.dispatchToMainThread(func);
59 * @param {function(*)} resolve
60 * To be called when the condition has been met. Will return the
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.
67 * The value from calling ``resolve``.
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.
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``.
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.
91 * let els = new PollPromise((resolve, reject) => {
92 * let res = document.querySelectorAll("p");
93 * if (res.length > 0) {
94 * resolve(Array.from(res));
98 * }, {timeout: 1000});
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.
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.
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();
127 if (timeout != null && typeof timeout != "number") {
128 throw new TypeError();
130 if (typeof interval != "number") {
131 throw new TypeError();
134 (timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
135 (!Number.isInteger(interval) || interval < 0)
137 throw new RangeError();
140 return new Promise((resolve, reject) => {
143 if (Number.isInteger(timeout)) {
144 start = new Date().getTime();
145 end = start + timeout;
150 .then(resolve, rejected => {
151 if (error.isError(rejected)) {
155 // return if there is a timeout and set to 0,
156 // allowing |func| to be evaluated at least once
158 typeof end != "undefined" &&
159 (start == end || new Date().getTime() >= end)
167 // the repeating slack timer waits |interval|
168 // before invoking |evalFn|
171 timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
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
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
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.<*>}
208 * @throws {TypeError}
209 * If `timeout` is not a number.
210 * @throws {RangeError}
211 * If `timeout` is not an unsigned integer.
213 function TimedPromise(
215 { timeout = PROMISE_TIMEOUT, throws = TimeoutError } = {}
217 const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
219 if (typeof fn != "function") {
220 throw new TypeError();
222 if (typeof timeout != "number") {
223 throw new TypeError();
225 if (!Number.isInteger(timeout) || timeout < 0) {
226 throw new RangeError();
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.
233 if (throws !== null) {
234 let err = new throws();
237 log.warn(`TimedPromise timed out after ${timeout} ms`, stack());
242 timer.initWithCallback({ notify: bail }, timeout, TYPE_ONE_SHOT);
262 * Pauses for the given duration.
264 * @param {number} timeout
265 * Duration to wait before fulfilling promise in milliseconds.
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.
275 function Sleep(timeout) {
276 if (typeof timeout != "number") {
277 throw new TypeError();
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
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.
305 * A promise that resolves when the message manager has been destroyed.
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");
318 Services.obs.addObserver(observe, "message-manager-disconnect");
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.
331 function IdlePromise(win) {
332 return new Promise(resolve => {
333 Services.tm.idleDispatchToMainThread(() => {
334 win.requestAnimationFrame(resolve);
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
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);
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,
371 * @param {number=} [timeout = 250] timeout
372 * Time since last event firing, before `fn` will be invoked.
374 class DebounceCallback {
375 constructor(fn, { timeout = 250 } = {}) {
376 if (typeof fn != "function" || typeof timeout != "number") {
377 throw new TypeError();
379 if (!Number.isInteger(timeout) || timeout < 0) {
380 throw new RangeError();
384 this.timeout = timeout;
385 this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
390 this.timer.initWithCallback(
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.
413 * let promiseEvent = waitForEvent(element, "eventName");
414 * // Do some processing here that will cause the event to be fired
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
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
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.
459 function waitForEvent(
462 { capture = false, checkFn = null, wantsUntrusted = false } = {}
464 if (subject == null || !("addEventListener" in subject)) {
465 throw new TypeError();
467 if (typeof eventName != "string") {
468 throw new TypeError();
470 if (capture != null && typeof capture != "boolean") {
471 throw new TypeError();
473 if (checkFn != null && typeof checkFn != "function") {
474 throw new TypeError();
476 if (wantsUntrusted != null && typeof wantsUntrusted != "boolean") {
477 throw new TypeError();
480 return new Promise((resolve, reject) => {
481 subject.addEventListener(
483 function listener(event) {
484 log.trace(`Received DOM event ${event.type} for ${event.target}`);
486 if (checkFn && !checkFn(event)) {
489 subject.removeEventListener(eventName, listener, capture);
490 executeSoon(() => resolve(event));
493 subject.removeEventListener(eventName, listener, capture);
495 // Maybe the provided object does not support removeEventListener.
497 executeSoon(() => reject(ex));
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
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
527 function waitForMessage(
530 { checkFn = undefined } = {}
532 if (messageManager == null || !("addMessageListener" in messageManager)) {
533 throw new TypeError();
535 if (typeof messageName != "string") {
536 throw new TypeError();
538 if (checkFn && typeof checkFn != "function") {
539 throw new TypeError();
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)) {
548 messageManager.removeMessageListener(messageName, onMessage);
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
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.
577 function waitForObserverTopic(topic, { checkFn = null } = {}) {
578 if (typeof topic != "string") {
579 throw new TypeError();
581 if (checkFn != null && typeof checkFn != "function") {
582 throw new TypeError();
585 return new Promise((resolve, reject) => {
586 Services.obs.addObserver(function observer(subject, topic, data) {
587 log.trace(`Received observer notification ${topic}`);
589 if (checkFn && !checkFn(subject, data)) {
592 Services.obs.removeObserver(observer, topic);
593 resolve({ subject, data });
595 Services.obs.removeObserver(observer, topic);