1.1 --- /dev/null Thu Jan 01 00:00:00 1970 +0000 1.2 +++ b/browser/devtools/webconsole/test/head.js Wed Dec 31 06:09:35 2014 +0100 1.3 @@ -0,0 +1,1536 @@ 1.4 +/* vim:set ts=2 sw=2 sts=2 et: */ 1.5 +/* This Source Code Form is subject to the terms of the Mozilla Public 1.6 + * License, v. 2.0. If a copy of the MPL was not distributed with this 1.7 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 1.8 + 1.9 +"use strict"; 1.10 + 1.11 +let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {}); 1.12 +let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {}); 1.13 +let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {}); 1.14 +let {Task} = Cu.import("resource://gre/modules/Task.jsm", {}); 1.15 +let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {}); 1.16 +let {require, TargetFactory} = devtools; 1.17 +let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils"); 1.18 +let {Messages} = require("devtools/webconsole/console-output"); 1.19 + 1.20 +// promise._reportErrors = true; // please never leave me. 1.21 + 1.22 +let gPendingOutputTest = 0; 1.23 + 1.24 +// The various categories of messages. 1.25 +const CATEGORY_NETWORK = 0; 1.26 +const CATEGORY_CSS = 1; 1.27 +const CATEGORY_JS = 2; 1.28 +const CATEGORY_WEBDEV = 3; 1.29 +const CATEGORY_INPUT = 4; 1.30 +const CATEGORY_OUTPUT = 5; 1.31 +const CATEGORY_SECURITY = 6; 1.32 + 1.33 +// The possible message severities. 1.34 +const SEVERITY_ERROR = 0; 1.35 +const SEVERITY_WARNING = 1; 1.36 +const SEVERITY_INFO = 2; 1.37 +const SEVERITY_LOG = 3; 1.38 + 1.39 +// The indent of a console group in pixels. 1.40 +const GROUP_INDENT = 12; 1.41 + 1.42 +const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties"; 1.43 +let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI); 1.44 + 1.45 +gDevTools.testing = true; 1.46 +SimpleTest.registerCleanupFunction(() => { 1.47 + gDevTools.testing = false; 1.48 +}); 1.49 + 1.50 +function log(aMsg) 1.51 +{ 1.52 + dump("*** WebConsoleTest: " + aMsg + "\n"); 1.53 +} 1.54 + 1.55 +function pprint(aObj) 1.56 +{ 1.57 + for (let prop in aObj) { 1.58 + if (typeof aObj[prop] == "function") { 1.59 + log("function " + prop); 1.60 + } 1.61 + else { 1.62 + log(prop + ": " + aObj[prop]); 1.63 + } 1.64 + } 1.65 +} 1.66 + 1.67 +let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs; 1.68 + 1.69 +function addTab(aURL) 1.70 +{ 1.71 + gBrowser.selectedTab = gBrowser.addTab(aURL); 1.72 + tab = gBrowser.selectedTab; 1.73 + browser = gBrowser.getBrowserForTab(tab); 1.74 +} 1.75 + 1.76 +function loadTab(url) { 1.77 + let deferred = promise.defer(); 1.78 + 1.79 + let tab = gBrowser.selectedTab = gBrowser.addTab(url); 1.80 + let browser = gBrowser.getBrowserForTab(tab); 1.81 + 1.82 + browser.addEventListener("load", function onLoad() { 1.83 + browser.removeEventListener("load", onLoad, true); 1.84 + deferred.resolve({tab: tab, browser: browser}); 1.85 + }, true); 1.86 + 1.87 + return deferred.promise; 1.88 +} 1.89 + 1.90 +function afterAllTabsLoaded(callback, win) { 1.91 + win = win || window; 1.92 + 1.93 + let stillToLoad = 0; 1.94 + 1.95 + function onLoad() { 1.96 + this.removeEventListener("load", onLoad, true); 1.97 + stillToLoad--; 1.98 + if (!stillToLoad) 1.99 + callback(); 1.100 + } 1.101 + 1.102 + for (let a = 0; a < win.gBrowser.tabs.length; a++) { 1.103 + let browser = win.gBrowser.tabs[a].linkedBrowser; 1.104 + if (browser.webProgress.isLoadingDocument) { 1.105 + stillToLoad++; 1.106 + browser.addEventListener("load", onLoad, true); 1.107 + } 1.108 + } 1.109 + 1.110 + if (!stillToLoad) 1.111 + callback(); 1.112 +} 1.113 + 1.114 +/** 1.115 + * Check if a log entry exists in the HUD output node. 1.116 + * 1.117 + * @param {Element} aOutputNode 1.118 + * the HUD output node. 1.119 + * @param {string} aMatchString 1.120 + * the string you want to check if it exists in the output node. 1.121 + * @param {string} aMsg 1.122 + * the message describing the test 1.123 + * @param {boolean} [aOnlyVisible=false] 1.124 + * find only messages that are visible, not hidden by the filter. 1.125 + * @param {boolean} [aFailIfFound=false] 1.126 + * fail the test if the string is found in the output node. 1.127 + * @param {string} aClass [optional] 1.128 + * find only messages with the given CSS class. 1.129 + */ 1.130 +function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible, 1.131 + aFailIfFound, aClass) 1.132 +{ 1.133 + let selector = ".message"; 1.134 + // Skip entries that are hidden by the filter. 1.135 + if (aOnlyVisible) { 1.136 + selector += ":not(.filtered-by-type):not(.filtered-by-string)"; 1.137 + } 1.138 + if (aClass) { 1.139 + selector += "." + aClass; 1.140 + } 1.141 + 1.142 + let msgs = aOutputNode.querySelectorAll(selector); 1.143 + let found = false; 1.144 + for (let i = 0, n = msgs.length; i < n; i++) { 1.145 + let message = msgs[i].textContent.indexOf(aMatchString); 1.146 + if (message > -1) { 1.147 + found = true; 1.148 + break; 1.149 + } 1.150 + } 1.151 + 1.152 + is(found, !aFailIfFound, aMsg); 1.153 +} 1.154 + 1.155 +/** 1.156 + * A convenience method to call testLogEntry(). 1.157 + * 1.158 + * @param string aString 1.159 + * The string to find. 1.160 + */ 1.161 +function findLogEntry(aString) 1.162 +{ 1.163 + testLogEntry(outputNode, aString, "found " + aString); 1.164 +} 1.165 + 1.166 +/** 1.167 + * Open the Web Console for the given tab. 1.168 + * 1.169 + * @param nsIDOMElement [aTab] 1.170 + * Optional tab element for which you want open the Web Console. The 1.171 + * default tab is taken from the global variable |tab|. 1.172 + * @param function [aCallback] 1.173 + * Optional function to invoke after the Web Console completes 1.174 + * initialization (web-console-created). 1.175 + * @return object 1.176 + * A promise that is resolved once the web console is open. 1.177 + */ 1.178 +function openConsole(aTab, aCallback = function() { }) 1.179 +{ 1.180 + let deferred = promise.defer(); 1.181 + let target = TargetFactory.forTab(aTab || tab); 1.182 + gDevTools.showToolbox(target, "webconsole").then(function(toolbox) { 1.183 + let hud = toolbox.getCurrentPanel().hud; 1.184 + hud.jsterm._lazyVariablesView = false; 1.185 + aCallback(hud); 1.186 + deferred.resolve(hud); 1.187 + }); 1.188 + return deferred.promise; 1.189 +} 1.190 + 1.191 +/** 1.192 + * Close the Web Console for the given tab. 1.193 + * 1.194 + * @param nsIDOMElement [aTab] 1.195 + * Optional tab element for which you want close the Web Console. The 1.196 + * default tab is taken from the global variable |tab|. 1.197 + * @param function [aCallback] 1.198 + * Optional function to invoke after the Web Console completes 1.199 + * closing (web-console-destroyed). 1.200 + * @return object 1.201 + * A promise that is resolved once the web console is closed. 1.202 + */ 1.203 +function closeConsole(aTab, aCallback = function() { }) 1.204 +{ 1.205 + let target = TargetFactory.forTab(aTab || tab); 1.206 + let toolbox = gDevTools.getToolbox(target); 1.207 + if (toolbox) { 1.208 + let panel = toolbox.getPanel("webconsole"); 1.209 + if (panel) { 1.210 + let hudId = panel.hud.hudId; 1.211 + return toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug); 1.212 + } 1.213 + return toolbox.destroy().then(aCallback.bind(null)); 1.214 + } 1.215 + 1.216 + aCallback(); 1.217 + return promise.resolve(null); 1.218 +} 1.219 + 1.220 +/** 1.221 + * Wait for a context menu popup to open. 1.222 + * 1.223 + * @param nsIDOMElement aPopup 1.224 + * The XUL popup you expect to open. 1.225 + * @param nsIDOMElement aButton 1.226 + * The button/element that receives the contextmenu event. This is 1.227 + * expected to open the popup. 1.228 + * @param function aOnShown 1.229 + * Function to invoke on popupshown event. 1.230 + * @param function aOnHidden 1.231 + * Function to invoke on popuphidden event. 1.232 + */ 1.233 +function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden) 1.234 +{ 1.235 + function onPopupShown() { 1.236 + info("onPopupShown"); 1.237 + aPopup.removeEventListener("popupshown", onPopupShown); 1.238 + 1.239 + aOnShown(); 1.240 + 1.241 + // Use executeSoon() to get out of the popupshown event. 1.242 + aPopup.addEventListener("popuphidden", onPopupHidden); 1.243 + executeSoon(() => aPopup.hidePopup()); 1.244 + } 1.245 + function onPopupHidden() { 1.246 + info("onPopupHidden"); 1.247 + aPopup.removeEventListener("popuphidden", onPopupHidden); 1.248 + aOnHidden(); 1.249 + } 1.250 + 1.251 + aPopup.addEventListener("popupshown", onPopupShown); 1.252 + 1.253 + info("wait for the context menu to open"); 1.254 + let eventDetails = { type: "contextmenu", button: 2}; 1.255 + EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails, 1.256 + aButton.ownerDocument.defaultView); 1.257 +} 1.258 + 1.259 +/** 1.260 + * Dump the output of all open Web Consoles - used only for debugging purposes. 1.261 + */ 1.262 +function dumpConsoles() 1.263 +{ 1.264 + if (gPendingOutputTest) { 1.265 + console.log("dumpConsoles start"); 1.266 + for (let [, hud] of HUDService.consoles) { 1.267 + if (!hud.outputNode) { 1.268 + console.debug("no output content for", hud.hudId); 1.269 + continue; 1.270 + } 1.271 + 1.272 + console.debug("output content for", hud.hudId); 1.273 + for (let elem of hud.outputNode.childNodes) { 1.274 + dumpMessageElement(elem); 1.275 + } 1.276 + } 1.277 + console.log("dumpConsoles end"); 1.278 + 1.279 + gPendingOutputTest = 0; 1.280 + } 1.281 +} 1.282 + 1.283 +/** 1.284 + * Dump to output debug information for the given webconsole message. 1.285 + * 1.286 + * @param nsIDOMNode aMessage 1.287 + * The message element you want to display. 1.288 + */ 1.289 +function dumpMessageElement(aMessage) 1.290 +{ 1.291 + let text = aMessage.textContent; 1.292 + let repeats = aMessage.querySelector(".message-repeats"); 1.293 + if (repeats) { 1.294 + repeats = repeats.getAttribute("value"); 1.295 + } 1.296 + console.debug("id", aMessage.getAttribute("id"), 1.297 + "date", aMessage.timestamp, 1.298 + "class", aMessage.className, 1.299 + "category", aMessage.category, 1.300 + "severity", aMessage.severity, 1.301 + "repeats", repeats, 1.302 + "clipboardText", aMessage.clipboardText, 1.303 + "text", text); 1.304 +} 1.305 + 1.306 +function finishTest() 1.307 +{ 1.308 + browser = hudId = hud = filterBox = outputNode = cs = hudBox = null; 1.309 + 1.310 + dumpConsoles(); 1.311 + 1.312 + let browserConsole = HUDService.getBrowserConsole(); 1.313 + if (browserConsole) { 1.314 + if (browserConsole.jsterm) { 1.315 + browserConsole.jsterm.clearOutput(true); 1.316 + } 1.317 + HUDService.toggleBrowserConsole().then(finishTest); 1.318 + return; 1.319 + } 1.320 + 1.321 + let hud = HUDService.getHudByWindow(content); 1.322 + if (!hud) { 1.323 + finish(); 1.324 + return; 1.325 + } 1.326 + 1.327 + if (hud.jsterm) { 1.328 + hud.jsterm.clearOutput(true); 1.329 + } 1.330 + 1.331 + closeConsole(hud.target.tab, finish); 1.332 + 1.333 + hud = null; 1.334 +} 1.335 + 1.336 +function tearDown() 1.337 +{ 1.338 + dumpConsoles(); 1.339 + 1.340 + if (HUDService.getBrowserConsole()) { 1.341 + HUDService.toggleBrowserConsole(); 1.342 + } 1.343 + 1.344 + let target = TargetFactory.forTab(gBrowser.selectedTab); 1.345 + gDevTools.closeToolbox(target); 1.346 + while (gBrowser.tabs.length > 1) { 1.347 + gBrowser.removeCurrentTab(); 1.348 + } 1.349 + WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null; 1.350 +} 1.351 + 1.352 +registerCleanupFunction(tearDown); 1.353 + 1.354 +waitForExplicitFinish(); 1.355 + 1.356 +/** 1.357 + * Polls a given function waiting for it to become true. 1.358 + * 1.359 + * @param object aOptions 1.360 + * Options object with the following properties: 1.361 + * - validatorFn 1.362 + * A validator function that returns a boolean. This is called every few 1.363 + * milliseconds to check if the result is true. When it is true, succesFn 1.364 + * is called and polling stops. If validatorFn never returns true, then 1.365 + * polling timeouts after several tries and a failure is recorded. 1.366 + * - successFn 1.367 + * A function called when the validator function returns true. 1.368 + * - failureFn 1.369 + * A function called if the validator function timeouts - fails to return 1.370 + * true in the given time. 1.371 + * - name 1.372 + * Name of test. This is used to generate the success and failure 1.373 + * messages. 1.374 + * - timeout 1.375 + * Timeout for validator function, in milliseconds. Default is 5000. 1.376 + */ 1.377 +function waitForSuccess(aOptions) 1.378 +{ 1.379 + let start = Date.now(); 1.380 + let timeout = aOptions.timeout || 5000; 1.381 + 1.382 + function wait(validatorFn, successFn, failureFn) 1.383 + { 1.384 + if ((Date.now() - start) > timeout) { 1.385 + // Log the failure. 1.386 + ok(false, "Timed out while waiting for: " + aOptions.name); 1.387 + failureFn(aOptions); 1.388 + return; 1.389 + } 1.390 + 1.391 + if (validatorFn(aOptions)) { 1.392 + ok(true, aOptions.name); 1.393 + successFn(); 1.394 + } 1.395 + else { 1.396 + setTimeout(function() wait(validatorFn, successFn, failureFn), 100); 1.397 + } 1.398 + } 1.399 + 1.400 + wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn); 1.401 +} 1.402 + 1.403 +function openInspector(aCallback, aTab = gBrowser.selectedTab) 1.404 +{ 1.405 + let target = TargetFactory.forTab(aTab); 1.406 + gDevTools.showToolbox(target, "inspector").then(function(toolbox) { 1.407 + aCallback(toolbox.getCurrentPanel()); 1.408 + }); 1.409 +} 1.410 + 1.411 +/** 1.412 + * Find variables or properties in a VariablesView instance. 1.413 + * 1.414 + * @param object aView 1.415 + * The VariablesView instance. 1.416 + * @param array aRules 1.417 + * The array of rules you want to match. Each rule is an object with: 1.418 + * - name (string|regexp): property name to match. 1.419 + * - value (string|regexp): property value to match. 1.420 + * - isIterator (boolean): check if the property is an iterator. 1.421 + * - isGetter (boolean): check if the property is a getter. 1.422 + * - isGenerator (boolean): check if the property is a generator. 1.423 + * - dontMatch (boolean): make sure the rule doesn't match any property. 1.424 + * @param object aOptions 1.425 + * Options for matching: 1.426 + * - webconsole: the WebConsole instance we work with. 1.427 + * @return object 1.428 + * A promise object that is resolved when all the rules complete 1.429 + * matching. The resolved callback is given an array of all the rules 1.430 + * you wanted to check. Each rule has a new property: |matchedProp| 1.431 + * which holds a reference to the Property object instance from the 1.432 + * VariablesView. If the rule did not match, then |matchedProp| is 1.433 + * undefined. 1.434 + */ 1.435 +function findVariableViewProperties(aView, aRules, aOptions) 1.436 +{ 1.437 + // Initialize the search. 1.438 + function init() 1.439 + { 1.440 + // Separate out the rules that require expanding properties throughout the 1.441 + // view. 1.442 + let expandRules = []; 1.443 + let rules = aRules.filter((aRule) => { 1.444 + if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) { 1.445 + expandRules.push(aRule); 1.446 + return false; 1.447 + } 1.448 + return true; 1.449 + }); 1.450 + 1.451 + // Search through the view those rules that do not require any properties to 1.452 + // be expanded. Build the array of matchers, outstanding promises to be 1.453 + // resolved. 1.454 + let outstanding = []; 1.455 + finder(rules, aView, outstanding); 1.456 + 1.457 + // Process the rules that need to expand properties. 1.458 + let lastStep = processExpandRules.bind(null, expandRules); 1.459 + 1.460 + // Return the results - a promise resolved to hold the updated aRules array. 1.461 + let returnResults = onAllRulesMatched.bind(null, aRules); 1.462 + 1.463 + return promise.all(outstanding).then(lastStep).then(returnResults); 1.464 + } 1.465 + 1.466 + function onMatch(aProp, aRule, aMatched) 1.467 + { 1.468 + if (aMatched && !aRule.matchedProp) { 1.469 + aRule.matchedProp = aProp; 1.470 + } 1.471 + } 1.472 + 1.473 + function finder(aRules, aVar, aPromises) 1.474 + { 1.475 + for (let [id, prop] of aVar) { 1.476 + for (let rule of aRules) { 1.477 + let matcher = matchVariablesViewProperty(prop, rule, aOptions); 1.478 + aPromises.push(matcher.then(onMatch.bind(null, prop, rule))); 1.479 + } 1.480 + } 1.481 + } 1.482 + 1.483 + function processExpandRules(aRules) 1.484 + { 1.485 + let rule = aRules.shift(); 1.486 + if (!rule) { 1.487 + return promise.resolve(null); 1.488 + } 1.489 + 1.490 + let deferred = promise.defer(); 1.491 + let expandOptions = { 1.492 + rootVariable: aView, 1.493 + expandTo: rule.name, 1.494 + webconsole: aOptions.webconsole, 1.495 + }; 1.496 + 1.497 + variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) { 1.498 + let name = rule.name; 1.499 + let lastName = name.split(".").pop(); 1.500 + rule.name = lastName; 1.501 + 1.502 + let matched = matchVariablesViewProperty(aProp, rule, aOptions); 1.503 + return matched.then(onMatch.bind(null, aProp, rule)).then(function() { 1.504 + rule.name = name; 1.505 + }); 1.506 + }, function onFailure() { 1.507 + return promise.resolve(null); 1.508 + }).then(processExpandRules.bind(null, aRules)).then(function() { 1.509 + deferred.resolve(null); 1.510 + }); 1.511 + 1.512 + return deferred.promise; 1.513 + } 1.514 + 1.515 + function onAllRulesMatched(aRules) 1.516 + { 1.517 + for (let rule of aRules) { 1.518 + let matched = rule.matchedProp; 1.519 + if (matched && !rule.dontMatch) { 1.520 + ok(true, "rule " + rule.name + " matched for property " + matched.name); 1.521 + } 1.522 + else if (matched && rule.dontMatch) { 1.523 + ok(false, "rule " + rule.name + " should not match property " + 1.524 + matched.name); 1.525 + } 1.526 + else { 1.527 + ok(rule.dontMatch, "rule " + rule.name + " did not match any property"); 1.528 + } 1.529 + } 1.530 + return aRules; 1.531 + } 1.532 + 1.533 + return init(); 1.534 +} 1.535 + 1.536 +/** 1.537 + * Check if a given Property object from the variables view matches the given 1.538 + * rule. 1.539 + * 1.540 + * @param object aProp 1.541 + * The variable's view Property instance. 1.542 + * @param object aRule 1.543 + * Rules for matching the property. See findVariableViewProperties() for 1.544 + * details. 1.545 + * @param object aOptions 1.546 + * Options for matching. See findVariableViewProperties(). 1.547 + * @return object 1.548 + * A promise that is resolved when all the checks complete. Resolution 1.549 + * result is a boolean that tells your promise callback the match 1.550 + * result: true or false. 1.551 + */ 1.552 +function matchVariablesViewProperty(aProp, aRule, aOptions) 1.553 +{ 1.554 + function resolve(aResult) { 1.555 + return promise.resolve(aResult); 1.556 + } 1.557 + 1.558 + if (aRule.name) { 1.559 + let match = aRule.name instanceof RegExp ? 1.560 + aRule.name.test(aProp.name) : 1.561 + aProp.name == aRule.name; 1.562 + if (!match) { 1.563 + return resolve(false); 1.564 + } 1.565 + } 1.566 + 1.567 + if (aRule.value) { 1.568 + let displayValue = aProp.displayValue; 1.569 + if (aProp.displayValueClassName == "token-string") { 1.570 + displayValue = displayValue.substring(1, displayValue.length - 1); 1.571 + } 1.572 + 1.573 + let match = aRule.value instanceof RegExp ? 1.574 + aRule.value.test(displayValue) : 1.575 + displayValue == aRule.value; 1.576 + if (!match) { 1.577 + info("rule " + aRule.name + " did not match value, expected '" + 1.578 + aRule.value + "', found '" + displayValue + "'"); 1.579 + return resolve(false); 1.580 + } 1.581 + } 1.582 + 1.583 + if ("isGetter" in aRule) { 1.584 + let isGetter = !!(aProp.getter && aProp.get("get")); 1.585 + if (aRule.isGetter != isGetter) { 1.586 + info("rule " + aRule.name + " getter test failed"); 1.587 + return resolve(false); 1.588 + } 1.589 + } 1.590 + 1.591 + if ("isGenerator" in aRule) { 1.592 + let isGenerator = aProp.displayValue == "Generator"; 1.593 + if (aRule.isGenerator != isGenerator) { 1.594 + info("rule " + aRule.name + " generator test failed"); 1.595 + return resolve(false); 1.596 + } 1.597 + } 1.598 + 1.599 + let outstanding = []; 1.600 + 1.601 + if ("isIterator" in aRule) { 1.602 + let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole); 1.603 + outstanding.push(isIterator.then((aResult) => { 1.604 + if (aResult != aRule.isIterator) { 1.605 + info("rule " + aRule.name + " iterator test failed"); 1.606 + } 1.607 + return aResult == aRule.isIterator; 1.608 + })); 1.609 + } 1.610 + 1.611 + outstanding.push(promise.resolve(true)); 1.612 + 1.613 + return promise.all(outstanding).then(function _onMatchDone(aResults) { 1.614 + let ruleMatched = aResults.indexOf(false) == -1; 1.615 + return resolve(ruleMatched); 1.616 + }); 1.617 +} 1.618 + 1.619 +/** 1.620 + * Check if the given variables view property is an iterator. 1.621 + * 1.622 + * @param object aProp 1.623 + * The Property instance you want to check. 1.624 + * @param object aWebConsole 1.625 + * The WebConsole instance to work with. 1.626 + * @return object 1.627 + * A promise that is resolved when the check completes. The resolved 1.628 + * callback is given a boolean: true if the property is an iterator, or 1.629 + * false otherwise. 1.630 + */ 1.631 +function isVariableViewPropertyIterator(aProp, aWebConsole) 1.632 +{ 1.633 + if (aProp.displayValue == "Iterator") { 1.634 + return promise.resolve(true); 1.635 + } 1.636 + 1.637 + let deferred = promise.defer(); 1.638 + 1.639 + variablesViewExpandTo({ 1.640 + rootVariable: aProp, 1.641 + expandTo: "__proto__.__iterator__", 1.642 + webconsole: aWebConsole, 1.643 + }).then(function onSuccess(aProp) { 1.644 + deferred.resolve(true); 1.645 + }, function onFailure() { 1.646 + deferred.resolve(false); 1.647 + }); 1.648 + 1.649 + return deferred.promise; 1.650 +} 1.651 + 1.652 + 1.653 +/** 1.654 + * Recursively expand the variables view up to a given property. 1.655 + * 1.656 + * @param aOptions 1.657 + * Options for view expansion: 1.658 + * - rootVariable: start from the given scope/variable/property. 1.659 + * - expandTo: string made up of property names you want to expand. 1.660 + * For example: "body.firstChild.nextSibling" given |rootVariable: 1.661 + * document|. 1.662 + * - webconsole: a WebConsole instance. If this is not provided all 1.663 + * property expand() calls will be considered sync. Things may fail! 1.664 + * @return object 1.665 + * A promise that is resolved only when the last property in |expandTo| 1.666 + * is found, and rejected otherwise. Resolution reason is always the 1.667 + * last property - |nextSibling| in the example above. Rejection is 1.668 + * always the last property that was found. 1.669 + */ 1.670 +function variablesViewExpandTo(aOptions) 1.671 +{ 1.672 + let root = aOptions.rootVariable; 1.673 + let expandTo = aOptions.expandTo.split("."); 1.674 + let jsterm = (aOptions.webconsole || {}).jsterm; 1.675 + let lastDeferred = promise.defer(); 1.676 + 1.677 + function fetch(aProp) 1.678 + { 1.679 + if (!aProp.onexpand) { 1.680 + ok(false, "property " + aProp.name + " cannot be expanded: !onexpand"); 1.681 + return promise.reject(aProp); 1.682 + } 1.683 + 1.684 + let deferred = promise.defer(); 1.685 + 1.686 + if (aProp._fetched || !jsterm) { 1.687 + executeSoon(function() { 1.688 + deferred.resolve(aProp); 1.689 + }); 1.690 + } 1.691 + else { 1.692 + jsterm.once("variablesview-fetched", function _onFetchProp() { 1.693 + executeSoon(() => deferred.resolve(aProp)); 1.694 + }); 1.695 + } 1.696 + 1.697 + aProp.expand(); 1.698 + 1.699 + return deferred.promise; 1.700 + } 1.701 + 1.702 + function getNext(aProp) 1.703 + { 1.704 + let name = expandTo.shift(); 1.705 + let newProp = aProp.get(name); 1.706 + 1.707 + if (expandTo.length > 0) { 1.708 + ok(newProp, "found property " + name); 1.709 + if (newProp) { 1.710 + fetch(newProp).then(getNext, fetchError); 1.711 + } 1.712 + else { 1.713 + lastDeferred.reject(aProp); 1.714 + } 1.715 + } 1.716 + else { 1.717 + if (newProp) { 1.718 + lastDeferred.resolve(newProp); 1.719 + } 1.720 + else { 1.721 + lastDeferred.reject(aProp); 1.722 + } 1.723 + } 1.724 + } 1.725 + 1.726 + function fetchError(aProp) 1.727 + { 1.728 + lastDeferred.reject(aProp); 1.729 + } 1.730 + 1.731 + if (!root._fetched) { 1.732 + fetch(root).then(getNext, fetchError); 1.733 + } 1.734 + else { 1.735 + getNext(root); 1.736 + } 1.737 + 1.738 + return lastDeferred.promise; 1.739 +} 1.740 + 1.741 + 1.742 +/** 1.743 + * Update the content of a property in the variables view. 1.744 + * 1.745 + * @param object aOptions 1.746 + * Options for the property update: 1.747 + * - property: the property you want to change. 1.748 + * - field: string that tells what you want to change: 1.749 + * - use "name" to change the property name, 1.750 + * - or "value" to change the property value. 1.751 + * - string: the new string to write into the field. 1.752 + * - webconsole: reference to the Web Console instance we work with. 1.753 + * - callback: function to invoke after the property is updated. 1.754 + */ 1.755 +function updateVariablesViewProperty(aOptions) 1.756 +{ 1.757 + let view = aOptions.property._variablesView; 1.758 + view.window.focus(); 1.759 + aOptions.property.focus(); 1.760 + 1.761 + switch (aOptions.field) { 1.762 + case "name": 1.763 + EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window); 1.764 + break; 1.765 + case "value": 1.766 + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); 1.767 + break; 1.768 + default: 1.769 + throw new Error("options.field is incorrect"); 1.770 + return; 1.771 + } 1.772 + 1.773 + executeSoon(() => { 1.774 + EventUtils.synthesizeKey("A", { accelKey: true }, view.window); 1.775 + 1.776 + for (let c of aOptions.string) { 1.777 + EventUtils.synthesizeKey(c, {}, gVariablesView.window); 1.778 + } 1.779 + 1.780 + if (aOptions.webconsole) { 1.781 + aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback); 1.782 + } 1.783 + 1.784 + EventUtils.synthesizeKey("VK_RETURN", {}, view.window); 1.785 + 1.786 + if (!aOptions.webconsole) { 1.787 + executeSoon(aOptions.callback); 1.788 + } 1.789 + }); 1.790 +} 1.791 + 1.792 +/** 1.793 + * Open the JavaScript debugger. 1.794 + * 1.795 + * @param object aOptions 1.796 + * Options for opening the debugger: 1.797 + * - tab: the tab you want to open the debugger for. 1.798 + * @return object 1.799 + * A promise that is resolved once the debugger opens, or rejected if 1.800 + * the open fails. The resolution callback is given one argument, an 1.801 + * object that holds the following properties: 1.802 + * - target: the Target object for the Tab. 1.803 + * - toolbox: the Toolbox instance. 1.804 + * - panel: the jsdebugger panel instance. 1.805 + * - panelWin: the window object of the panel iframe. 1.806 + */ 1.807 +function openDebugger(aOptions = {}) 1.808 +{ 1.809 + if (!aOptions.tab) { 1.810 + aOptions.tab = gBrowser.selectedTab; 1.811 + } 1.812 + 1.813 + let deferred = promise.defer(); 1.814 + 1.815 + let target = TargetFactory.forTab(aOptions.tab); 1.816 + let toolbox = gDevTools.getToolbox(target); 1.817 + let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger"); 1.818 + 1.819 + gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) { 1.820 + let panel = aToolbox.getCurrentPanel(); 1.821 + let panelWin = panel.panelWin; 1.822 + 1.823 + panel._view.Variables.lazyEmpty = false; 1.824 + 1.825 + let resolveObject = { 1.826 + target: target, 1.827 + toolbox: aToolbox, 1.828 + panel: panel, 1.829 + panelWin: panelWin, 1.830 + }; 1.831 + 1.832 + if (dbgPanelAlreadyOpen) { 1.833 + deferred.resolve(resolveObject); 1.834 + } 1.835 + else { 1.836 + panelWin.once(panelWin.EVENTS.SOURCES_ADDED, () => { 1.837 + deferred.resolve(resolveObject); 1.838 + }); 1.839 + } 1.840 + }, function onFailure(aReason) { 1.841 + console.debug("failed to open the toolbox for 'jsdebugger'", aReason); 1.842 + deferred.reject(aReason); 1.843 + }); 1.844 + 1.845 + return deferred.promise; 1.846 +} 1.847 + 1.848 +/** 1.849 + * Wait for messages in the Web Console output. 1.850 + * 1.851 + * @param object aOptions 1.852 + * Options for what you want to wait for: 1.853 + * - webconsole: the webconsole instance you work with. 1.854 + * - matchCondition: "any" or "all". Default: "all". The promise 1.855 + * returned by this function resolves when all of the messages are 1.856 + * matched, if the |matchCondition| is "all". If you set the condition to 1.857 + * "any" then the promise is resolved by any message rule that matches, 1.858 + * irrespective of order - waiting for messages stops whenever any rule 1.859 + * matches. 1.860 + * - messages: an array of objects that tells which messages to wait for. 1.861 + * Properties: 1.862 + * - text: string or RegExp to match the textContent of each new 1.863 + * message. 1.864 + * - noText: string or RegExp that must not match in the message 1.865 + * textContent. 1.866 + * - repeats: the number of message repeats, as displayed by the Web 1.867 + * Console. 1.868 + * - category: match message category. See CATEGORY_* constants at 1.869 + * the top of this file. 1.870 + * - severity: match message severity. See SEVERITY_* constants at 1.871 + * the top of this file. 1.872 + * - count: how many unique web console messages should be matched by 1.873 + * this rule. 1.874 + * - consoleTrace: boolean, set to |true| to match a console.trace() 1.875 + * message. Optionally this can be an object of the form 1.876 + * { file, fn, line } that can match the specified file, function 1.877 + * and/or line number in the trace message. 1.878 + * - consoleTime: string that matches a console.time() timer name. 1.879 + * Provide this if you want to match a console.time() message. 1.880 + * - consoleTimeEnd: same as above, but for console.timeEnd(). 1.881 + * - consoleDir: boolean, set to |true| to match a console.dir() 1.882 + * message. 1.883 + * - consoleGroup: boolean, set to |true| to match a console.group() 1.884 + * message. 1.885 + * - longString: boolean, set to |true} to match long strings in the 1.886 + * message. 1.887 + * - collapsible: boolean, set to |true| to match messages that can 1.888 + * be collapsed/expanded. 1.889 + * - type: match messages that are instances of the given object. For 1.890 + * example, you can point to Messages.NavigationMarker to match any 1.891 + * such message. 1.892 + * - objects: boolean, set to |true| if you expect inspectable 1.893 + * objects in the message. 1.894 + * - source: object of the shape { url, line }. This is used to 1.895 + * match the source URL and line number of the error message or 1.896 + * console API call. 1.897 + * - stacktrace: array of objects of the form { file, fn, line } that 1.898 + * can match frames in the stacktrace associated with the message. 1.899 + * - groupDepth: number used to check the depth of the message in 1.900 + * a group. 1.901 + * - url: URL to match for network requests. 1.902 + * @return object 1.903 + * A promise object is returned once the messages you want are found. 1.904 + * The promise is resolved with the array of rule objects you give in 1.905 + * the |messages| property. Each objects is the same as provided, with 1.906 + * additional properties: 1.907 + * - matched: a Set of web console messages that matched the rule. 1.908 + * - clickableElements: a list of inspectable objects. This is available 1.909 + * if any of the following properties are present in the rule: 1.910 + * |consoleTrace| or |objects|. 1.911 + * - longStrings: a list of long string ellipsis elements you can click 1.912 + * in the message element, to expand a long string. This is available 1.913 + * only if |longString| is present in the matching rule. 1.914 + */ 1.915 +function waitForMessages(aOptions) 1.916 +{ 1.917 + gPendingOutputTest++; 1.918 + let webconsole = aOptions.webconsole; 1.919 + let rules = WebConsoleUtils.cloneObject(aOptions.messages, true); 1.920 + let rulesMatched = 0; 1.921 + let listenerAdded = false; 1.922 + let deferred = promise.defer(); 1.923 + aOptions.matchCondition = aOptions.matchCondition || "all"; 1.924 + 1.925 + function checkText(aRule, aText) 1.926 + { 1.927 + let result = false; 1.928 + if (Array.isArray(aRule)) { 1.929 + result = aRule.every((s) => checkText(s, aText)); 1.930 + } 1.931 + else if (typeof aRule == "string") { 1.932 + result = aText.indexOf(aRule) > -1; 1.933 + } 1.934 + else if (aRule instanceof RegExp) { 1.935 + result = aRule.test(aText); 1.936 + } 1.937 + else { 1.938 + result = aRule == aText; 1.939 + } 1.940 + return result; 1.941 + } 1.942 + 1.943 + function checkConsoleTrace(aRule, aElement) 1.944 + { 1.945 + let elemText = aElement.textContent; 1.946 + let trace = aRule.consoleTrace; 1.947 + 1.948 + if (!checkText("console.trace():", elemText)) { 1.949 + return false; 1.950 + } 1.951 + 1.952 + aRule.category = CATEGORY_WEBDEV; 1.953 + aRule.severity = SEVERITY_LOG; 1.954 + aRule.type = Messages.ConsoleTrace; 1.955 + 1.956 + if (!aRule.stacktrace && typeof trace == "object" && trace !== true) { 1.957 + if (Array.isArray(trace)) { 1.958 + aRule.stacktrace = trace; 1.959 + } else { 1.960 + aRule.stacktrace = [trace]; 1.961 + } 1.962 + } 1.963 + 1.964 + return true; 1.965 + } 1.966 + 1.967 + function checkConsoleTime(aRule, aElement) 1.968 + { 1.969 + let elemText = aElement.textContent; 1.970 + let time = aRule.consoleTime; 1.971 + 1.972 + if (!checkText(time + ": timer started", elemText)) { 1.973 + return false; 1.974 + } 1.975 + 1.976 + aRule.category = CATEGORY_WEBDEV; 1.977 + aRule.severity = SEVERITY_LOG; 1.978 + 1.979 + return true; 1.980 + } 1.981 + 1.982 + function checkConsoleTimeEnd(aRule, aElement) 1.983 + { 1.984 + let elemText = aElement.textContent; 1.985 + let time = aRule.consoleTimeEnd; 1.986 + let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms"); 1.987 + 1.988 + if (!checkText(regex, elemText)) { 1.989 + return false; 1.990 + } 1.991 + 1.992 + aRule.category = CATEGORY_WEBDEV; 1.993 + aRule.severity = SEVERITY_LOG; 1.994 + 1.995 + return true; 1.996 + } 1.997 + 1.998 + function checkConsoleDir(aRule, aElement) 1.999 + { 1.1000 + if (!aElement.classList.contains("inlined-variables-view")) { 1.1001 + return false; 1.1002 + } 1.1003 + 1.1004 + let elemText = aElement.textContent; 1.1005 + if (!checkText(aRule.consoleDir, elemText)) { 1.1006 + return false; 1.1007 + } 1.1008 + 1.1009 + let iframe = aElement.querySelector("iframe"); 1.1010 + if (!iframe) { 1.1011 + ok(false, "console.dir message has no iframe"); 1.1012 + return false; 1.1013 + } 1.1014 + 1.1015 + return true; 1.1016 + } 1.1017 + 1.1018 + function checkConsoleGroup(aRule, aElement) 1.1019 + { 1.1020 + if (!isNaN(parseInt(aRule.consoleGroup))) { 1.1021 + aRule.groupDepth = aRule.consoleGroup; 1.1022 + } 1.1023 + aRule.category = CATEGORY_WEBDEV; 1.1024 + aRule.severity = SEVERITY_LOG; 1.1025 + 1.1026 + return true; 1.1027 + } 1.1028 + 1.1029 + function checkSource(aRule, aElement) 1.1030 + { 1.1031 + let location = aElement.querySelector(".message-location"); 1.1032 + if (!location) { 1.1033 + return false; 1.1034 + } 1.1035 + 1.1036 + if (!checkText(aRule.source.url, location.getAttribute("title"))) { 1.1037 + return false; 1.1038 + } 1.1039 + 1.1040 + if ("line" in aRule.source && location.sourceLine != aRule.source.line) { 1.1041 + return false; 1.1042 + } 1.1043 + 1.1044 + return true; 1.1045 + } 1.1046 + 1.1047 + function checkCollapsible(aRule, aElement) 1.1048 + { 1.1049 + let msg = aElement._messageObject; 1.1050 + if (!msg || !!msg.collapsible != aRule.collapsible) { 1.1051 + return false; 1.1052 + } 1.1053 + 1.1054 + return true; 1.1055 + } 1.1056 + 1.1057 + function checkStacktrace(aRule, aElement) 1.1058 + { 1.1059 + let stack = aRule.stacktrace; 1.1060 + let frames = aElement.querySelectorAll(".stacktrace > li"); 1.1061 + if (!frames.length) { 1.1062 + return false; 1.1063 + } 1.1064 + 1.1065 + for (let i = 0; i < stack.length; i++) { 1.1066 + let frame = frames[i]; 1.1067 + let expected = stack[i]; 1.1068 + if (!frame) { 1.1069 + ok(false, "expected frame #" + i + " but didnt find it"); 1.1070 + return false; 1.1071 + } 1.1072 + 1.1073 + if (expected.file) { 1.1074 + let file = frame.querySelector(".message-location").title; 1.1075 + if (!checkText(expected.file, file)) { 1.1076 + ok(false, "frame #" + i + " does not match file name: " + 1.1077 + expected.file); 1.1078 + displayErrorContext(aRule, aElement); 1.1079 + return false; 1.1080 + } 1.1081 + } 1.1082 + 1.1083 + if (expected.fn) { 1.1084 + let fn = frame.querySelector(".function").textContent; 1.1085 + if (!checkText(expected.fn, fn)) { 1.1086 + ok(false, "frame #" + i + " does not match the function name: " + 1.1087 + expected.fn); 1.1088 + displayErrorContext(aRule, aElement); 1.1089 + return false; 1.1090 + } 1.1091 + } 1.1092 + 1.1093 + if (expected.line) { 1.1094 + let line = frame.querySelector(".message-location").sourceLine; 1.1095 + if (!checkText(expected.line, line)) { 1.1096 + ok(false, "frame #" + i + " does not match the line number: " + 1.1097 + expected.line); 1.1098 + displayErrorContext(aRule, aElement); 1.1099 + return false; 1.1100 + } 1.1101 + } 1.1102 + } 1.1103 + 1.1104 + return true; 1.1105 + } 1.1106 + 1.1107 + function checkMessage(aRule, aElement) 1.1108 + { 1.1109 + let elemText = aElement.textContent; 1.1110 + 1.1111 + if (aRule.text && !checkText(aRule.text, elemText)) { 1.1112 + return false; 1.1113 + } 1.1114 + 1.1115 + if (aRule.noText && checkText(aRule.noText, elemText)) { 1.1116 + return false; 1.1117 + } 1.1118 + 1.1119 + if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) { 1.1120 + return false; 1.1121 + } 1.1122 + 1.1123 + if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) { 1.1124 + return false; 1.1125 + } 1.1126 + 1.1127 + if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) { 1.1128 + return false; 1.1129 + } 1.1130 + 1.1131 + if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) { 1.1132 + return false; 1.1133 + } 1.1134 + 1.1135 + if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) { 1.1136 + return false; 1.1137 + } 1.1138 + 1.1139 + if (aRule.source && !checkSource(aRule, aElement)) { 1.1140 + return false; 1.1141 + } 1.1142 + 1.1143 + if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) { 1.1144 + return false; 1.1145 + } 1.1146 + 1.1147 + let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime || 1.1148 + aRule.consoleTimeEnd); 1.1149 + 1.1150 + // The rule tries to match the newer types of messages, based on their 1.1151 + // object constructor. 1.1152 + if (aRule.type) { 1.1153 + if (!aElement._messageObject || 1.1154 + !(aElement._messageObject instanceof aRule.type)) { 1.1155 + if (partialMatch) { 1.1156 + ok(false, "message type for rule: " + displayRule(aRule)); 1.1157 + displayErrorContext(aRule, aElement); 1.1158 + } 1.1159 + return false; 1.1160 + } 1.1161 + partialMatch = true; 1.1162 + } 1.1163 + 1.1164 + if ("category" in aRule && aElement.category != aRule.category) { 1.1165 + if (partialMatch) { 1.1166 + is(aElement.category, aRule.category, 1.1167 + "message category for rule: " + displayRule(aRule)); 1.1168 + displayErrorContext(aRule, aElement); 1.1169 + } 1.1170 + return false; 1.1171 + } 1.1172 + 1.1173 + if ("severity" in aRule && aElement.severity != aRule.severity) { 1.1174 + if (partialMatch) { 1.1175 + is(aElement.severity, aRule.severity, 1.1176 + "message severity for rule: " + displayRule(aRule)); 1.1177 + displayErrorContext(aRule, aElement); 1.1178 + } 1.1179 + return false; 1.1180 + } 1.1181 + 1.1182 + if (aRule.text) { 1.1183 + partialMatch = true; 1.1184 + } 1.1185 + 1.1186 + if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) { 1.1187 + if (partialMatch) { 1.1188 + ok(false, "failed to match stacktrace for rule: " + displayRule(aRule)); 1.1189 + displayErrorContext(aRule, aElement); 1.1190 + } 1.1191 + return false; 1.1192 + } 1.1193 + 1.1194 + if (aRule.category == CATEGORY_NETWORK && "url" in aRule && 1.1195 + !checkText(aRule.url, aElement.url)) { 1.1196 + return false; 1.1197 + } 1.1198 + 1.1199 + if ("repeats" in aRule) { 1.1200 + let repeats = aElement.querySelector(".message-repeats"); 1.1201 + if (!repeats || repeats.getAttribute("value") != aRule.repeats) { 1.1202 + return false; 1.1203 + } 1.1204 + } 1.1205 + 1.1206 + if ("groupDepth" in aRule) { 1.1207 + let indentNode = aElement.querySelector(".indent"); 1.1208 + let indent = (GROUP_INDENT * aRule.groupDepth) + "px"; 1.1209 + if (!indentNode || indentNode.style.width != indent) { 1.1210 + is(indentNode.style.width, indent, 1.1211 + "group depth check failed for message rule: " + displayRule(aRule)); 1.1212 + return false; 1.1213 + } 1.1214 + } 1.1215 + 1.1216 + if ("longString" in aRule) { 1.1217 + let longStrings = aElement.querySelectorAll(".longStringEllipsis"); 1.1218 + if (aRule.longString != !!longStrings[0]) { 1.1219 + if (partialMatch) { 1.1220 + is(!!longStrings[0], aRule.longString, 1.1221 + "long string existence check failed for message rule: " + 1.1222 + displayRule(aRule)); 1.1223 + displayErrorContext(aRule, aElement); 1.1224 + } 1.1225 + return false; 1.1226 + } 1.1227 + aRule.longStrings = longStrings; 1.1228 + } 1.1229 + 1.1230 + if ("objects" in aRule) { 1.1231 + let clickables = aElement.querySelectorAll(".message-body a"); 1.1232 + if (aRule.objects != !!clickables[0]) { 1.1233 + if (partialMatch) { 1.1234 + is(!!clickables[0], aRule.objects, 1.1235 + "objects existence check failed for message rule: " + 1.1236 + displayRule(aRule)); 1.1237 + displayErrorContext(aRule, aElement); 1.1238 + } 1.1239 + return false; 1.1240 + } 1.1241 + aRule.clickableElements = clickables; 1.1242 + } 1.1243 + 1.1244 + let count = aRule.count || 1; 1.1245 + if (!aRule.matched) { 1.1246 + aRule.matched = new Set(); 1.1247 + } 1.1248 + aRule.matched.add(aElement); 1.1249 + 1.1250 + return aRule.matched.size == count; 1.1251 + } 1.1252 + 1.1253 + function onMessagesAdded(aEvent, aNewElements) 1.1254 + { 1.1255 + for (let elem of aNewElements) { 1.1256 + let location = elem.querySelector(".message-location"); 1.1257 + if (location) { 1.1258 + let url = location.title; 1.1259 + // Prevent recursion with the browser console and any potential 1.1260 + // messages coming from head.js. 1.1261 + if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) { 1.1262 + continue; 1.1263 + } 1.1264 + } 1.1265 + 1.1266 + for (let rule of rules) { 1.1267 + if (rule._ruleMatched) { 1.1268 + continue; 1.1269 + } 1.1270 + 1.1271 + let matched = checkMessage(rule, elem); 1.1272 + if (matched) { 1.1273 + rule._ruleMatched = true; 1.1274 + rulesMatched++; 1.1275 + ok(1, "matched rule: " + displayRule(rule)); 1.1276 + if (maybeDone()) { 1.1277 + return; 1.1278 + } 1.1279 + } 1.1280 + } 1.1281 + } 1.1282 + } 1.1283 + 1.1284 + function allRulesMatched() 1.1285 + { 1.1286 + return aOptions.matchCondition == "all" && rulesMatched == rules.length || 1.1287 + aOptions.matchCondition == "any" && rulesMatched > 0; 1.1288 + } 1.1289 + 1.1290 + function maybeDone() 1.1291 + { 1.1292 + if (allRulesMatched()) { 1.1293 + if (listenerAdded) { 1.1294 + webconsole.ui.off("messages-added", onMessagesAdded); 1.1295 + webconsole.ui.off("messages-updated", onMessagesAdded); 1.1296 + } 1.1297 + gPendingOutputTest--; 1.1298 + deferred.resolve(rules); 1.1299 + return true; 1.1300 + } 1.1301 + return false; 1.1302 + } 1.1303 + 1.1304 + function testCleanup() { 1.1305 + if (allRulesMatched()) { 1.1306 + return; 1.1307 + } 1.1308 + 1.1309 + if (webconsole.ui) { 1.1310 + webconsole.ui.off("messages-added", onMessagesAdded); 1.1311 + } 1.1312 + 1.1313 + for (let rule of rules) { 1.1314 + if (!rule._ruleMatched) { 1.1315 + ok(false, "failed to match rule: " + displayRule(rule)); 1.1316 + } 1.1317 + } 1.1318 + } 1.1319 + 1.1320 + function displayRule(aRule) 1.1321 + { 1.1322 + return aRule.name || aRule.text; 1.1323 + } 1.1324 + 1.1325 + function displayErrorContext(aRule, aElement) 1.1326 + { 1.1327 + console.log("error occured during rule " + displayRule(aRule)); 1.1328 + console.log("while checking the following message"); 1.1329 + dumpMessageElement(aElement); 1.1330 + } 1.1331 + 1.1332 + executeSoon(() => { 1.1333 + onMessagesAdded("messages-added", webconsole.outputNode.childNodes); 1.1334 + if (!allRulesMatched()) { 1.1335 + listenerAdded = true; 1.1336 + registerCleanupFunction(testCleanup); 1.1337 + webconsole.ui.on("messages-added", onMessagesAdded); 1.1338 + webconsole.ui.on("messages-updated", onMessagesAdded); 1.1339 + } 1.1340 + }); 1.1341 + 1.1342 + return deferred.promise; 1.1343 +} 1.1344 + 1.1345 +function whenDelayedStartupFinished(aWindow, aCallback) 1.1346 +{ 1.1347 + Services.obs.addObserver(function observer(aSubject, aTopic) { 1.1348 + if (aWindow == aSubject) { 1.1349 + Services.obs.removeObserver(observer, aTopic); 1.1350 + executeSoon(aCallback); 1.1351 + } 1.1352 + }, "browser-delayed-startup-finished", false); 1.1353 +} 1.1354 + 1.1355 +/** 1.1356 + * Check the web console output for the given inputs. Each input is checked for 1.1357 + * the expected JS eval result, the result of calling print(), the result of 1.1358 + * console.log(). The JS eval result is also checked if it opens the variables 1.1359 + * view on click. 1.1360 + * 1.1361 + * @param object hud 1.1362 + * The web console instance to work with. 1.1363 + * @param array inputTests 1.1364 + * An array of input tests. An input test element is an object. Each 1.1365 + * object has the following properties: 1.1366 + * - input: string, JS input value to execute. 1.1367 + * 1.1368 + * - output: string|RegExp, expected JS eval result. 1.1369 + * 1.1370 + * - inspectable: boolean, when true, the test runner expects the JS eval 1.1371 + * result is an object that can be clicked for inspection. 1.1372 + * 1.1373 + * - noClick: boolean, when true, the test runner does not click the JS 1.1374 + * eval result. Some objects, like |window|, have a lot of properties and 1.1375 + * opening vview for them is very slow (they can cause timeouts in debug 1.1376 + * builds). 1.1377 + * 1.1378 + * - printOutput: string|RegExp, optional, expected output for 1.1379 + * |print(input)|. If this is not provided, printOutput = output. 1.1380 + * 1.1381 + * - variablesViewLabel: string|RegExp, optional, the expected variables 1.1382 + * view label when the object is inspected. If this is not provided, then 1.1383 + * |output| is used. 1.1384 + * 1.1385 + * - inspectorIcon: boolean, when true, the test runner expects the 1.1386 + * result widget to contain an inspectorIcon element (className 1.1387 + * open-inspector). 1.1388 + */ 1.1389 +function checkOutputForInputs(hud, inputTests) 1.1390 +{ 1.1391 + let eventHandlers = new Set(); 1.1392 + 1.1393 + function* runner() 1.1394 + { 1.1395 + for (let [i, entry] of inputTests.entries()) { 1.1396 + info("checkInput(" + i + "): " + entry.input); 1.1397 + yield checkInput(entry); 1.1398 + } 1.1399 + 1.1400 + for (let fn of eventHandlers) { 1.1401 + hud.jsterm.off("variablesview-open", fn); 1.1402 + } 1.1403 + } 1.1404 + 1.1405 + function* checkInput(entry) 1.1406 + { 1.1407 + yield checkConsoleLog(entry); 1.1408 + yield checkPrintOutput(entry); 1.1409 + yield checkJSEval(entry); 1.1410 + } 1.1411 + 1.1412 + function* checkConsoleLog(entry) 1.1413 + { 1.1414 + hud.jsterm.clearOutput(); 1.1415 + hud.jsterm.execute("console.log(" + entry.input + ")"); 1.1416 + 1.1417 + let [result] = yield waitForMessages({ 1.1418 + webconsole: hud, 1.1419 + messages: [{ 1.1420 + name: "console.log() output: " + entry.output, 1.1421 + text: entry.output, 1.1422 + category: CATEGORY_WEBDEV, 1.1423 + severity: SEVERITY_LOG, 1.1424 + }], 1.1425 + }); 1.1426 + 1.1427 + if (typeof entry.inspectorIcon == "boolean") { 1.1428 + let msg = [...result.matched][0]; 1.1429 + yield checkLinkToInspector(entry, msg); 1.1430 + } 1.1431 + } 1.1432 + 1.1433 + function checkPrintOutput(entry) 1.1434 + { 1.1435 + hud.jsterm.clearOutput(); 1.1436 + hud.jsterm.execute("print(" + entry.input + ")"); 1.1437 + 1.1438 + let printOutput = entry.printOutput || entry.output; 1.1439 + 1.1440 + return waitForMessages({ 1.1441 + webconsole: hud, 1.1442 + messages: [{ 1.1443 + name: "print() output: " + printOutput, 1.1444 + text: printOutput, 1.1445 + category: CATEGORY_OUTPUT, 1.1446 + }], 1.1447 + }); 1.1448 + } 1.1449 + 1.1450 + function* checkJSEval(entry) 1.1451 + { 1.1452 + hud.jsterm.clearOutput(); 1.1453 + hud.jsterm.execute(entry.input); 1.1454 + 1.1455 + let [result] = yield waitForMessages({ 1.1456 + webconsole: hud, 1.1457 + messages: [{ 1.1458 + name: "JS eval output: " + entry.output, 1.1459 + text: entry.output, 1.1460 + category: CATEGORY_OUTPUT, 1.1461 + }], 1.1462 + }); 1.1463 + 1.1464 + let msg = [...result.matched][0]; 1.1465 + if (!entry.noClick) { 1.1466 + yield checkObjectClick(entry, msg); 1.1467 + } 1.1468 + if (typeof entry.inspectorIcon == "boolean") { 1.1469 + yield checkLinkToInspector(entry, msg); 1.1470 + } 1.1471 + } 1.1472 + 1.1473 + function checkObjectClick(entry, msg) 1.1474 + { 1.1475 + let body = msg.querySelector(".message-body a") || 1.1476 + msg.querySelector(".message-body"); 1.1477 + ok(body, "the message body"); 1.1478 + 1.1479 + let deferred = promise.defer(); 1.1480 + 1.1481 + entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred); 1.1482 + hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen); 1.1483 + eventHandlers.add(entry._onVariablesViewOpen); 1.1484 + 1.1485 + body.scrollIntoView(); 1.1486 + EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow); 1.1487 + 1.1488 + if (entry.inspectable) { 1.1489 + info("message body tagName '" + body.tagName + "' className '" + body.className + "'"); 1.1490 + return deferred.promise; // wait for the panel to open if we need to. 1.1491 + } 1.1492 + 1.1493 + return promise.resolve(null); 1.1494 + } 1.1495 + 1.1496 + function checkLinkToInspector(entry, msg) 1.1497 + { 1.1498 + let elementNodeWidget = [...msg._messageObject.widgets][0]; 1.1499 + if (!elementNodeWidget) { 1.1500 + ok(!entry.inspectorIcon, "The message has no ElementNode widget"); 1.1501 + return; 1.1502 + } 1.1503 + 1.1504 + return elementNodeWidget.linkToInspector().then(() => { 1.1505 + // linkToInspector resolved, check for the .open-inspector element 1.1506 + if (entry.inspectorIcon) { 1.1507 + ok(msg.querySelectorAll(".open-inspector").length, 1.1508 + "The ElementNode widget is linked to the inspector"); 1.1509 + } else { 1.1510 + ok(!msg.querySelectorAll(".open-inspector").length, 1.1511 + "The ElementNode widget isn't linked to the inspector"); 1.1512 + } 1.1513 + }, () => { 1.1514 + // linkToInspector promise rejected, node not linked to inspector 1.1515 + ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector"); 1.1516 + }); 1.1517 + } 1.1518 + 1.1519 + function onVariablesViewOpen(entry, deferred, event, view, options) 1.1520 + { 1.1521 + let label = entry.variablesViewLabel || entry.output; 1.1522 + if (typeof label == "string" && options.label != label) { 1.1523 + return; 1.1524 + } 1.1525 + if (label instanceof RegExp && !label.test(options.label)) { 1.1526 + return; 1.1527 + } 1.1528 + 1.1529 + hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen); 1.1530 + eventHandlers.delete(entry._onVariablesViewOpen); 1.1531 + entry._onVariablesViewOpen = null; 1.1532 + 1.1533 + ok(entry.inspectable, "variables view was shown"); 1.1534 + 1.1535 + deferred.resolve(null); 1.1536 + } 1.1537 + 1.1538 + return Task.spawn(runner); 1.1539 +}