diff options
Diffstat (limited to 'testing/modules/TestUtils.jsm')
-rw-r--r-- | testing/modules/TestUtils.jsm | 209 |
1 files changed, 209 insertions, 0 deletions
diff --git a/testing/modules/TestUtils.jsm b/testing/modules/TestUtils.jsm new file mode 100644 index 0000000000..3334982b06 --- /dev/null +++ b/testing/modules/TestUtils.jsm @@ -0,0 +1,209 @@ +/* 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/. */ + +/** + * Contains a limited number of testing functions that are commonly used in a + * wide variety of situations, for example waiting for an event loop tick or an + * observer notification. + * + * More complex functions are likely to belong to a separate test-only module. + * Examples include Assert.jsm for generic assertions, FileTestUtils.jsm to work + * with local files and their contents, and BrowserTestUtils.jsm to work with + * browser windows and tabs. + * + * Individual components also offer testing functions to other components, for + * example LoginTestUtils.jsm. + */ + +"use strict"; + +var EXPORTED_SYMBOLS = ["TestUtils"]; + +const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm"); +const { setTimeout } = ChromeUtils.import("resource://gre/modules/Timer.jsm"); + +var TestUtils = { + executeSoon(callbackFn) { + Services.tm.dispatchToMainThread(callbackFn); + }, + + waitForTick() { + return new Promise(resolve => this.executeSoon(resolve)); + }, + + /** + * Waits for the specified topic to be observed. + * + * @param {string} topic + * The topic to observe. + * @param {function} checkFn [optional] + * Called with (subject, 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. + * + * @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 notification, since this is probably a bug in the test. + * + * @return {Promise} + * @resolves The array [subject, data] from the observed notification. + */ + topicObserved(topic, checkFn) { + return new Promise((resolve, reject) => { + Services.obs.addObserver(function observer(subject, topic, data) { + try { + if (checkFn && !checkFn(subject, data)) { + return; + } + Services.obs.removeObserver(observer, topic); + resolve([subject, data]); + } catch (ex) { + Services.obs.removeObserver(observer, topic); + reject(ex); + } + }, topic); + }); + }, + + /** + * Waits for the specified preference to be change. + * + * @param {string} prefName + * The pref to observe. + * @param {function} checkFn [optional] + * Called with the new preference value as argument, 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. + * + * @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 notification, since this is probably a bug in the test. + * + * @return {Promise} + * @resolves The value of the preference. + */ + waitForPrefChange(prefName, checkFn) { + return new Promise((resolve, reject) => { + Services.prefs.addObserver(prefName, function observer( + subject, + topic, + data + ) { + try { + let prefValue = null; + switch (Services.prefs.getPrefType(prefName)) { + case Services.prefs.PREF_STRING: + prefValue = Services.prefs.getStringPref(prefName); + break; + case Services.prefs.PREF_INT: + prefValue = Services.prefs.getIntPref(prefName); + break; + case Services.prefs.PREF_BOOL: + prefValue = Services.prefs.getBoolPref(prefName); + break; + } + if (checkFn && !checkFn(prefValue)) { + return; + } + Services.prefs.removeObserver(prefName, observer); + resolve(prefValue); + } catch (ex) { + Services.prefs.removeObserver(prefName, observer); + reject(ex); + } + }); + }); + }, + + /** + * Takes a screenshot of an area and returns it as a data URL. + * + * @param eltOrRect + * The DOM node or rect ({left, top, width, height}) to screenshot. + * @param win + * The current window. + */ + screenshotArea(eltOrRect, win) { + if (Element.isInstance(eltOrRect)) { + eltOrRect = eltOrRect.getBoundingClientRect(); + } + let { left, top, width, height } = eltOrRect; + let canvas = win.document.createElementNS( + "http://www.w3.org/1999/xhtml", + "canvas" + ); + let ctx = canvas.getContext("2d"); + let ratio = win.devicePixelRatio; + canvas.width = width * ratio; + canvas.height = height * ratio; + ctx.scale(ratio, ratio); + ctx.drawWindow(win, left, top, width, height, "#fff"); + return canvas.toDataURL(); + }, + + /** + * 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. The + * function can be an async function. + * @param interval + * The time interval to poll the condition function. Defaults + * to 100ms. + * @param attempts + * 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 with the return value of the condition function. + * Rejects if timeout is exceeded or condition ever throws. + * + * NOTE: This is intentionally not using setInterval, using setTimeout + * instead. setInterval is not promise-safe. + */ + waitForCondition(condition, msg, interval = 100, maxTries = 50) { + return new Promise((resolve, reject) => { + let tries = 0; + async function tryOnce() { + if (tries >= maxTries) { + msg += ` - timed out after ${maxTries} tries.`; + reject(msg); + return; + } + + let conditionPassed = false; + try { + conditionPassed = await condition(); + } catch (e) { + msg += ` - threw exception: ${e}`; + reject(msg); + return; + } + + if (conditionPassed) { + resolve(conditionPassed); + return; + } + tries++; + setTimeout(tryOnce, interval); + } + + // FIXME(bug 1596165): This could be a direct call, ideally. + setTimeout(tryOnce, interval); + }); + }, + + shuffle(array) { + let results = []; + for (let i = 0; i < array.length; ++i) { + let randomIndex = Math.floor(Math.random() * (i + 1)); + results[i] = results[randomIndex]; + results[randomIndex] = array[i]; + } + return results; + }, +}; |