Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
/*
* This module implements a number of utility functions that can be loaded
* into content scope.
*
* All asynchronous helper methods should return promises, rather than being
* callback based.
*/
// Disable ownerGlobal use since that's not available on content-privileged elements.
/* eslint-disable mozilla/use-ownerGlobal */
import { setTimeout } from "resource://gre/modules/Timer.sys.mjs";
export var ContentTaskUtils = {
/**
* Checks if a DOM element is hidden.
*
* @param {Element} element
* The element which is to be checked.
*
* @return {boolean}
*/
isHidden(element) {
let style = element.ownerDocument.defaultView.getComputedStyle(element);
if (style.display == "none") {
return true;
}
if (style.visibility != "visible") {
return true;
}
// Hiding a parent element will hide all its children
if (
element.parentNode != element.ownerDocument &&
element.parentNode.nodeType != Node.DOCUMENT_FRAGMENT_NODE
) {
return ContentTaskUtils.isHidden(element.parentNode);
}
// Walk up the shadow DOM if we've reached the top of the shadow root
if (element.parentNode.host) {
return ContentTaskUtils.isHidden(element.parentNode.host);
}
return false;
},
/**
* Checks if a DOM element is visible.
*
* @param {Element} element
* The element which is to be checked.
*
* @return {boolean}
*/
isVisible(element) {
return !this.isHidden(element);
},
/**
* Will poll a condition function until it returns true.
*
* @param condition
* A condition function that must return true or false. If the
* condition ever throws, this is also treated as a false.
* @param msg
* The message to use when the returned promise is rejected.
* This message will be extended with additional information
* about the number of tries or the thrown exception.
* @param interval
* The time interval to poll the condition function. Defaults
* to 100ms.
* @param maxTries
* The number of times to poll before giving up and rejecting
* if the condition has not yet returned true. Defaults to 50
* (~5 seconds for 100ms intervals)
* @return Promise
* Resolves when condition is true.
* Rejects if timeout is exceeded or condition ever throws.
*/
async waitForCondition(condition, msg, interval = 100, maxTries = 50) {
let startTime = Cu.now();
for (let tries = 0; tries < maxTries; ++tries) {
await new Promise(resolve => setTimeout(resolve, interval));
let conditionPassed = false;
try {
conditionPassed = await condition();
} catch (e) {
msg += ` - threw exception: ${e}`;
ChromeUtils.addProfilerMarker(
"ContentTaskUtils",
{ startTime, category: "Test" },
`waitForCondition - ${msg}`
);
throw msg;
}
if (conditionPassed) {
ChromeUtils.addProfilerMarker(
"ContentTaskUtils",
{ startTime, category: "Test" },
`waitForCondition succeeded after ${tries} retries - ${msg}`
);
return conditionPassed;
}
}
msg += ` - timed out after ${maxTries} tries.`;
ChromeUtils.addProfilerMarker(
"ContentTaskUtils",
{ startTime, category: "Test" },
`waitForCondition - ${msg}`
);
throw msg;
},
/**
* Waits for an event to be fired on a specified element.
*
* Usage:
* let promiseEvent = ContentTasKUtils.waitForEvent(element, "eventName");
* // Do some processing here that will cause the event to be fired
* // ...
* // Now yield until the Promise is fulfilled
* let receivedEvent = yield promiseEvent;
*
* @param {Element} subject
* The element that should receive the event.
* @param {string} eventName
* Name of the event to listen to.
* @param {bool} capture [optional]
* True to use a capturing listener.
* @param {function} checkFn [optional]
* Called with the Event object as argument, should return true if the
* event is the expected one, or false if it should be ignored and
* listening should continue. If not specified, the first event with
* the specified name resolves the returned promise.
*
* @note 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 event, since this is probably a bug in the test.
*
* @returns {Promise}
* @resolves The Event object.
*/
waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted = false) {
return new Promise((resolve, reject) => {
let startTime = Cu.now();
subject.addEventListener(
eventName,
function listener(event) {
try {
if (checkFn && !checkFn(event)) {
return;
}
subject.removeEventListener(eventName, listener, capture);
setTimeout(() => {
ChromeUtils.addProfilerMarker(
"ContentTaskUtils",
{ category: "Test", startTime },
"waitForEvent - " + eventName
);
resolve(event);
}, 0);
} catch (ex) {
try {
subject.removeEventListener(eventName, listener, capture);
} catch (ex2) {
// Maybe the provided object does not support removeEventListener.
}
setTimeout(() => reject(ex), 0);
}
},
capture,
wantsUntrusted
);
});
},
/**
* Wait until DOM mutations cause the condition expressed in checkFn to pass.
* Intended as an easy-to-use alternative to waitForCondition.
*
* @param {Element} subject
* The element on which to observe mutations.
* @param {Object} options
* The options to pass to MutationObserver.observe();
* @param {function} checkFn [optional]
* Function that returns true when it wants the promise to be resolved.
* If not specified, the first mutation will resolve the promise.
*
* @returns {Promise<void>}
*/
waitForMutationCondition(subject, options, checkFn) {
if (checkFn?.()) {
return Promise.resolve();
}
return new Promise(resolve => {
let obs = new subject.ownerGlobal.MutationObserver(function () {
if (checkFn && !checkFn()) {
return;
}
obs.disconnect();
resolve();
});
obs.observe(subject, options);
});
},
/**
* Gets an instance of the `EventUtils` helper module for usage in
*
* @param content
* The `content` global object from your content task.
*
* @returns an EventUtils instance.
*/
getEventUtils(content) {
if (content._EventUtils) {
return content._EventUtils;
}
let EventUtils = (content._EventUtils = {});
EventUtils.window = {};
EventUtils.setTimeout = setTimeout;
EventUtils.parent = EventUtils.window;
/* eslint-disable camelcase */
EventUtils._EU_Ci = Ci;
EventUtils._EU_Cc = Cc;
/* eslint-enable camelcase */
// EventUtils' `sendChar` function relies on the navigator to synthetize events.
EventUtils.navigator = content.navigator;
EventUtils.KeyboardEvent = content.KeyboardEvent;
Services.scriptloader.loadSubScript(
EventUtils
);
return EventUtils;
},
};