summaryrefslogtreecommitdiffstats
path: root/testing/modules/TestUtils.jsm
diff options
context:
space:
mode:
Diffstat (limited to 'testing/modules/TestUtils.jsm')
-rw-r--r--testing/modules/TestUtils.jsm209
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;
+ },
+};