michael@0: /* vim:set ts=2 sw=2 sts=2 et: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); michael@0: let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); michael@0: let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); michael@0: let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); michael@0: let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); michael@0: let {require, TargetFactory} = devtools; michael@0: let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils"); michael@0: let {Messages} = require("devtools/webconsole/console-output"); michael@0: michael@0: // promise._reportErrors = true; // please never leave me. michael@0: michael@0: let gPendingOutputTest = 0; michael@0: michael@0: // The various categories of messages. michael@0: const CATEGORY_NETWORK = 0; michael@0: const CATEGORY_CSS = 1; michael@0: const CATEGORY_JS = 2; michael@0: const CATEGORY_WEBDEV = 3; michael@0: const CATEGORY_INPUT = 4; michael@0: const CATEGORY_OUTPUT = 5; michael@0: const CATEGORY_SECURITY = 6; michael@0: michael@0: // The possible message severities. michael@0: const SEVERITY_ERROR = 0; michael@0: const SEVERITY_WARNING = 1; michael@0: const SEVERITY_INFO = 2; michael@0: const SEVERITY_LOG = 3; michael@0: michael@0: // The indent of a console group in pixels. michael@0: const GROUP_INDENT = 12; michael@0: michael@0: const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; michael@0: let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); michael@0: michael@0: gDevTools.testing = true; michael@0: SimpleTest.registerCleanupFunction(() => { michael@0: gDevTools.testing = false; michael@0: }); michael@0: michael@0: function log(aMsg) michael@0: { michael@0: dump("*** WebConsoleTest: " + aMsg + "\n"); michael@0: } michael@0: michael@0: function pprint(aObj) michael@0: { michael@0: for (let prop in aObj) { michael@0: if (typeof aObj[prop] == "function") { michael@0: log("function " + prop); michael@0: } michael@0: else { michael@0: log(prop + ": " + aObj[prop]); michael@0: } michael@0: } michael@0: } michael@0: michael@0: let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs; michael@0: michael@0: function addTab(aURL) michael@0: { michael@0: gBrowser.selectedTab = gBrowser.addTab(aURL); michael@0: tab = gBrowser.selectedTab; michael@0: browser = gBrowser.getBrowserForTab(tab); michael@0: } michael@0: michael@0: function loadTab(url) { michael@0: let deferred = promise.defer(); michael@0: michael@0: let tab = gBrowser.selectedTab = gBrowser.addTab(url); michael@0: let browser = gBrowser.getBrowserForTab(tab); michael@0: michael@0: browser.addEventListener("load", function onLoad() { michael@0: browser.removeEventListener("load", onLoad, true); michael@0: deferred.resolve({tab: tab, browser: browser}); michael@0: }, true); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function afterAllTabsLoaded(callback, win) { michael@0: win = win || window; michael@0: michael@0: let stillToLoad = 0; michael@0: michael@0: function onLoad() { michael@0: this.removeEventListener("load", onLoad, true); michael@0: stillToLoad--; michael@0: if (!stillToLoad) michael@0: callback(); michael@0: } michael@0: michael@0: for (let a = 0; a < win.gBrowser.tabs.length; a++) { michael@0: let browser = win.gBrowser.tabs[a].linkedBrowser; michael@0: if (browser.webProgress.isLoadingDocument) { michael@0: stillToLoad++; michael@0: browser.addEventListener("load", onLoad, true); michael@0: } michael@0: } michael@0: michael@0: if (!stillToLoad) michael@0: callback(); michael@0: } michael@0: michael@0: /** michael@0: * Check if a log entry exists in the HUD output node. michael@0: * michael@0: * @param {Element} aOutputNode michael@0: * the HUD output node. michael@0: * @param {string} aMatchString michael@0: * the string you want to check if it exists in the output node. michael@0: * @param {string} aMsg michael@0: * the message describing the test michael@0: * @param {boolean} [aOnlyVisible=false] michael@0: * find only messages that are visible, not hidden by the filter. michael@0: * @param {boolean} [aFailIfFound=false] michael@0: * fail the test if the string is found in the output node. michael@0: * @param {string} aClass [optional] michael@0: * find only messages with the given CSS class. michael@0: */ michael@0: function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, michael@0: aFailIfFound, aClass) michael@0: { michael@0: let selector = ".message"; michael@0: // Skip entries that are hidden by the filter. michael@0: if (aOnlyVisible) { michael@0: selector += ":not(.filtered-by-type):not(.filtered-by-string)"; michael@0: } michael@0: if (aClass) { michael@0: selector += "." + aClass; michael@0: } michael@0: michael@0: let msgs = aOutputNode.querySelectorAll(selector); michael@0: let found = false; michael@0: for (let i = 0, n = msgs.length; i < n; i++) { michael@0: let message = msgs[i].textContent.indexOf(aMatchString); michael@0: if (message > -1) { michael@0: found = true; michael@0: break; michael@0: } michael@0: } michael@0: michael@0: is(found, !aFailIfFound, aMsg); michael@0: } michael@0: michael@0: /** michael@0: * A convenience method to call testLogEntry(). michael@0: * michael@0: * @param string aString michael@0: * The string to find. michael@0: */ michael@0: function findLogEntry(aString) michael@0: { michael@0: testLogEntry(outputNode, aString, "found " + aString); michael@0: } michael@0: michael@0: /** michael@0: * Open the Web Console for the given tab. michael@0: * michael@0: * @param nsIDOMElement [aTab] michael@0: * Optional tab element for which you want open the Web Console. The michael@0: * default tab is taken from the global variable |tab|. michael@0: * @param function [aCallback] michael@0: * Optional function to invoke after the Web Console completes michael@0: * initialization (web-console-created). michael@0: * @return object michael@0: * A promise that is resolved once the web console is open. michael@0: */ michael@0: function openConsole(aTab, aCallback = function() { }) michael@0: { michael@0: let deferred = promise.defer(); michael@0: let target = TargetFactory.forTab(aTab || tab); michael@0: gDevTools.showToolbox(target, "webconsole").then(function(toolbox) { michael@0: let hud = toolbox.getCurrentPanel().hud; michael@0: hud.jsterm._lazyVariablesView = false; michael@0: aCallback(hud); michael@0: deferred.resolve(hud); michael@0: }); michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Close the Web Console for the given tab. michael@0: * michael@0: * @param nsIDOMElement [aTab] michael@0: * Optional tab element for which you want close the Web Console. The michael@0: * default tab is taken from the global variable |tab|. michael@0: * @param function [aCallback] michael@0: * Optional function to invoke after the Web Console completes michael@0: * closing (web-console-destroyed). michael@0: * @return object michael@0: * A promise that is resolved once the web console is closed. michael@0: */ michael@0: function closeConsole(aTab, aCallback = function() { }) michael@0: { michael@0: let target = TargetFactory.forTab(aTab || tab); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: if (toolbox) { michael@0: let panel = toolbox.getPanel("webconsole"); michael@0: if (panel) { michael@0: let hudId = panel.hud.hudId; michael@0: return toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug); michael@0: } michael@0: return toolbox.destroy().then(aCallback.bind(null)); michael@0: } michael@0: michael@0: aCallback(); michael@0: return promise.resolve(null); michael@0: } michael@0: michael@0: /** michael@0: * Wait for a context menu popup to open. michael@0: * michael@0: * @param nsIDOMElement aPopup michael@0: * The XUL popup you expect to open. michael@0: * @param nsIDOMElement aButton michael@0: * The button/element that receives the contextmenu event. This is michael@0: * expected to open the popup. michael@0: * @param function aOnShown michael@0: * Function to invoke on popupshown event. michael@0: * @param function aOnHidden michael@0: * Function to invoke on popuphidden event. michael@0: */ michael@0: function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden) michael@0: { michael@0: function onPopupShown() { michael@0: info("onPopupShown"); michael@0: aPopup.removeEventListener("popupshown", onPopupShown); michael@0: michael@0: aOnShown(); michael@0: michael@0: // Use executeSoon() to get out of the popupshown event. michael@0: aPopup.addEventListener("popuphidden", onPopupHidden); michael@0: executeSoon(() => aPopup.hidePopup()); michael@0: } michael@0: function onPopupHidden() { michael@0: info("onPopupHidden"); michael@0: aPopup.removeEventListener("popuphidden", onPopupHidden); michael@0: aOnHidden(); michael@0: } michael@0: michael@0: aPopup.addEventListener("popupshown", onPopupShown); michael@0: michael@0: info("wait for the context menu to open"); michael@0: let eventDetails = { type: "contextmenu", button: 2}; michael@0: EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails, michael@0: aButton.ownerDocument.defaultView); michael@0: } michael@0: michael@0: /** michael@0: * Dump the output of all open Web Consoles - used only for debugging purposes. michael@0: */ michael@0: function dumpConsoles() michael@0: { michael@0: if (gPendingOutputTest) { michael@0: console.log("dumpConsoles start"); michael@0: for (let [, hud] of HUDService.consoles) { michael@0: if (!hud.outputNode) { michael@0: console.debug("no output content for", hud.hudId); michael@0: continue; michael@0: } michael@0: michael@0: console.debug("output content for", hud.hudId); michael@0: for (let elem of hud.outputNode.childNodes) { michael@0: dumpMessageElement(elem); michael@0: } michael@0: } michael@0: console.log("dumpConsoles end"); michael@0: michael@0: gPendingOutputTest = 0; michael@0: } michael@0: } michael@0: michael@0: /** michael@0: * Dump to output debug information for the given webconsole message. michael@0: * michael@0: * @param nsIDOMNode aMessage michael@0: * The message element you want to display. michael@0: */ michael@0: function dumpMessageElement(aMessage) michael@0: { michael@0: let text = aMessage.textContent; michael@0: let repeats = aMessage.querySelector(".message-repeats"); michael@0: if (repeats) { michael@0: repeats = repeats.getAttribute("value"); michael@0: } michael@0: console.debug("id", aMessage.getAttribute("id"), michael@0: "date", aMessage.timestamp, michael@0: "class", aMessage.className, michael@0: "category", aMessage.category, michael@0: "severity", aMessage.severity, michael@0: "repeats", repeats, michael@0: "clipboardText", aMessage.clipboardText, michael@0: "text", text); michael@0: } michael@0: michael@0: function finishTest() michael@0: { michael@0: browser = hudId = hud = filterBox = outputNode = cs = hudBox = null; michael@0: michael@0: dumpConsoles(); michael@0: michael@0: let browserConsole = HUDService.getBrowserConsole(); michael@0: if (browserConsole) { michael@0: if (browserConsole.jsterm) { michael@0: browserConsole.jsterm.clearOutput(true); michael@0: } michael@0: HUDService.toggleBrowserConsole().then(finishTest); michael@0: return; michael@0: } michael@0: michael@0: let hud = HUDService.getHudByWindow(content); michael@0: if (!hud) { michael@0: finish(); michael@0: return; michael@0: } michael@0: michael@0: if (hud.jsterm) { michael@0: hud.jsterm.clearOutput(true); michael@0: } michael@0: michael@0: closeConsole(hud.target.tab, finish); michael@0: michael@0: hud = null; michael@0: } michael@0: michael@0: function tearDown() michael@0: { michael@0: dumpConsoles(); michael@0: michael@0: if (HUDService.getBrowserConsole()) { michael@0: HUDService.toggleBrowserConsole(); michael@0: } michael@0: michael@0: let target = TargetFactory.forTab(gBrowser.selectedTab); michael@0: gDevTools.closeToolbox(target); michael@0: while (gBrowser.tabs.length > 1) { michael@0: gBrowser.removeCurrentTab(); michael@0: } michael@0: WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null; michael@0: } michael@0: michael@0: registerCleanupFunction(tearDown); michael@0: michael@0: waitForExplicitFinish(); michael@0: michael@0: /** michael@0: * Polls a given function waiting for it to become true. michael@0: * michael@0: * @param object aOptions michael@0: * Options object with the following properties: michael@0: * - validatorFn michael@0: * A validator function that returns a boolean. This is called every few michael@0: * milliseconds to check if the result is true. When it is true, succesFn michael@0: * is called and polling stops. If validatorFn never returns true, then michael@0: * polling timeouts after several tries and a failure is recorded. michael@0: * - successFn michael@0: * A function called when the validator function returns true. michael@0: * - failureFn michael@0: * A function called if the validator function timeouts - fails to return michael@0: * true in the given time. michael@0: * - name michael@0: * Name of test. This is used to generate the success and failure michael@0: * messages. michael@0: * - timeout michael@0: * Timeout for validator function, in milliseconds. Default is 5000. michael@0: */ michael@0: function waitForSuccess(aOptions) michael@0: { michael@0: let start = Date.now(); michael@0: let timeout = aOptions.timeout || 5000; michael@0: michael@0: function wait(validatorFn, successFn, failureFn) michael@0: { michael@0: if ((Date.now() - start) > timeout) { michael@0: // Log the failure. michael@0: ok(false, "Timed out while waiting for: " + aOptions.name); michael@0: failureFn(aOptions); michael@0: return; michael@0: } michael@0: michael@0: if (validatorFn(aOptions)) { michael@0: ok(true, aOptions.name); michael@0: successFn(); michael@0: } michael@0: else { michael@0: setTimeout(function() wait(validatorFn, successFn, failureFn), 100); michael@0: } michael@0: } michael@0: michael@0: wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn); michael@0: } michael@0: michael@0: function openInspector(aCallback, aTab = gBrowser.selectedTab) michael@0: { michael@0: let target = TargetFactory.forTab(aTab); michael@0: gDevTools.showToolbox(target, "inspector").then(function(toolbox) { michael@0: aCallback(toolbox.getCurrentPanel()); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Find variables or properties in a VariablesView instance. michael@0: * michael@0: * @param object aView michael@0: * The VariablesView instance. michael@0: * @param array aRules michael@0: * The array of rules you want to match. Each rule is an object with: michael@0: * - name (string|regexp): property name to match. michael@0: * - value (string|regexp): property value to match. michael@0: * - isIterator (boolean): check if the property is an iterator. michael@0: * - isGetter (boolean): check if the property is a getter. michael@0: * - isGenerator (boolean): check if the property is a generator. michael@0: * - dontMatch (boolean): make sure the rule doesn't match any property. michael@0: * @param object aOptions michael@0: * Options for matching: michael@0: * - webconsole: the WebConsole instance we work with. michael@0: * @return object michael@0: * A promise object that is resolved when all the rules complete michael@0: * matching. The resolved callback is given an array of all the rules michael@0: * you wanted to check. Each rule has a new property: |matchedProp| michael@0: * which holds a reference to the Property object instance from the michael@0: * VariablesView. If the rule did not match, then |matchedProp| is michael@0: * undefined. michael@0: */ michael@0: function findVariableViewProperties(aView, aRules, aOptions) michael@0: { michael@0: // Initialize the search. michael@0: function init() michael@0: { michael@0: // Separate out the rules that require expanding properties throughout the michael@0: // view. michael@0: let expandRules = []; michael@0: let rules = aRules.filter((aRule) => { michael@0: if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { michael@0: expandRules.push(aRule); michael@0: return false; michael@0: } michael@0: return true; michael@0: }); michael@0: michael@0: // Search through the view those rules that do not require any properties to michael@0: // be expanded. Build the array of matchers, outstanding promises to be michael@0: // resolved. michael@0: let outstanding = []; michael@0: finder(rules, aView, outstanding); michael@0: michael@0: // Process the rules that need to expand properties. michael@0: let lastStep = processExpandRules.bind(null, expandRules); michael@0: michael@0: // Return the results - a promise resolved to hold the updated aRules array. michael@0: let returnResults = onAllRulesMatched.bind(null, aRules); michael@0: michael@0: return promise.all(outstanding).then(lastStep).then(returnResults); michael@0: } michael@0: michael@0: function onMatch(aProp, aRule, aMatched) michael@0: { michael@0: if (aMatched && !aRule.matchedProp) { michael@0: aRule.matchedProp = aProp; michael@0: } michael@0: } michael@0: michael@0: function finder(aRules, aVar, aPromises) michael@0: { michael@0: for (let [id, prop] of aVar) { michael@0: for (let rule of aRules) { michael@0: let matcher = matchVariablesViewProperty(prop, rule, aOptions); michael@0: aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function processExpandRules(aRules) michael@0: { michael@0: let rule = aRules.shift(); michael@0: if (!rule) { michael@0: return promise.resolve(null); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: let expandOptions = { michael@0: rootVariable: aView, michael@0: expandTo: rule.name, michael@0: webconsole: aOptions.webconsole, michael@0: }; michael@0: michael@0: variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { michael@0: let name = rule.name; michael@0: let lastName = name.split(".").pop(); michael@0: rule.name = lastName; michael@0: michael@0: let matched = matchVariablesViewProperty(aProp, rule, aOptions); michael@0: return matched.then(onMatch.bind(null, aProp, rule)).then(function() { michael@0: rule.name = name; michael@0: }); michael@0: }, function onFailure() { michael@0: return promise.resolve(null); michael@0: }).then(processExpandRules.bind(null, aRules)).then(function() { michael@0: deferred.resolve(null); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function onAllRulesMatched(aRules) michael@0: { michael@0: for (let rule of aRules) { michael@0: let matched = rule.matchedProp; michael@0: if (matched && !rule.dontMatch) { michael@0: ok(true, "rule " + rule.name + " matched for property " + matched.name); michael@0: } michael@0: else if (matched && rule.dontMatch) { michael@0: ok(false, "rule " + rule.name + " should not match property " + michael@0: matched.name); michael@0: } michael@0: else { michael@0: ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); michael@0: } michael@0: } michael@0: return aRules; michael@0: } michael@0: michael@0: return init(); michael@0: } michael@0: michael@0: /** michael@0: * Check if a given Property object from the variables view matches the given michael@0: * rule. michael@0: * michael@0: * @param object aProp michael@0: * The variable's view Property instance. michael@0: * @param object aRule michael@0: * Rules for matching the property. See findVariableViewProperties() for michael@0: * details. michael@0: * @param object aOptions michael@0: * Options for matching. See findVariableViewProperties(). michael@0: * @return object michael@0: * A promise that is resolved when all the checks complete. Resolution michael@0: * result is a boolean that tells your promise callback the match michael@0: * result: true or false. michael@0: */ michael@0: function matchVariablesViewProperty(aProp, aRule, aOptions) michael@0: { michael@0: function resolve(aResult) { michael@0: return promise.resolve(aResult); michael@0: } michael@0: michael@0: if (aRule.name) { michael@0: let match = aRule.name instanceof RegExp ? michael@0: aRule.name.test(aProp.name) : michael@0: aProp.name == aRule.name; michael@0: if (!match) { michael@0: return resolve(false); michael@0: } michael@0: } michael@0: michael@0: if (aRule.value) { michael@0: let displayValue = aProp.displayValue; michael@0: if (aProp.displayValueClassName == "token-string") { michael@0: displayValue = displayValue.substring(1, displayValue.length - 1); michael@0: } michael@0: michael@0: let match = aRule.value instanceof RegExp ? michael@0: aRule.value.test(displayValue) : michael@0: displayValue == aRule.value; michael@0: if (!match) { michael@0: info("rule " + aRule.name + " did not match value, expected '" + michael@0: aRule.value + "', found '" + displayValue + "'"); michael@0: return resolve(false); michael@0: } michael@0: } michael@0: michael@0: if ("isGetter" in aRule) { michael@0: let isGetter = !!(aProp.getter && aProp.get("get")); michael@0: if (aRule.isGetter != isGetter) { michael@0: info("rule " + aRule.name + " getter test failed"); michael@0: return resolve(false); michael@0: } michael@0: } michael@0: michael@0: if ("isGenerator" in aRule) { michael@0: let isGenerator = aProp.displayValue == "Generator"; michael@0: if (aRule.isGenerator != isGenerator) { michael@0: info("rule " + aRule.name + " generator test failed"); michael@0: return resolve(false); michael@0: } michael@0: } michael@0: michael@0: let outstanding = []; michael@0: michael@0: if ("isIterator" in aRule) { michael@0: let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); michael@0: outstanding.push(isIterator.then((aResult) => { michael@0: if (aResult != aRule.isIterator) { michael@0: info("rule " + aRule.name + " iterator test failed"); michael@0: } michael@0: return aResult == aRule.isIterator; michael@0: })); michael@0: } michael@0: michael@0: outstanding.push(promise.resolve(true)); michael@0: michael@0: return promise.all(outstanding).then(function _onMatchDone(aResults) { michael@0: let ruleMatched = aResults.indexOf(false) == -1; michael@0: return resolve(ruleMatched); michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Check if the given variables view property is an iterator. michael@0: * michael@0: * @param object aProp michael@0: * The Property instance you want to check. michael@0: * @param object aWebConsole michael@0: * The WebConsole instance to work with. michael@0: * @return object michael@0: * A promise that is resolved when the check completes. The resolved michael@0: * callback is given a boolean: true if the property is an iterator, or michael@0: * false otherwise. michael@0: */ michael@0: function isVariableViewPropertyIterator(aProp, aWebConsole) michael@0: { michael@0: if (aProp.displayValue == "Iterator") { michael@0: return promise.resolve(true); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: variablesViewExpandTo({ michael@0: rootVariable: aProp, michael@0: expandTo: "__proto__.__iterator__", michael@0: webconsole: aWebConsole, michael@0: }).then(function onSuccess(aProp) { michael@0: deferred.resolve(true); michael@0: }, function onFailure() { michael@0: deferred.resolve(false); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Recursively expand the variables view up to a given property. michael@0: * michael@0: * @param aOptions michael@0: * Options for view expansion: michael@0: * - rootVariable: start from the given scope/variable/property. michael@0: * - expandTo: string made up of property names you want to expand. michael@0: * For example: "body.firstChild.nextSibling" given |rootVariable: michael@0: * document|. michael@0: * - webconsole: a WebConsole instance. If this is not provided all michael@0: * property expand() calls will be considered sync. Things may fail! michael@0: * @return object michael@0: * A promise that is resolved only when the last property in |expandTo| michael@0: * is found, and rejected otherwise. Resolution reason is always the michael@0: * last property - |nextSibling| in the example above. Rejection is michael@0: * always the last property that was found. michael@0: */ michael@0: function variablesViewExpandTo(aOptions) michael@0: { michael@0: let root = aOptions.rootVariable; michael@0: let expandTo = aOptions.expandTo.split("."); michael@0: let jsterm = (aOptions.webconsole || {}).jsterm; michael@0: let lastDeferred = promise.defer(); michael@0: michael@0: function fetch(aProp) michael@0: { michael@0: if (!aProp.onexpand) { michael@0: ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); michael@0: return promise.reject(aProp); michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: if (aProp._fetched || !jsterm) { michael@0: executeSoon(function() { michael@0: deferred.resolve(aProp); michael@0: }); michael@0: } michael@0: else { michael@0: jsterm.once("variablesview-fetched", function _onFetchProp() { michael@0: executeSoon(() => deferred.resolve(aProp)); michael@0: }); michael@0: } michael@0: michael@0: aProp.expand(); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function getNext(aProp) michael@0: { michael@0: let name = expandTo.shift(); michael@0: let newProp = aProp.get(name); michael@0: michael@0: if (expandTo.length > 0) { michael@0: ok(newProp, "found property " + name); michael@0: if (newProp) { michael@0: fetch(newProp).then(getNext, fetchError); michael@0: } michael@0: else { michael@0: lastDeferred.reject(aProp); michael@0: } michael@0: } michael@0: else { michael@0: if (newProp) { michael@0: lastDeferred.resolve(newProp); michael@0: } michael@0: else { michael@0: lastDeferred.reject(aProp); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function fetchError(aProp) michael@0: { michael@0: lastDeferred.reject(aProp); michael@0: } michael@0: michael@0: if (!root._fetched) { michael@0: fetch(root).then(getNext, fetchError); michael@0: } michael@0: else { michael@0: getNext(root); michael@0: } michael@0: michael@0: return lastDeferred.promise; michael@0: } michael@0: michael@0: michael@0: /** michael@0: * Update the content of a property in the variables view. michael@0: * michael@0: * @param object aOptions michael@0: * Options for the property update: michael@0: * - property: the property you want to change. michael@0: * - field: string that tells what you want to change: michael@0: * - use "name" to change the property name, michael@0: * - or "value" to change the property value. michael@0: * - string: the new string to write into the field. michael@0: * - webconsole: reference to the Web Console instance we work with. michael@0: * - callback: function to invoke after the property is updated. michael@0: */ michael@0: function updateVariablesViewProperty(aOptions) michael@0: { michael@0: let view = aOptions.property._variablesView; michael@0: view.window.focus(); michael@0: aOptions.property.focus(); michael@0: michael@0: switch (aOptions.field) { michael@0: case "name": michael@0: EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window); michael@0: break; michael@0: case "value": michael@0: EventUtils.synthesizeKey("VK_RETURN", {}, view.window); michael@0: break; michael@0: default: michael@0: throw new Error("options.field is incorrect"); michael@0: return; michael@0: } michael@0: michael@0: executeSoon(() => { michael@0: EventUtils.synthesizeKey("A", { accelKey: true }, view.window); michael@0: michael@0: for (let c of aOptions.string) { michael@0: EventUtils.synthesizeKey(c, {}, gVariablesView.window); michael@0: } michael@0: michael@0: if (aOptions.webconsole) { michael@0: aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback); michael@0: } michael@0: michael@0: EventUtils.synthesizeKey("VK_RETURN", {}, view.window); michael@0: michael@0: if (!aOptions.webconsole) { michael@0: executeSoon(aOptions.callback); michael@0: } michael@0: }); michael@0: } michael@0: michael@0: /** michael@0: * Open the JavaScript debugger. michael@0: * michael@0: * @param object aOptions michael@0: * Options for opening the debugger: michael@0: * - tab: the tab you want to open the debugger for. michael@0: * @return object michael@0: * A promise that is resolved once the debugger opens, or rejected if michael@0: * the open fails. The resolution callback is given one argument, an michael@0: * object that holds the following properties: michael@0: * - target: the Target object for the Tab. michael@0: * - toolbox: the Toolbox instance. michael@0: * - panel: the jsdebugger panel instance. michael@0: * - panelWin: the window object of the panel iframe. michael@0: */ michael@0: function openDebugger(aOptions = {}) michael@0: { michael@0: if (!aOptions.tab) { michael@0: aOptions.tab = gBrowser.selectedTab; michael@0: } michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: let target = TargetFactory.forTab(aOptions.tab); michael@0: let toolbox = gDevTools.getToolbox(target); michael@0: let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger"); michael@0: michael@0: gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { michael@0: let panel = aToolbox.getCurrentPanel(); michael@0: let panelWin = panel.panelWin; michael@0: michael@0: panel._view.Variables.lazyEmpty = false; michael@0: michael@0: let resolveObject = { michael@0: target: target, michael@0: toolbox: aToolbox, michael@0: panel: panel, michael@0: panelWin: panelWin, michael@0: }; michael@0: michael@0: if (dbgPanelAlreadyOpen) { michael@0: deferred.resolve(resolveObject); michael@0: } michael@0: else { michael@0: panelWin.once(panelWin.EVENTS.SOURCES_ADDED, () => { michael@0: deferred.resolve(resolveObject); michael@0: }); michael@0: } michael@0: }, function onFailure(aReason) { michael@0: console.debug("failed to open the toolbox for 'jsdebugger'", aReason); michael@0: deferred.reject(aReason); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: /** michael@0: * Wait for messages in the Web Console output. michael@0: * michael@0: * @param object aOptions michael@0: * Options for what you want to wait for: michael@0: * - webconsole: the webconsole instance you work with. michael@0: * - matchCondition: "any" or "all". Default: "all". The promise michael@0: * returned by this function resolves when all of the messages are michael@0: * matched, if the |matchCondition| is "all". If you set the condition to michael@0: * "any" then the promise is resolved by any message rule that matches, michael@0: * irrespective of order - waiting for messages stops whenever any rule michael@0: * matches. michael@0: * - messages: an array of objects that tells which messages to wait for. michael@0: * Properties: michael@0: * - text: string or RegExp to match the textContent of each new michael@0: * message. michael@0: * - noText: string or RegExp that must not match in the message michael@0: * textContent. michael@0: * - repeats: the number of message repeats, as displayed by the Web michael@0: * Console. michael@0: * - category: match message category. See CATEGORY_* constants at michael@0: * the top of this file. michael@0: * - severity: match message severity. See SEVERITY_* constants at michael@0: * the top of this file. michael@0: * - count: how many unique web console messages should be matched by michael@0: * this rule. michael@0: * - consoleTrace: boolean, set to |true| to match a console.trace() michael@0: * message. Optionally this can be an object of the form michael@0: * { file, fn, line } that can match the specified file, function michael@0: * and/or line number in the trace message. michael@0: * - consoleTime: string that matches a console.time() timer name. michael@0: * Provide this if you want to match a console.time() message. michael@0: * - consoleTimeEnd: same as above, but for console.timeEnd(). michael@0: * - consoleDir: boolean, set to |true| to match a console.dir() michael@0: * message. michael@0: * - consoleGroup: boolean, set to |true| to match a console.group() michael@0: * message. michael@0: * - longString: boolean, set to |true} to match long strings in the michael@0: * message. michael@0: * - collapsible: boolean, set to |true| to match messages that can michael@0: * be collapsed/expanded. michael@0: * - type: match messages that are instances of the given object. For michael@0: * example, you can point to Messages.NavigationMarker to match any michael@0: * such message. michael@0: * - objects: boolean, set to |true| if you expect inspectable michael@0: * objects in the message. michael@0: * - source: object of the shape { url, line }. This is used to michael@0: * match the source URL and line number of the error message or michael@0: * console API call. michael@0: * - stacktrace: array of objects of the form { file, fn, line } that michael@0: * can match frames in the stacktrace associated with the message. michael@0: * - groupDepth: number used to check the depth of the message in michael@0: * a group. michael@0: * - url: URL to match for network requests. michael@0: * @return object michael@0: * A promise object is returned once the messages you want are found. michael@0: * The promise is resolved with the array of rule objects you give in michael@0: * the |messages| property. Each objects is the same as provided, with michael@0: * additional properties: michael@0: * - matched: a Set of web console messages that matched the rule. michael@0: * - clickableElements: a list of inspectable objects. This is available michael@0: * if any of the following properties are present in the rule: michael@0: * |consoleTrace| or |objects|. michael@0: * - longStrings: a list of long string ellipsis elements you can click michael@0: * in the message element, to expand a long string. This is available michael@0: * only if |longString| is present in the matching rule. michael@0: */ michael@0: function waitForMessages(aOptions) michael@0: { michael@0: gPendingOutputTest++; michael@0: let webconsole = aOptions.webconsole; michael@0: let rules = WebConsoleUtils.cloneObject(aOptions.messages, true); michael@0: let rulesMatched = 0; michael@0: let listenerAdded = false; michael@0: let deferred = promise.defer(); michael@0: aOptions.matchCondition = aOptions.matchCondition || "all"; michael@0: michael@0: function checkText(aRule, aText) michael@0: { michael@0: let result = false; michael@0: if (Array.isArray(aRule)) { michael@0: result = aRule.every((s) => checkText(s, aText)); michael@0: } michael@0: else if (typeof aRule == "string") { michael@0: result = aText.indexOf(aRule) > -1; michael@0: } michael@0: else if (aRule instanceof RegExp) { michael@0: result = aRule.test(aText); michael@0: } michael@0: else { michael@0: result = aRule == aText; michael@0: } michael@0: return result; michael@0: } michael@0: michael@0: function checkConsoleTrace(aRule, aElement) michael@0: { michael@0: let elemText = aElement.textContent; michael@0: let trace = aRule.consoleTrace; michael@0: michael@0: if (!checkText("console.trace():", elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: aRule.category = CATEGORY_WEBDEV; michael@0: aRule.severity = SEVERITY_LOG; michael@0: aRule.type = Messages.ConsoleTrace; michael@0: michael@0: if (!aRule.stacktrace && typeof trace == "object" && trace !== true) { michael@0: if (Array.isArray(trace)) { michael@0: aRule.stacktrace = trace; michael@0: } else { michael@0: aRule.stacktrace = [trace]; michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkConsoleTime(aRule, aElement) michael@0: { michael@0: let elemText = aElement.textContent; michael@0: let time = aRule.consoleTime; michael@0: michael@0: if (!checkText(time + ": timer started", elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: aRule.category = CATEGORY_WEBDEV; michael@0: aRule.severity = SEVERITY_LOG; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkConsoleTimeEnd(aRule, aElement) michael@0: { michael@0: let elemText = aElement.textContent; michael@0: let time = aRule.consoleTimeEnd; michael@0: let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms"); michael@0: michael@0: if (!checkText(regex, elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: aRule.category = CATEGORY_WEBDEV; michael@0: aRule.severity = SEVERITY_LOG; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkConsoleDir(aRule, aElement) michael@0: { michael@0: if (!aElement.classList.contains("inlined-variables-view")) { michael@0: return false; michael@0: } michael@0: michael@0: let elemText = aElement.textContent; michael@0: if (!checkText(aRule.consoleDir, elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: let iframe = aElement.querySelector("iframe"); michael@0: if (!iframe) { michael@0: ok(false, "console.dir message has no iframe"); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkConsoleGroup(aRule, aElement) michael@0: { michael@0: if (!isNaN(parseInt(aRule.consoleGroup))) { michael@0: aRule.groupDepth = aRule.consoleGroup; michael@0: } michael@0: aRule.category = CATEGORY_WEBDEV; michael@0: aRule.severity = SEVERITY_LOG; michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkSource(aRule, aElement) michael@0: { michael@0: let location = aElement.querySelector(".message-location"); michael@0: if (!location) { michael@0: return false; michael@0: } michael@0: michael@0: if (!checkText(aRule.source.url, location.getAttribute("title"))) { michael@0: return false; michael@0: } michael@0: michael@0: if ("line" in aRule.source && location.sourceLine != aRule.source.line) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkCollapsible(aRule, aElement) michael@0: { michael@0: let msg = aElement._messageObject; michael@0: if (!msg || !!msg.collapsible != aRule.collapsible) { michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkStacktrace(aRule, aElement) michael@0: { michael@0: let stack = aRule.stacktrace; michael@0: let frames = aElement.querySelectorAll(".stacktrace > li"); michael@0: if (!frames.length) { michael@0: return false; michael@0: } michael@0: michael@0: for (let i = 0; i < stack.length; i++) { michael@0: let frame = frames[i]; michael@0: let expected = stack[i]; michael@0: if (!frame) { michael@0: ok(false, "expected frame #" + i + " but didnt find it"); michael@0: return false; michael@0: } michael@0: michael@0: if (expected.file) { michael@0: let file = frame.querySelector(".message-location").title; michael@0: if (!checkText(expected.file, file)) { michael@0: ok(false, "frame #" + i + " does not match file name: " + michael@0: expected.file); michael@0: displayErrorContext(aRule, aElement); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if (expected.fn) { michael@0: let fn = frame.querySelector(".function").textContent; michael@0: if (!checkText(expected.fn, fn)) { michael@0: ok(false, "frame #" + i + " does not match the function name: " + michael@0: expected.fn); michael@0: displayErrorContext(aRule, aElement); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if (expected.line) { michael@0: let line = frame.querySelector(".message-location").sourceLine; michael@0: if (!checkText(expected.line, line)) { michael@0: ok(false, "frame #" + i + " does not match the line number: " + michael@0: expected.line); michael@0: displayErrorContext(aRule, aElement); michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: return true; michael@0: } michael@0: michael@0: function checkMessage(aRule, aElement) michael@0: { michael@0: let elemText = aElement.textContent; michael@0: michael@0: if (aRule.text && !checkText(aRule.text, elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.noText && checkText(aRule.noText, elemText)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.source && !checkSource(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) { michael@0: return false; michael@0: } michael@0: michael@0: let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || michael@0: aRule.consoleTimeEnd); michael@0: michael@0: // The rule tries to match the newer types of messages, based on their michael@0: // object constructor. michael@0: if (aRule.type) { michael@0: if (!aElement._messageObject || michael@0: !(aElement._messageObject instanceof aRule.type)) { michael@0: if (partialMatch) { michael@0: ok(false, "message type for rule: " + displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: partialMatch = true; michael@0: } michael@0: michael@0: if ("category" in aRule && aElement.category != aRule.category) { michael@0: if (partialMatch) { michael@0: is(aElement.category, aRule.category, michael@0: "message category for rule: " + displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: if ("severity" in aRule && aElement.severity != aRule.severity) { michael@0: if (partialMatch) { michael@0: is(aElement.severity, aRule.severity, michael@0: "message severity for rule: " + displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.text) { michael@0: partialMatch = true; michael@0: } michael@0: michael@0: if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) { michael@0: if (partialMatch) { michael@0: ok(false, "failed to match stacktrace for rule: " + displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: if (aRule.category == CATEGORY_NETWORK && "url" in aRule && michael@0: !checkText(aRule.url, aElement.url)) { michael@0: return false; michael@0: } michael@0: michael@0: if ("repeats" in aRule) { michael@0: let repeats = aElement.querySelector(".message-repeats"); michael@0: if (!repeats || repeats.getAttribute("value") != aRule.repeats) { michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if ("groupDepth" in aRule) { michael@0: let indentNode = aElement.querySelector(".indent"); michael@0: let indent = (GROUP_INDENT * aRule.groupDepth) + "px"; michael@0: if (!indentNode || indentNode.style.width != indent) { michael@0: is(indentNode.style.width, indent, michael@0: "group depth check failed for message rule: " + displayRule(aRule)); michael@0: return false; michael@0: } michael@0: } michael@0: michael@0: if ("longString" in aRule) { michael@0: let longStrings = aElement.querySelectorAll(".longStringEllipsis"); michael@0: if (aRule.longString != !!longStrings[0]) { michael@0: if (partialMatch) { michael@0: is(!!longStrings[0], aRule.longString, michael@0: "long string existence check failed for message rule: " + michael@0: displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: aRule.longStrings = longStrings; michael@0: } michael@0: michael@0: if ("objects" in aRule) { michael@0: let clickables = aElement.querySelectorAll(".message-body a"); michael@0: if (aRule.objects != !!clickables[0]) { michael@0: if (partialMatch) { michael@0: is(!!clickables[0], aRule.objects, michael@0: "objects existence check failed for message rule: " + michael@0: displayRule(aRule)); michael@0: displayErrorContext(aRule, aElement); michael@0: } michael@0: return false; michael@0: } michael@0: aRule.clickableElements = clickables; michael@0: } michael@0: michael@0: let count = aRule.count || 1; michael@0: if (!aRule.matched) { michael@0: aRule.matched = new Set(); michael@0: } michael@0: aRule.matched.add(aElement); michael@0: michael@0: return aRule.matched.size == count; michael@0: } michael@0: michael@0: function onMessagesAdded(aEvent, aNewElements) michael@0: { michael@0: for (let elem of aNewElements) { michael@0: let location = elem.querySelector(".message-location"); michael@0: if (location) { michael@0: let url = location.title; michael@0: // Prevent recursion with the browser console and any potential michael@0: // messages coming from head.js. michael@0: if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) { michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: for (let rule of rules) { michael@0: if (rule._ruleMatched) { michael@0: continue; michael@0: } michael@0: michael@0: let matched = checkMessage(rule, elem); michael@0: if (matched) { michael@0: rule._ruleMatched = true; michael@0: rulesMatched++; michael@0: ok(1, "matched rule: " + displayRule(rule)); michael@0: if (maybeDone()) { michael@0: return; michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: function allRulesMatched() michael@0: { michael@0: return aOptions.matchCondition == "all" && rulesMatched == rules.length || michael@0: aOptions.matchCondition == "any" && rulesMatched > 0; michael@0: } michael@0: michael@0: function maybeDone() michael@0: { michael@0: if (allRulesMatched()) { michael@0: if (listenerAdded) { michael@0: webconsole.ui.off("messages-added", onMessagesAdded); michael@0: webconsole.ui.off("messages-updated", onMessagesAdded); michael@0: } michael@0: gPendingOutputTest--; michael@0: deferred.resolve(rules); michael@0: return true; michael@0: } michael@0: return false; michael@0: } michael@0: michael@0: function testCleanup() { michael@0: if (allRulesMatched()) { michael@0: return; michael@0: } michael@0: michael@0: if (webconsole.ui) { michael@0: webconsole.ui.off("messages-added", onMessagesAdded); michael@0: } michael@0: michael@0: for (let rule of rules) { michael@0: if (!rule._ruleMatched) { michael@0: ok(false, "failed to match rule: " + displayRule(rule)); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function displayRule(aRule) michael@0: { michael@0: return aRule.name || aRule.text; michael@0: } michael@0: michael@0: function displayErrorContext(aRule, aElement) michael@0: { michael@0: console.log("error occured during rule " + displayRule(aRule)); michael@0: console.log("while checking the following message"); michael@0: dumpMessageElement(aElement); michael@0: } michael@0: michael@0: executeSoon(() => { michael@0: onMessagesAdded("messages-added", webconsole.outputNode.childNodes); michael@0: if (!allRulesMatched()) { michael@0: listenerAdded = true; michael@0: registerCleanupFunction(testCleanup); michael@0: webconsole.ui.on("messages-added", onMessagesAdded); michael@0: webconsole.ui.on("messages-updated", onMessagesAdded); michael@0: } michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: } michael@0: michael@0: function whenDelayedStartupFinished(aWindow, aCallback) michael@0: { michael@0: Services.obs.addObserver(function observer(aSubject, aTopic) { michael@0: if (aWindow == aSubject) { michael@0: Services.obs.removeObserver(observer, aTopic); michael@0: executeSoon(aCallback); michael@0: } michael@0: }, "browser-delayed-startup-finished", false); michael@0: } michael@0: michael@0: /** michael@0: * Check the web console output for the given inputs. Each input is checked for michael@0: * the expected JS eval result, the result of calling print(), the result of michael@0: * console.log(). The JS eval result is also checked if it opens the variables michael@0: * view on click. michael@0: * michael@0: * @param object hud michael@0: * The web console instance to work with. michael@0: * @param array inputTests michael@0: * An array of input tests. An input test element is an object. Each michael@0: * object has the following properties: michael@0: * - input: string, JS input value to execute. michael@0: * michael@0: * - output: string|RegExp, expected JS eval result. michael@0: * michael@0: * - inspectable: boolean, when true, the test runner expects the JS eval michael@0: * result is an object that can be clicked for inspection. michael@0: * michael@0: * - noClick: boolean, when true, the test runner does not click the JS michael@0: * eval result. Some objects, like |window|, have a lot of properties and michael@0: * opening vview for them is very slow (they can cause timeouts in debug michael@0: * builds). michael@0: * michael@0: * - printOutput: string|RegExp, optional, expected output for michael@0: * |print(input)|. If this is not provided, printOutput = output. michael@0: * michael@0: * - variablesViewLabel: string|RegExp, optional, the expected variables michael@0: * view label when the object is inspected. If this is not provided, then michael@0: * |output| is used. michael@0: * michael@0: * - inspectorIcon: boolean, when true, the test runner expects the michael@0: * result widget to contain an inspectorIcon element (className michael@0: * open-inspector). michael@0: */ michael@0: function checkOutputForInputs(hud, inputTests) michael@0: { michael@0: let eventHandlers = new Set(); michael@0: michael@0: function* runner() michael@0: { michael@0: for (let [i, entry] of inputTests.entries()) { michael@0: info("checkInput(" + i + "): " + entry.input); michael@0: yield checkInput(entry); michael@0: } michael@0: michael@0: for (let fn of eventHandlers) { michael@0: hud.jsterm.off("variablesview-open", fn); michael@0: } michael@0: } michael@0: michael@0: function* checkInput(entry) michael@0: { michael@0: yield checkConsoleLog(entry); michael@0: yield checkPrintOutput(entry); michael@0: yield checkJSEval(entry); michael@0: } michael@0: michael@0: function* checkConsoleLog(entry) michael@0: { michael@0: hud.jsterm.clearOutput(); michael@0: hud.jsterm.execute("console.log(" + entry.input + ")"); michael@0: michael@0: let [result] = yield waitForMessages({ michael@0: webconsole: hud, michael@0: messages: [{ michael@0: name: "console.log() output: " + entry.output, michael@0: text: entry.output, michael@0: category: CATEGORY_WEBDEV, michael@0: severity: SEVERITY_LOG, michael@0: }], michael@0: }); michael@0: michael@0: if (typeof entry.inspectorIcon == "boolean") { michael@0: let msg = [...result.matched][0]; michael@0: yield checkLinkToInspector(entry, msg); michael@0: } michael@0: } michael@0: michael@0: function checkPrintOutput(entry) michael@0: { michael@0: hud.jsterm.clearOutput(); michael@0: hud.jsterm.execute("print(" + entry.input + ")"); michael@0: michael@0: let printOutput = entry.printOutput || entry.output; michael@0: michael@0: return waitForMessages({ michael@0: webconsole: hud, michael@0: messages: [{ michael@0: name: "print() output: " + printOutput, michael@0: text: printOutput, michael@0: category: CATEGORY_OUTPUT, michael@0: }], michael@0: }); michael@0: } michael@0: michael@0: function* checkJSEval(entry) michael@0: { michael@0: hud.jsterm.clearOutput(); michael@0: hud.jsterm.execute(entry.input); michael@0: michael@0: let [result] = yield waitForMessages({ michael@0: webconsole: hud, michael@0: messages: [{ michael@0: name: "JS eval output: " + entry.output, michael@0: text: entry.output, michael@0: category: CATEGORY_OUTPUT, michael@0: }], michael@0: }); michael@0: michael@0: let msg = [...result.matched][0]; michael@0: if (!entry.noClick) { michael@0: yield checkObjectClick(entry, msg); michael@0: } michael@0: if (typeof entry.inspectorIcon == "boolean") { michael@0: yield checkLinkToInspector(entry, msg); michael@0: } michael@0: } michael@0: michael@0: function checkObjectClick(entry, msg) michael@0: { michael@0: let body = msg.querySelector(".message-body a") || michael@0: msg.querySelector(".message-body"); michael@0: ok(body, "the message body"); michael@0: michael@0: let deferred = promise.defer(); michael@0: michael@0: entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred); michael@0: hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen); michael@0: eventHandlers.add(entry._onVariablesViewOpen); michael@0: michael@0: body.scrollIntoView(); michael@0: EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow); michael@0: michael@0: if (entry.inspectable) { michael@0: info("message body tagName '" + body.tagName + "' className '" + body.className + "'"); michael@0: return deferred.promise; // wait for the panel to open if we need to. michael@0: } michael@0: michael@0: return promise.resolve(null); michael@0: } michael@0: michael@0: function checkLinkToInspector(entry, msg) michael@0: { michael@0: let elementNodeWidget = [...msg._messageObject.widgets][0]; michael@0: if (!elementNodeWidget) { michael@0: ok(!entry.inspectorIcon, "The message has no ElementNode widget"); michael@0: return; michael@0: } michael@0: michael@0: return elementNodeWidget.linkToInspector().then(() => { michael@0: // linkToInspector resolved, check for the .open-inspector element michael@0: if (entry.inspectorIcon) { michael@0: ok(msg.querySelectorAll(".open-inspector").length, michael@0: "The ElementNode widget is linked to the inspector"); michael@0: } else { michael@0: ok(!msg.querySelectorAll(".open-inspector").length, michael@0: "The ElementNode widget isn't linked to the inspector"); michael@0: } michael@0: }, () => { michael@0: // linkToInspector promise rejected, node not linked to inspector michael@0: ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector"); michael@0: }); michael@0: } michael@0: michael@0: function onVariablesViewOpen(entry, deferred, event, view, options) michael@0: { michael@0: let label = entry.variablesViewLabel || entry.output; michael@0: if (typeof label == "string" && options.label != label) { michael@0: return; michael@0: } michael@0: if (label instanceof RegExp && !label.test(options.label)) { michael@0: return; michael@0: } michael@0: michael@0: hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen); michael@0: eventHandlers.delete(entry._onVariablesViewOpen); michael@0: entry._onVariablesViewOpen = null; michael@0: michael@0: ok(entry.inspectable, "variables view was shown"); michael@0: michael@0: deferred.resolve(null); michael@0: } michael@0: michael@0: return Task.spawn(runner); michael@0: }