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