head.js 55 KB

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