12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499150015011502150315041505150615071508150915101511151215131514151515161517151815191520152115221523152415251526152715281529153015311532153315341535153615371538153915401541154215431544154515461547154815491550155115521553155415551556155715581559156015611562156315641565156615671568156915701571157215731574157515761577157815791580158115821583158415851586158715881589159015911592159315941595159615971598159916001601160216031604160516061607160816091610161116121613161416151616161716181619162016211622162316241625162616271628162916301631163216331634163516361637163816391640164116421643164416451646164716481649165016511652165316541655165616571658165916601661166216631664166516661667166816691670167116721673167416751676167716781679168016811682168316841685168616871688168916901691169216931694169516961697169816991700170117021703170417051706170717081709171017111712171317141715171617171718171917201721172217231724172517261727172817291730173117321733173417351736173717381739174017411742174317441745174617471748174917501751175217531754175517561757175817591760176117621763176417651766176717681769177017711772177317741775177617771778177917801781178217831784178517861787178817891790179117921793179417951796179717981799180018011802180318041805180618071808180918101811181218131814181518161817181818191820182118221823182418251826182718281829183018311832183318341835183618371838183918401841184218431844184518461847184818491850185118521853185418551856185718581859 |
- /* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
- /* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
- /* import-globals-from ../../framework/test/shared-head.js */
- "use strict";
- // shared-head.js handles imports, constants, and utility functions
- Services.scriptloader.loadSubScript("chrome://mochitests/content/browser/devtools/client/framework/test/shared-head.js", this);
- var {Utils: WebConsoleUtils} = require("devtools/client/webconsole/utils");
- var {Messages} = require("devtools/client/webconsole/console-output");
- const asyncStorage = require("devtools/shared/async-storage");
- const {HUDService} = require("devtools/client/webconsole/hudservice");
- // Services.prefs.setBoolPref("devtools.debugger.log", true);
- var gPendingOutputTest = 0;
- // The various categories of messages.
- const CATEGORY_NETWORK = 0;
- const CATEGORY_CSS = 1;
- const CATEGORY_JS = 2;
- const CATEGORY_WEBDEV = 3;
- const CATEGORY_INPUT = 4;
- const CATEGORY_OUTPUT = 5;
- const CATEGORY_SECURITY = 6;
- const CATEGORY_SERVER = 7;
- // The possible message severities.
- const SEVERITY_ERROR = 0;
- const SEVERITY_WARNING = 1;
- const SEVERITY_INFO = 2;
- const SEVERITY_LOG = 3;
- // The indent of a console group in pixels.
- const GROUP_INDENT = 12;
- const WEBCONSOLE_STRINGS_URI = "devtools/client/locales/webconsole.properties";
- var WCUL10n = new WebConsoleUtils.L10n(WEBCONSOLE_STRINGS_URI);
- const DOCS_GA_PARAMS = "?utm_source=mozilla" +
- "&utm_medium=firefox-console-errors" +
- "&utm_campaign=default";
- flags.testing = true;
- function loadTab(url) {
- let deferred = promise.defer();
- let tab = gBrowser.selectedTab = gBrowser.addTab(url);
- let browser = gBrowser.getBrowserForTab(tab);
- browser.addEventListener("load", function onLoad() {
- browser.removeEventListener("load", onLoad, true);
- deferred.resolve({tab: tab, browser: browser});
- }, true);
- return deferred.promise;
- }
- function loadBrowser(browser) {
- return BrowserTestUtils.browserLoaded(browser);
- }
- function closeTab(tab) {
- let deferred = promise.defer();
- let container = gBrowser.tabContainer;
- container.addEventListener("TabClose", function onTabClose() {
- container.removeEventListener("TabClose", onTabClose, true);
- deferred.resolve(null);
- }, true);
- gBrowser.removeTab(tab);
- return deferred.promise;
- }
- /**
- * Load the page and return the associated HUD.
- *
- * @param string uri
- * The URI of the page to load.
- * @param string consoleType [optional]
- * The console type, either "browserConsole" or "webConsole". Defaults to
- * "webConsole".
- * @return object
- * The HUD associated with the console
- */
- function* loadPageAndGetHud(uri, consoleType) {
- let { browser } = yield loadTab("data:text/html;charset=utf-8,Loading tab for tests");
- let hud;
- if (consoleType === "browserConsole") {
- hud = yield HUDService.openBrowserConsoleOrFocus();
- } else {
- hud = yield openConsole();
- }
- ok(hud, "Console was opened");
- let loaded = loadBrowser(browser);
- yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, uri);
- yield loaded;
- yield waitForMessages({
- webconsole: hud,
- messages: [{
- text: uri,
- category: CATEGORY_NETWORK,
- severity: SEVERITY_LOG,
- }],
- });
- return hud;
- }
- function afterAllTabsLoaded(callback, win) {
- win = win || window;
- let stillToLoad = 0;
- function onLoad() {
- this.removeEventListener("load", onLoad, true);
- stillToLoad--;
- if (!stillToLoad) {
- callback();
- }
- }
- for (let a = 0; a < win.gBrowser.tabs.length; a++) {
- let browser = win.gBrowser.tabs[a].linkedBrowser;
- if (browser.webProgress.isLoadingDocument) {
- stillToLoad++;
- browser.addEventListener("load", onLoad, true);
- }
- }
- if (!stillToLoad) {
- callback();
- }
- }
- /**
- * Check if a log entry exists in the HUD output node.
- *
- * @param {Element} outputNode
- * the HUD output node.
- * @param {string} matchString
- * the string you want to check if it exists in the output node.
- * @param {string} msg
- * the message describing the test
- * @param {boolean} [onlyVisible=false]
- * find only messages that are visible, not hidden by the filter.
- * @param {boolean} [failIfFound=false]
- * fail the test if the string is found in the output node.
- * @param {string} cssClass [optional]
- * find only messages with the given CSS class.
- */
- function testLogEntry(outputNode, matchString, msg, onlyVisible,
- failIfFound, cssClass) {
- let selector = ".message";
- // Skip entries that are hidden by the filter.
- if (onlyVisible) {
- selector += ":not(.filtered-by-type):not(.filtered-by-string)";
- }
- if (cssClass) {
- selector += "." + aClass;
- }
- let msgs = outputNode.querySelectorAll(selector);
- let found = false;
- for (let i = 0, n = msgs.length; i < n; i++) {
- let message = msgs[i].textContent.indexOf(matchString);
- if (message > -1) {
- found = true;
- break;
- }
- }
- is(found, !failIfFound, msg);
- }
- /**
- * A convenience method to call testLogEntry().
- *
- * @param str string
- * The string to find.
- */
- function findLogEntry(str) {
- testLogEntry(outputNode, str, "found " + str);
- }
- /**
- * Open the Web Console for the given tab.
- *
- * @param nsIDOMElement [tab]
- * Optional tab element for which you want open the Web Console. The
- * default tab is taken from the global variable |tab|.
- * @param function [callback]
- * Optional function to invoke after the Web Console completes
- * initialization (web-console-created).
- * @return object
- * A promise that is resolved once the web console is open.
- */
- var openConsole = function (tab) {
- let webconsoleOpened = promise.defer();
- let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
- gDevTools.showToolbox(target, "webconsole").then(toolbox => {
- let hud = toolbox.getCurrentPanel().hud;
- hud.jsterm._lazyVariablesView = false;
- webconsoleOpened.resolve(hud);
- });
- return webconsoleOpened.promise;
- };
- /**
- * Close the Web Console for the given tab.
- *
- * @param nsIDOMElement [tab]
- * Optional tab element for which you want close the Web Console. The
- * default tab is taken from the global variable |tab|.
- * @param function [callback]
- * Optional function to invoke after the Web Console completes
- * closing (web-console-destroyed).
- * @return object
- * A promise that is resolved once the web console is closed.
- */
- var closeConsole = Task.async(function* (tab) {
- let target = TargetFactory.forTab(tab || gBrowser.selectedTab);
- let toolbox = gDevTools.getToolbox(target);
- if (toolbox) {
- yield toolbox.destroy();
- }
- });
- /**
- * Listen for a new tab to open and return a promise that resolves when one
- * does and completes the load event.
- * @return a promise that resolves to the tab object
- */
- var waitForTab = Task.async(function* () {
- info("Waiting for a tab to open");
- yield once(gBrowser.tabContainer, "TabOpen");
- let tab = gBrowser.selectedTab;
- let browser = tab.linkedBrowser;
- yield once(browser, "load", true);
- info("The tab load completed");
- return tab;
- });
- /**
- * Dump the output of all open Web Consoles - used only for debugging purposes.
- */
- function dumpConsoles() {
- if (gPendingOutputTest) {
- console.log("dumpConsoles start");
- for (let [, hud] of HUDService.consoles) {
- if (!hud.outputNode) {
- console.debug("no output content for", hud.hudId);
- continue;
- }
- console.debug("output content for", hud.hudId);
- for (let elem of hud.outputNode.childNodes) {
- dumpMessageElement(elem);
- }
- }
- console.log("dumpConsoles end");
- gPendingOutputTest = 0;
- }
- }
- /**
- * Dump to output debug information for the given webconsole message.
- *
- * @param nsIDOMNode message
- * The message element you want to display.
- */
- function dumpMessageElement(message) {
- let text = message.textContent;
- let repeats = message.querySelector(".message-repeats");
- if (repeats) {
- repeats = repeats.getAttribute("value");
- }
- console.debug("id", message.getAttribute("id"),
- "date", message.timestamp,
- "class", message.className,
- "category", message.category,
- "severity", message.severity,
- "repeats", repeats,
- "clipboardText", message.clipboardText,
- "text", text);
- }
- var finishTest = Task.async(function* () {
- dumpConsoles();
- let browserConsole = HUDService.getBrowserConsole();
- if (browserConsole) {
- if (browserConsole.jsterm) {
- browserConsole.jsterm.clearOutput(true);
- }
- yield HUDService.toggleBrowserConsole();
- }
- let target = TargetFactory.forTab(gBrowser.selectedTab);
- yield gDevTools.closeToolbox(target);
- finish();
- });
- // Always use the 'old' frontend for tests that rely on it
- Services.prefs.setBoolPref("devtools.webconsole.new-frontend-enabled", false);
- registerCleanupFunction(function* () {
- Services.prefs.clearUserPref("devtools.webconsole.new-frontend-enabled");
- });
- registerCleanupFunction(function* () {
- flags.testing = false;
- // Remove stored console commands in between tests
- yield asyncStorage.removeItem("webConsoleHistory");
- dumpConsoles();
- let browserConsole = HUDService.getBrowserConsole();
- if (browserConsole) {
- if (browserConsole.jsterm) {
- browserConsole.jsterm.clearOutput(true);
- }
- yield HUDService.toggleBrowserConsole();
- }
- let target = TargetFactory.forTab(gBrowser.selectedTab);
- yield gDevTools.closeToolbox(target);
- while (gBrowser.tabs.length > 1) {
- gBrowser.removeCurrentTab();
- }
- });
- waitForExplicitFinish();
- /**
- * Polls a given function waiting for it to become true.
- *
- * @param object options
- * Options object with the following properties:
- * - validator
- * A validator function that returns a boolean. This is called every few
- * milliseconds to check if the result is true. When it is true, the
- * promise is resolved and polling stops. If validator never returns
- * true, then polling timeouts after several tries and the promise is
- * rejected.
- * - name
- * Name of test. This is used to generate the success and failure
- * messages.
- * - timeout
- * Timeout for validator function, in milliseconds. Default is 5000.
- * @return object
- * A Promise object that is resolved based on the validator function.
- */
- function waitForSuccess(options) {
- let deferred = promise.defer();
- let start = Date.now();
- let timeout = options.timeout || 5000;
- let {validator} = options;
- function wait() {
- if ((Date.now() - start) > timeout) {
- // Log the failure.
- ok(false, "Timed out while waiting for: " + options.name);
- deferred.reject(null);
- return;
- }
- if (validator(options)) {
- ok(true, options.name);
- deferred.resolve(null);
- } else {
- setTimeout(wait, 100);
- }
- }
- setTimeout(wait, 100);
- return deferred.promise;
- }
- var openInspector = Task.async(function* (tab = gBrowser.selectedTab) {
- let target = TargetFactory.forTab(tab);
- let toolbox = yield gDevTools.showToolbox(target, "inspector");
- return toolbox.getCurrentPanel();
- });
- /**
- * Find variables or properties in a VariablesView instance.
- *
- * @param object view
- * The VariablesView instance.
- * @param array rules
- * The array of rules you want to match. Each rule is an object with:
- * - name (string|regexp): property name to match.
- * - value (string|regexp): property value to match.
- * - isIterator (boolean): check if the property is an iterator.
- * - isGetter (boolean): check if the property is a getter.
- * - isGenerator (boolean): check if the property is a generator.
- * - dontMatch (boolean): make sure the rule doesn't match any property.
- * @param object options
- * Options for matching:
- * - webconsole: the WebConsole instance we work with.
- * @return object
- * A promise object that is resolved when all the rules complete
- * matching. The resolved callback is given an array of all the rules
- * you wanted to check. Each rule has a new property: |matchedProp|
- * which holds a reference to the Property object instance from the
- * VariablesView. If the rule did not match, then |matchedProp| is
- * undefined.
- */
- function findVariableViewProperties(view, rules, options) {
- // Initialize the search.
- function init() {
- // Separate out the rules that require expanding properties throughout the
- // view.
- let expandRules = [];
- let filterRules = rules.filter((rule) => {
- if (typeof rule.name == "string" && rule.name.indexOf(".") > -1) {
- expandRules.push(rule);
- return false;
- }
- return true;
- });
- // Search through the view those rules that do not require any properties to
- // be expanded. Build the array of matchers, outstanding promises to be
- // resolved.
- let outstanding = [];
- finder(filterRules, view, outstanding);
- // Process the rules that need to expand properties.
- let lastStep = processExpandRules.bind(null, expandRules);
- // Return the results - a promise resolved to hold the updated rules array.
- let returnResults = onAllRulesMatched.bind(null, rules);
- return promise.all(outstanding).then(lastStep).then(returnResults);
- }
- function onMatch(prop, rule, matched) {
- if (matched && !rule.matchedProp) {
- rule.matchedProp = prop;
- }
- }
- function finder(rules, vars, promises) {
- for (let [, prop] of vars) {
- for (let rule of rules) {
- let matcher = matchVariablesViewProperty(prop, rule, options);
- promises.push(matcher.then(onMatch.bind(null, prop, rule)));
- }
- }
- }
- function processExpandRules(rules) {
- let rule = rules.shift();
- if (!rule) {
- return promise.resolve(null);
- }
- let deferred = promise.defer();
- let expandOptions = {
- rootVariable: view,
- expandTo: rule.name,
- webconsole: options.webconsole,
- };
- variablesViewExpandTo(expandOptions).then(function onSuccess(prop) {
- let name = rule.name;
- let lastName = name.split(".").pop();
- rule.name = lastName;
- let matched = matchVariablesViewProperty(prop, rule, options);
- return matched.then(onMatch.bind(null, prop, rule)).then(function () {
- rule.name = name;
- });
- }, function onFailure() {
- return promise.resolve(null);
- }).then(processExpandRules.bind(null, rules)).then(function () {
- deferred.resolve(null);
- });
- return deferred.promise;
- }
- function onAllRulesMatched(rules) {
- for (let rule of rules) {
- let matched = rule.matchedProp;
- if (matched && !rule.dontMatch) {
- ok(true, "rule " + rule.name + " matched for property " + matched.name);
- } else if (matched && rule.dontMatch) {
- ok(false, "rule " + rule.name + " should not match property " +
- matched.name);
- } else {
- ok(rule.dontMatch, "rule " + rule.name + " did not match any property");
- }
- }
- return rules;
- }
- return init();
- }
- /**
- * Check if a given Property object from the variables view matches the given
- * rule.
- *
- * @param object prop
- * The variable's view Property instance.
- * @param object rule
- * Rules for matching the property. See findVariableViewProperties() for
- * details.
- * @param object options
- * Options for matching. See findVariableViewProperties().
- * @return object
- * A promise that is resolved when all the checks complete. Resolution
- * result is a boolean that tells your promise callback the match
- * result: true or false.
- */
- function matchVariablesViewProperty(prop, rule, options) {
- function resolve(result) {
- return promise.resolve(result);
- }
- if (rule.name) {
- let match = rule.name instanceof RegExp ?
- rule.name.test(prop.name) :
- prop.name == rule.name;
- if (!match) {
- return resolve(false);
- }
- }
- if (rule.value) {
- let displayValue = prop.displayValue;
- if (prop.displayValueClassName == "token-string") {
- displayValue = displayValue.substring(1, displayValue.length - 1);
- }
- let match = rule.value instanceof RegExp ?
- rule.value.test(displayValue) :
- displayValue == rule.value;
- if (!match) {
- info("rule " + rule.name + " did not match value, expected '" +
- rule.value + "', found '" + displayValue + "'");
- return resolve(false);
- }
- }
- if ("isGetter" in rule) {
- let isGetter = !!(prop.getter && prop.get("get"));
- if (rule.isGetter != isGetter) {
- info("rule " + rule.name + " getter test failed");
- return resolve(false);
- }
- }
- if ("isGenerator" in rule) {
- let isGenerator = prop.displayValue == "Generator";
- if (rule.isGenerator != isGenerator) {
- info("rule " + rule.name + " generator test failed");
- return resolve(false);
- }
- }
- let outstanding = [];
- if ("isIterator" in rule) {
- let isIterator = isVariableViewPropertyIterator(prop, options.webconsole);
- outstanding.push(isIterator.then((result) => {
- if (result != rule.isIterator) {
- info("rule " + rule.name + " iterator test failed");
- }
- return result == rule.isIterator;
- }));
- }
- outstanding.push(promise.resolve(true));
- return promise.all(outstanding).then(function _onMatchDone(results) {
- let ruleMatched = results.indexOf(false) == -1;
- return resolve(ruleMatched);
- });
- }
- /**
- * Check if the given variables view property is an iterator.
- *
- * @param object prop
- * The Property instance you want to check.
- * @param object webConsole
- * The WebConsole instance to work with.
- * @return object
- * A promise that is resolved when the check completes. The resolved
- * callback is given a boolean: true if the property is an iterator, or
- * false otherwise.
- */
- function isVariableViewPropertyIterator(prop, webConsole) {
- if (prop.displayValue == "Iterator") {
- return promise.resolve(true);
- }
- let deferred = promise.defer();
- variablesViewExpandTo({
- rootVariable: prop,
- expandTo: "__proto__.__iterator__",
- webconsole: webConsole,
- }).then(function onSuccess() {
- deferred.resolve(true);
- }, function onFailure() {
- deferred.resolve(false);
- });
- return deferred.promise;
- }
- /**
- * Recursively expand the variables view up to a given property.
- *
- * @param options
- * Options for view expansion:
- * - rootVariable: start from the given scope/variable/property.
- * - expandTo: string made up of property names you want to expand.
- * For example: "body.firstChild.nextSibling" given |rootVariable:
- * document|.
- * - webconsole: a WebConsole instance. If this is not provided all
- * property expand() calls will be considered sync. Things may fail!
- * @return object
- * A promise that is resolved only when the last property in |expandTo|
- * is found, and rejected otherwise. Resolution reason is always the
- * last property - |nextSibling| in the example above. Rejection is
- * always the last property that was found.
- */
- function variablesViewExpandTo(options) {
- let root = options.rootVariable;
- let expandTo = options.expandTo.split(".");
- let jsterm = (options.webconsole || {}).jsterm;
- let lastDeferred = promise.defer();
- function fetch(prop) {
- if (!prop.onexpand) {
- ok(false, "property " + prop.name + " cannot be expanded: !onexpand");
- return promise.reject(prop);
- }
- let deferred = promise.defer();
- if (prop._fetched || !jsterm) {
- executeSoon(function () {
- deferred.resolve(prop);
- });
- } else {
- jsterm.once("variablesview-fetched", function _onFetchProp() {
- executeSoon(() => deferred.resolve(prop));
- });
- }
- prop.expand();
- return deferred.promise;
- }
- function getNext(prop) {
- let name = expandTo.shift();
- let newProp = prop.get(name);
- if (expandTo.length > 0) {
- ok(newProp, "found property " + name);
- if (newProp) {
- fetch(newProp).then(getNext, fetchError);
- } else {
- lastDeferred.reject(prop);
- }
- } else if (newProp) {
- lastDeferred.resolve(newProp);
- } else {
- lastDeferred.reject(prop);
- }
- }
- function fetchError(prop) {
- lastDeferred.reject(prop);
- }
- if (!root._fetched) {
- fetch(root).then(getNext, fetchError);
- } else {
- getNext(root);
- }
- return lastDeferred.promise;
- }
- /**
- * Update the content of a property in the variables view.
- *
- * @param object options
- * Options for the property update:
- * - property: the property you want to change.
- * - field: string that tells what you want to change:
- * - use "name" to change the property name,
- * - or "value" to change the property value.
- * - string: the new string to write into the field.
- * - webconsole: reference to the Web Console instance we work with.
- * @return object
- * A Promise object that is resolved once the property is updated.
- */
- var updateVariablesViewProperty = Task.async(function* (options) {
- let view = options.property._variablesView;
- view.window.focus();
- options.property.focus();
- switch (options.field) {
- case "name":
- EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true }, view.window);
- break;
- case "value":
- EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
- break;
- default:
- throw new Error("options.field is incorrect");
- }
- let deferred = promise.defer();
- executeSoon(() => {
- EventUtils.synthesizeKey("A", { accelKey: true }, view.window);
- for (let c of options.string) {
- EventUtils.synthesizeKey(c, {}, view.window);
- }
- if (options.webconsole) {
- options.webconsole.jsterm.once("variablesview-fetched")
- .then((varView) => deferred.resolve(varView));
- }
- EventUtils.synthesizeKey("VK_RETURN", {}, view.window);
- if (!options.webconsole) {
- executeSoon(() => {
- deferred.resolve(null);
- });
- }
- });
- return deferred.promise;
- });
- /**
- * Open the JavaScript debugger.
- *
- * @param object options
- * Options for opening the debugger:
- * - tab: the tab you want to open the debugger for.
- * @return object
- * A promise that is resolved once the debugger opens, or rejected if
- * the open fails. The resolution callback is given one argument, an
- * object that holds the following properties:
- * - target: the Target object for the Tab.
- * - toolbox: the Toolbox instance.
- * - panel: the jsdebugger panel instance.
- * - panelWin: the window object of the panel iframe.
- */
- function openDebugger(options = {}) {
- if (!options.tab) {
- options.tab = gBrowser.selectedTab;
- }
- let deferred = promise.defer();
- let target = TargetFactory.forTab(options.tab);
- let toolbox = gDevTools.getToolbox(target);
- let dbgPanelAlreadyOpen = toolbox && toolbox.getPanel("jsdebugger");
- gDevTools.showToolbox(target, "jsdebugger").then(function onSuccess(tool) {
- let panel = tool.getCurrentPanel();
- let panelWin = panel.panelWin;
- panel._view.Variables.lazyEmpty = false;
- let resolveObject = {
- target: target,
- toolbox: tool,
- panel: panel,
- panelWin: panelWin,
- };
- if (dbgPanelAlreadyOpen) {
- deferred.resolve(resolveObject);
- } else {
- panelWin.DebuggerController.waitForSourcesLoaded().then(() => {
- deferred.resolve(resolveObject);
- });
- }
- }, function onFailure(reason) {
- console.debug("failed to open the toolbox for 'jsdebugger'", reason);
- deferred.reject(reason);
- });
- return deferred.promise;
- }
- /**
- * Returns true if the caret in the debugger editor is placed at the specified
- * position.
- * @param panel The debugger panel.
- * @param {number} line The line number.
- * @param {number} [col] The column number.
- * @returns {boolean}
- */
- function isDebuggerCaretPos(panel, line, col = 1) {
- let editor = panel.panelWin.DebuggerView.editor;
- let cursor = editor.getCursor();
- // Source editor starts counting line and column numbers from 0.
- info("Current editor caret position: " + (cursor.line + 1) + ", " +
- (cursor.ch + 1));
- return cursor.line == (line - 1) && cursor.ch == (col - 1);
- }
- /**
- * Wait for messages in the Web Console output.
- *
- * @param object options
- * Options for what you want to wait for:
- * - webconsole: the webconsole instance you work with.
- * - matchCondition: "any" or "all". Default: "all". The promise
- * returned by this function resolves when all of the messages are
- * matched, if the |matchCondition| is "all". If you set the condition to
- * "any" then the promise is resolved by any message rule that matches,
- * irrespective of order - waiting for messages stops whenever any rule
- * matches.
- * - messages: an array of objects that tells which messages to wait for.
- * Properties:
- * - text: string or RegExp to match the textContent of each new
- * message.
- * - noText: string or RegExp that must not match in the message
- * textContent.
- * - repeats: the number of message repeats, as displayed by the Web
- * Console.
- * - category: match message category. See CATEGORY_* constants at
- * the top of this file.
- * - severity: match message severity. See SEVERITY_* constants at
- * the top of this file.
- * - count: how many unique web console messages should be matched by
- * this rule.
- * - consoleTrace: boolean, set to |true| to match a console.trace()
- * message. Optionally this can be an object of the form
- * { file, fn, line } that can match the specified file, function
- * and/or line number in the trace message.
- * - consoleTime: string that matches a console.time() timer name.
- * Provide this if you want to match a console.time() message.
- * - consoleTimeEnd: same as above, but for console.timeEnd().
- * - consoleDir: boolean, set to |true| to match a console.dir()
- * message.
- * - consoleGroup: boolean, set to |true| to match a console.group()
- * message.
- * - consoleTable: boolean, set to |true| to match a console.table()
- * message.
- * - longString: boolean, set to |true} to match long strings in the
- * message.
- * - collapsible: boolean, set to |true| to match messages that can
- * be collapsed/expanded.
- * - type: match messages that are instances of the given object. For
- * example, you can point to Messages.NavigationMarker to match any
- * such message.
- * - objects: boolean, set to |true| if you expect inspectable
- * objects in the message.
- * - source: object of the shape { url, line }. This is used to
- * match the source URL and line number of the error message or
- * console API call.
- * - prefix: prefix text to check for in the prefix element.
- * - stacktrace: array of objects of the form { file, fn, line } that
- * can match frames in the stacktrace associated with the message.
- * - groupDepth: number used to check the depth of the message in
- * a group.
- * - url: URL to match for network requests.
- * @return object
- * A promise object is returned once the messages you want are found.
- * The promise is resolved with the array of rule objects you give in
- * the |messages| property. Each objects is the same as provided, with
- * additional properties:
- * - matched: a Set of web console messages that matched the rule.
- * - clickableElements: a list of inspectable objects. This is available
- * if any of the following properties are present in the rule:
- * |consoleTrace| or |objects|.
- * - longStrings: a list of long string ellipsis elements you can click
- * in the message element, to expand a long string. This is available
- * only if |longString| is present in the matching rule.
- */
- function waitForMessages(options) {
- info("Waiting for messages...");
- gPendingOutputTest++;
- let webconsole = options.webconsole;
- let rules = WebConsoleUtils.cloneObject(options.messages, true);
- let rulesMatched = 0;
- let listenerAdded = false;
- let deferred = promise.defer();
- options.matchCondition = options.matchCondition || "all";
- function checkText(rule, text) {
- let result = false;
- if (Array.isArray(rule)) {
- result = rule.every((s) => checkText(s, text));
- } else if (typeof rule == "string") {
- result = text.indexOf(rule) > -1;
- } else if (rule instanceof RegExp) {
- result = rule.test(text);
- } else {
- result = rule == text;
- }
- return result;
- }
- function checkConsoleTable(rule, element) {
- let elemText = element.textContent;
- if (!checkText("console.table():", elemText)) {
- return false;
- }
- rule.category = CATEGORY_WEBDEV;
- rule.severity = SEVERITY_LOG;
- rule.type = Messages.ConsoleTable;
- return true;
- }
- function checkConsoleTrace(rule, element) {
- let elemText = element.textContent;
- let trace = rule.consoleTrace;
- if (!checkText("console.trace():", elemText)) {
- return false;
- }
- rule.category = CATEGORY_WEBDEV;
- rule.severity = SEVERITY_LOG;
- rule.type = Messages.ConsoleTrace;
- if (!rule.stacktrace && typeof trace == "object" && trace !== true) {
- if (Array.isArray(trace)) {
- rule.stacktrace = trace;
- } else {
- rule.stacktrace = [trace];
- }
- }
- return true;
- }
- function checkConsoleTime(rule, element) {
- let elemText = element.textContent;
- let time = rule.consoleTime;
- if (!checkText(time + ": timer started", elemText)) {
- return false;
- }
- rule.category = CATEGORY_WEBDEV;
- rule.severity = SEVERITY_LOG;
- return true;
- }
- function checkConsoleTimeEnd(rule, element) {
- let elemText = element.textContent;
- let time = rule.consoleTimeEnd;
- let regex = new RegExp(time + ": -?\\d+([,.]\\d+)?ms");
- if (!checkText(regex, elemText)) {
- return false;
- }
- rule.category = CATEGORY_WEBDEV;
- rule.severity = SEVERITY_LOG;
- return true;
- }
- function checkConsoleDir(rule, element) {
- if (!element.classList.contains("inlined-variables-view")) {
- return false;
- }
- let elemText = element.textContent;
- if (!checkText(rule.consoleDir, elemText)) {
- return false;
- }
- let iframe = element.querySelector("iframe");
- if (!iframe) {
- ok(false, "console.dir message has no iframe");
- return false;
- }
- return true;
- }
- function checkConsoleGroup(rule) {
- if (!isNaN(parseInt(rule.consoleGroup, 10))) {
- rule.groupDepth = rule.consoleGroup;
- }
- rule.category = CATEGORY_WEBDEV;
- rule.severity = SEVERITY_LOG;
- return true;
- }
- function checkSource(rule, element) {
- let location = getRenderedSource(element);
- if (!location) {
- return false;
- }
- if (!checkText(rule.source.url, location.url)) {
- return false;
- }
- if ("line" in rule.source && location.line != rule.source.line) {
- return false;
- }
- return true;
- }
- function checkCollapsible(rule, element) {
- let msg = element._messageObject;
- if (!msg || !!msg.collapsible != rule.collapsible) {
- return false;
- }
- return true;
- }
- function checkStacktrace(rule, element) {
- let stack = rule.stacktrace;
- let frames = element.querySelectorAll(".stacktrace > .stack-trace > .frame-link");
- if (!frames.length) {
- return false;
- }
- for (let i = 0; i < stack.length; i++) {
- let frame = frames[i];
- let expected = stack[i];
- if (!frame) {
- ok(false, "expected frame #" + i + " but didnt find it");
- return false;
- }
- if (expected.file) {
- let url = frame.getAttribute("data-url");
- if (!checkText(expected.file, url)) {
- ok(false, "frame #" + i + " does not match file name: " +
- expected.file + " != " + url);
- displayErrorContext(rule, element);
- return false;
- }
- }
- if (expected.fn) {
- let fn = frame.querySelector(".frame-link-function-display-name").textContent;
- if (!checkText(expected.fn, fn)) {
- ok(false, "frame #" + i + " does not match the function name: " +
- expected.fn + " != " + fn);
- displayErrorContext(rule, element);
- return false;
- }
- }
- if (expected.line) {
- let line = frame.getAttribute("data-line");
- if (!checkText(expected.line, line)) {
- ok(false, "frame #" + i + " does not match the line number: " +
- expected.line + " != " + line);
- displayErrorContext(rule, element);
- return false;
- }
- }
- }
- return true;
- }
- function hasXhrLabel(element) {
- let xhr = element.querySelector(".xhr");
- if (!xhr) {
- return false;
- }
- return true;
- }
- function checkMessage(rule, element) {
- let elemText = element.textContent;
- if (rule.text && !checkText(rule.text, elemText)) {
- return false;
- }
- if (rule.noText && checkText(rule.noText, elemText)) {
- return false;
- }
- if (rule.consoleTable && !checkConsoleTable(rule, element)) {
- return false;
- }
- if (rule.consoleTrace && !checkConsoleTrace(rule, element)) {
- return false;
- }
- if (rule.consoleTime && !checkConsoleTime(rule, element)) {
- return false;
- }
- if (rule.consoleTimeEnd && !checkConsoleTimeEnd(rule, element)) {
- return false;
- }
- if (rule.consoleDir && !checkConsoleDir(rule, element)) {
- return false;
- }
- if (rule.consoleGroup && !checkConsoleGroup(rule, element)) {
- return false;
- }
- if (rule.source && !checkSource(rule, element)) {
- return false;
- }
- if ("collapsible" in rule && !checkCollapsible(rule, element)) {
- return false;
- }
- if (rule.isXhr && !hasXhrLabel(element)) {
- return false;
- }
- if (!rule.isXhr && hasXhrLabel(element)) {
- return false;
- }
- let partialMatch = !!(rule.consoleTrace || rule.consoleTime ||
- rule.consoleTimeEnd);
- // The rule tries to match the newer types of messages, based on their
- // object constructor.
- if (rule.type) {
- if (!element._messageObject ||
- !(element._messageObject instanceof rule.type)) {
- if (partialMatch) {
- ok(false, "message type for rule: " + displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- partialMatch = true;
- }
- if ("category" in rule && element.category != rule.category) {
- if (partialMatch) {
- is(element.category, rule.category,
- "message category for rule: " + displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- if ("severity" in rule && element.severity != rule.severity) {
- if (partialMatch) {
- is(element.severity, rule.severity,
- "message severity for rule: " + displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- if (rule.text) {
- partialMatch = true;
- }
- if (rule.stacktrace && !checkStacktrace(rule, element)) {
- if (partialMatch) {
- ok(false, "failed to match stacktrace for rule: " + displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- if (rule.category == CATEGORY_NETWORK && "url" in rule &&
- !checkText(rule.url, element.url)) {
- return false;
- }
- if ("repeats" in rule) {
- let repeats = element.querySelector(".message-repeats");
- if (!repeats || repeats.getAttribute("value") != rule.repeats) {
- return false;
- }
- }
- if ("groupDepth" in rule) {
- let indentNode = element.querySelector(".indent");
- let indent = (GROUP_INDENT * rule.groupDepth) + "px";
- if (!indentNode || indentNode.style.width != indent) {
- is(indentNode.style.width, indent,
- "group depth check failed for message rule: " + displayRule(rule));
- return false;
- }
- }
- if ("longString" in rule) {
- let longStrings = element.querySelectorAll(".longStringEllipsis");
- if (rule.longString != !!longStrings[0]) {
- if (partialMatch) {
- is(!!longStrings[0], rule.longString,
- "long string existence check failed for message rule: " +
- displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- rule.longStrings = longStrings;
- }
- if ("objects" in rule) {
- let clickables = element.querySelectorAll(".message-body a");
- if (rule.objects != !!clickables[0]) {
- if (partialMatch) {
- is(!!clickables[0], rule.objects,
- "objects existence check failed for message rule: " +
- displayRule(rule));
- displayErrorContext(rule, element);
- }
- return false;
- }
- rule.clickableElements = clickables;
- }
- if ("prefix" in rule) {
- let prefixNode = element.querySelector(".prefix");
- is(prefixNode && prefixNode.textContent, rule.prefix, "Check prefix");
- }
- let count = rule.count || 1;
- if (!rule.matched) {
- rule.matched = new Set();
- }
- rule.matched.add(element);
- return rule.matched.size == count;
- }
- function onMessagesAdded(event, newMessages) {
- for (let msg of newMessages) {
- let elem = msg.node;
- let location = getRenderedSource(elem);
- if (location && location.url) {
- let url = location.url;
- // Prevent recursion with the browser console and any potential
- // messages coming from head.js.
- if (url.indexOf("devtools/client/webconsole/test/head.js") != -1) {
- continue;
- }
- }
- for (let rule of rules) {
- if (rule._ruleMatched) {
- continue;
- }
- let matched = checkMessage(rule, elem);
- if (matched) {
- rule._ruleMatched = true;
- rulesMatched++;
- ok(1, "matched rule: " + displayRule(rule));
- if (maybeDone()) {
- return;
- }
- }
- }
- }
- }
- function allRulesMatched() {
- return options.matchCondition == "all" && rulesMatched == rules.length ||
- options.matchCondition == "any" && rulesMatched > 0;
- }
- function maybeDone() {
- if (allRulesMatched()) {
- if (listenerAdded) {
- webconsole.ui.off("new-messages", onMessagesAdded);
- }
- gPendingOutputTest--;
- deferred.resolve(rules);
- return true;
- }
- return false;
- }
- function testCleanup() {
- if (allRulesMatched()) {
- return;
- }
- if (webconsole.ui) {
- webconsole.ui.off("new-messages", onMessagesAdded);
- }
- for (let rule of rules) {
- if (!rule._ruleMatched) {
- ok(false, "failed to match rule: " + displayRule(rule));
- }
- }
- }
- function displayRule(rule) {
- return rule.name || rule.text;
- }
- function displayErrorContext(rule, element) {
- console.log("error occured during rule " + displayRule(rule));
- console.log("while checking the following message");
- dumpMessageElement(element);
- }
- executeSoon(() => {
- let messages = [];
- for (let elem of webconsole.outputNode.childNodes) {
- messages.push({
- node: elem,
- update: false,
- });
- }
- onMessagesAdded("new-messages", messages);
- if (!allRulesMatched()) {
- listenerAdded = true;
- registerCleanupFunction(testCleanup);
- webconsole.ui.on("new-messages", onMessagesAdded);
- }
- });
- return deferred.promise;
- }
- function whenDelayedStartupFinished(win, callback) {
- Services.obs.addObserver(function observer(subject, topic) {
- if (win == subject) {
- Services.obs.removeObserver(observer, topic);
- executeSoon(callback);
- }
- }, "browser-delayed-startup-finished", false);
- }
- /**
- * Check the web console output for the given inputs. Each input is checked for
- * the expected JS eval result, the result of calling print(), the result of
- * console.log(). The JS eval result is also checked if it opens the variables
- * view on click.
- *
- * @param object hud
- * The web console instance to work with.
- * @param array inputTests
- * An array of input tests. An input test element is an object. Each
- * object has the following properties:
- * - input: string, JS input value to execute.
- *
- * - output: string|RegExp, expected JS eval result.
- *
- * - inspectable: boolean, when true, the test runner expects the JS eval
- * result is an object that can be clicked for inspection.
- *
- * - noClick: boolean, when true, the test runner does not click the JS
- * eval result. Some objects, like |window|, have a lot of properties and
- * opening vview for them is very slow (they can cause timeouts in debug
- * builds).
- *
- * - consoleOutput: string|RegExp, optional, expected consoleOutput
- * If not provided consoleOuput = output;
- *
- * - printOutput: string|RegExp, optional, expected output for
- * |print(input)|. If this is not provided, printOutput = output.
- *
- * - variablesViewLabel: string|RegExp, optional, the expected variables
- * view label when the object is inspected. If this is not provided, then
- * |output| is used.
- *
- * - inspectorIcon: boolean, when true, the test runner expects the
- * result widget to contain an inspectorIcon element (className
- * open-inspector).
- *
- * - expectedTab: string, optional, the full URL of the new tab which
- * must open. If this is not provided, any new tabs that open will cause
- * a test failure.
- */
- function checkOutputForInputs(hud, inputTests) {
- let container = gBrowser.tabContainer;
- function* runner() {
- for (let [i, entry] of inputTests.entries()) {
- info("checkInput(" + i + "): " + entry.input);
- yield checkInput(entry);
- }
- container = null;
- }
- function* checkInput(entry) {
- yield checkConsoleLog(entry);
- yield checkPrintOutput(entry);
- yield checkJSEval(entry);
- }
- function* checkConsoleLog(entry) {
- info("Logging");
- hud.jsterm.clearOutput();
- hud.jsterm.execute("console.log(" + entry.input + ")");
- let consoleOutput = "consoleOutput" in entry ?
- entry.consoleOutput : entry.output;
- let [result] = yield waitForMessages({
- webconsole: hud,
- messages: [{
- name: "console.log() output: " + consoleOutput,
- text: consoleOutput,
- category: CATEGORY_WEBDEV,
- severity: SEVERITY_LOG,
- }],
- });
- let msg = [...result.matched][0];
- if (entry.consoleLogClick) {
- yield checkObjectClick(entry, msg);
- }
- if (typeof entry.inspectorIcon == "boolean") {
- info("Checking Inspector Link");
- yield checkLinkToInspector(entry.inspectorIcon, msg);
- }
- }
- function checkPrintOutput(entry) {
- info("Printing");
- hud.jsterm.clearOutput();
- hud.jsterm.execute("print(" + entry.input + ")");
- let printOutput = entry.printOutput || entry.output;
- return waitForMessages({
- webconsole: hud,
- messages: [{
- name: "print() output: " + printOutput,
- text: printOutput,
- category: CATEGORY_OUTPUT,
- }],
- });
- }
- function* checkJSEval(entry) {
- info("Evaluating");
- hud.jsterm.clearOutput();
- hud.jsterm.execute(entry.input);
- let evalOutput = entry.evalOutput || entry.output;
- let [result] = yield waitForMessages({
- webconsole: hud,
- messages: [{
- name: "JS eval output: " + entry.evalOutput,
- text: entry.evalOutput,
- category: CATEGORY_OUTPUT,
- }],
- });
- let msg = [...result.matched][0];
- if (!entry.noClick) {
- yield checkObjectClick(entry, msg);
- }
- if (typeof entry.inspectorIcon == "boolean") {
- info("Checking Inspector Link: " + entry.input);
- yield checkLinkToInspector(entry.inspectorIcon, msg);
- }
- }
- function* checkObjectClick(entry, msg) {
- info("Clicking");
- let body;
- if (entry.getClickableNode) {
- body = entry.getClickableNode(msg);
- } else {
- body = msg.querySelector(".message-body a") ||
- msg.querySelector(".message-body");
- }
- ok(body, "the message body");
- let deferredVariablesView = promise.defer();
- entry._onVariablesViewOpen = onVariablesViewOpen.bind(null, entry,
- deferredVariablesView);
- hud.jsterm.on("variablesview-open", entry._onVariablesViewOpen);
- let deferredTab = promise.defer();
- entry._onTabOpen = onTabOpen.bind(null, entry, deferredTab);
- container.addEventListener("TabOpen", entry._onTabOpen, true);
- body.scrollIntoView();
- if (!entry.suppressClick) {
- EventUtils.synthesizeMouse(body, 2, 2, {}, hud.iframeWindow);
- }
- if (entry.inspectable) {
- info("message body tagName '" + body.tagName + "' className '" +
- body.className + "'");
- yield deferredVariablesView.promise;
- } else {
- hud.jsterm.off("variablesview-open", entry._onVariablesView);
- entry._onVariablesView = null;
- }
- if (entry.expectedTab) {
- yield deferredTab.promise;
- } else {
- container.removeEventListener("TabOpen", entry._onTabOpen, true);
- entry._onTabOpen = null;
- }
- yield promise.resolve(null);
- }
- function onVariablesViewOpen(entry, {resolve, reject}, event, view, options) {
- info("Variables view opened");
- let label = entry.variablesViewLabel || entry.output;
- if (typeof label == "string" && options.label != label) {
- return;
- }
- if (label instanceof RegExp && !label.test(options.label)) {
- return;
- }
- hud.jsterm.off("variablesview-open", entry._onVariablesViewOpen);
- entry._onVariablesViewOpen = null;
- ok(entry.inspectable, "variables view was shown");
- resolve(null);
- }
- function onTabOpen(entry, {resolve, reject}, event) {
- container.removeEventListener("TabOpen", entry._onTabOpen, true);
- entry._onTabOpen = null;
- let tab = event.target;
- let browser = gBrowser.getBrowserForTab(tab);
- Task.spawn(function* () {
- yield loadBrowser(browser);
- let uri = yield ContentTask.spawn(browser, {}, function* () {
- return content.location.href;
- });
- ok(entry.expectedTab && entry.expectedTab == uri,
- "opened tab '" + uri + "', expected tab '" + entry.expectedTab + "'");
- yield closeTab(tab);
- }).then(resolve, reject);
- }
- return Task.spawn(runner);
- }
- /**
- * Check the web console DOM element output for the given inputs.
- * Each input is checked for the expected JS eval result. The JS eval result is
- * also checked if it opens the inspector with the correct node selected on
- * inspector icon click
- *
- * @param object hud
- * The web console instance to work with.
- * @param array inputTests
- * An array of input tests. An input test element is an object. Each
- * object has the following properties:
- * - input: string, JS input value to execute.
- *
- * - output: string, expected JS eval result.
- *
- * - displayName: string, expected NodeFront's displayName.
- *
- * - attr: Array, expected NodeFront's attributes
- */
- function checkDomElementHighlightingForInputs(hud, inputs) {
- function* runner() {
- let toolbox = gDevTools.getToolbox(hud.target);
- // Loading the inspector panel at first, to make it possible to listen for
- // new node selections
- yield toolbox.selectTool("inspector");
- let inspector = toolbox.getCurrentPanel();
- yield toolbox.selectTool("webconsole");
- info("Iterating over the test data");
- for (let data of inputs) {
- let [result] = yield jsEval(data.input, {text: data.output});
- let {msg} = yield checkWidgetAndMessage(result);
- yield checkNodeHighlight(toolbox, inspector, msg, data);
- }
- }
- function jsEval(input, message) {
- info("Executing '" + input + "' in the web console");
- hud.jsterm.clearOutput();
- hud.jsterm.execute(input);
- return waitForMessages({
- webconsole: hud,
- messages: [message]
- });
- }
- function* checkWidgetAndMessage(result) {
- info("Getting the output ElementNode widget");
- let msg = [...result.matched][0];
- let widget = [...msg._messageObject.widgets][0];
- ok(widget, "ElementNode widget found in the output");
- info("Waiting for the ElementNode widget to be linked to the inspector");
- yield widget.linkToInspector();
- return {widget, msg};
- }
- function* checkNodeHighlight(toolbox, inspector, msg, testData) {
- let inspectorIcon = msg.querySelector(".open-inspector");
- ok(inspectorIcon, "Inspector icon found in the ElementNode widget");
- info("Clicking on the inspector icon and waiting for the " +
- "inspector to be selected");
- let onInspectorSelected = toolbox.once("inspector-selected");
- let onInspectorUpdated = inspector.once("inspector-updated");
- let onNewNode = toolbox.selection.once("new-node-front");
- let onNodeHighlight = toolbox.once("node-highlight");
- EventUtils.synthesizeMouseAtCenter(inspectorIcon, {},
- inspectorIcon.ownerDocument.defaultView);
- yield onInspectorSelected;
- yield onInspectorUpdated;
- yield onNodeHighlight;
- let nodeFront = yield onNewNode;
- ok(true, "Inspector selected and new node got selected");
- is(nodeFront.displayName, testData.displayName,
- "The correct node was highlighted");
- if (testData.attrs) {
- let attrs = nodeFront.attributes;
- for (let i in testData.attrs) {
- is(attrs[i].name, testData.attrs[i].name,
- "Expected attribute's name is present");
- is(attrs[i].value, testData.attrs[i].value,
- "Expected attribute's value is present");
- }
- }
- info("Unhighlight the node by moving away from the markup view");
- let onNodeUnhighlight = toolbox.once("node-unhighlight");
- let btn = inspector.toolbox.doc.querySelector(".toolbox-dock-button");
- EventUtils.synthesizeMouseAtCenter(btn, {type: "mousemove"},
- inspector.toolbox.win);
- yield onNodeUnhighlight;
- info("Switching back to the console");
- yield toolbox.selectTool("webconsole");
- }
- return Task.spawn(runner);
- }
- /**
- * Finish the request and resolve with the request object.
- *
- * @param {Function} predicate A predicate function that takes the request
- * object as an argument and returns true if the request was the expected one,
- * false otherwise. The returned promise is resolved ONLY if the predicate
- * matches a request. Defaults to accepting any request.
- * @return promise
- * @resolves The request object.
- */
- function waitForFinishedRequest(predicate = () => true) {
- registerCleanupFunction(function () {
- HUDService.lastFinishedRequest.callback = null;
- });
- return new Promise(resolve => {
- HUDService.lastFinishedRequest.callback = request => {
- // Check if this is the expected request
- if (predicate(request)) {
- // Match found. Clear the listener.
- HUDService.lastFinishedRequest.callback = null;
- resolve(request);
- } else {
- info(`Ignoring unexpected request ${JSON.stringify(request, null, 2)}`);
- }
- };
- });
- }
- /**
- * Wait for eventName on target.
- * @param {Object} target An observable object that either supports on/off or
- * addEventListener/removeEventListener
- * @param {String} eventName
- * @param {Boolean} useCapture Optional for addEventListener/removeEventListener
- * @return A promise that resolves when the event has been handled
- */
- function once(target, eventName, useCapture = false) {
- info("Waiting for event: '" + eventName + "' on " + target + ".");
- let deferred = promise.defer();
- for (let [add, remove] of [
- ["addEventListener", "removeEventListener"],
- ["addListener", "removeListener"],
- ["on", "off"]
- ]) {
- if ((add in target) && (remove in target)) {
- target[add](eventName, function onEvent(...aArgs) {
- target[remove](eventName, onEvent, useCapture);
- deferred.resolve.apply(deferred, aArgs);
- }, useCapture);
- break;
- }
- }
- return deferred.promise;
- }
- /**
- * Checks a link to the inspector
- *
- * @param {boolean} hasLinkToInspector Set to true if the message should
- * link to the inspector panel.
- * @param {element} msg The message to test.
- */
- function checkLinkToInspector(hasLinkToInspector, msg) {
- let elementNodeWidget = [...msg._messageObject.widgets][0];
- if (!elementNodeWidget) {
- ok(!hasLinkToInspector, "The message has no ElementNode widget");
- return true;
- }
- return elementNodeWidget.linkToInspector().then(() => {
- // linkToInspector resolved, check for the .open-inspector element
- if (hasLinkToInspector) {
- ok(msg.querySelectorAll(".open-inspector").length,
- "The ElementNode widget is linked to the inspector");
- } else {
- ok(!msg.querySelectorAll(".open-inspector").length,
- "The ElementNode widget isn't linked to the inspector");
- }
- }, () => {
- // linkToInspector promise rejected, node not linked to inspector
- ok(!hasLinkToInspector,
- "The ElementNode widget isn't linked to the inspector");
- });
- }
- function getSourceActor(sources, URL) {
- let item = sources.getItemForAttachment(a => a.source.url === URL);
- return item && item.value;
- }
- /**
- * Make a request against an actor and resolve with the packet.
- * @param object client
- * The client to use when making the request.
- * @param function requestType
- * The client request function to run.
- * @param array args
- * The arguments to pass into the function.
- */
- function getPacket(client, requestType, args) {
- return new Promise(resolve => {
- client[requestType](...args, packet => resolve(packet));
- });
- }
- /**
- * Verify that clicking on a link from a popup notification message tries to
- * open the expected URL.
- */
- function simulateMessageLinkClick(element, expectedLink) {
- let deferred = promise.defer();
- // Invoke the click event and check if a new tab would
- // open to the correct page.
- let oldOpenUILinkIn = window.openUILinkIn;
- window.openUILinkIn = function (link) {
- if (link == expectedLink) {
- ok(true, "Clicking the message link opens the desired page");
- window.openUILinkIn = oldOpenUILinkIn;
- deferred.resolve();
- }
- };
- let event = new MouseEvent("click", {
- detail: 1,
- button: 0,
- bubbles: true,
- cancelable: true
- });
- element.dispatchEvent(event);
- return deferred.promise;
- }
- function getRenderedSource(root) {
- let location = root.querySelector(".message-location .frame-link");
- return location ? {
- url: location.getAttribute("data-url"),
- line: location.getAttribute("data-line"),
- column: location.getAttribute("data-column"),
- } : null;
- }
- function waitForBrowserConsole() {
- return new Promise(resolve => {
- Services.obs.addObserver(function observer(subject) {
- Services.obs.removeObserver(observer, "web-console-created");
- subject.QueryInterface(Ci.nsISupportsString);
- let hud = HUDService.getBrowserConsole();
- ok(hud, "browser console is open");
- is(subject.data, hud.hudId, "notification hudId is correct");
- executeSoon(() => resolve(hud));
- }, "web-console-created");
- });
- }
|