browser/devtools/webconsole/test/head.js

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

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

mercurial