browser/devtools/webconsole/test/head.js

Wed, 31 Dec 2014 06:09:35 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 06:09:35 +0100
changeset 0
6474c204b198
permissions
-rw-r--r--

Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.

michael@0 1 /* vim:set ts=2 sw=2 sts=2 et: */
michael@0 2 /* This Source Code Form is subject to the terms of the Mozilla Public
michael@0 3 * License, v. 2.0. If a copy of the MPL was not distributed with this
michael@0 4 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
michael@0 5
michael@0 6 "use strict";
michael@0 7
michael@0 8 let {gDevTools} = Cu.import("resource:///modules/devtools/gDevTools.jsm", {});
michael@0 9 let {console} = Cu.import("resource://gre/modules/devtools/Console.jsm", {});
michael@0 10 let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
michael@0 11 let {Task} = Cu.import("resource://gre/modules/Task.jsm", {});
michael@0 12 let {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
michael@0 13 let {require, TargetFactory} = devtools;
michael@0 14 let {Utils: WebConsoleUtils} = require("devtools/toolkit/webconsole/utils");
michael@0 15 let {Messages} = require("devtools/webconsole/console-output");
michael@0 16
michael@0 17 // promise._reportErrors = true; // please never leave me.
michael@0 18
michael@0 19 let gPendingOutputTest = 0;
michael@0 20
michael@0 21 // The various categories of messages.
michael@0 22 const CATEGORY_NETWORK = 0;
michael@0 23 const CATEGORY_CSS = 1;
michael@0 24 const CATEGORY_JS = 2;
michael@0 25 const CATEGORY_WEBDEV = 3;
michael@0 26 const CATEGORY_INPUT = 4;
michael@0 27 const CATEGORY_OUTPUT = 5;
michael@0 28 const CATEGORY_SECURITY = 6;
michael@0 29
michael@0 30 // The possible message severities.
michael@0 31 const SEVERITY_ERROR = 0;
michael@0 32 const SEVERITY_WARNING = 1;
michael@0 33 const SEVERITY_INFO = 2;
michael@0 34 const SEVERITY_LOG = 3;
michael@0 35
michael@0 36 // The indent of a console group in pixels.
michael@0 37 const GROUP_INDENT = 12;
michael@0 38
michael@0 39 const WEBCONSOLE_STRINGS_URI = "chrome://browser/locale/devtools/webconsole.properties";
michael@0 40 let WCU_l10n = new WebConsoleUtils.l10n(WEBCONSOLE_STRINGS_URI);
michael@0 41
michael@0 42 gDevTools.testing = true;
michael@0 43 SimpleTest.registerCleanupFunction(() => {
michael@0 44 gDevTools.testing = false;
michael@0 45 });
michael@0 46
michael@0 47 function log(aMsg)
michael@0 48 {
michael@0 49 dump("*** WebConsoleTest: " + aMsg + "\n");
michael@0 50 }
michael@0 51
michael@0 52 function pprint(aObj)
michael@0 53 {
michael@0 54 for (let prop in aObj) {
michael@0 55 if (typeof aObj[prop] == "function") {
michael@0 56 log("function " + prop);
michael@0 57 }
michael@0 58 else {
michael@0 59 log(prop + ": " + aObj[prop]);
michael@0 60 }
michael@0 61 }
michael@0 62 }
michael@0 63
michael@0 64 let tab, browser, hudId, hud, hudBox, filterBox, outputNode, cs;
michael@0 65
michael@0 66 function addTab(aURL)
michael@0 67 {
michael@0 68 gBrowser.selectedTab = gBrowser.addTab(aURL);
michael@0 69 tab = gBrowser.selectedTab;
michael@0 70 browser = gBrowser.getBrowserForTab(tab);
michael@0 71 }
michael@0 72
michael@0 73 function loadTab(url) {
michael@0 74 let deferred = promise.defer();
michael@0 75
michael@0 76 let tab = gBrowser.selectedTab = gBrowser.addTab(url);
michael@0 77 let browser = gBrowser.getBrowserForTab(tab);
michael@0 78
michael@0 79 browser.addEventListener("load", function onLoad() {
michael@0 80 browser.removeEventListener("load", onLoad, true);
michael@0 81 deferred.resolve({tab: tab, browser: browser});
michael@0 82 }, true);
michael@0 83
michael@0 84 return deferred.promise;
michael@0 85 }
michael@0 86
michael@0 87 function afterAllTabsLoaded(callback, win) {
michael@0 88 win = win || window;
michael@0 89
michael@0 90 let stillToLoad = 0;
michael@0 91
michael@0 92 function onLoad() {
michael@0 93 this.removeEventListener("load", onLoad, true);
michael@0 94 stillToLoad--;
michael@0 95 if (!stillToLoad)
michael@0 96 callback();
michael@0 97 }
michael@0 98
michael@0 99 for (let a = 0; a < win.gBrowser.tabs.length; a++) {
michael@0 100 let browser = win.gBrowser.tabs[a].linkedBrowser;
michael@0 101 if (browser.webProgress.isLoadingDocument) {
michael@0 102 stillToLoad++;
michael@0 103 browser.addEventListener("load", onLoad, true);
michael@0 104 }
michael@0 105 }
michael@0 106
michael@0 107 if (!stillToLoad)
michael@0 108 callback();
michael@0 109 }
michael@0 110
michael@0 111 /**
michael@0 112 * Check if a log entry exists in the HUD output node.
michael@0 113 *
michael@0 114 * @param {Element} aOutputNode
michael@0 115 * the HUD output node.
michael@0 116 * @param {string} aMatchString
michael@0 117 * the string you want to check if it exists in the output node.
michael@0 118 * @param {string} aMsg
michael@0 119 * the message describing the test
michael@0 120 * @param {boolean} [aOnlyVisible=false]
michael@0 121 * find only messages that are visible, not hidden by the filter.
michael@0 122 * @param {boolean} [aFailIfFound=false]
michael@0 123 * fail the test if the string is found in the output node.
michael@0 124 * @param {string} aClass [optional]
michael@0 125 * find only messages with the given CSS class.
michael@0 126 */
michael@0 127 function testLogEntry(aOutputNode, aMatchString, aMsg, aOnlyVisible,
michael@0 128 aFailIfFound, aClass)
michael@0 129 {
michael@0 130 let selector = ".message";
michael@0 131 // Skip entries that are hidden by the filter.
michael@0 132 if (aOnlyVisible) {
michael@0 133 selector += ":not(.filtered-by-type):not(.filtered-by-string)";
michael@0 134 }
michael@0 135 if (aClass) {
michael@0 136 selector += "." + aClass;
michael@0 137 }
michael@0 138
michael@0 139 let msgs = aOutputNode.querySelectorAll(selector);
michael@0 140 let found = false;
michael@0 141 for (let i = 0, n = msgs.length; i < n; i++) {
michael@0 142 let message = msgs[i].textContent.indexOf(aMatchString);
michael@0 143 if (message > -1) {
michael@0 144 found = true;
michael@0 145 break;
michael@0 146 }
michael@0 147 }
michael@0 148
michael@0 149 is(found, !aFailIfFound, aMsg);
michael@0 150 }
michael@0 151
michael@0 152 /**
michael@0 153 * A convenience method to call testLogEntry().
michael@0 154 *
michael@0 155 * @param string aString
michael@0 156 * The string to find.
michael@0 157 */
michael@0 158 function findLogEntry(aString)
michael@0 159 {
michael@0 160 testLogEntry(outputNode, aString, "found " + aString);
michael@0 161 }
michael@0 162
michael@0 163 /**
michael@0 164 * Open the Web Console for the given tab.
michael@0 165 *
michael@0 166 * @param nsIDOMElement [aTab]
michael@0 167 * Optional tab element for which you want open the Web Console. The
michael@0 168 * default tab is taken from the global variable |tab|.
michael@0 169 * @param function [aCallback]
michael@0 170 * Optional function to invoke after the Web Console completes
michael@0 171 * initialization (web-console-created).
michael@0 172 * @return object
michael@0 173 * A promise that is resolved once the web console is open.
michael@0 174 */
michael@0 175 function openConsole(aTab, aCallback = function() { })
michael@0 176 {
michael@0 177 let deferred = promise.defer();
michael@0 178 let target = TargetFactory.forTab(aTab || tab);
michael@0 179 gDevTools.showToolbox(target, "webconsole").then(function(toolbox) {
michael@0 180 let hud = toolbox.getCurrentPanel().hud;
michael@0 181 hud.jsterm._lazyVariablesView = false;
michael@0 182 aCallback(hud);
michael@0 183 deferred.resolve(hud);
michael@0 184 });
michael@0 185 return deferred.promise;
michael@0 186 }
michael@0 187
michael@0 188 /**
michael@0 189 * Close the Web Console for the given tab.
michael@0 190 *
michael@0 191 * @param nsIDOMElement [aTab]
michael@0 192 * Optional tab element for which you want close the Web Console. The
michael@0 193 * default tab is taken from the global variable |tab|.
michael@0 194 * @param function [aCallback]
michael@0 195 * Optional function to invoke after the Web Console completes
michael@0 196 * closing (web-console-destroyed).
michael@0 197 * @return object
michael@0 198 * A promise that is resolved once the web console is closed.
michael@0 199 */
michael@0 200 function closeConsole(aTab, aCallback = function() { })
michael@0 201 {
michael@0 202 let target = TargetFactory.forTab(aTab || tab);
michael@0 203 let toolbox = gDevTools.getToolbox(target);
michael@0 204 if (toolbox) {
michael@0 205 let panel = toolbox.getPanel("webconsole");
michael@0 206 if (panel) {
michael@0 207 let hudId = panel.hud.hudId;
michael@0 208 return toolbox.destroy().then(aCallback.bind(null, hudId)).then(null, console.debug);
michael@0 209 }
michael@0 210 return toolbox.destroy().then(aCallback.bind(null));
michael@0 211 }
michael@0 212
michael@0 213 aCallback();
michael@0 214 return promise.resolve(null);
michael@0 215 }
michael@0 216
michael@0 217 /**
michael@0 218 * Wait for a context menu popup to open.
michael@0 219 *
michael@0 220 * @param nsIDOMElement aPopup
michael@0 221 * The XUL popup you expect to open.
michael@0 222 * @param nsIDOMElement aButton
michael@0 223 * The button/element that receives the contextmenu event. This is
michael@0 224 * expected to open the popup.
michael@0 225 * @param function aOnShown
michael@0 226 * Function to invoke on popupshown event.
michael@0 227 * @param function aOnHidden
michael@0 228 * Function to invoke on popuphidden event.
michael@0 229 */
michael@0 230 function waitForContextMenu(aPopup, aButton, aOnShown, aOnHidden)
michael@0 231 {
michael@0 232 function onPopupShown() {
michael@0 233 info("onPopupShown");
michael@0 234 aPopup.removeEventListener("popupshown", onPopupShown);
michael@0 235
michael@0 236 aOnShown();
michael@0 237
michael@0 238 // Use executeSoon() to get out of the popupshown event.
michael@0 239 aPopup.addEventListener("popuphidden", onPopupHidden);
michael@0 240 executeSoon(() => aPopup.hidePopup());
michael@0 241 }
michael@0 242 function onPopupHidden() {
michael@0 243 info("onPopupHidden");
michael@0 244 aPopup.removeEventListener("popuphidden", onPopupHidden);
michael@0 245 aOnHidden();
michael@0 246 }
michael@0 247
michael@0 248 aPopup.addEventListener("popupshown", onPopupShown);
michael@0 249
michael@0 250 info("wait for the context menu to open");
michael@0 251 let eventDetails = { type: "contextmenu", button: 2};
michael@0 252 EventUtils.synthesizeMouse(aButton, 2, 2, eventDetails,
michael@0 253 aButton.ownerDocument.defaultView);
michael@0 254 }
michael@0 255
michael@0 256 /**
michael@0 257 * Dump the output of all open Web Consoles - used only for debugging purposes.
michael@0 258 */
michael@0 259 function dumpConsoles()
michael@0 260 {
michael@0 261 if (gPendingOutputTest) {
michael@0 262 console.log("dumpConsoles start");
michael@0 263 for (let [, hud] of HUDService.consoles) {
michael@0 264 if (!hud.outputNode) {
michael@0 265 console.debug("no output content for", hud.hudId);
michael@0 266 continue;
michael@0 267 }
michael@0 268
michael@0 269 console.debug("output content for", hud.hudId);
michael@0 270 for (let elem of hud.outputNode.childNodes) {
michael@0 271 dumpMessageElement(elem);
michael@0 272 }
michael@0 273 }
michael@0 274 console.log("dumpConsoles end");
michael@0 275
michael@0 276 gPendingOutputTest = 0;
michael@0 277 }
michael@0 278 }
michael@0 279
michael@0 280 /**
michael@0 281 * Dump to output debug information for the given webconsole message.
michael@0 282 *
michael@0 283 * @param nsIDOMNode aMessage
michael@0 284 * The message element you want to display.
michael@0 285 */
michael@0 286 function dumpMessageElement(aMessage)
michael@0 287 {
michael@0 288 let text = aMessage.textContent;
michael@0 289 let repeats = aMessage.querySelector(".message-repeats");
michael@0 290 if (repeats) {
michael@0 291 repeats = repeats.getAttribute("value");
michael@0 292 }
michael@0 293 console.debug("id", aMessage.getAttribute("id"),
michael@0 294 "date", aMessage.timestamp,
michael@0 295 "class", aMessage.className,
michael@0 296 "category", aMessage.category,
michael@0 297 "severity", aMessage.severity,
michael@0 298 "repeats", repeats,
michael@0 299 "clipboardText", aMessage.clipboardText,
michael@0 300 "text", text);
michael@0 301 }
michael@0 302
michael@0 303 function finishTest()
michael@0 304 {
michael@0 305 browser = hudId = hud = filterBox = outputNode = cs = hudBox = null;
michael@0 306
michael@0 307 dumpConsoles();
michael@0 308
michael@0 309 let browserConsole = HUDService.getBrowserConsole();
michael@0 310 if (browserConsole) {
michael@0 311 if (browserConsole.jsterm) {
michael@0 312 browserConsole.jsterm.clearOutput(true);
michael@0 313 }
michael@0 314 HUDService.toggleBrowserConsole().then(finishTest);
michael@0 315 return;
michael@0 316 }
michael@0 317
michael@0 318 let hud = HUDService.getHudByWindow(content);
michael@0 319 if (!hud) {
michael@0 320 finish();
michael@0 321 return;
michael@0 322 }
michael@0 323
michael@0 324 if (hud.jsterm) {
michael@0 325 hud.jsterm.clearOutput(true);
michael@0 326 }
michael@0 327
michael@0 328 closeConsole(hud.target.tab, finish);
michael@0 329
michael@0 330 hud = null;
michael@0 331 }
michael@0 332
michael@0 333 function tearDown()
michael@0 334 {
michael@0 335 dumpConsoles();
michael@0 336
michael@0 337 if (HUDService.getBrowserConsole()) {
michael@0 338 HUDService.toggleBrowserConsole();
michael@0 339 }
michael@0 340
michael@0 341 let target = TargetFactory.forTab(gBrowser.selectedTab);
michael@0 342 gDevTools.closeToolbox(target);
michael@0 343 while (gBrowser.tabs.length > 1) {
michael@0 344 gBrowser.removeCurrentTab();
michael@0 345 }
michael@0 346 WCU_l10n = tab = browser = hudId = hud = filterBox = outputNode = cs = null;
michael@0 347 }
michael@0 348
michael@0 349 registerCleanupFunction(tearDown);
michael@0 350
michael@0 351 waitForExplicitFinish();
michael@0 352
michael@0 353 /**
michael@0 354 * Polls a given function waiting for it to become true.
michael@0 355 *
michael@0 356 * @param object aOptions
michael@0 357 * Options object with the following properties:
michael@0 358 * - validatorFn
michael@0 359 * A validator function that returns a boolean. This is called every few
michael@0 360 * milliseconds to check if the result is true. When it is true, succesFn
michael@0 361 * is called and polling stops. If validatorFn never returns true, then
michael@0 362 * polling timeouts after several tries and a failure is recorded.
michael@0 363 * - successFn
michael@0 364 * A function called when the validator function returns true.
michael@0 365 * - failureFn
michael@0 366 * A function called if the validator function timeouts - fails to return
michael@0 367 * true in the given time.
michael@0 368 * - name
michael@0 369 * Name of test. This is used to generate the success and failure
michael@0 370 * messages.
michael@0 371 * - timeout
michael@0 372 * Timeout for validator function, in milliseconds. Default is 5000.
michael@0 373 */
michael@0 374 function waitForSuccess(aOptions)
michael@0 375 {
michael@0 376 let start = Date.now();
michael@0 377 let timeout = aOptions.timeout || 5000;
michael@0 378
michael@0 379 function wait(validatorFn, successFn, failureFn)
michael@0 380 {
michael@0 381 if ((Date.now() - start) > timeout) {
michael@0 382 // Log the failure.
michael@0 383 ok(false, "Timed out while waiting for: " + aOptions.name);
michael@0 384 failureFn(aOptions);
michael@0 385 return;
michael@0 386 }
michael@0 387
michael@0 388 if (validatorFn(aOptions)) {
michael@0 389 ok(true, aOptions.name);
michael@0 390 successFn();
michael@0 391 }
michael@0 392 else {
michael@0 393 setTimeout(function() wait(validatorFn, successFn, failureFn), 100);
michael@0 394 }
michael@0 395 }
michael@0 396
michael@0 397 wait(aOptions.validatorFn, aOptions.successFn, aOptions.failureFn);
michael@0 398 }
michael@0 399
michael@0 400 function openInspector(aCallback, aTab = gBrowser.selectedTab)
michael@0 401 {
michael@0 402 let target = TargetFactory.forTab(aTab);
michael@0 403 gDevTools.showToolbox(target, "inspector").then(function(toolbox) {
michael@0 404 aCallback(toolbox.getCurrentPanel());
michael@0 405 });
michael@0 406 }
michael@0 407
michael@0 408 /**
michael@0 409 * Find variables or properties in a VariablesView instance.
michael@0 410 *
michael@0 411 * @param object aView
michael@0 412 * The VariablesView instance.
michael@0 413 * @param array aRules
michael@0 414 * The array of rules you want to match. Each rule is an object with:
michael@0 415 * - name (string|regexp): property name to match.
michael@0 416 * - value (string|regexp): property value to match.
michael@0 417 * - isIterator (boolean): check if the property is an iterator.
michael@0 418 * - isGetter (boolean): check if the property is a getter.
michael@0 419 * - isGenerator (boolean): check if the property is a generator.
michael@0 420 * - dontMatch (boolean): make sure the rule doesn't match any property.
michael@0 421 * @param object aOptions
michael@0 422 * Options for matching:
michael@0 423 * - webconsole: the WebConsole instance we work with.
michael@0 424 * @return object
michael@0 425 * A promise object that is resolved when all the rules complete
michael@0 426 * matching. The resolved callback is given an array of all the rules
michael@0 427 * you wanted to check. Each rule has a new property: |matchedProp|
michael@0 428 * which holds a reference to the Property object instance from the
michael@0 429 * VariablesView. If the rule did not match, then |matchedProp| is
michael@0 430 * undefined.
michael@0 431 */
michael@0 432 function findVariableViewProperties(aView, aRules, aOptions)
michael@0 433 {
michael@0 434 // Initialize the search.
michael@0 435 function init()
michael@0 436 {
michael@0 437 // Separate out the rules that require expanding properties throughout the
michael@0 438 // view.
michael@0 439 let expandRules = [];
michael@0 440 let rules = aRules.filter((aRule) => {
michael@0 441 if (typeof aRule.name == "string" && aRule.name.indexOf(".") > -1) {
michael@0 442 expandRules.push(aRule);
michael@0 443 return false;
michael@0 444 }
michael@0 445 return true;
michael@0 446 });
michael@0 447
michael@0 448 // Search through the view those rules that do not require any properties to
michael@0 449 // be expanded. Build the array of matchers, outstanding promises to be
michael@0 450 // resolved.
michael@0 451 let outstanding = [];
michael@0 452 finder(rules, aView, outstanding);
michael@0 453
michael@0 454 // Process the rules that need to expand properties.
michael@0 455 let lastStep = processExpandRules.bind(null, expandRules);
michael@0 456
michael@0 457 // Return the results - a promise resolved to hold the updated aRules array.
michael@0 458 let returnResults = onAllRulesMatched.bind(null, aRules);
michael@0 459
michael@0 460 return promise.all(outstanding).then(lastStep).then(returnResults);
michael@0 461 }
michael@0 462
michael@0 463 function onMatch(aProp, aRule, aMatched)
michael@0 464 {
michael@0 465 if (aMatched && !aRule.matchedProp) {
michael@0 466 aRule.matchedProp = aProp;
michael@0 467 }
michael@0 468 }
michael@0 469
michael@0 470 function finder(aRules, aVar, aPromises)
michael@0 471 {
michael@0 472 for (let [id, prop] of aVar) {
michael@0 473 for (let rule of aRules) {
michael@0 474 let matcher = matchVariablesViewProperty(prop, rule, aOptions);
michael@0 475 aPromises.push(matcher.then(onMatch.bind(null, prop, rule)));
michael@0 476 }
michael@0 477 }
michael@0 478 }
michael@0 479
michael@0 480 function processExpandRules(aRules)
michael@0 481 {
michael@0 482 let rule = aRules.shift();
michael@0 483 if (!rule) {
michael@0 484 return promise.resolve(null);
michael@0 485 }
michael@0 486
michael@0 487 let deferred = promise.defer();
michael@0 488 let expandOptions = {
michael@0 489 rootVariable: aView,
michael@0 490 expandTo: rule.name,
michael@0 491 webconsole: aOptions.webconsole,
michael@0 492 };
michael@0 493
michael@0 494 variablesViewExpandTo(expandOptions).then(function onSuccess(aProp) {
michael@0 495 let name = rule.name;
michael@0 496 let lastName = name.split(".").pop();
michael@0 497 rule.name = lastName;
michael@0 498
michael@0 499 let matched = matchVariablesViewProperty(aProp, rule, aOptions);
michael@0 500 return matched.then(onMatch.bind(null, aProp, rule)).then(function() {
michael@0 501 rule.name = name;
michael@0 502 });
michael@0 503 }, function onFailure() {
michael@0 504 return promise.resolve(null);
michael@0 505 }).then(processExpandRules.bind(null, aRules)).then(function() {
michael@0 506 deferred.resolve(null);
michael@0 507 });
michael@0 508
michael@0 509 return deferred.promise;
michael@0 510 }
michael@0 511
michael@0 512 function onAllRulesMatched(aRules)
michael@0 513 {
michael@0 514 for (let rule of aRules) {
michael@0 515 let matched = rule.matchedProp;
michael@0 516 if (matched && !rule.dontMatch) {
michael@0 517 ok(true, "rule " + rule.name + " matched for property " + matched.name);
michael@0 518 }
michael@0 519 else if (matched && rule.dontMatch) {
michael@0 520 ok(false, "rule " + rule.name + " should not match property " +
michael@0 521 matched.name);
michael@0 522 }
michael@0 523 else {
michael@0 524 ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
michael@0 525 }
michael@0 526 }
michael@0 527 return aRules;
michael@0 528 }
michael@0 529
michael@0 530 return init();
michael@0 531 }
michael@0 532
michael@0 533 /**
michael@0 534 * Check if a given Property object from the variables view matches the given
michael@0 535 * rule.
michael@0 536 *
michael@0 537 * @param object aProp
michael@0 538 * The variable's view Property instance.
michael@0 539 * @param object aRule
michael@0 540 * Rules for matching the property. See findVariableViewProperties() for
michael@0 541 * details.
michael@0 542 * @param object aOptions
michael@0 543 * Options for matching. See findVariableViewProperties().
michael@0 544 * @return object
michael@0 545 * A promise that is resolved when all the checks complete. Resolution
michael@0 546 * result is a boolean that tells your promise callback the match
michael@0 547 * result: true or false.
michael@0 548 */
michael@0 549 function matchVariablesViewProperty(aProp, aRule, aOptions)
michael@0 550 {
michael@0 551 function resolve(aResult) {
michael@0 552 return promise.resolve(aResult);
michael@0 553 }
michael@0 554
michael@0 555 if (aRule.name) {
michael@0 556 let match = aRule.name instanceof RegExp ?
michael@0 557 aRule.name.test(aProp.name) :
michael@0 558 aProp.name == aRule.name;
michael@0 559 if (!match) {
michael@0 560 return resolve(false);
michael@0 561 }
michael@0 562 }
michael@0 563
michael@0 564 if (aRule.value) {
michael@0 565 let displayValue = aProp.displayValue;
michael@0 566 if (aProp.displayValueClassName == "token-string") {
michael@0 567 displayValue = displayValue.substring(1, displayValue.length - 1);
michael@0 568 }
michael@0 569
michael@0 570 let match = aRule.value instanceof RegExp ?
michael@0 571 aRule.value.test(displayValue) :
michael@0 572 displayValue == aRule.value;
michael@0 573 if (!match) {
michael@0 574 info("rule " + aRule.name + " did not match value, expected '" +
michael@0 575 aRule.value + "', found '" + displayValue + "'");
michael@0 576 return resolve(false);
michael@0 577 }
michael@0 578 }
michael@0 579
michael@0 580 if ("isGetter" in aRule) {
michael@0 581 let isGetter = !!(aProp.getter && aProp.get("get"));
michael@0 582 if (aRule.isGetter != isGetter) {
michael@0 583 info("rule " + aRule.name + " getter test failed");
michael@0 584 return resolve(false);
michael@0 585 }
michael@0 586 }
michael@0 587
michael@0 588 if ("isGenerator" in aRule) {
michael@0 589 let isGenerator = aProp.displayValue == "Generator";
michael@0 590 if (aRule.isGenerator != isGenerator) {
michael@0 591 info("rule " + aRule.name + " generator test failed");
michael@0 592 return resolve(false);
michael@0 593 }
michael@0 594 }
michael@0 595
michael@0 596 let outstanding = [];
michael@0 597
michael@0 598 if ("isIterator" in aRule) {
michael@0 599 let isIterator = isVariableViewPropertyIterator(aProp, aOptions.webconsole);
michael@0 600 outstanding.push(isIterator.then((aResult) => {
michael@0 601 if (aResult != aRule.isIterator) {
michael@0 602 info("rule " + aRule.name + " iterator test failed");
michael@0 603 }
michael@0 604 return aResult == aRule.isIterator;
michael@0 605 }));
michael@0 606 }
michael@0 607
michael@0 608 outstanding.push(promise.resolve(true));
michael@0 609
michael@0 610 return promise.all(outstanding).then(function _onMatchDone(aResults) {
michael@0 611 let ruleMatched = aResults.indexOf(false) == -1;
michael@0 612 return resolve(ruleMatched);
michael@0 613 });
michael@0 614 }
michael@0 615
michael@0 616 /**
michael@0 617 * Check if the given variables view property is an iterator.
michael@0 618 *
michael@0 619 * @param object aProp
michael@0 620 * The Property instance you want to check.
michael@0 621 * @param object aWebConsole
michael@0 622 * The WebConsole instance to work with.
michael@0 623 * @return object
michael@0 624 * A promise that is resolved when the check completes. The resolved
michael@0 625 * callback is given a boolean: true if the property is an iterator, or
michael@0 626 * false otherwise.
michael@0 627 */
michael@0 628 function isVariableViewPropertyIterator(aProp, aWebConsole)
michael@0 629 {
michael@0 630 if (aProp.displayValue == "Iterator") {
michael@0 631 return promise.resolve(true);
michael@0 632 }
michael@0 633
michael@0 634 let deferred = promise.defer();
michael@0 635
michael@0 636 variablesViewExpandTo({
michael@0 637 rootVariable: aProp,
michael@0 638 expandTo: "__proto__.__iterator__",
michael@0 639 webconsole: aWebConsole,
michael@0 640 }).then(function onSuccess(aProp) {
michael@0 641 deferred.resolve(true);
michael@0 642 }, function onFailure() {
michael@0 643 deferred.resolve(false);
michael@0 644 });
michael@0 645
michael@0 646 return deferred.promise;
michael@0 647 }
michael@0 648
michael@0 649
michael@0 650 /**
michael@0 651 * Recursively expand the variables view up to a given property.
michael@0 652 *
michael@0 653 * @param aOptions
michael@0 654 * Options for view expansion:
michael@0 655 * - rootVariable: start from the given scope/variable/property.
michael@0 656 * - expandTo: string made up of property names you want to expand.
michael@0 657 * For example: "body.firstChild.nextSibling" given |rootVariable:
michael@0 658 * document|.
michael@0 659 * - webconsole: a WebConsole instance. If this is not provided all
michael@0 660 * property expand() calls will be considered sync. Things may fail!
michael@0 661 * @return object
michael@0 662 * A promise that is resolved only when the last property in |expandTo|
michael@0 663 * is found, and rejected otherwise. Resolution reason is always the
michael@0 664 * last property - |nextSibling| in the example above. Rejection is
michael@0 665 * always the last property that was found.
michael@0 666 */
michael@0 667 function variablesViewExpandTo(aOptions)
michael@0 668 {
michael@0 669 let root = aOptions.rootVariable;
michael@0 670 let expandTo = aOptions.expandTo.split(".");
michael@0 671 let jsterm = (aOptions.webconsole || {}).jsterm;
michael@0 672 let lastDeferred = promise.defer();
michael@0 673
michael@0 674 function fetch(aProp)
michael@0 675 {
michael@0 676 if (!aProp.onexpand) {
michael@0 677 ok(false, "property " + aProp.name + " cannot be expanded: !onexpand");
michael@0 678 return promise.reject(aProp);
michael@0 679 }
michael@0 680
michael@0 681 let deferred = promise.defer();
michael@0 682
michael@0 683 if (aProp._fetched || !jsterm) {
michael@0 684 executeSoon(function() {
michael@0 685 deferred.resolve(aProp);
michael@0 686 });
michael@0 687 }
michael@0 688 else {
michael@0 689 jsterm.once("variablesview-fetched", function _onFetchProp() {
michael@0 690 executeSoon(() => deferred.resolve(aProp));
michael@0 691 });
michael@0 692 }
michael@0 693
michael@0 694 aProp.expand();
michael@0 695
michael@0 696 return deferred.promise;
michael@0 697 }
michael@0 698
michael@0 699 function getNext(aProp)
michael@0 700 {
michael@0 701 let name = expandTo.shift();
michael@0 702 let newProp = aProp.get(name);
michael@0 703
michael@0 704 if (expandTo.length > 0) {
michael@0 705 ok(newProp, "found property " + name);
michael@0 706 if (newProp) {
michael@0 707 fetch(newProp).then(getNext, fetchError);
michael@0 708 }
michael@0 709 else {
michael@0 710 lastDeferred.reject(aProp);
michael@0 711 }
michael@0 712 }
michael@0 713 else {
michael@0 714 if (newProp) {
michael@0 715 lastDeferred.resolve(newProp);
michael@0 716 }
michael@0 717 else {
michael@0 718 lastDeferred.reject(aProp);
michael@0 719 }
michael@0 720 }
michael@0 721 }
michael@0 722
michael@0 723 function fetchError(aProp)
michael@0 724 {
michael@0 725 lastDeferred.reject(aProp);
michael@0 726 }
michael@0 727
michael@0 728 if (!root._fetched) {
michael@0 729 fetch(root).then(getNext, fetchError);
michael@0 730 }
michael@0 731 else {
michael@0 732 getNext(root);
michael@0 733 }
michael@0 734
michael@0 735 return lastDeferred.promise;
michael@0 736 }
michael@0 737
michael@0 738
michael@0 739 /**
michael@0 740 * Update the content of a property in the variables view.
michael@0 741 *
michael@0 742 * @param object aOptions
michael@0 743 * Options for the property update:
michael@0 744 * - property: the property you want to change.
michael@0 745 * - field: string that tells what you want to change:
michael@0 746 * - use "name" to change the property name,
michael@0 747 * - or "value" to change the property value.
michael@0 748 * - string: the new string to write into the field.
michael@0 749 * - webconsole: reference to the Web Console instance we work with.
michael@0 750 * - callback: function to invoke after the property is updated.
michael@0 751 */
michael@0 752 function updateVariablesViewProperty(aOptions)
michael@0 753 {
michael@0 754 let view = aOptions.property._variablesView;
michael@0 755 view.window.focus();
michael@0 756 aOptions.property.focus();
michael@0 757
michael@0 758 switch (aOptions.field) {
michael@0 759 case "name":
michael@0 760 EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window);
michael@0 761 break;
michael@0 762 case "value":
michael@0 763 EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
michael@0 764 break;
michael@0 765 default:
michael@0 766 throw new Error("options.field is incorrect");
michael@0 767 return;
michael@0 768 }
michael@0 769
michael@0 770 executeSoon(() => {
michael@0 771 EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
michael@0 772
michael@0 773 for (let c of aOptions.string) {
michael@0 774 EventUtils.synthesizeKey(c, {}, gVariablesView.window);
michael@0 775 }
michael@0 776
michael@0 777 if (aOptions.webconsole) {
michael@0 778 aOptions.webconsole.jsterm.once("variablesview-fetched", aOptions.callback);
michael@0 779 }
michael@0 780
michael@0 781 EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
michael@0 782
michael@0 783 if (!aOptions.webconsole) {
michael@0 784 executeSoon(aOptions.callback);
michael@0 785 }
michael@0 786 });
michael@0 787 }
michael@0 788
michael@0 789 /**
michael@0 790 * Open the JavaScript debugger.
michael@0 791 *
michael@0 792 * @param object aOptions
michael@0 793 * Options for opening the debugger:
michael@0 794 * - tab: the tab you want to open the debugger for.
michael@0 795 * @return object
michael@0 796 * A promise that is resolved once the debugger opens, or rejected if
michael@0 797 * the open fails. The resolution callback is given one argument, an
michael@0 798 * object that holds the following properties:
michael@0 799 * - target: the Target object for the Tab.
michael@0 800 * - toolbox: the Toolbox instance.
michael@0 801 * - panel: the jsdebugger panel instance.
michael@0 802 * - panelWin: the window object of the panel iframe.
michael@0 803 */
michael@0 804 function openDebugger(aOptions = {})
michael@0 805 {
michael@0 806 if (!aOptions.tab) {
michael@0 807 aOptions.tab = gBrowser.selectedTab;
michael@0 808 }
michael@0 809
michael@0 810 let deferred = promise.defer();
michael@0 811
michael@0 812 let target = TargetFactory.forTab(aOptions.tab);
michael@0 813 let toolbox = gDevTools.getToolbox(target);
michael@0 814 let dbgPanelAlreadyOpen = toolbox.getPanel("jsdebugger");
michael@0 815
michael@0 816 gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(aToolbox) {
michael@0 817 let panel = aToolbox.getCurrentPanel();
michael@0 818 let panelWin = panel.panelWin;
michael@0 819
michael@0 820 panel._view.Variables.lazyEmpty = false;
michael@0 821
michael@0 822 let resolveObject = {
michael@0 823 target: target,
michael@0 824 toolbox: aToolbox,
michael@0 825 panel: panel,
michael@0 826 panelWin: panelWin,
michael@0 827 };
michael@0 828
michael@0 829 if (dbgPanelAlreadyOpen) {
michael@0 830 deferred.resolve(resolveObject);
michael@0 831 }
michael@0 832 else {
michael@0 833 panelWin.once(panelWin.EVENTS.SOURCES_ADDED, () => {
michael@0 834 deferred.resolve(resolveObject);
michael@0 835 });
michael@0 836 }
michael@0 837 }, function onFailure(aReason) {
michael@0 838 console.debug("failed to open the toolbox for 'jsdebugger'", aReason);
michael@0 839 deferred.reject(aReason);
michael@0 840 });
michael@0 841
michael@0 842 return deferred.promise;
michael@0 843 }
michael@0 844
michael@0 845 /**
michael@0 846 * Wait for messages in the Web Console output.
michael@0 847 *
michael@0 848 * @param object aOptions
michael@0 849 * Options for what you want to wait for:
michael@0 850 * - webconsole: the webconsole instance you work with.
michael@0 851 * - matchCondition: "any" or "all". Default: "all". The promise
michael@0 852 * returned by this function resolves when all of the messages are
michael@0 853 * matched, if the |matchCondition| is "all". If you set the condition to
michael@0 854 * "any" then the promise is resolved by any message rule that matches,
michael@0 855 * irrespective of order - waiting for messages stops whenever any rule
michael@0 856 * matches.
michael@0 857 * - messages: an array of objects that tells which messages to wait for.
michael@0 858 * Properties:
michael@0 859 * - text: string or RegExp to match the textContent of each new
michael@0 860 * message.
michael@0 861 * - noText: string or RegExp that must not match in the message
michael@0 862 * textContent.
michael@0 863 * - repeats: the number of message repeats, as displayed by the Web
michael@0 864 * Console.
michael@0 865 * - category: match message category. See CATEGORY_* constants at
michael@0 866 * the top of this file.
michael@0 867 * - severity: match message severity. See SEVERITY_* constants at
michael@0 868 * the top of this file.
michael@0 869 * - count: how many unique web console messages should be matched by
michael@0 870 * this rule.
michael@0 871 * - consoleTrace: boolean, set to |true| to match a console.trace()
michael@0 872 * message. Optionally this can be an object of the form
michael@0 873 * { file, fn, line } that can match the specified file, function
michael@0 874 * and/or line number in the trace message.
michael@0 875 * - consoleTime: string that matches a console.time() timer name.
michael@0 876 * Provide this if you want to match a console.time() message.
michael@0 877 * - consoleTimeEnd: same as above, but for console.timeEnd().
michael@0 878 * - consoleDir: boolean, set to |true| to match a console.dir()
michael@0 879 * message.
michael@0 880 * - consoleGroup: boolean, set to |true| to match a console.group()
michael@0 881 * message.
michael@0 882 * - longString: boolean, set to |true} to match long strings in the
michael@0 883 * message.
michael@0 884 * - collapsible: boolean, set to |true| to match messages that can
michael@0 885 * be collapsed/expanded.
michael@0 886 * - type: match messages that are instances of the given object. For
michael@0 887 * example, you can point to Messages.NavigationMarker to match any
michael@0 888 * such message.
michael@0 889 * - objects: boolean, set to |true| if you expect inspectable
michael@0 890 * objects in the message.
michael@0 891 * - source: object of the shape { url, line }. This is used to
michael@0 892 * match the source URL and line number of the error message or
michael@0 893 * console API call.
michael@0 894 * - stacktrace: array of objects of the form { file, fn, line } that
michael@0 895 * can match frames in the stacktrace associated with the message.
michael@0 896 * - groupDepth: number used to check the depth of the message in
michael@0 897 * a group.
michael@0 898 * - url: URL to match for network requests.
michael@0 899 * @return object
michael@0 900 * A promise object is returned once the messages you want are found.
michael@0 901 * The promise is resolved with the array of rule objects you give in
michael@0 902 * the |messages| property. Each objects is the same as provided, with
michael@0 903 * additional properties:
michael@0 904 * - matched: a Set of web console messages that matched the rule.
michael@0 905 * - clickableElements: a list of inspectable objects. This is available
michael@0 906 * if any of the following properties are present in the rule:
michael@0 907 * |consoleTrace| or |objects|.
michael@0 908 * - longStrings: a list of long string ellipsis elements you can click
michael@0 909 * in the message element, to expand a long string. This is available
michael@0 910 * only if |longString| is present in the matching rule.
michael@0 911 */
michael@0 912 function waitForMessages(aOptions)
michael@0 913 {
michael@0 914 gPendingOutputTest++;
michael@0 915 let webconsole = aOptions.webconsole;
michael@0 916 let rules = WebConsoleUtils.cloneObject(aOptions.messages, true);
michael@0 917 let rulesMatched = 0;
michael@0 918 let listenerAdded = false;
michael@0 919 let deferred = promise.defer();
michael@0 920 aOptions.matchCondition = aOptions.matchCondition || "all";
michael@0 921
michael@0 922 function checkText(aRule, aText)
michael@0 923 {
michael@0 924 let result = false;
michael@0 925 if (Array.isArray(aRule)) {
michael@0 926 result = aRule.every((s) => checkText(s, aText));
michael@0 927 }
michael@0 928 else if (typeof aRule == "string") {
michael@0 929 result = aText.indexOf(aRule) > -1;
michael@0 930 }
michael@0 931 else if (aRule instanceof RegExp) {
michael@0 932 result = aRule.test(aText);
michael@0 933 }
michael@0 934 else {
michael@0 935 result = aRule == aText;
michael@0 936 }
michael@0 937 return result;
michael@0 938 }
michael@0 939
michael@0 940 function checkConsoleTrace(aRule, aElement)
michael@0 941 {
michael@0 942 let elemText = aElement.textContent;
michael@0 943 let trace = aRule.consoleTrace;
michael@0 944
michael@0 945 if (!checkText("console.trace():", elemText)) {
michael@0 946 return false;
michael@0 947 }
michael@0 948
michael@0 949 aRule.category = CATEGORY_WEBDEV;
michael@0 950 aRule.severity = SEVERITY_LOG;
michael@0 951 aRule.type = Messages.ConsoleTrace;
michael@0 952
michael@0 953 if (!aRule.stacktrace && typeof trace == "object" && trace !== true) {
michael@0 954 if (Array.isArray(trace)) {
michael@0 955 aRule.stacktrace = trace;
michael@0 956 } else {
michael@0 957 aRule.stacktrace = [trace];
michael@0 958 }
michael@0 959 }
michael@0 960
michael@0 961 return true;
michael@0 962 }
michael@0 963
michael@0 964 function checkConsoleTime(aRule, aElement)
michael@0 965 {
michael@0 966 let elemText = aElement.textContent;
michael@0 967 let time = aRule.consoleTime;
michael@0 968
michael@0 969 if (!checkText(time + ": timer started", elemText)) {
michael@0 970 return false;
michael@0 971 }
michael@0 972
michael@0 973 aRule.category = CATEGORY_WEBDEV;
michael@0 974 aRule.severity = SEVERITY_LOG;
michael@0 975
michael@0 976 return true;
michael@0 977 }
michael@0 978
michael@0 979 function checkConsoleTimeEnd(aRule, aElement)
michael@0 980 {
michael@0 981 let elemText = aElement.textContent;
michael@0 982 let time = aRule.consoleTimeEnd;
michael@0 983 let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms");
michael@0 984
michael@0 985 if (!checkText(regex, elemText)) {
michael@0 986 return false;
michael@0 987 }
michael@0 988
michael@0 989 aRule.category = CATEGORY_WEBDEV;
michael@0 990 aRule.severity = SEVERITY_LOG;
michael@0 991
michael@0 992 return true;
michael@0 993 }
michael@0 994
michael@0 995 function checkConsoleDir(aRule, aElement)
michael@0 996 {
michael@0 997 if (!aElement.classList.contains("inlined-variables-view")) {
michael@0 998 return false;
michael@0 999 }
michael@0 1000
michael@0 1001 let elemText = aElement.textContent;
michael@0 1002 if (!checkText(aRule.consoleDir, elemText)) {
michael@0 1003 return false;
michael@0 1004 }
michael@0 1005
michael@0 1006 let iframe = aElement.querySelector("iframe");
michael@0 1007 if (!iframe) {
michael@0 1008 ok(false, "console.dir message has no iframe");
michael@0 1009 return false;
michael@0 1010 }
michael@0 1011
michael@0 1012 return true;
michael@0 1013 }
michael@0 1014
michael@0 1015 function checkConsoleGroup(aRule, aElement)
michael@0 1016 {
michael@0 1017 if (!isNaN(parseInt(aRule.consoleGroup))) {
michael@0 1018 aRule.groupDepth = aRule.consoleGroup;
michael@0 1019 }
michael@0 1020 aRule.category = CATEGORY_WEBDEV;
michael@0 1021 aRule.severity = SEVERITY_LOG;
michael@0 1022
michael@0 1023 return true;
michael@0 1024 }
michael@0 1025
michael@0 1026 function checkSource(aRule, aElement)
michael@0 1027 {
michael@0 1028 let location = aElement.querySelector(".message-location");
michael@0 1029 if (!location) {
michael@0 1030 return false;
michael@0 1031 }
michael@0 1032
michael@0 1033 if (!checkText(aRule.source.url, location.getAttribute("title"))) {
michael@0 1034 return false;
michael@0 1035 }
michael@0 1036
michael@0 1037 if ("line" in aRule.source && location.sourceLine != aRule.source.line) {
michael@0 1038 return false;
michael@0 1039 }
michael@0 1040
michael@0 1041 return true;
michael@0 1042 }
michael@0 1043
michael@0 1044 function checkCollapsible(aRule, aElement)
michael@0 1045 {
michael@0 1046 let msg = aElement._messageObject;
michael@0 1047 if (!msg || !!msg.collapsible != aRule.collapsible) {
michael@0 1048 return false;
michael@0 1049 }
michael@0 1050
michael@0 1051 return true;
michael@0 1052 }
michael@0 1053
michael@0 1054 function checkStacktrace(aRule, aElement)
michael@0 1055 {
michael@0 1056 let stack = aRule.stacktrace;
michael@0 1057 let frames = aElement.querySelectorAll(".stacktrace > li");
michael@0 1058 if (!frames.length) {
michael@0 1059 return false;
michael@0 1060 }
michael@0 1061
michael@0 1062 for (let i = 0; i < stack.length; i++) {
michael@0 1063 let frame = frames[i];
michael@0 1064 let expected = stack[i];
michael@0 1065 if (!frame) {
michael@0 1066 ok(false, "expected frame #" + i + " but didnt find it");
michael@0 1067 return false;
michael@0 1068 }
michael@0 1069
michael@0 1070 if (expected.file) {
michael@0 1071 let file = frame.querySelector(".message-location").title;
michael@0 1072 if (!checkText(expected.file, file)) {
michael@0 1073 ok(false, "frame #" + i + " does not match file name: " +
michael@0 1074 expected.file);
michael@0 1075 displayErrorContext(aRule, aElement);
michael@0 1076 return false;
michael@0 1077 }
michael@0 1078 }
michael@0 1079
michael@0 1080 if (expected.fn) {
michael@0 1081 let fn = frame.querySelector(".function").textContent;
michael@0 1082 if (!checkText(expected.fn, fn)) {
michael@0 1083 ok(false, "frame #" + i + " does not match the function name: " +
michael@0 1084 expected.fn);
michael@0 1085 displayErrorContext(aRule, aElement);
michael@0 1086 return false;
michael@0 1087 }
michael@0 1088 }
michael@0 1089
michael@0 1090 if (expected.line) {
michael@0 1091 let line = frame.querySelector(".message-location").sourceLine;
michael@0 1092 if (!checkText(expected.line, line)) {
michael@0 1093 ok(false, "frame #" + i + " does not match the line number: " +
michael@0 1094 expected.line);
michael@0 1095 displayErrorContext(aRule, aElement);
michael@0 1096 return false;
michael@0 1097 }
michael@0 1098 }
michael@0 1099 }
michael@0 1100
michael@0 1101 return true;
michael@0 1102 }
michael@0 1103
michael@0 1104 function checkMessage(aRule, aElement)
michael@0 1105 {
michael@0 1106 let elemText = aElement.textContent;
michael@0 1107
michael@0 1108 if (aRule.text && !checkText(aRule.text, elemText)) {
michael@0 1109 return false;
michael@0 1110 }
michael@0 1111
michael@0 1112 if (aRule.noText && checkText(aRule.noText, elemText)) {
michael@0 1113 return false;
michael@0 1114 }
michael@0 1115
michael@0 1116 if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) {
michael@0 1117 return false;
michael@0 1118 }
michael@0 1119
michael@0 1120 if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) {
michael@0 1121 return false;
michael@0 1122 }
michael@0 1123
michael@0 1124 if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) {
michael@0 1125 return false;
michael@0 1126 }
michael@0 1127
michael@0 1128 if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) {
michael@0 1129 return false;
michael@0 1130 }
michael@0 1131
michael@0 1132 if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) {
michael@0 1133 return false;
michael@0 1134 }
michael@0 1135
michael@0 1136 if (aRule.source && !checkSource(aRule, aElement)) {
michael@0 1137 return false;
michael@0 1138 }
michael@0 1139
michael@0 1140 if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) {
michael@0 1141 return false;
michael@0 1142 }
michael@0 1143
michael@0 1144 let partialMatch = !!(aRule.consoleTrace || aRule.consoleTime ||
michael@0 1145 aRule.consoleTimeEnd);
michael@0 1146
michael@0 1147 // The rule tries to match the newer types of messages, based on their
michael@0 1148 // object constructor.
michael@0 1149 if (aRule.type) {
michael@0 1150 if (!aElement._messageObject ||
michael@0 1151 !(aElement._messageObject instanceof aRule.type)) {
michael@0 1152 if (partialMatch) {
michael@0 1153 ok(false, "message type for rule: " + displayRule(aRule));
michael@0 1154 displayErrorContext(aRule, aElement);
michael@0 1155 }
michael@0 1156 return false;
michael@0 1157 }
michael@0 1158 partialMatch = true;
michael@0 1159 }
michael@0 1160
michael@0 1161 if ("category" in aRule && aElement.category != aRule.category) {
michael@0 1162 if (partialMatch) {
michael@0 1163 is(aElement.category, aRule.category,
michael@0 1164 "message category for rule: " + displayRule(aRule));
michael@0 1165 displayErrorContext(aRule, aElement);
michael@0 1166 }
michael@0 1167 return false;
michael@0 1168 }
michael@0 1169
michael@0 1170 if ("severity" in aRule && aElement.severity != aRule.severity) {
michael@0 1171 if (partialMatch) {
michael@0 1172 is(aElement.severity, aRule.severity,
michael@0 1173 "message severity for rule: " + displayRule(aRule));
michael@0 1174 displayErrorContext(aRule, aElement);
michael@0 1175 }
michael@0 1176 return false;
michael@0 1177 }
michael@0 1178
michael@0 1179 if (aRule.text) {
michael@0 1180 partialMatch = true;
michael@0 1181 }
michael@0 1182
michael@0 1183 if (aRule.stacktrace && !checkStacktrace(aRule, aElement)) {
michael@0 1184 if (partialMatch) {
michael@0 1185 ok(false, "failed to match stacktrace for rule: " + displayRule(aRule));
michael@0 1186 displayErrorContext(aRule, aElement);
michael@0 1187 }
michael@0 1188 return false;
michael@0 1189 }
michael@0 1190
michael@0 1191 if (aRule.category == CATEGORY_NETWORK && "url" in aRule &&
michael@0 1192 !checkText(aRule.url, aElement.url)) {
michael@0 1193 return false;
michael@0 1194 }
michael@0 1195
michael@0 1196 if ("repeats" in aRule) {
michael@0 1197 let repeats = aElement.querySelector(".message-repeats");
michael@0 1198 if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
michael@0 1199 return false;
michael@0 1200 }
michael@0 1201 }
michael@0 1202
michael@0 1203 if ("groupDepth" in aRule) {
michael@0 1204 let indentNode = aElement.querySelector(".indent");
michael@0 1205 let indent = (GROUP_INDENT * aRule.groupDepth) + "px";
michael@0 1206 if (!indentNode || indentNode.style.width != indent) {
michael@0 1207 is(indentNode.style.width, indent,
michael@0 1208 "group depth check failed for message rule: " + displayRule(aRule));
michael@0 1209 return false;
michael@0 1210 }
michael@0 1211 }
michael@0 1212
michael@0 1213 if ("longString" in aRule) {
michael@0 1214 let longStrings = aElement.querySelectorAll(".longStringEllipsis");
michael@0 1215 if (aRule.longString != !!longStrings[0]) {
michael@0 1216 if (partialMatch) {
michael@0 1217 is(!!longStrings[0], aRule.longString,
michael@0 1218 "long string existence check failed for message rule: " +
michael@0 1219 displayRule(aRule));
michael@0 1220 displayErrorContext(aRule, aElement);
michael@0 1221 }
michael@0 1222 return false;
michael@0 1223 }
michael@0 1224 aRule.longStrings = longStrings;
michael@0 1225 }
michael@0 1226
michael@0 1227 if ("objects" in aRule) {
michael@0 1228 let clickables = aElement.querySelectorAll(".message-body a");
michael@0 1229 if (aRule.objects != !!clickables[0]) {
michael@0 1230 if (partialMatch) {
michael@0 1231 is(!!clickables[0], aRule.objects,
michael@0 1232 "objects existence check failed for message rule: " +
michael@0 1233 displayRule(aRule));
michael@0 1234 displayErrorContext(aRule, aElement);
michael@0 1235 }
michael@0 1236 return false;
michael@0 1237 }
michael@0 1238 aRule.clickableElements = clickables;
michael@0 1239 }
michael@0 1240
michael@0 1241 let count = aRule.count || 1;
michael@0 1242 if (!aRule.matched) {
michael@0 1243 aRule.matched = new Set();
michael@0 1244 }
michael@0 1245 aRule.matched.add(aElement);
michael@0 1246
michael@0 1247 return aRule.matched.size == count;
michael@0 1248 }
michael@0 1249
michael@0 1250 function onMessagesAdded(aEvent, aNewElements)
michael@0 1251 {
michael@0 1252 for (let elem of aNewElements) {
michael@0 1253 let location = elem.querySelector(".message-location");
michael@0 1254 if (location) {
michael@0 1255 let url = location.title;
michael@0 1256 // Prevent recursion with the browser console and any potential
michael@0 1257 // messages coming from head.js.
michael@0 1258 if (url.indexOf("browser/devtools/webconsole/test/head.js") != -1) {
michael@0 1259 continue;
michael@0 1260 }
michael@0 1261 }
michael@0 1262
michael@0 1263 for (let rule of rules) {
michael@0 1264 if (rule._ruleMatched) {
michael@0 1265 continue;
michael@0 1266 }
michael@0 1267
michael@0 1268 let matched = checkMessage(rule, elem);
michael@0 1269 if (matched) {
michael@0 1270 rule._ruleMatched = true;
michael@0 1271 rulesMatched++;
michael@0 1272 ok(1, "matched rule: " + displayRule(rule));
michael@0 1273 if (maybeDone()) {
michael@0 1274 return;
michael@0 1275 }
michael@0 1276 }
michael@0 1277 }
michael@0 1278 }
michael@0 1279 }
michael@0 1280
michael@0 1281 function allRulesMatched()
michael@0 1282 {
michael@0 1283 return aOptions.matchCondition == "all" && rulesMatched == rules.length ||
michael@0 1284 aOptions.matchCondition == "any" && rulesMatched > 0;
michael@0 1285 }
michael@0 1286
michael@0 1287 function maybeDone()
michael@0 1288 {
michael@0 1289 if (allRulesMatched()) {
michael@0 1290 if (listenerAdded) {
michael@0 1291 webconsole.ui.off("messages-added", onMessagesAdded);
michael@0 1292 webconsole.ui.off("messages-updated", onMessagesAdded);
michael@0 1293 }
michael@0 1294 gPendingOutputTest--;
michael@0 1295 deferred.resolve(rules);
michael@0 1296 return true;
michael@0 1297 }
michael@0 1298 return false;
michael@0 1299 }
michael@0 1300
michael@0 1301 function testCleanup() {
michael@0 1302 if (allRulesMatched()) {
michael@0 1303 return;
michael@0 1304 }
michael@0 1305
michael@0 1306 if (webconsole.ui) {
michael@0 1307 webconsole.ui.off("messages-added", onMessagesAdded);
michael@0 1308 }
michael@0 1309
michael@0 1310 for (let rule of rules) {
michael@0 1311 if (!rule._ruleMatched) {
michael@0 1312 ok(false, "failed to match rule: " + displayRule(rule));
michael@0 1313 }
michael@0 1314 }
michael@0 1315 }
michael@0 1316
michael@0 1317 function displayRule(aRule)
michael@0 1318 {
michael@0 1319 return aRule.name || aRule.text;
michael@0 1320 }
michael@0 1321
michael@0 1322 function displayErrorContext(aRule, aElement)
michael@0 1323 {
michael@0 1324 console.log("error occured during rule " + displayRule(aRule));
michael@0 1325 console.log("while checking the following message");
michael@0 1326 dumpMessageElement(aElement);
michael@0 1327 }
michael@0 1328
michael@0 1329 executeSoon(() => {
michael@0 1330 onMessagesAdded("messages-added", webconsole.outputNode.childNodes);
michael@0 1331 if (!allRulesMatched()) {
michael@0 1332 listenerAdded = true;
michael@0 1333 registerCleanupFunction(testCleanup);
michael@0 1334 webconsole.ui.on("messages-added", onMessagesAdded);
michael@0 1335 webconsole.ui.on("messages-updated", onMessagesAdded);
michael@0 1336 }
michael@0 1337 });
michael@0 1338
michael@0 1339 return deferred.promise;
michael@0 1340 }
michael@0 1341
michael@0 1342 function whenDelayedStartupFinished(aWindow, aCallback)
michael@0 1343 {
michael@0 1344 Services.obs.addObserver(function observer(aSubject, aTopic) {
michael@0 1345 if (aWindow == aSubject) {
michael@0 1346 Services.obs.removeObserver(observer, aTopic);
michael@0 1347 executeSoon(aCallback);
michael@0 1348 }
michael@0 1349 }, "browser-delayed-startup-finished", false);
michael@0 1350 }
michael@0 1351
michael@0 1352 /**
michael@0 1353 * Check the web console output for the given inputs. Each input is checked for
michael@0 1354 * the expected JS eval result, the result of calling print(), the result of
michael@0 1355 * console.log(). The JS eval result is also checked if it opens the variables
michael@0 1356 * view on click.
michael@0 1357 *
michael@0 1358 * @param object hud
michael@0 1359 * The web console instance to work with.
michael@0 1360 * @param array inputTests
michael@0 1361 * An array of input tests. An input test element is an object. Each
michael@0 1362 * object has the following properties:
michael@0 1363 * - input: string, JS input value to execute.
michael@0 1364 *
michael@0 1365 * - output: string|RegExp, expected JS eval result.
michael@0 1366 *
michael@0 1367 * - inspectable: boolean, when true, the test runner expects the JS eval
michael@0 1368 * result is an object that can be clicked for inspection.
michael@0 1369 *
michael@0 1370 * - noClick: boolean, when true, the test runner does not click the JS
michael@0 1371 * eval result. Some objects, like |window|, have a lot of properties and
michael@0 1372 * opening vview for them is very slow (they can cause timeouts in debug
michael@0 1373 * builds).
michael@0 1374 *
michael@0 1375 * - printOutput: string|RegExp, optional, expected output for
michael@0 1376 * |print(input)|. If this is not provided, printOutput = output.
michael@0 1377 *
michael@0 1378 * - variablesViewLabel: string|RegExp, optional, the expected variables
michael@0 1379 * view label when the object is inspected. If this is not provided, then
michael@0 1380 * |output| is used.
michael@0 1381 *
michael@0 1382 * - inspectorIcon: boolean, when true, the test runner expects the
michael@0 1383 * result widget to contain an inspectorIcon element (className
michael@0 1384 * open-inspector).
michael@0 1385 */
michael@0 1386 function checkOutputForInputs(hud, inputTests)
michael@0 1387 {
michael@0 1388 let eventHandlers = new Set();
michael@0 1389
michael@0 1390 function* runner()
michael@0 1391 {
michael@0 1392 for (let [i, entry] of inputTests.entries()) {
michael@0 1393 info("checkInput(" + i + "): " + entry.input);
michael@0 1394 yield checkInput(entry);
michael@0 1395 }
michael@0 1396
michael@0 1397 for (let fn of eventHandlers) {
michael@0 1398 hud.jsterm.off("variablesview-open", fn);
michael@0 1399 }
michael@0 1400 }
michael@0 1401
michael@0 1402 function* checkInput(entry)
michael@0 1403 {
michael@0 1404 yield checkConsoleLog(entry);
michael@0 1405 yield checkPrintOutput(entry);
michael@0 1406 yield checkJSEval(entry);
michael@0 1407 }
michael@0 1408
michael@0 1409 function* checkConsoleLog(entry)
michael@0 1410 {
michael@0 1411 hud.jsterm.clearOutput();
michael@0 1412 hud.jsterm.execute("console.log(" + entry.input + ")");
michael@0 1413
michael@0 1414 let [result] = yield waitForMessages({
michael@0 1415 webconsole: hud,
michael@0 1416 messages: [{
michael@0 1417 name: "console.log() output: " + entry.output,
michael@0 1418 text: entry.output,
michael@0 1419 category: CATEGORY_WEBDEV,
michael@0 1420 severity: SEVERITY_LOG,
michael@0 1421 }],
michael@0 1422 });
michael@0 1423
michael@0 1424 if (typeof entry.inspectorIcon == "boolean") {
michael@0 1425 let msg = [...result.matched][0];
michael@0 1426 yield checkLinkToInspector(entry, msg);
michael@0 1427 }
michael@0 1428 }
michael@0 1429
michael@0 1430 function checkPrintOutput(entry)
michael@0 1431 {
michael@0 1432 hud.jsterm.clearOutput();
michael@0 1433 hud.jsterm.execute("print(" + entry.input + ")");
michael@0 1434
michael@0 1435 let printOutput = entry.printOutput || entry.output;
michael@0 1436
michael@0 1437 return waitForMessages({
michael@0 1438 webconsole: hud,
michael@0 1439 messages: [{
michael@0 1440 name: "print() output: " + printOutput,
michael@0 1441 text: printOutput,
michael@0 1442 category: CATEGORY_OUTPUT,
michael@0 1443 }],
michael@0 1444 });
michael@0 1445 }
michael@0 1446
michael@0 1447 function* checkJSEval(entry)
michael@0 1448 {
michael@0 1449 hud.jsterm.clearOutput();
michael@0 1450 hud.jsterm.execute(entry.input);
michael@0 1451
michael@0 1452 let [result] = yield waitForMessages({
michael@0 1453 webconsole: hud,
michael@0 1454 messages: [{
michael@0 1455 name: "JS eval output: " + entry.output,
michael@0 1456 text: entry.output,
michael@0 1457 category: CATEGORY_OUTPUT,
michael@0 1458 }],
michael@0 1459 });
michael@0 1460
michael@0 1461 let msg = [...result.matched][0];
michael@0 1462 if (!entry.noClick) {
michael@0 1463 yield checkObjectClick(entry, msg);
michael@0 1464 }
michael@0 1465 if (typeof entry.inspectorIcon == "boolean") {
michael@0 1466 yield checkLinkToInspector(entry, msg);
michael@0 1467 }
michael@0 1468 }
michael@0 1469
michael@0 1470 function checkObjectClick(entry, msg)
michael@0 1471 {
michael@0 1472 let body = msg.querySelector(".message-body a") ||
michael@0 1473 msg.querySelector(".message-body");
michael@0 1474 ok(body, "the message body");
michael@0 1475
michael@0 1476 let deferred = promise.defer();
michael@0 1477
michael@0 1478 entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry, deferred);
michael@0 1479 hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
michael@0 1480 eventHandlers.add(entry._onVariablesViewOpen);
michael@0 1481
michael@0 1482 body.scrollIntoView();
michael@0 1483 EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
michael@0 1484
michael@0 1485 if (entry.inspectable) {
michael@0 1486 info("message body tagName '" + body.tagName + "' className '" + body.className + "'");
michael@0 1487 return deferred.promise; // wait for the panel to open if we need to.
michael@0 1488 }
michael@0 1489
michael@0 1490 return promise.resolve(null);
michael@0 1491 }
michael@0 1492
michael@0 1493 function checkLinkToInspector(entry, msg)
michael@0 1494 {
michael@0 1495 let elementNodeWidget = [...msg._messageObject.widgets][0];
michael@0 1496 if (!elementNodeWidget) {
michael@0 1497 ok(!entry.inspectorIcon, "The message has no ElementNode widget");
michael@0 1498 return;
michael@0 1499 }
michael@0 1500
michael@0 1501 return elementNodeWidget.linkToInspector().then(() => {
michael@0 1502 // linkToInspector resolved, check for the .open-inspector element
michael@0 1503 if (entry.inspectorIcon) {
michael@0 1504 ok(msg.querySelectorAll(".open-inspector").length,
michael@0 1505 "The ElementNode widget is linked to the inspector");
michael@0 1506 } else {
michael@0 1507 ok(!msg.querySelectorAll(".open-inspector").length,
michael@0 1508 "The ElementNode widget isn't linked to the inspector");
michael@0 1509 }
michael@0 1510 }, () => {
michael@0 1511 // linkToInspector promise rejected, node not linked to inspector
michael@0 1512 ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector");
michael@0 1513 });
michael@0 1514 }
michael@0 1515
michael@0 1516 function onVariablesViewOpen(entry, deferred, event, view, options)
michael@0 1517 {
michael@0 1518 let label = entry.variablesViewLabel || entry.output;
michael@0 1519 if (typeof label == "string" && options.label != label) {
michael@0 1520 return;
michael@0 1521 }
michael@0 1522 if (label instanceof RegExp && !label.test(options.label)) {
michael@0 1523 return;
michael@0 1524 }
michael@0 1525
michael@0 1526 hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
michael@0 1527 eventHandlers.delete(entry._onVariablesViewOpen);
michael@0 1528 entry._onVariablesViewOpen = null;
michael@0 1529
michael@0 1530 ok(entry.inspectable, "variables view was shown");
michael@0 1531
michael@0 1532 deferred.resolve(null);
michael@0 1533 }
michael@0 1534
michael@0 1535 return Task.spawn(runner);
michael@0 1536 }

mercurial