1 /* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
6 * Contains a limited number of testing functions that are commonly used in a
7 * wide variety of situations, for example waiting for an event loop tick or an
8 * observer notification.
10 * More complex functions are likely to belong to a separate test-only module.
11 * Examples include Assert.sys.mjs for generic assertions, FileTestUtils.sys.mjs
12 * to work with local files and their contents, and BrowserTestUtils.sys.mjs to
13 * work with browser windows and tabs.
15 * Individual components also offer testing functions to other components, for
16 * example LoginTestUtils.sys.mjs.
19 import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
21 const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
22 Ci.nsIConsoleAPIStorage
26 * TestUtils provides generally useful test utilities.
27 * It can be used from mochitests, browser mochitests and xpcshell tests alike.
31 export var TestUtils = {
32 executeSoon(callbackFn) {
33 Services.tm.dispatchToMainThread(callbackFn);
37 return new Promise(resolve => this.executeSoon(resolve));
41 * Waits for a console message matching the specified check function to be
44 * @param {function} checkFn [optional]
45 * Called with the message as its argument, should return true if the
46 * notification is the expected one, or false if it should be ignored
47 * and listening should continue.
49 * @note Because this function is intended for testing, any error in checkFn
50 * will cause the returned promise to be rejected instead of waiting for
51 * the next notification, since this is probably a bug in the test.
54 * @resolves The message from the observed notification.
56 consoleMessageObserved(checkFn) {
57 return new Promise((resolve, reject) => {
59 function observe(message) {
61 if (checkFn && !checkFn(message)) {
64 ConsoleAPIStorage.removeLogEventListener(observe);
65 // checkFn could reference objects that need to be destroyed before
66 // the end of the test, so avoid keeping a reference to it after the
73 ConsoleAPIStorage.removeLogEventListener(observe);
80 ConsoleAPIStorage.addLogEventListener(
82 Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
85 TestUtils.promiseTestFinished?.then(() => {
90 ConsoleAPIStorage.removeLogEventListener(observe);
92 "Console message observer not removed before the end of test";
99 * Listens for any console messages (logged via console.*) and returns them
100 * when the returned function is called.
102 * @returns {function}
103 * Returns an async function that when called will wait for a tick, then stop
104 * listening to any more console messages and finally will return the
105 * messages that have been received.
107 listenForConsoleMessages() {
109 function observe(message) {
110 messages.push(message);
113 ConsoleAPIStorage.addLogEventListener(
115 Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
119 await TestUtils.waitForTick();
120 ConsoleAPIStorage.removeLogEventListener(observe);
126 * Waits for the specified topic to be observed.
128 * @param {string} topic
129 * The topic to observe.
130 * @param {function} checkFn [optional]
131 * Called with (subject, data) as arguments, should return true if the
132 * notification is the expected one, or false if it should be ignored
133 * and listening should continue. If not specified, the first
134 * notification for the specified topic resolves the returned promise.
136 * @note Because this function is intended for testing, any error in checkFn
137 * will cause the returned promise to be rejected instead of waiting for
138 * the next notification, since this is probably a bug in the test.
141 * @resolves The array [subject, data] from the observed notification.
143 topicObserved(topic, checkFn) {
144 let startTime = Cu.now();
145 return new Promise((resolve, reject) => {
147 function observer(subject, topic, data) {
149 if (checkFn && !checkFn(subject, data)) {
152 Services.obs.removeObserver(observer, topic);
153 // checkFn could reference objects that need to be destroyed before
154 // the end of the test, so avoid keeping a reference to it after the
158 ChromeUtils.addProfilerMarker(
160 { startTime, category: "Test" },
161 "topicObserved: " + topic
163 resolve([subject, data]);
165 Services.obs.removeObserver(observer, topic);
171 Services.obs.addObserver(observer, topic);
173 TestUtils.promiseTestFinished?.then(() => {
178 Services.obs.removeObserver(observer, topic);
179 let text = topic + " observer not removed before the end of test";
181 ChromeUtils.addProfilerMarker(
183 { startTime, category: "Test" },
184 "topicObserved: " + text
191 * Waits for the specified preference to be change.
193 * @param {string} prefName
194 * The pref to observe.
195 * @param {function} checkFn [optional]
196 * Called with the new preference value as argument, should return true if the
197 * notification is the expected one, or false if it should be ignored
198 * and listening should continue. If not specified, the first
199 * notification for the specified topic resolves the returned promise.
201 * @note Because this function is intended for testing, any error in checkFn
202 * will cause the returned promise to be rejected instead of waiting for
203 * the next notification, since this is probably a bug in the test.
206 * @resolves The value of the preference.
208 waitForPrefChange(prefName, checkFn) {
209 return new Promise((resolve, reject) => {
210 Services.prefs.addObserver(prefName, function observer() {
212 let prefValue = null;
213 switch (Services.prefs.getPrefType(prefName)) {
214 case Services.prefs.PREF_STRING:
215 prefValue = Services.prefs.getStringPref(prefName);
217 case Services.prefs.PREF_INT:
218 prefValue = Services.prefs.getIntPref(prefName);
220 case Services.prefs.PREF_BOOL:
221 prefValue = Services.prefs.getBoolPref(prefName);
224 if (checkFn && !checkFn(prefValue)) {
227 Services.prefs.removeObserver(prefName, observer);
230 Services.prefs.removeObserver(prefName, observer);
238 * Takes a screenshot of an area and returns it as a data URL.
240 * @param eltOrRect {Element|Rect}
241 * The DOM node or rect ({left, top, width, height}) to screenshot.
242 * @param win {Window}
243 * The current window.
245 screenshotArea(eltOrRect, win) {
246 if (Element.isInstance(eltOrRect)) {
247 eltOrRect = eltOrRect.getBoundingClientRect();
249 let { left, top, width, height } = eltOrRect;
250 let canvas = win.document.createElementNS(
251 "http://www.w3.org/1999/xhtml",
254 let ctx = canvas.getContext("2d");
255 let ratio = win.devicePixelRatio;
256 canvas.width = width * ratio;
257 canvas.height = height * ratio;
258 ctx.scale(ratio, ratio);
259 ctx.drawWindow(win, left, top, width, height, "#fff");
260 return canvas.toDataURL();
264 * Will poll a condition function until it returns true.
267 * A condition function that must return true or false. If the
268 * condition ever throws, this function fails and rejects the
269 * returned promise. The function can be an async function.
271 * A message used to describe the condition being waited for.
272 * This message will be used to reject the promise should the
273 * wait fail. It is also used to add a profiler marker.
275 * The time interval to poll the condition function. Defaults
278 * The number of times to poll before giving up and rejecting
279 * if the condition has not yet returned true. Defaults to 50
280 * (~5 seconds for 100ms intervals)
282 * Resolves with the return value of the condition function.
283 * Rejects if timeout is exceeded or condition ever throws.
285 * NOTE: This is intentionally not using setInterval, using setTimeout
286 * instead. setInterval is not promise-safe.
288 waitForCondition(condition, msg, interval = 100, maxTries = 50) {
289 let startTime = Cu.now();
290 return new Promise((resolve, reject) => {
293 async function tryOnce() {
295 if (tries >= maxTries) {
296 msg += ` - timed out after ${maxTries} tries.`;
297 ChromeUtils.addProfilerMarker(
299 { startTime, category: "Test" },
300 `waitForCondition - ${msg}`
307 let conditionPassed = false;
309 conditionPassed = await condition();
311 ChromeUtils.addProfilerMarker(
313 { startTime, category: "Test" },
314 `waitForCondition - ${msg}`
316 msg += ` - threw exception: ${e}`;
322 if (conditionPassed) {
323 ChromeUtils.addProfilerMarker(
325 { startTime, category: "Test" },
326 `waitForCondition succeeded after ${tries} retries - ${msg}`
328 // Avoid keeping a reference to the condition function after the
329 // promise resolves, as this function could itself reference objects
330 // that should be GC'ed before the end of the test.
332 resolve(conditionPassed);
336 timeoutId = setTimeout(tryOnce, interval);
339 TestUtils.promiseTestFinished?.then(() => {
344 clearTimeout(timeoutId);
345 msg += " - still pending at the end of the test";
346 ChromeUtils.addProfilerMarker(
348 { startTime, category: "Test" },
349 `waitForCondition - ${msg}`
351 reject("waitForCondition timer - " + msg);
354 TestUtils.executeSoon(tryOnce);
360 for (let i = 0; i < array.length; ++i) {
361 let randomIndex = Math.floor(Math.random() * (i + 1));
362 results[i] = results[randomIndex];
363 results[randomIndex] = array[i];
368 assertPackagedBuild() {
369 const omniJa = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
370 omniJa.append("omni.ja");
371 if (!omniJa.exists()) {
373 "This test requires a packaged build, " +
374 "run 'mach package' and then use --appname=dist"