summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/test/shared-head.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/client/framework/test/shared-head.js')
-rw-r--r--devtools/client/framework/test/shared-head.js596
1 files changed, 596 insertions, 0 deletions
diff --git a/devtools/client/framework/test/shared-head.js b/devtools/client/framework/test/shared-head.js
new file mode 100644
index 000000000..a89c6d752
--- /dev/null
+++ b/devtools/client/framework/test/shared-head.js
@@ -0,0 +1,596 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+/* eslint no-unused-vars: [2, {"vars": "local"}] */
+
+"use strict";
+
+// This shared-head.js file is used for multiple mochitest test directories in
+// devtools.
+// It contains various common helper functions.
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC}
+ = Components;
+
+function scopedCuImport(path) {
+ const scope = {};
+ Cu.import(path, scope);
+ return scope;
+}
+
+const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
+const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
+const {loader, require} = scopedCuImport("resource://devtools/shared/Loader.jsm");
+
+const {gDevTools} = require("devtools/client/framework/devtools");
+const {TargetFactory} = require("devtools/client/framework/target");
+const DevToolsUtils = require("devtools/shared/DevToolsUtils");
+const flags = require("devtools/shared/flags");
+let promise = require("promise");
+let defer = require("devtools/shared/defer");
+const Services = require("Services");
+const {Task} = require("devtools/shared/task");
+const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");
+
+const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+const CHROME_URL_ROOT = TEST_DIR + "/";
+const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "http://example.com/");
+const URL_ROOT_SSL = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
+ "https://example.com/");
+
+// All test are asynchronous
+waitForExplicitFinish();
+
+var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;
+
+registerCleanupFunction(function () {
+ if (DevToolsUtils.assertionFailureCount !==
+ EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
+ ok(false,
+ "Should have had the expected number of DevToolsUtils.assert() failures."
+ + " Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT
+ + ", got " + DevToolsUtils.assertionFailureCount);
+ }
+});
+
+// Uncomment this pref to dump all devtools emitted events to the console.
+// Services.prefs.setBoolPref("devtools.dump.emit", true);
+
+/**
+ * Watch console messages for failed propType definitions in React components.
+ */
+const ConsoleObserver = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),
+
+ observe: function (subject, topic, data) {
+ let message = subject.wrappedJSObject.arguments[0];
+
+ if (/Failed propType/.test(message)) {
+ ok(false, message);
+ }
+ }
+};
+
+Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false);
+registerCleanupFunction(() => {
+ Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
+});
+
+var waitForTime = DevToolsUtils.waitForTime;
+
+function getFrameScript() {
+ let mm = gBrowser.selectedBrowser.messageManager;
+ let frameURL = "chrome://devtools/content/shared/frame-script-utils.js";
+ mm.loadFrameScript(frameURL, false);
+ SimpleTest.registerCleanupFunction(() => {
+ mm = null;
+ });
+ return mm;
+}
+
+flags.testing = true;
+registerCleanupFunction(() => {
+ flags.testing = false;
+ Services.prefs.clearUserPref("devtools.dump.emit");
+ Services.prefs.clearUserPref("devtools.toolbox.host");
+ Services.prefs.clearUserPref("devtools.toolbox.previousHost");
+ Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
+});
+
+registerCleanupFunction(function* cleanup() {
+ while (gBrowser.tabs.length > 1) {
+ yield closeTabAndToolbox(gBrowser.selectedTab);
+ }
+});
+
+/**
+ * Add a new test tab in the browser and load the given url.
+ * @param {String} url The url to be loaded in the new tab
+ * @param {Object} options Object with various optional fields:
+ * - {Boolean} background If true, open the tab in background
+ * - {ChromeWindow} window Firefox top level window we should use to open the tab
+ * @return a promise that resolves to the tab object when the url is loaded
+ */
+var addTab = Task.async(function* (url, options = { background: false, window: window }) {
+ info("Adding a new tab with URL: " + url);
+
+ let { background } = options;
+ let { gBrowser } = options.window ? options.window : window;
+
+ let tab = gBrowser.addTab(url);
+ if (!background) {
+ gBrowser.selectedTab = tab;
+ }
+ yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ info("Tab added and finished loading");
+
+ return tab;
+});
+
+/**
+ * Remove the given tab.
+ * @param {Object} tab The tab to be removed.
+ * @return Promise<undefined> resolved when the tab is successfully removed.
+ */
+var removeTab = Task.async(function* (tab) {
+ info("Removing tab.");
+
+ let { gBrowser } = tab.ownerDocument.defaultView;
+ let onClose = once(gBrowser.tabContainer, "TabClose");
+ gBrowser.removeTab(tab);
+ yield onClose;
+
+ info("Tab removed and finished closing");
+});
+
+/**
+ * Refresh the given tab.
+ * @param {Object} tab The tab to be refreshed.
+ * @return Promise<undefined> resolved when the tab is successfully refreshed.
+ */
+var refreshTab = Task.async(function*(tab) {
+ info("Refreshing tab.");
+ const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ gBrowser.reloadTab(gBrowser.selectedTab);
+ yield finished;
+ info("Tab finished refreshing.");
+});
+
+/**
+ * Simulate a key event from a <key> element.
+ * @param {DOMNode} key
+ */
+function synthesizeKeyFromKeyTag(key) {
+ is(key && key.tagName, "key", "Successfully retrieved the <key> node");
+
+ let modifiersAttr = key.getAttribute("modifiers");
+
+ let name = null;
+
+ if (key.getAttribute("keycode")) {
+ name = key.getAttribute("keycode");
+ } else if (key.getAttribute("key")) {
+ name = key.getAttribute("key");
+ }
+
+ isnot(name, null, "Successfully retrieved keycode/key");
+
+ let modifiers = {
+ shiftKey: !!modifiersAttr.match("shift"),
+ ctrlKey: !!modifiersAttr.match("control"),
+ altKey: !!modifiersAttr.match("alt"),
+ metaKey: !!modifiersAttr.match("meta"),
+ accelKey: !!modifiersAttr.match("accel")
+ };
+
+ info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
+ EventUtils.synthesizeKey(name, modifiers);
+}
+
+/**
+ * Simulate a key event from an electron key shortcut string:
+ * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
+ *
+ * @param {String} key
+ * @param {DOMWindow} target
+ * Optional window where to fire the key event
+ */
+function synthesizeKeyShortcut(key, target) {
+ // parseElectronKey requires any window, just to access `KeyboardEvent`
+ let window = Services.appShell.hiddenDOMWindow;
+ let shortcut = KeyShortcuts.parseElectronKey(window, key);
+ let keyEvent = {
+ altKey: shortcut.alt,
+ ctrlKey: shortcut.ctrl,
+ metaKey: shortcut.meta,
+ shiftKey: shortcut.shift
+ };
+ if (shortcut.keyCode) {
+ keyEvent.keyCode = shortcut.keyCode;
+ }
+
+ info("Synthesizing key shortcut: " + key);
+ EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
+}
+
+/**
+ * Wait for eventName on target to be delivered a number of times.
+ *
+ * @param {Object} target
+ * An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Number} numTimes
+ * Number of deliveries to wait for.
+ * @param {Boolean} useCapture
+ * Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function waitForNEvents(target, eventName, numTimes, useCapture = false) {
+ info("Waiting for event: '" + eventName + "' on " + target + ".");
+
+ let deferred = defer();
+ let count = 0;
+
+ for (let [add, remove] of [
+ ["addEventListener", "removeEventListener"],
+ ["addListener", "removeListener"],
+ ["on", "off"]
+ ]) {
+ if ((add in target) && (remove in target)) {
+ target[add](eventName, function onEvent(...aArgs) {
+ info("Got event: '" + eventName + "' on " + target + ".");
+ if (++count == numTimes) {
+ target[remove](eventName, onEvent, useCapture);
+ deferred.resolve.apply(deferred, aArgs);
+ }
+ }, useCapture);
+ break;
+ }
+ }
+
+ return deferred.promise;
+}
+
+/**
+ * Wait for eventName on target.
+ *
+ * @param {Object} target
+ * An observable object that either supports on/off or
+ * addEventListener/removeEventListener
+ * @param {String} eventName
+ * @param {Boolean} useCapture
+ * Optional, for addEventListener/removeEventListener
+ * @return A promise that resolves when the event has been handled
+ */
+function once(target, eventName, useCapture = false) {
+ return waitForNEvents(target, eventName, 1, useCapture);
+}
+
+/**
+ * Some tests may need to import one or more of the test helper scripts.
+ * A test helper script is simply a js file that contains common test code that
+ * is either not common-enough to be in head.js, or that is located in a
+ * separate directory.
+ * The script will be loaded synchronously and in the test's scope.
+ * @param {String} filePath The file path, relative to the current directory.
+ * Examples:
+ * - "helper_attributes_test_runner.js"
+ * - "../../../commandline/test/helpers.js"
+ */
+function loadHelperScript(filePath) {
+ let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
+ Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
+}
+
+/**
+ * Wait for a tick.
+ * @return {Promise}
+ */
+function waitForTick() {
+ let deferred = defer();
+ executeSoon(deferred.resolve);
+ return deferred.promise;
+}
+
+/**
+ * This shouldn't be used in the tests, but is useful when writing new tests or
+ * debugging existing tests in order to introduce delays in the test steps
+ *
+ * @param {Number} ms
+ * The time to wait
+ * @return A promise that resolves when the time is passed
+ */
+function wait(ms) {
+ return new promise(resolve => setTimeout(resolve, ms));
+}
+
+/**
+ * Open the toolbox in a given tab.
+ * @param {XULNode} tab The tab the toolbox should be opened in.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves with the toolbox, when it has been opened.
+ */
+var openToolboxForTab = Task.async(function* (tab, toolId, hostType) {
+ info("Opening the toolbox");
+
+ let toolbox;
+ let target = TargetFactory.forTab(tab);
+ yield target.makeRemote();
+
+ // Check if the toolbox is already loaded.
+ toolbox = gDevTools.getToolbox(target);
+ if (toolbox) {
+ if (!toolId || (toolId && toolbox.getPanel(toolId))) {
+ info("Toolbox is already opened");
+ return toolbox;
+ }
+ }
+
+ // If not, load it now.
+ toolbox = yield gDevTools.showToolbox(target, toolId, hostType);
+
+ // Make sure that the toolbox frame is focused.
+ yield new Promise(resolve => waitForFocus(resolve, toolbox.win));
+
+ info("Toolbox opened and focused");
+
+ return toolbox;
+});
+
+/**
+ * Add a new tab and open the toolbox in it.
+ * @param {String} url The URL for the tab to be opened.
+ * @param {String} toolId Optional. The ID of the tool to be selected.
+ * @param {String} hostType Optional. The type of toolbox host to be used.
+ * @return {Promise} Resolves when the tab has been added, loaded and the
+ * toolbox has been opened. Resolves to the toolbox.
+ */
+var openNewTabAndToolbox = Task.async(function* (url, toolId, hostType) {
+ let tab = yield addTab(url);
+ return openToolboxForTab(tab, toolId, hostType);
+});
+
+/**
+ * Close a tab and if necessary, the toolbox that belongs to it
+ * @param {Tab} tab The tab to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeTabAndToolbox = Task.async(function* (tab = gBrowser.selectedTab) {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ if (target) {
+ yield gDevTools.closeToolbox(target);
+ }
+
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Close a toolbox and the current tab.
+ * @param {Toolbox} toolbox The toolbox to close.
+ * @return {Promise} Resolves when the toolbox and tab have been destroyed and
+ * closed.
+ */
+var closeToolboxAndTab = Task.async(function* (toolbox) {
+ yield toolbox.destroy();
+ yield removeTab(gBrowser.selectedTab);
+});
+
+/**
+ * Waits until a predicate returns true.
+ *
+ * @param function predicate
+ * Invoked once in a while until it returns true.
+ * @param number interval [optional]
+ * How often the predicate is invoked, in milliseconds.
+ */
+function waitUntil(predicate, interval = 10) {
+ if (predicate()) {
+ return Promise.resolve(true);
+ }
+ return new Promise(resolve => {
+ setTimeout(function () {
+ waitUntil(predicate, interval).then(() => resolve(true));
+ }, interval);
+ });
+}
+
+/**
+ * Takes a string `script` and evaluates it directly in the content
+ * in potentially a different process.
+ */
+let MM_INC_ID = 0;
+function evalInDebuggee(mm, script) {
+ return new Promise(function (resolve, reject) {
+ let id = MM_INC_ID++;
+ mm.sendAsyncMessage("devtools:test:eval", { script, id });
+ mm.addMessageListener("devtools:test:eval:response", handler);
+
+ function handler({ data }) {
+ if (id !== data.id) {
+ return;
+ }
+
+ info(`Successfully evaled in debuggee: ${script}`);
+ mm.removeMessageListener("devtools:test:eval:response", handler);
+ resolve(data.value);
+ }
+ });
+}
+
+/**
+ * Wait for a context menu popup to open.
+ *
+ * @param nsIDOMElement popup
+ * The XUL popup you expect to open.
+ * @param nsIDOMElement button
+ * The button/element that receives the contextmenu event. This is
+ * expected to open the popup.
+ * @param function onShown
+ * Function to invoke on popupshown event.
+ * @param function onHidden
+ * Function to invoke on popuphidden event.
+ * @return object
+ * A Promise object that is resolved after the popuphidden event
+ * callback is invoked.
+ */
+function waitForContextMenu(popup, button, onShown, onHidden) {
+ let deferred = defer();
+
+ function onPopupShown() {
+ info("onPopupShown");
+ popup.removeEventListener("popupshown", onPopupShown);
+
+ onShown && onShown();
+
+ // Use executeSoon() to get out of the popupshown event.
+ popup.addEventListener("popuphidden", onPopupHidden);
+ executeSoon(() => popup.hidePopup());
+ }
+ function onPopupHidden() {
+ info("onPopupHidden");
+ popup.removeEventListener("popuphidden", onPopupHidden);
+
+ onHidden && onHidden();
+
+ deferred.resolve(popup);
+ }
+
+ popup.addEventListener("popupshown", onPopupShown);
+
+ info("wait for the context menu to open");
+ button.scrollIntoView();
+ let eventDetails = {type: "contextmenu", button: 2};
+ EventUtils.synthesizeMouse(button, 5, 2, eventDetails,
+ button.ownerDocument.defaultView);
+ return deferred.promise;
+}
+
+/**
+ * Promise wrapper around SimpleTest.waitForClipboard
+ */
+function waitForClipboardPromise(setup, expected) {
+ return new Promise((resolve, reject) => {
+ SimpleTest.waitForClipboard(expected, setup, resolve, reject);
+ });
+}
+
+/**
+ * Simple helper to push a temporary preference. Wrapper on SpecialPowers
+ * pushPrefEnv that returns a promise resolving when the preferences have been
+ * updated.
+ *
+ * @param {String} preferenceName
+ * The name of the preference to updated
+ * @param {} value
+ * The preference value, type can vary
+ * @return {Promise} resolves when the preferences have been updated
+ */
+function pushPref(preferenceName, value) {
+ return new Promise(resolve => {
+ let options = {"set": [[preferenceName, value]]};
+ SpecialPowers.pushPrefEnv(options, resolve);
+ });
+}
+
+/**
+ * Lookup the provided dotted path ("prop1.subprop2.myProp") in the provided object.
+ *
+ * @param {Object} obj
+ * Object to expand.
+ * @param {String} path
+ * Dotted path to use to expand the object.
+ * @return {?} anything that is found at the provided path in the object.
+ */
+function lookupPath(obj, path) {
+ let segments = path.split(".");
+ return segments.reduce((prev, current) => prev[current], obj);
+}
+
+var closeToolbox = Task.async(function* () {
+ let target = TargetFactory.forTab(gBrowser.selectedTab);
+ yield gDevTools.closeToolbox(target);
+});
+
+/**
+ * Load the Telemetry utils, then stub Telemetry.prototype.log and
+ * Telemetry.prototype.logKeyed in order to record everything that's logged in
+ * it.
+ * Store all recordings in Telemetry.telemetryInfo.
+ * @return {Telemetry}
+ */
+function loadTelemetryAndRecordLogs() {
+ info("Mock the Telemetry log function to record logged information");
+
+ let Telemetry = require("devtools/client/shared/telemetry");
+ Telemetry.prototype.telemetryInfo = {};
+ Telemetry.prototype._oldlog = Telemetry.prototype.log;
+ Telemetry.prototype.log = function (histogramId, value) {
+ if (!this.telemetryInfo) {
+ // Telemetry instance still in use after stopRecordingTelemetryLogs
+ return;
+ }
+ if (histogramId) {
+ if (!this.telemetryInfo[histogramId]) {
+ this.telemetryInfo[histogramId] = [];
+ }
+ this.telemetryInfo[histogramId].push(value);
+ }
+ };
+ Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
+ Telemetry.prototype.logKeyed = function (histogramId, key, value) {
+ this.log(`${histogramId}|${key}`, value);
+ };
+
+ return Telemetry;
+}
+
+/**
+ * Stop recording the Telemetry logs and put back the utils as it was before.
+ * @param {Telemetry} Required Telemetry
+ * Telemetry object that needs to be stopped.
+ */
+function stopRecordingTelemetryLogs(Telemetry) {
+ info("Stopping Telemetry");
+ Telemetry.prototype.log = Telemetry.prototype._oldlog;
+ Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype._oldlog;
+ delete Telemetry.prototype._oldlogKeyed;
+ delete Telemetry.prototype.telemetryInfo;
+}
+
+/**
+ * Clean the logical clipboard content. This method only clears the OS clipboard on
+ * Windows (see Bug 666254).
+ */
+function emptyClipboard() {
+ let clipboard = Cc["@mozilla.org/widget/clipboard;1"]
+ .getService(SpecialPowers.Ci.nsIClipboard);
+ clipboard.emptyClipboard(clipboard.kGlobalClipboard);
+}
+
+/**
+ * Check if the current operating system is Windows.
+ */
+function isWindows() {
+ return Services.appinfo.OS === "WINNT";
+}
+
+/**
+ * Wait for a given toolbox to get its title updated.
+ */
+function waitForTitleChange(toolbox) {
+ let deferred = defer();
+ toolbox.win.parent.addEventListener("message", function onmessage(event) {
+ if (event.data.name == "set-host-title") {
+ toolbox.win.parent.removeEventListener("message", onmessage);
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}