407 lines
13 KiB
JavaScript
407 lines
13 KiB
JavaScript
/* This Source Code Form is subject to the terms of the Mozilla Public
|
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
error: "chrome://remote/content/shared/webdriver/Errors.sys.mjs",
|
|
Log: "chrome://remote/content/shared/Log.sys.mjs",
|
|
});
|
|
|
|
ChromeUtils.defineLazyGetter(lazy, "logger", () =>
|
|
lazy.Log.get(lazy.Log.TYPES.MARIONETTE)
|
|
);
|
|
|
|
const { TYPE_ONE_SHOT, TYPE_REPEATING_SLACK } = Ci.nsITimer;
|
|
|
|
/**
|
|
* Runs a Promise-like function off the main thread until it is resolved
|
|
* through ``resolve`` or ``rejected`` callbacks. The function is
|
|
* guaranteed to be run at least once, irregardless of the timeout.
|
|
*
|
|
* The ``func`` is evaluated every ``interval`` for as long as its
|
|
* runtime duration does not exceed ``interval``. Evaluations occur
|
|
* sequentially, meaning that evaluations of ``func`` are queued if
|
|
* the runtime evaluation duration of ``func`` is greater than ``interval``.
|
|
*
|
|
* ``func`` is given two arguments, ``resolve`` and ``reject``,
|
|
* of which one must be called for the evaluation to complete.
|
|
* Calling ``resolve`` with an argument indicates that the expected
|
|
* wait condition was met and will return the passed value to the
|
|
* caller. Conversely, calling ``reject`` will evaluate ``func``
|
|
* again until the ``timeout`` duration has elapsed or ``func`` throws.
|
|
* The passed value to ``reject`` will also be returned to the caller
|
|
* once the wait has expired.
|
|
*
|
|
* Usage::
|
|
*
|
|
* let els = new PollPromise((resolve, reject) => {
|
|
* let res = document.querySelectorAll("p");
|
|
* if (res.length > 0) {
|
|
* resolve(Array.from(res));
|
|
* } else {
|
|
* reject([]);
|
|
* }
|
|
* }, {timeout: 1000});
|
|
*
|
|
* @param {Condition} func
|
|
* Function to run off the main thread.
|
|
* @param {object=} options
|
|
* @param {number=} options.timeout
|
|
* Desired timeout if wanted. If 0 or less than the runtime evaluation
|
|
* time of ``func``, ``func`` is guaranteed to run at least once.
|
|
* Defaults to using no timeout.
|
|
* @param {number=} options.interval
|
|
* Duration between each poll of ``func`` in milliseconds.
|
|
* Defaults to 10 milliseconds.
|
|
*
|
|
* @returns {Promise.<*>}
|
|
* Yields the value passed to ``func``'s
|
|
* ``resolve`` or ``reject`` callbacks.
|
|
*
|
|
* @throws {*}
|
|
* If ``func`` throws, its error is propagated.
|
|
* @throws {TypeError}
|
|
* If `timeout` or `interval`` are not numbers.
|
|
* @throws {RangeError}
|
|
* If `timeout` or `interval` are not unsigned integers.
|
|
*/
|
|
export function PollPromise(func, { timeout = null, interval = 10 } = {}) {
|
|
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
|
|
if (typeof func != "function") {
|
|
throw new TypeError();
|
|
}
|
|
if (timeout != null && typeof timeout != "number") {
|
|
throw new TypeError();
|
|
}
|
|
if (typeof interval != "number") {
|
|
throw new TypeError();
|
|
}
|
|
if (
|
|
(timeout && (!Number.isInteger(timeout) || timeout < 0)) ||
|
|
!Number.isInteger(interval) ||
|
|
interval < 0
|
|
) {
|
|
throw new RangeError();
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let start, end;
|
|
|
|
if (Number.isInteger(timeout)) {
|
|
start = new Date().getTime();
|
|
end = start + timeout;
|
|
}
|
|
|
|
let evalFn = () => {
|
|
new Promise(func)
|
|
.then(resolve, rejected => {
|
|
if (lazy.error.isError(rejected)) {
|
|
throw rejected;
|
|
}
|
|
|
|
// return if there is a timeout and set to 0,
|
|
// allowing |func| to be evaluated at least once
|
|
if (
|
|
typeof end != "undefined" &&
|
|
(start == end || new Date().getTime() >= end)
|
|
) {
|
|
resolve(rejected);
|
|
}
|
|
})
|
|
.catch(reject);
|
|
};
|
|
|
|
// the repeating slack timer waits |interval|
|
|
// before invoking |evalFn|
|
|
evalFn();
|
|
|
|
timer.init(evalFn, interval, TYPE_REPEATING_SLACK);
|
|
}).then(
|
|
res => {
|
|
timer.cancel();
|
|
return res;
|
|
},
|
|
err => {
|
|
timer.cancel();
|
|
throw err;
|
|
}
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Pauses for the given duration.
|
|
*
|
|
* @param {number} timeout
|
|
* Duration to wait before fulfilling promise in milliseconds.
|
|
*
|
|
* @returns {Promise}
|
|
* Promise that fulfills when the `timeout` is elapsed.
|
|
*
|
|
* @throws {TypeError}
|
|
* If `timeout` is not a number.
|
|
* @throws {RangeError}
|
|
* If `timeout` is not an unsigned integer.
|
|
*/
|
|
export function Sleep(timeout) {
|
|
if (typeof timeout != "number") {
|
|
throw new TypeError();
|
|
}
|
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
throw new RangeError();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
const timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.init(
|
|
() => {
|
|
// Bug 1663880 - Explicitely cancel the timer for now to prevent a hang
|
|
timer.cancel();
|
|
resolve();
|
|
},
|
|
timeout,
|
|
TYPE_ONE_SHOT
|
|
);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Detects when the specified message manager has been destroyed.
|
|
*
|
|
* One can observe the removal and detachment of a content browser
|
|
* (`<xul:browser>`) or a chrome window by its message manager
|
|
* disconnecting.
|
|
*
|
|
* When a browser is associated with a tab, this is safer than only
|
|
* relying on the event `TabClose` which signalises the _intent to_
|
|
* remove a tab and consequently would lead to the destruction of
|
|
* the content browser and its browser message manager.
|
|
*
|
|
* When closing a chrome window it is safer than only relying on
|
|
* the event 'unload' which signalises the _intent to_ close the
|
|
* chrome window and consequently would lead to the destruction of
|
|
* the window and its window message manager.
|
|
*
|
|
* @param {MessageListenerManager} messageManager
|
|
* The message manager to observe for its disconnect state.
|
|
* Use the browser message manager when closing a content browser,
|
|
* and the window message manager when closing a chrome window.
|
|
*
|
|
* @returns {Promise}
|
|
* A promise that resolves when the message manager has been destroyed.
|
|
*/
|
|
export function MessageManagerDestroyedPromise(messageManager) {
|
|
return new Promise(resolve => {
|
|
function observe(subject, topic) {
|
|
lazy.logger.trace(`Received observer notification ${topic}`);
|
|
|
|
if (subject == messageManager) {
|
|
Services.obs.removeObserver(this, "message-manager-disconnect");
|
|
resolve();
|
|
}
|
|
}
|
|
|
|
Services.obs.addObserver(observe, "message-manager-disconnect");
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wraps a callback function, that, as long as it continues to be
|
|
* invoked, will not be triggered. The given function will be
|
|
* called after the timeout duration is reached, after no more
|
|
* events fire.
|
|
*
|
|
* This class implements the {@link EventListener} interface,
|
|
* which means it can be used interchangably with `addEventHandler`.
|
|
*
|
|
* Debouncing events can be useful when dealing with e.g. DOM events
|
|
* that fire at a high rate. It is generally advisable to avoid
|
|
* computationally expensive operations such as DOM modifications
|
|
* under these circumstances.
|
|
*
|
|
* One such high frequenecy event is `resize` that can fire multiple
|
|
* times before the window reaches its final dimensions. In order
|
|
* to delay an operation until the window has completed resizing,
|
|
* it is possible to use this technique to only invoke the callback
|
|
* after the last event has fired::
|
|
*
|
|
* let cb = new DebounceCallback(event => {
|
|
* // fires after the final resize event
|
|
* console.log("resize", event);
|
|
* });
|
|
* window.addEventListener("resize", cb);
|
|
*
|
|
* Note that it is not possible to use this synchronisation primitive
|
|
* with `addEventListener(..., {once: true})`.
|
|
*
|
|
* @param {function(Event): void} fn
|
|
* Callback function that is guaranteed to be invoked once only,
|
|
* after `timeout`.
|
|
* @param {number=} [timeout = 250] timeout
|
|
* Time since last event firing, before `fn` will be invoked.
|
|
*/
|
|
export class DebounceCallback {
|
|
constructor(fn, { timeout = 250 } = {}) {
|
|
if (typeof fn != "function" || typeof timeout != "number") {
|
|
throw new TypeError();
|
|
}
|
|
if (!Number.isInteger(timeout) || timeout < 0) {
|
|
throw new RangeError();
|
|
}
|
|
|
|
this.fn = fn;
|
|
this.timeout = timeout;
|
|
this.timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
}
|
|
|
|
handleEvent(ev) {
|
|
this.timer.cancel();
|
|
this.timer.initWithCallback(
|
|
() => {
|
|
this.timer.cancel();
|
|
this.fn(ev);
|
|
},
|
|
this.timeout,
|
|
TYPE_ONE_SHOT
|
|
);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Wait for a message to be fired from a particular message manager.
|
|
*
|
|
* This method has been duplicated from BrowserTestUtils.sys.mjs.
|
|
*
|
|
* @param {nsIMessageManager} messageManager
|
|
* The message manager that should be used.
|
|
* @param {string} messageName
|
|
* The message to wait for.
|
|
* @param {object=} options
|
|
* Extra options.
|
|
* @param {function(Message): boolean=} options.checkFn
|
|
* Called with the ``Message`` object as argument, should return ``true``
|
|
* if the message is the expected one, or ``false`` if it should be
|
|
* ignored and listening should continue. If not specified, the first
|
|
* message with the specified name resolves the returned promise.
|
|
*
|
|
* @returns {Promise.<object>}
|
|
* Promise which resolves to the data property of the received
|
|
* ``Message``.
|
|
*/
|
|
export function waitForMessage(
|
|
messageManager,
|
|
messageName,
|
|
{ checkFn = undefined } = {}
|
|
) {
|
|
if (messageManager == null || !("addMessageListener" in messageManager)) {
|
|
throw new TypeError();
|
|
}
|
|
if (typeof messageName != "string") {
|
|
throw new TypeError();
|
|
}
|
|
if (checkFn && typeof checkFn != "function") {
|
|
throw new TypeError();
|
|
}
|
|
|
|
return new Promise(resolve => {
|
|
messageManager.addMessageListener(messageName, function onMessage(msg) {
|
|
lazy.logger.trace(`Received ${messageName} for ${msg.target}`);
|
|
if (checkFn && !checkFn(msg)) {
|
|
return;
|
|
}
|
|
messageManager.removeMessageListener(messageName, onMessage);
|
|
resolve(msg.data);
|
|
});
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wait for the specified observer topic to be observed.
|
|
*
|
|
* This method has been duplicated from TestUtils.sys.mjs.
|
|
*
|
|
* Because this function is intended for testing, any error in checkFn
|
|
* will cause the returned promise to be rejected instead of waiting for
|
|
* the next notification, since this is probably a bug in the test.
|
|
*
|
|
* @param {string} topic
|
|
* The topic to observe.
|
|
* @param {object=} options
|
|
* Extra options.
|
|
* @param {function(string, object): boolean=} options.checkFn
|
|
* Called with ``subject``, and ``data`` as arguments, should return true
|
|
* if the notification is the expected one, or false if it should be
|
|
* ignored and listening should continue. If not specified, the first
|
|
* notification for the specified topic resolves the returned promise.
|
|
* @param {number=} options.timeout
|
|
* Timeout duration in milliseconds, if provided.
|
|
* If specified, then the returned promise will be rejected with
|
|
* TimeoutError, if not already resolved, after this duration has elapsed.
|
|
* If not specified, then no timeout is used. Defaults to null.
|
|
*
|
|
* @returns {Promise.<Array<string, object>>}
|
|
* Promise which is either resolved to an array of ``subject``, and ``data``
|
|
* from the observed notification, or rejected with TimeoutError after
|
|
* options.timeout milliseconds if specified.
|
|
*
|
|
* @throws {TypeError}
|
|
* @throws {RangeError}
|
|
*/
|
|
export function waitForObserverTopic(topic, options = {}) {
|
|
const { checkFn = null, timeout = null } = options;
|
|
if (typeof topic != "string") {
|
|
throw new TypeError();
|
|
}
|
|
if (
|
|
(checkFn != null && typeof checkFn != "function") ||
|
|
(timeout !== null && typeof timeout != "number")
|
|
) {
|
|
throw new TypeError();
|
|
}
|
|
if (timeout && (!Number.isInteger(timeout) || timeout < 0)) {
|
|
throw new RangeError();
|
|
}
|
|
|
|
return new Promise((resolve, reject) => {
|
|
let timer;
|
|
|
|
function cleanUp() {
|
|
Services.obs.removeObserver(observer, topic);
|
|
timer?.cancel();
|
|
}
|
|
|
|
function observer(subject, _topic, data) {
|
|
lazy.logger.trace(`Received observer notification ${_topic}`);
|
|
try {
|
|
if (checkFn && !checkFn(subject, data)) {
|
|
return;
|
|
}
|
|
cleanUp();
|
|
resolve({ subject, data });
|
|
} catch (ex) {
|
|
cleanUp();
|
|
reject(ex);
|
|
}
|
|
}
|
|
|
|
Services.obs.addObserver(observer, topic);
|
|
|
|
if (timeout !== null) {
|
|
timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
|
timer.init(
|
|
() => {
|
|
cleanUp();
|
|
reject(
|
|
new lazy.error.TimeoutError(
|
|
`waitForObserverTopic timed out after ${timeout} ms`
|
|
)
|
|
);
|
|
},
|
|
timeout,
|
|
TYPE_ONE_SHOT
|
|
);
|
|
}
|
|
});
|
|
}
|