forked from mirrors/gecko-dev
		
	
		
			
				
	
	
		
			382 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			382 lines
		
	
	
	
		
			12 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| /* 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.sys.mjs for generic assertions, FileTestUtils.sys.mjs
 | |
|  * to work with local files and their contents, and BrowserTestUtils.sys.mjs to
 | |
|  * work with browser windows and tabs.
 | |
|  *
 | |
|  * Individual components also offer testing functions to other components, for
 | |
|  * example LoginTestUtils.jsm.
 | |
|  */
 | |
| 
 | |
| import { clearTimeout, setTimeout } from "resource://gre/modules/Timer.sys.mjs";
 | |
| 
 | |
| const ConsoleAPIStorage = Cc["@mozilla.org/consoleAPI-storage;1"].getService(
 | |
|   Ci.nsIConsoleAPIStorage
 | |
| );
 | |
| 
 | |
| /**
 | |
|  * TestUtils provides generally useful test utilities.
 | |
|  * It can be used from mochitests, browser mochitests and xpcshell tests alike.
 | |
|  *
 | |
|  * @class
 | |
|  */
 | |
| export var TestUtils = {
 | |
|   executeSoon(callbackFn) {
 | |
|     Services.tm.dispatchToMainThread(callbackFn);
 | |
|   },
 | |
| 
 | |
|   waitForTick() {
 | |
|     return new Promise(resolve => this.executeSoon(resolve));
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Waits for a console message matching the specified check function to be
 | |
|    * observed.
 | |
|    *
 | |
|    * @param {function} checkFn [optional]
 | |
|    *        Called with the message as its argument, should return true if the
 | |
|    *        notification is the expected one, or false if it should be ignored
 | |
|    *        and listening should continue.
 | |
|    *
 | |
|    * @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 message from the observed notification.
 | |
|    */
 | |
|   consoleMessageObserved(checkFn) {
 | |
|     return new Promise((resolve, reject) => {
 | |
|       let removed = false;
 | |
|       function observe(message) {
 | |
|         try {
 | |
|           if (checkFn && !checkFn(message)) {
 | |
|             return;
 | |
|           }
 | |
|           ConsoleAPIStorage.removeLogEventListener(observe);
 | |
|           // checkFn could reference objects that need to be destroyed before
 | |
|           // the end of the test, so avoid keeping a reference to it after the
 | |
|           // promise resolves.
 | |
|           checkFn = null;
 | |
|           removed = true;
 | |
| 
 | |
|           resolve(message);
 | |
|         } catch (ex) {
 | |
|           ConsoleAPIStorage.removeLogEventListener(observe);
 | |
|           checkFn = null;
 | |
|           removed = true;
 | |
|           reject(ex);
 | |
|         }
 | |
|       }
 | |
| 
 | |
|       ConsoleAPIStorage.addLogEventListener(
 | |
|         observe,
 | |
|         Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
 | |
|       );
 | |
| 
 | |
|       TestUtils.promiseTestFinished?.then(() => {
 | |
|         if (removed) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         ConsoleAPIStorage.removeLogEventListener(observe);
 | |
|         let text =
 | |
|           "Console message observer not removed before the end of test";
 | |
|         reject(text);
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * Listens for any console messages (logged via console.*) and returns them
 | |
|    * when the returned function is called.
 | |
|    *
 | |
|    * @returns {function}
 | |
|    *   Returns an async function that when called will wait for a tick, then stop
 | |
|    *   listening to any more console messages and finally will return the
 | |
|    *   messages that have been received.
 | |
|    */
 | |
|   listenForConsoleMessages() {
 | |
|     let messages = [];
 | |
|     function observe(message) {
 | |
|       messages.push(message);
 | |
|     }
 | |
| 
 | |
|     ConsoleAPIStorage.addLogEventListener(
 | |
|       observe,
 | |
|       Cc["@mozilla.org/systemprincipal;1"].createInstance(Ci.nsIPrincipal)
 | |
|     );
 | |
| 
 | |
|     return async () => {
 | |
|       await TestUtils.waitForTick();
 | |
|       ConsoleAPIStorage.removeLogEventListener(observe);
 | |
|       return messages;
 | |
|     };
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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) {
 | |
|     let startTime = Cu.now();
 | |
|     return new Promise((resolve, reject) => {
 | |
|       let removed = false;
 | |
|       function observer(subject, topic, data) {
 | |
|         try {
 | |
|           if (checkFn && !checkFn(subject, data)) {
 | |
|             return;
 | |
|           }
 | |
|           Services.obs.removeObserver(observer, topic);
 | |
|           // checkFn could reference objects that need to be destroyed before
 | |
|           // the end of the test, so avoid keeping a reference to it after the
 | |
|           // promise resolves.
 | |
|           checkFn = null;
 | |
|           removed = true;
 | |
|           ChromeUtils.addProfilerMarker(
 | |
|             "TestUtils",
 | |
|             { startTime, category: "Test" },
 | |
|             "topicObserved: " + topic
 | |
|           );
 | |
|           resolve([subject, data]);
 | |
|         } catch (ex) {
 | |
|           Services.obs.removeObserver(observer, topic);
 | |
|           checkFn = null;
 | |
|           removed = true;
 | |
|           reject(ex);
 | |
|         }
 | |
|       }
 | |
|       Services.obs.addObserver(observer, topic);
 | |
| 
 | |
|       TestUtils.promiseTestFinished?.then(() => {
 | |
|         if (removed) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         Services.obs.removeObserver(observer, topic);
 | |
|         let text = topic + " observer not removed before the end of test";
 | |
|         reject(text);
 | |
|         ChromeUtils.addProfilerMarker(
 | |
|           "TestUtils",
 | |
|           { startTime, category: "Test" },
 | |
|           "topicObserved: " + text
 | |
|         );
 | |
|       });
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   /**
 | |
|    * 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 {Element|Rect}
 | |
|    *        The DOM node or rect ({left, top, width, height}) to screenshot.
 | |
|    * @param win {Window}
 | |
|    *        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 function fails and rejects the
 | |
|    *        returned promise. The function can be an async function.
 | |
|    * @param msg
 | |
|    *        A message used to describe the condition being waited for.
 | |
|    *        This message will be used to reject the promise should the
 | |
|    *        wait fail. It is also used to add a profiler marker.
 | |
|    * @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 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) {
 | |
|     let startTime = Cu.now();
 | |
|     return new Promise((resolve, reject) => {
 | |
|       let tries = 0;
 | |
|       let timeoutId = 0;
 | |
|       async function tryOnce() {
 | |
|         timeoutId = 0;
 | |
|         if (tries >= maxTries) {
 | |
|           msg += ` - timed out after ${maxTries} tries.`;
 | |
|           ChromeUtils.addProfilerMarker(
 | |
|             "TestUtils",
 | |
|             { startTime, category: "Test" },
 | |
|             `waitForCondition - ${msg}`
 | |
|           );
 | |
|           condition = null;
 | |
|           reject(msg);
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         let conditionPassed = false;
 | |
|         try {
 | |
|           conditionPassed = await condition();
 | |
|         } catch (e) {
 | |
|           ChromeUtils.addProfilerMarker(
 | |
|             "TestUtils",
 | |
|             { startTime, category: "Test" },
 | |
|             `waitForCondition - ${msg}`
 | |
|           );
 | |
|           msg += ` - threw exception: ${e}`;
 | |
|           condition = null;
 | |
|           reject(msg);
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         if (conditionPassed) {
 | |
|           ChromeUtils.addProfilerMarker(
 | |
|             "TestUtils",
 | |
|             { startTime, category: "Test" },
 | |
|             `waitForCondition succeeded after ${tries} retries - ${msg}`
 | |
|           );
 | |
|           // Avoid keeping a reference to the condition function after the
 | |
|           // promise resolves, as this function could itself reference objects
 | |
|           // that should be GC'ed before the end of the test.
 | |
|           condition = null;
 | |
|           resolve(conditionPassed);
 | |
|           return;
 | |
|         }
 | |
|         tries++;
 | |
|         timeoutId = setTimeout(tryOnce, interval);
 | |
|       }
 | |
| 
 | |
|       TestUtils.promiseTestFinished?.then(() => {
 | |
|         if (!timeoutId) {
 | |
|           return;
 | |
|         }
 | |
| 
 | |
|         clearTimeout(timeoutId);
 | |
|         msg += " - still pending at the end of the test";
 | |
|         ChromeUtils.addProfilerMarker(
 | |
|           "TestUtils",
 | |
|           { startTime, category: "Test" },
 | |
|           `waitForCondition - ${msg}`
 | |
|         );
 | |
|         reject("waitForCondition timer - " + msg);
 | |
|       });
 | |
| 
 | |
|       TestUtils.executeSoon(tryOnce);
 | |
|     });
 | |
|   },
 | |
| 
 | |
|   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;
 | |
|   },
 | |
| 
 | |
|   assertPackagedBuild() {
 | |
|     const omniJa = Services.dirsvc.get("XCurProcD", Ci.nsIFile);
 | |
|     omniJa.append("omni.ja");
 | |
|     if (!omniJa.exists()) {
 | |
|       throw new Error(
 | |
|         "This test requires a packaged build, " +
 | |
|           "run 'mach package' and then use --appname=dist"
 | |
|       );
 | |
|     }
 | |
|   },
 | |
| };
 | 
