/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ /* eslint no-unused-vars: [2, {"vars": "local", "args": "none"}] */ /* 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); const {DOMHelpers} = Cu.import("resource://devtools/client/shared/DOMHelpers.jsm", {}); const {Hosts} = require("devtools/client/framework/toolbox-hosts"); const TEST_URI_ROOT = "http://example.com/browser/devtools/client/shared/test/"; const OPTIONS_VIEW_URL = TEST_URI_ROOT + "doc_options-view.xul"; function catchFail(func) { return function () { try { return func.apply(null, arguments); } catch (ex) { ok(false, ex); console.error(ex); finish(); throw ex; } }; } /** * Polls a given function waiting for the given value. * * @param object options * Options object with the following properties: * - validator * A validator function that should return the expected value. This is * called every few milliseconds to check if the result is the expected * one. When the returned result is the expected one, then the |success| * function is called and polling stops. If |validator| never returns * the expected value, then polling timeouts after several tries and * a failure is recorded - the given |failure| function is invoked. * - success * A function called when the validator function returns the expected * value. * - failure * A function called if the validator function timeouts - fails to return * the expected value in the given time. * - name * Name of test. This is used to generate the success and failure * messages. * - timeout * Timeout for validator function, in milliseconds. Default is 5000 ms. * - value * The expected value. If this option is omitted then the |validator| * function must return a trueish value. * Each of the provided callback functions will receive two arguments: * the |options| object and the last value returned by |validator|. */ function waitForValue(options) { let start = Date.now(); let timeout = options.timeout || 5000; let lastValue; function wait(validatorFn, successFn, failureFn) { if ((Date.now() - start) > timeout) { // Log the failure. ok(false, "Timed out while waiting for: " + options.name); let expected = "value" in options ? "'" + options.value + "'" : "a trueish value"; info("timeout info :: got '" + lastValue + "', expected " + expected); failureFn(options, lastValue); return; } lastValue = validatorFn(options, lastValue); let successful = "value" in options ? lastValue == options.value : lastValue; if (successful) { ok(true, options.name); successFn(options, lastValue); } else { setTimeout(() => { wait(validatorFn, successFn, failureFn); }, 100); } } wait(options.validator, options.success, options.failure); } function oneTimeObserve(name, callback) { return new Promise((resolve) => { let func = function () { Services.obs.removeObserver(func, name); if (callback) { callback(); } resolve(); }; Services.obs.addObserver(func, name, false); }); } let createHost = Task.async(function* (type = "bottom", src = "data:text/html;charset=utf-8,") { let host = new Hosts[type](gBrowser.selectedTab); let iframe = yield host.create(); yield new Promise(resolve => { let domHelper = new DOMHelpers(iframe.contentWindow); iframe.setAttribute("src", src); domHelper.onceDOMReady(resolve); }); return [host, iframe.contentWindow, iframe.contentDocument]; }); /** * Check the correctness of the data recorded in Telemetry after * loadTelemetryAndRecordLogs was called. */ function checkTelemetryResults(Telemetry) { let result = Telemetry.prototype.telemetryInfo; for (let histId in result) { let value = result[histId]; if (histId.endsWith("OPENED_COUNT")) { ok(value.length > 1, histId + " has more than one entry"); let okay = value.every(function (element) { return element === true; }); ok(okay, "All " + histId + " entries are === true"); } else if (histId.endsWith("TIME_ACTIVE_SECONDS")) { ok(value.length > 1, histId + " has more than one entry"); let okay = value.every(function (element) { return element > 0; }); ok(okay, "All " + histId + " entries have time > 0"); } } } /** * Open and close the toolbox in the current browser tab, several times, waiting * some amount of time in between. * @param {Number} nbOfTimes * @param {Number} usageTime in milliseconds * @param {String} toolId */ function* openAndCloseToolbox(nbOfTimes, usageTime, toolId) { for (let i = 0; i < nbOfTimes; i++) { info("Opening toolbox " + (i + 1)); let target = TargetFactory.forTab(gBrowser.selectedTab); yield gDevTools.showToolbox(target, toolId); // We use a timeout to check the toolbox's active time yield new Promise(resolve => setTimeout(resolve, usageTime)); info("Closing toolbox " + (i + 1)); yield gDevTools.closeToolbox(target); } } /** * Synthesize a profile for testing. */ function synthesizeProfileForTest(samples) { const RecordingUtils = require("devtools/shared/performance/recording-utils"); samples.unshift({ time: 0, frames: [] }); let uniqueStacks = new RecordingUtils.UniqueStacks(); return RecordingUtils.deflateThread({ samples: samples, markers: [] }, uniqueStacks); } /** * Waits until a predicate returns true. * * @param function predicate * Invoked once in a while until it returns true. * @param number interval [optional] * How often the predicate is invoked, in milliseconds. */ function waitUntil(predicate, interval = 10) { if (predicate()) { return Promise.resolve(true); } return new Promise(resolve => { setTimeout(function () { waitUntil(predicate).then(() => resolve(true)); }, interval); }); } /** * Show the presets list sidebar in the cssfilter widget popup * @param {CSSFilterWidget} widget * @return {Promise} */ function showFilterPopupPresets(widget) { let onRender = widget.once("render"); widget._togglePresets(); return onRender; } /** * Show presets list and create a sample preset with the name and value provided * @param {CSSFilterWidget} widget * @param {string} name * @param {string} value * @return {Promise} */ let showFilterPopupPresetsAndCreatePreset = Task.async(function* (widget, name, value) { yield showFilterPopupPresets(widget); let onRender = widget.once("render"); widget.setCssValue(value); yield onRender; let footer = widget.el.querySelector(".presets-list .footer"); footer.querySelector("input").value = name; onRender = widget.once("render"); widget._savePreset({ preventDefault: () => {} }); yield onRender; }); /** * Utility function for testing CSS code samples that have been * syntax-highlighted. * * The CSS syntax highlighter emits a collection of DOM nodes that have * CSS classes applied to them. This function checks that those nodes * are what we expect. * * @param {array} expectedNodes * A representation of the nodes we expect to see. * Each node is an object containing two properties: * - type: a string which can be one of: * - text, comment, property-name, property-value * - text: the textContent of the node * * For example, given a string like this: * " The part we want \n this: is-the-part-we-want;" * * we would represent the expected output like this: * [{type: "comment", text: " The part we want "}, * {type: "text", text: "\n"}, * {type: "property-name", text: "this"}, * {type: "text", text: ":"}, * {type: "text", text: " "}, * {type: "property-value", text: "is-the-part-we-want"}, * {type: "text", text: ";"}]; * * @param {Node} parent * The DOM node whose children are the output of the syntax highlighter. */ function checkCssSyntaxHighlighterOutput(expectedNodes, parent) { /** * The classes applied to the output nodes by the syntax highlighter. * These must be same as the definitions in MdnDocsWidget.js. */ const PROPERTY_NAME_COLOR = "theme-fg-color5"; const PROPERTY_VALUE_COLOR = "theme-fg-color1"; const COMMENT_COLOR = "theme-comment"; /** * Check the type and content of a single node. */ function checkNode(expected, actual) { ok(actual.textContent == expected.text, "Check that node has the expected textContent"); info("Expected text content: [" + expected.text + "]"); info("Actual text content: [" + actual.textContent + "]"); info("Check that node has the expected type"); if (expected.type == "text") { ok(actual.nodeType == 3, "Check that node is a text node"); } else { ok(actual.tagName.toUpperCase() == "SPAN", "Check that node is a SPAN"); } info("Check that node has the expected className"); let expectedClassName = null; let actualClassName = null; switch (expected.type) { case "property-name": expectedClassName = PROPERTY_NAME_COLOR; break; case "property-value": expectedClassName = PROPERTY_VALUE_COLOR; break; case "comment": expectedClassName = COMMENT_COLOR; break; default: ok(!actual.classList, "No className expected"); return; } ok(actual.classList.length == 1, "One className expected"); actualClassName = actual.classList[0]; ok(expectedClassName == actualClassName, "Check className value"); info("Expected className: " + expectedClassName); info("Actual className: " + actualClassName); } info("Logging the actual nodes we have:"); for (let j = 0; j < parent.childNodes.length; j++) { let n = parent.childNodes[j]; info(j + " / " + "nodeType: " + n.nodeType + " / " + "textContent: " + n.textContent); } ok(parent.childNodes.length == parent.childNodes.length, "Check we have the expected number of nodes"); info("Expected node count " + expectedNodes.length); info("Actual node count " + expectedNodes.length); for (let i = 0; i < expectedNodes.length; i++) { info("Check node " + i); checkNode(expectedNodes[i], parent.childNodes[i]); } }