summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/test/head.js
blob: fbf0e84af39f5d4b65cb0535a0af324baca4161b (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
/* 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:
 * "<comment> The part we want   </comment>\n this: is-the-part-we-want;"
 *
 * we would represent the expected output like this:
 * [{type: "comment",        text: "<comment> The part we want   </comment>"},
 *  {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]);
  }
}