Wed, 31 Dec 2014 06:09:35 +0100
Cloned upstream origin tor-browser at tor-browser-31.3.0esr-4.5-1-build1
revision ID fc1c9ff7c1b2defdbc039f12214767608f46423f for hacking purpose.
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;
1004 }
1006 let iframe = aElement.querySelector("iframe");
1007 if (!iframe) {
1008 ok(false, "console.dir message has no iframe");
1009 return false;
1010 }
1012 return true;
1013 }
1015 function checkConsoleGroup(aRule, aElement)
1016 {
1017 if (!isNaN(parseInt(aRule.consoleGroup))) {
1018 aRule.groupDepth = aRule.consoleGroup;
1019 }
1020 aRule.category = CATEGORY_WEBDEV;
1021 aRule.severity = SEVERITY_LOG;
1023 return true;
1024 }
1026 function checkSource(aRule, aElement)
1027 {
1028 let location = aElement.querySelector(".message-location");
1029 if (!location) {
1030 return false;
1031 }
1033 if (!checkText(aRule.source.url, location.getAttribute("title"))) {
1034 return false;
1035 }
1037 if ("line" in aRule.source && location.sourceLine != aRule.source.line) {
1038 return false;
1039 }
1041 return true;
1042 }
1044 function checkCollapsible(aRule, aElement)
1045 {
1046 let msg = aElement._messageObject;
1047 if (!msg || !!msg.collapsible != aRule.collapsible) {
1048 return false;
1049 }
1051 return true;
1052 }
1054 function checkStacktrace(aRule, aElement)
1055 {
1056 let stack = aRule.stacktrace;
1057 let frames = aElement.querySelectorAll(".stacktrace > li");
1058 if (!frames.length) {
1059 return false;
1060 }
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;
1068 }
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;
1077 }
1078 }
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;
1087 }
1088 }
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;
1097 }
1098 }
1099 }
1101 return true;
1102 }
1104 function checkMessage(aRule, aElement)
1105 {
1106 let elemText = aElement.textContent;
1108 if (aRule.text && !checkText(aRule.text, elemText)) {
1109 return false;
1110 }
1112 if (aRule.noText && checkText(aRule.noText, elemText)) {
1113 return false;
1114 }
1116 if (aRule.consoleTrace && !checkConsoleTrace(aRule, aElement)) {
1117 return false;
1118 }
1120 if (aRule.consoleTime && !checkConsoleTime(aRule, aElement)) {
1121 return false;
1122 }
1124 if (aRule.consoleTimeEnd && !checkConsoleTimeEnd(aRule, aElement)) {
1125 return false;
1126 }
1128 if (aRule.consoleDir && !checkConsoleDir(aRule, aElement)) {
1129 return false;
1130 }
1132 if (aRule.consoleGroup && !checkConsoleGroup(aRule, aElement)) {
1133 return false;
1134 }
1136 if (aRule.source && !checkSource(aRule, aElement)) {
1137 return false;
1138 }
1140 if ("collapsible" in aRule && !checkCollapsible(aRule, aElement)) {
1141 return false;
1142 }
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);
1155 }
1156 return false;
1157 }
1158 partialMatch = true;
1159 }
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);
1166 }
1167 return false;
1168 }
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);
1175 }
1176 return false;
1177 }
1179 if (aRule.text) {
1180 partialMatch = true;
1181 }
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);
1187 }
1188 return false;
1189 }
1191 if (aRule.category == CATEGORY_NETWORK && "url" in aRule &&
1192 !checkText(aRule.url, aElement.url)) {
1193 return false;
1194 }
1196 if ("repeats" in aRule) {
1197 let repeats = aElement.querySelector(".message-repeats");
1198 if (!repeats || repeats.getAttribute("value") != aRule.repeats) {
1199 return false;
1200 }
1201 }
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;
1210 }
1211 }
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);
1221 }
1222 return false;
1223 }
1224 aRule.longStrings = longStrings;
1225 }
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);
1235 }
1236 return false;
1237 }
1238 aRule.clickableElements = clickables;
1239 }
1241 let count = aRule.count || 1;
1242 if (!aRule.matched) {
1243 aRule.matched = new Set();
1244 }
1245 aRule.matched.add(aElement);
1247 return aRule.matched.size == count;
1248 }
1250 function onMessagesAdded(aEvent, aNewElements)
1251 {
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;
1260 }
1261 }
1263 for (let rule of rules) {
1264 if (rule._ruleMatched) {
1265 continue;
1266 }
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;
1275 }
1276 }
1277 }
1278 }
1279 }
1281 function allRulesMatched()
1282 {
1283 return aOptions.matchCondition == "all" && rulesMatched == rules.length ||
1284 aOptions.matchCondition == "any" && rulesMatched > 0;
1285 }
1287 function maybeDone()
1288 {
1289 if (allRulesMatched()) {
1290 if (listenerAdded) {
1291 webconsole.ui.off("messages-added", onMessagesAdded);
1292 webconsole.ui.off("messages-updated", onMessagesAdded);
1293 }
1294 gPendingOutputTest--;
1295 deferred.resolve(rules);
1296 return true;
1297 }
1298 return false;
1299 }
1301 function testCleanup() {
1302 if (allRulesMatched()) {
1303 return;
1304 }
1306 if (webconsole.ui) {
1307 webconsole.ui.off("messages-added", onMessagesAdded);
1308 }
1310 for (let rule of rules) {
1311 if (!rule._ruleMatched) {
1312 ok(false, "failed to match rule: " + displayRule(rule));
1313 }
1314 }
1315 }
1317 function displayRule(aRule)
1318 {
1319 return aRule.name || aRule.text;
1320 }
1322 function displayErrorContext(aRule, aElement)
1323 {
1324 console.log("error occured during rule " + displayRule(aRule));
1325 console.log("while checking the following message");
1326 dumpMessageElement(aElement);
1327 }
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);
1336 }
1337 });
1339 return deferred.promise;
1340 }
1342 function whenDelayedStartupFinished(aWindow, aCallback)
1343 {
1344 Services.obs.addObserver(function observer(aSubject, aTopic) {
1345 if (aWindow == aSubject) {
1346 Services.obs.removeObserver(observer, aTopic);
1347 executeSoon(aCallback);
1348 }
1349 }, "browser-delayed-startup-finished", false);
1350 }
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.
1357 *
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.
1364 *
1365 * - output: string|RegExp, expected JS eval result.
1366 *
1367 * - inspectable: boolean, when true, the test runner expects the JS eval
1368 * result is an object that can be clicked for inspection.
1369 *
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).
1374 *
1375 * - printOutput: string|RegExp, optional, expected output for
1376 * |print(input)|. If this is not provided, printOutput = output.
1377 *
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.
1381 *
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)
1387 {
1388 let eventHandlers = new Set();
1390 function* runner()
1391 {
1392 for (let [i, entry] of inputTests.entries()) {
1393 info("checkInput(" + i + "): " + entry.input);
1394 yield checkInput(entry);
1395 }
1397 for (let fn of eventHandlers) {
1398 hud.jsterm.off("variablesview-open", fn);
1399 }
1400 }
1402 function* checkInput(entry)
1403 {
1404 yield checkConsoleLog(entry);
1405 yield checkPrintOutput(entry);
1406 yield checkJSEval(entry);
1407 }
1409 function* checkConsoleLog(entry)
1410 {
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);
1427 }
1428 }
1430 function checkPrintOutput(entry)
1431 {
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 });
1445 }
1447 function* checkJSEval(entry)
1448 {
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);
1464 }
1465 if (typeof entry.inspectorIcon == "boolean") {
1466 yield checkLinkToInspector(entry, msg);
1467 }
1468 }
1470 function checkObjectClick(entry, msg)
1471 {
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.
1488 }
1490 return promise.resolve(null);
1491 }
1493 function checkLinkToInspector(entry, msg)
1494 {
1495 let elementNodeWidget = [...msg._messageObject.widgets][0];
1496 if (!elementNodeWidget) {
1497 ok(!entry.inspectorIcon, "The message has no ElementNode widget");
1498 return;
1499 }
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");
1509 }
1510 }, () => {
1511 // linkToInspector promise rejected, node not linked to inspector
1512 ok(!entry.inspectorIcon, "The ElementNode widget isn't linked to the inspector");
1513 });
1514 }
1516 function onVariablesViewOpen(entry, deferred, event, view, options)
1517 {
1518 let label = entry.variablesViewLabel || entry.output;
1519 if (typeof label == "string" && options.label != label) {
1520 return;
1521 }
1522 if (label instanceof RegExp && !label.test(options.label)) {
1523 return;
1524 }
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);
1533 }
1535 return Task.spawn(runner);
1536 }