diff options
Diffstat (limited to '')
-rw-r--r-- | testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs | 249 |
1 files changed, 249 insertions, 0 deletions
diff --git a/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs new file mode 100644 index 0000000000..f1ff280db1 --- /dev/null +++ b/testing/mochitest/BrowserTestUtils/ContentTaskUtils.sys.mjs @@ -0,0 +1,249 @@ +/* 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} + */ + is_hidden(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.is_hidden(element.parentNode); + } + + // Walk up the shadow DOM if we've reached the top of the shadow root + if (element.parentNode.host) { + return ContentTaskUtils.is_hidden(element.parentNode.host); + } + + return false; + }, + + /** + * Checks if a DOM element is visible. + * + * @param {Element} element + * The element which is to be checked. + * + * @return {boolean} + */ + is_visible(element) { + return !this.is_hidden(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 + * content tasks. See https://searchfox.org/mozilla-central/source/testing/mochitest/tests/SimpleTest/EventUtils.js + * + * @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( + "chrome://mochikit/content/tests/SimpleTest/EventUtils.js", + EventUtils + ); + + return EventUtils; + }, +}; |