browser/devtools/webconsole/test/head.js

changeset 0
6474c204b198
     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 +}

mercurial