/* vim:set ts=2 sw=2 sts=2 et: */ /* 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/. */ let tempScope = {}; Cu.import("resource:///modules/HUDService.jsm", tempScope); let HUDService = tempScope.HUDService; Cu.import("resource://gre/modules/devtools/WebConsoleUtils.jsm", tempScope); let WebConsoleUtils = tempScope.WebConsoleUtils; Cu.import("resource:///modules/devtools/gDevTools.jsm", tempScope); let gDevTools = tempScope.gDevTools; Cu.import("resource:///modules/devtools/Target.jsm", tempScope); let TargetFactory = tempScope.TargetFactory; Components.utils.import("resource://gre/modules/devtools/Console.jsm", tempScope); let console = tempScope.console; let Promise = Cu.import("resource://gre/modules/commonjs/sdk/core/promise.js", {}).Promise; const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); function log(aMsg) { dump("*** WebConsoleTest: " + aMsg + "\n"); } function pprint(aObj) { for (let prop in aObj) { if (typeof aObj[prop] == "function") { log("function " + prop); } else { log(prop + ": " + aObj[prop]); } } } let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs; function addTab(aURL) { gBrowser.selectedTab = gBrowser.addTab(aURL); tab = gBrowser.selectedTab; browser = gBrowser.getBrowserForTab(tab); } function afterAllTabsLoaded(callback, win) { win = win || window; let stillToLoad = 0; function onLoad() { this.removeEventListener("load", onLoad, true); stillToLoad--; if (!stillToLoad) callback(); } for (let a = 0; a < win.gBrowser.tabs.length; a++) { let browser = win.gBrowser.tabs[a].linkedBrowser; if (browser.contentDocument.readyState != "complete") { stillToLoad++; browser.addEventListener("load", onLoad, true); } } if (!stillToLoad) callback(); } /** * Check if a log entry exists in the HUD output node. * * @param {Element} aOutputNode * the HUD output node. * @param {string} aMatchString * the string you want to check if it exists in the output node. * @param {string} aMsg * the message describing the test * @param {boolean} [aOnlyVisible=false] * find only messages that are visible, not hidden by the filter. * @param {boolean} [aFailIfFound=false] * fail the test if the string is found in the output node. * @param {string} aClass [optional] * find only messages with the given CSS class. */ function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, aFailIfFound, aClass) { let selector = ".hud-msg-node"; // Skip entries that are hidden by the filter. if (aOnlyVisible) { selector += ":not(.hud-filtered-by-type)"; } if (aClass) { selector += "." + aClass; } let msgs = aOutputNode.querySelectorAll(selector); let found = false; for (let i = 0, n = msgs.length; i < n; i++) { let message = msgs[i].textContent.indexOf(aMatchString); if (message > -1) { found = true; break; } // Search the labels too. let labels = msgs[i].querySelectorAll("label"); for (let j = 0; j < labels.length; j++) { if (labels[j].getAttribute("value").indexOf(aMatchString) > -1) { found = true; break; } } } is(found, !aFailIfFound, aMsg); } /** * A convenience method to call testLogEntry(). * * @param string aString * The string to find. */ function findLogEntry(aString) { testLogEntry(outputNode, aString, "found " + aString); } /** * Open the Web Console for the given tab. * * @param nsIDOMElement [aTab] * Optional tab element for which you want open the Web Console. The * default tab is taken from the global variable |tab|. * @param function [aCallback] * Optional function to invoke after the Web Console completes * initialization (web-console-created). */ function openConsole(aTab, aCallback = function() { }) { let target = TargetFactory.forTab(aTab || tab); gDevTools.showToolbox(target, "webconsole").then(function(toolbox) { let hud = toolbox.getCurrentPanel().hud; hud.jsterm._lazyVariablesView = false; aCallback(hud); }); } /** * Close the Web Console for the given tab. * * @param nsIDOMElement [aTab] * Optional tab element for which you want close the Web Console. The * default tab is taken from the global variable |tab|. * @param function [aCallback] * Optional function to invoke after the Web Console completes * closing (web-console-destroyed). */ function closeConsole(aTab, aCallback = function() { }) { let target = TargetFactory.forTab(aTab || tab); let toolbox = gDevTools.getToolbox(target); if (toolbox) { let panel = toolbox.getPanel("webconsole"); if (panel) { let hudId = panel.hud.hudId; toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug); } else { toolbox.destroy().then(aCallback.bind(null)); } } else { aCallback(); } } /** * Polls a given function waiting for opening context menu. * * @Param {nsIDOMElement} aContextMenu * @param object aOptions * Options object with the following properties: * - successFn * A function called if opening the given context menu - success to return. * - failureFn * A function called if not opening the given context menu - fails to return. * - target * The target element for showing a context menu. * - timeout * Timeout for popup shown, in milliseconds. Default is 5000. */ function waitForOpenContextMenu(aContextMenu, aOptions) { let start = Date.now(); let timeout = aOptions.timeout || 5000; let targetElement = aOptions.target; if (!aContextMenu) { ok(false, "Can't get a context menu."); aOptions.failureFn(); return; } if (!targetElement) { ok(false, "Can't get a target element."); aOptions.failureFn(); return; } function onPopupShown() { aContextMenu.removeEventListener("popupshown", onPopupShown); clearTimeout(onTimeout); aOptions.successFn(); } aContextMenu.addEventListener("popupshown", onPopupShown); let onTimeout = setTimeout(function(){ aContextMenu.removeEventListener("popupshown", onPopupShown); aOptions.failureFn(); }, timeout); // open a context menu. let eventDetails = { type : "contextmenu", button : 2}; EventUtils.synthesizeMouse(targetElement, 2, 2, eventDetails, targetElement.ownerDocument.defaultView); } function finishTest() { browser = hudId = hud = filterBox = outputNode = cs = null; if (HUDConsoleUI.browserConsole) { HUDConsoleUI.toggleBrowserConsole().then(finishTest); return; } let hud = HUDService.getHudByWindow(content); if (!hud) { finish(); return; } if (hud.jsterm) { hud.jsterm.clearOutput(true); } closeConsole(hud.target.tab, finish); hud = null; } function tearDown() { if (HUDConsoleUI.browserConsole) { HUDConsoleUI.toggleBrowserConsole(); } let target = TargetFactory.forTab(gBrowser.selectedTab); gDevTools.closeToolbox(target); while (gBrowser.tabs.length > 1) { gBrowser.removeCurrentTab(); } WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null; } registerCleanupFunction(tearDown); waitForExplicitFinish(); /** * Polls a given function waiting for it to become true. * * @param object aOptions * Options object with the following properties: * - validatorFn * A validator function that returns a boolean. This is called every few * milliseconds to check if the result is true. When it is true, succesFn * is called and polling stops. If validatorFn never returns true, then * polling timeouts after several tries and a failure is recorded. * - successFn * A function called when the validator function returns true. * - failureFn * A function called if the validator function timeouts - fails to return * true in the given time. * - name * Name of test. This is used to generate the success and failure * messages. * - timeout * Timeout for validator function, in milliseconds. Default is 5000. */ function waitForSuccess(aOptions) { let start = Date.now(); let timeout = aOptions.timeout || 5000; function wait(validatorFn, successFn, failureFn) { if ((Date.now() - start) > timeout) { // Log the failure. ok(false, "Timed out while waiting for: " + aOptions.name); failureFn(aOptions); return; } if (validatorFn(aOptions)) { ok(true, aOptions.name); successFn(); } else { setTimeout(function() wait(validatorFn, successFn, failureFn), 100); } } wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn); } function openInspector(aCallback, aTab = gBrowser.selectedTab) { let target = TargetFactory.forTab(aTab); gDevTools.showToolbox(target, "inspector").then(function(toolbox) { aCallback(toolbox.getCurrentPanel()); }); } /** * Find variables or properties in a VariablesView instance. * * @param object aView * The VariablesView instance. * @param array aRules * The array of rules you want to match. Each rule is an object with: * - name (string|regexp): property name to match. * - value (string|regexp): property value to match. * - isIterator (boolean): check if the property is an iterator. * - isGetter (boolean): check if the property is a getter. * - isGenerator (boolean): check if the property is a generator. * - dontMatch (boolean): make sure the rule doesn't match any property. * @param object aOptions * Options for matching: * - webconsole: the WebConsole instance we work with. * @return object * A Promise object that is resolved when all the rules complete * matching. The resolved callback is given an array of all the rules * you wanted to check. Each rule has a new property: |matchedProp| * which holds a reference to the Property object instance from the * VariablesView. If the rule did not match, then |matchedProp| is * undefined. */ function findVariableViewProperties(aView, aRules, aOptions) { // Initialize the search. function init() { // Separate out the rules that require expanding properties throughout the // view. let expandRules = []; let rules = aRules.filter((aRule) => { if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { expandRules.push(aRule); return false; } return true; }); // Search through the view those rules that do not require any properties to // be expanded. Build the array of matchers, outstanding promises to be // resolved. let outstanding = []; finder(rules, aView, outstanding); // Process the rules that need to expand properties. let lastStep = processExpandRules.bind(null, expandRules); // Return the results - a Promise resolved to hold the updated aRules array. let returnResults = onAllRulesMatched.bind(null, aRules); return Promise.all(outstanding).then(lastStep).then(returnResults); } function onMatch(aProp, aRule, aMatched) { if (aMatched && !aRule.matchedProp) { aRule.matchedProp = aProp; } } function finder(aRules, aVar, aPromises) { for (let [id, prop] in aVar) { for (let rule of aRules) { let matcher = matchVariablesViewProperty(prop, rule, aOptions); aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); } } } function processExpandRules(aRules) { let rule = aRules.shift(); if (!rule) { return Promise.resolve(null); } let deferred = Promise.defer(); let expandOptions = { rootVariable: aView, expandTo: rule.name, webconsole: aOptions.webconsole, }; variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { let name = rule.name; let lastName = name.split(".").pop(); rule.name = lastName; let matched = matchVariablesViewProperty(aProp, rule, aOptions); return matched.then(onMatch.bind(null, aProp, rule)).then(function() { rule.name = name; }); }, function onFailure() { return Promise.resolve(null); }).then(processExpandRules.bind(null, aRules)).then(function() { deferred.resolve(null); }); return deferred.promise; } function onAllRulesMatched(aRules) { for (let rule of aRules) { let matched = rule.matchedProp; if (matched && !rule.dontMatch) { ok(true, "rule " + rule.name + " matched for property " + matched.name); } else if (matched && rule.dontMatch) { ok(false, "rule " + rule.name + " should not match property " + matched.name); } else { ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); } } return aRules; } return init(); } /** * Check if a given Property object from the variables view matches the given * rule. * * @param object aProp * The variable's view Property instance. * @param object aRule * Rules for matching the property. See findVariableViewProperties() for * details. * @param object aOptions * Options for matching. See findVariableViewProperties(). * @return object * A Promise that is resolved when all the checks complete. Resolution * result is a boolean that tells your promise callback the match * result: true or false. */ function matchVariablesViewProperty(aProp, aRule, aOptions) { function resolve(aResult) { return Promise.resolve(aResult); } if (aRule.name) { let match = aRule.name instanceof RegExp ? aRule.name.test(aProp.name) : aProp.name == aRule.name; if (!match) { return resolve(false); } } if (aRule.value) { let displayValue = aProp.displayValue; if (aProp.displayValueClassName == "token-string") { displayValue = displayValue.substring(1, displayValue.length - 1); } let match = aRule.value instanceof RegExp ? aRule.value.test(displayValue) : displayValue == aRule.value; if (!match) { info("rule " + aRule.name + " did not match value, expected '" + aRule.value + "', found '" + displayValue + "'"); return resolve(false); } } if ("isGetter" in aRule) { let isGetter = !!(aProp.getter && aProp.get("get")); if (aRule.isGetter != isGetter) { info("rule " + aRule.name + " getter test failed"); return resolve(false); } } if ("isGenerator" in aRule) { let isGenerator = aProp.displayValue == "[object Generator]"; if (aRule.isGenerator != isGenerator) { info("rule " + aRule.name + " generator test failed"); return resolve(false); } } let outstanding = []; if ("isIterator" in aRule) { let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); outstanding.push(isIterator.then((aResult) => { if (aResult != aRule.isIterator) { info("rule " + aRule.name + " iterator test failed"); } return aResult == aRule.isIterator; })); } outstanding.push(Promise.resolve(true)); return Promise.all(outstanding).then(function _onMatchDone(aResults) { let ruleMatched = aResults.indexOf(false) == -1; return resolve(ruleMatched); }); } /** * Check if the given variables view property is an iterator. * * @param object aProp * The Property instance you want to check. * @param object aWebConsole * The WebConsole instance to work with. * @return object * A Promise that is resolved when the check completes. The resolved * callback is given a boolean: true if the property is an iterator, or * false otherwise. */ function isVariableViewPropertyIterator(aProp, aWebConsole) { if (aProp.displayValue == "[object Iterator]") { return Promise.resolve(true); } let deferred = Promise.defer(); variablesViewExpandTo({ rootVariable: aProp, expandTo: "__proto__.__iterator__", webconsole: aWebConsole, }).then(function onSuccess(aProp) { deferred.resolve(true); }, function onFailure() { deferred.resolve(false); }); return deferred.promise; } /** * Recursively expand the variables view up to a given property. * * @param aOptions * Options for view expansion: * - rootVariable: start from the given scope/variable/property. * - expandTo: string made up of property names you want to expand. * For example: "body.firstChild.nextSibling" given |rootVariable: * document|. * - webconsole: a WebConsole instance. If this is not provided all * property expand() calls will be considered sync. Things may fail! * @return object * A Promise that is resolved only when the last property in |expandTo| * is found, and rejected otherwise. Resolution reason is always the * last property - |nextSibling| in the example above. Rejection is * always the last property that was found. */ function variablesViewExpandTo(aOptions) { let root = aOptions.rootVariable; let expandTo = aOptions.expandTo.split("."); let jsterm = (aOptions.webconsole || {}).jsterm; let lastDeferred = Promise.defer(); function fetch(aProp) { if (!aProp.onexpand) { ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); return Promise.reject(aProp); } let deferred = Promise.defer(); if (aProp._fetched || !jsterm) { executeSoon(function() { deferred.resolve(aProp); }); } else { jsterm.once("variablesview-fetched", function _onFetchProp() { executeSoon(() => deferred.resolve(aProp)); }); } aProp.expand(); return deferred.promise; } function getNext(aProp) { let name = expandTo.shift(); let newProp = aProp.get(name); if (expandTo.length > 0) { ok(newProp, "found property " + name); if (newProp) { fetch(newProp).then(getNext, fetchError); } else { lastDeferred.reject(aProp); } } else { if (newProp) { lastDeferred.resolve(newProp); } else { lastDeferred.reject(aProp); } } } function fetchError(aProp) { lastDeferred.reject(aProp); } if (!root._fetched) { fetch(root).then(getNext, fetchError); } else { getNext(root); } return lastDeferred.promise; } /** * Update the content of a property in the variables view. * * @param object aOptions * Options for the property update: * - property: the property you want to change. * - field: string that tells what you want to change: * - use "name" to change the property name, * - or "value" to change the property value. * - string: the new string to write into the field. * - webconsole: reference to the Web Console instance we work with. * - callback: function to invoke after the property is updated. */ function updateVariablesViewProperty(aOptions) { let view = aOptions.property._variablesView; view.window.focus(); aOptions.property.focus(); switch (aOptions.field) { case "name": EventUtils.synthesizeKey("VK_ENTER", { shiftKey: true }, view.window); break; case "value": EventUtils.synthesizeKey("VK_ENTER", {}, view.window); break; default: throw new Error("options.field is incorrect"); return; } executeSoon(() => { EventUtils.synthesizeKey("A", { accelKey: true }, view.window); for (let c of aOptions.string) { EventUtils.synthesizeKey(c, {}, gVariablesView.window); } if (aOptions.webconsole) { aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback); } EventUtils.synthesizeKey("VK_ENTER", {}, view.window); if (!aOptions.webconsole) { executeSoon(aOptions.callback); } }); } /** * Open the JavaScript debugger. * * @param object aOptions * Options for opening the debugger: * - tab: the tab you want to open the debugger for. * @return object * A Promise that is resolved once the debugger opens, or rejected if * the open fails. The resolution callback is given one argument, an * object that holds the following properties: * - target: the Target object for the Tab. * - toolbox: the Toolbox instance. * - panel: the jsdebugger panel instance. * - panelWin: the window object of the panel iframe. */ function openDebugger(aOptions = {}) { if (!aOptions.tab) { aOptions.tab = gBrowser.selectedTab; } let deferred = Promise.defer(); let target = TargetFactory.forTab(aOptions.tab); let toolbox = gDevTools.getToolbox(target); let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger"); gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { let panel = aToolbox.getCurrentPanel(); let panelWin = panel.panelWin; panel._view.Variables.lazyEmpty = false; panel._view.Variables.lazyAppend = false; let resolveObject = { target: target, toolbox: aToolbox, panel: panel, panelWin: panelWin, }; if (dbgPanelAlreadyOpen) { deferred.resolve(resolveObject); } else { panelWin.addEventListener("Debugger:AfterSourcesAdded", function onAfterSourcesAdded() { panelWin.removeEventListener("Debugger:AfterSourcesAdded", onAfterSourcesAdded); deferred.resolve(resolveObject); }); } }, function onFailure(aReason) { console.debug("failed to open the toolbox for 'jsdebugger'", aReason); deferred.reject(aReason); }); return deferred.promise; }