summaryrefslogtreecommitdiffstats
path: root/devtools/client/framework/test/shared-head.js
blob: a89c6d7525ecae4870ceb8d54fd6dab39d99e75d (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
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
/* Any copyright is dedicated to the Public Domain.
 * http://creativecommons.org/publicdomain/zero/1.0/ */
/* eslint no-unused-vars: [2, {"vars": "local"}] */

"use strict";

// This shared-head.js file is used for multiple mochitest test directories in
// devtools.
// It contains various common helper functions.

const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr, Constructor: CC}
  = Components;

function scopedCuImport(path) {
  const scope = {};
  Cu.import(path, scope);
  return scope;
}

const {console} = scopedCuImport("resource://gre/modules/Console.jsm");
const {ScratchpadManager} = scopedCuImport("resource://devtools/client/scratchpad/scratchpad-manager.jsm");
const {loader, require} = scopedCuImport("resource://devtools/shared/Loader.jsm");

const {gDevTools} = require("devtools/client/framework/devtools");
const {TargetFactory} = require("devtools/client/framework/target");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const flags = require("devtools/shared/flags");
let promise = require("promise");
let defer = require("devtools/shared/defer");
const Services = require("Services");
const {Task} = require("devtools/shared/task");
const {KeyShortcuts} = require("devtools/client/shared/key-shortcuts");

const TEST_DIR = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
const CHROME_URL_ROOT = TEST_DIR + "/";
const URL_ROOT = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
                                         "http://example.com/");
const URL_ROOT_SSL = CHROME_URL_ROOT.replace("chrome://mochitests/content/",
                                             "https://example.com/");

// All test are asynchronous
waitForExplicitFinish();

var EXPECTED_DTU_ASSERT_FAILURE_COUNT = 0;

registerCleanupFunction(function () {
  if (DevToolsUtils.assertionFailureCount !==
      EXPECTED_DTU_ASSERT_FAILURE_COUNT) {
    ok(false,
      "Should have had the expected number of DevToolsUtils.assert() failures."
      + " Expected " + EXPECTED_DTU_ASSERT_FAILURE_COUNT
      + ", got " + DevToolsUtils.assertionFailureCount);
  }
});

// Uncomment this pref to dump all devtools emitted events to the console.
// Services.prefs.setBoolPref("devtools.dump.emit", true);

/**
 * Watch console messages for failed propType definitions in React components.
 */
const ConsoleObserver = {
  QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver]),

  observe: function (subject, topic, data) {
    let message = subject.wrappedJSObject.arguments[0];

    if (/Failed propType/.test(message)) {
      ok(false, message);
    }
  }
};

Services.obs.addObserver(ConsoleObserver, "console-api-log-event", false);
registerCleanupFunction(() => {
  Services.obs.removeObserver(ConsoleObserver, "console-api-log-event");
});

var waitForTime = DevToolsUtils.waitForTime;

function getFrameScript() {
  let mm = gBrowser.selectedBrowser.messageManager;
  let frameURL = "chrome://devtools/content/shared/frame-script-utils.js";
  mm.loadFrameScript(frameURL, false);
  SimpleTest.registerCleanupFunction(() => {
    mm = null;
  });
  return mm;
}

flags.testing = true;
registerCleanupFunction(() => {
  flags.testing = false;
  Services.prefs.clearUserPref("devtools.dump.emit");
  Services.prefs.clearUserPref("devtools.toolbox.host");
  Services.prefs.clearUserPref("devtools.toolbox.previousHost");
  Services.prefs.clearUserPref("devtools.toolbox.splitconsoleEnabled");
});

registerCleanupFunction(function* cleanup() {
  while (gBrowser.tabs.length > 1) {
    yield closeTabAndToolbox(gBrowser.selectedTab);
  }
});

/**
 * Add a new test tab in the browser and load the given url.
 * @param {String} url The url to be loaded in the new tab
 * @param {Object} options Object with various optional fields:
 *   - {Boolean} background If true, open the tab in background
 *   - {ChromeWindow} window Firefox top level window we should use to open the tab
 * @return a promise that resolves to the tab object when the url is loaded
 */
var addTab = Task.async(function* (url, options = { background: false, window: window }) {
  info("Adding a new tab with URL: " + url);

  let { background } = options;
  let { gBrowser } = options.window ? options.window : window;

  let tab = gBrowser.addTab(url);
  if (!background) {
    gBrowser.selectedTab = tab;
  }
  yield BrowserTestUtils.browserLoaded(tab.linkedBrowser);

  info("Tab added and finished loading");

  return tab;
});

/**
 * Remove the given tab.
 * @param {Object} tab The tab to be removed.
 * @return Promise<undefined> resolved when the tab is successfully removed.
 */
var removeTab = Task.async(function* (tab) {
  info("Removing tab.");

  let { gBrowser } = tab.ownerDocument.defaultView;
  let onClose = once(gBrowser.tabContainer, "TabClose");
  gBrowser.removeTab(tab);
  yield onClose;

  info("Tab removed and finished closing");
});

/**
 * Refresh the given tab.
 * @param {Object} tab The tab to be refreshed.
 * @return Promise<undefined> resolved when the tab is successfully refreshed.
 */
var refreshTab = Task.async(function*(tab) {
  info("Refreshing tab.");
  const finished = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
  gBrowser.reloadTab(gBrowser.selectedTab);
  yield finished;
  info("Tab finished refreshing.");
});

/**
 * Simulate a key event from a <key> element.
 * @param {DOMNode} key
 */
function synthesizeKeyFromKeyTag(key) {
  is(key && key.tagName, "key", "Successfully retrieved the <key> node");

  let modifiersAttr = key.getAttribute("modifiers");

  let name = null;

  if (key.getAttribute("keycode")) {
    name = key.getAttribute("keycode");
  } else if (key.getAttribute("key")) {
    name = key.getAttribute("key");
  }

  isnot(name, null, "Successfully retrieved keycode/key");

  let modifiers = {
    shiftKey: !!modifiersAttr.match("shift"),
    ctrlKey: !!modifiersAttr.match("control"),
    altKey: !!modifiersAttr.match("alt"),
    metaKey: !!modifiersAttr.match("meta"),
    accelKey: !!modifiersAttr.match("accel")
  };

  info("Synthesizing key " + name + " " + JSON.stringify(modifiers));
  EventUtils.synthesizeKey(name, modifiers);
}

/**
 * Simulate a key event from an electron key shortcut string:
 * https://github.com/electron/electron/blob/master/docs/api/accelerator.md
 *
 * @param {String} key
 * @param {DOMWindow} target
 *        Optional window where to fire the key event
 */
function synthesizeKeyShortcut(key, target) {
  // parseElectronKey requires any window, just to access `KeyboardEvent`
  let window = Services.appShell.hiddenDOMWindow;
  let shortcut = KeyShortcuts.parseElectronKey(window, key);
  let keyEvent = {
    altKey: shortcut.alt,
    ctrlKey: shortcut.ctrl,
    metaKey: shortcut.meta,
    shiftKey: shortcut.shift
  };
  if (shortcut.keyCode) {
    keyEvent.keyCode = shortcut.keyCode;
  }

  info("Synthesizing key shortcut: " + key);
  EventUtils.synthesizeKey(shortcut.key || "", keyEvent, target);
}

/**
 * Wait for eventName on target to be delivered a number of times.
 *
 * @param {Object} target
 *        An observable object that either supports on/off or
 *        addEventListener/removeEventListener
 * @param {String} eventName
 * @param {Number} numTimes
 *        Number of deliveries to wait for.
 * @param {Boolean} useCapture
 *        Optional, for addEventListener/removeEventListener
 * @return A promise that resolves when the event has been handled
 */
function waitForNEvents(target, eventName, numTimes, useCapture = false) {
  info("Waiting for event: '" + eventName + "' on " + target + ".");

  let deferred = defer();
  let count = 0;

  for (let [add, remove] of [
    ["addEventListener", "removeEventListener"],
    ["addListener", "removeListener"],
    ["on", "off"]
  ]) {
    if ((add in target) && (remove in target)) {
      target[add](eventName, function onEvent(...aArgs) {
        info("Got event: '" + eventName + "' on " + target + ".");
        if (++count == numTimes) {
          target[remove](eventName, onEvent, useCapture);
          deferred.resolve.apply(deferred, aArgs);
        }
      }, useCapture);
      break;
    }
  }

  return deferred.promise;
}

/**
 * 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) {
  return waitForNEvents(target, eventName, 1, useCapture);
}

/**
 * Some tests may need to import one or more of the test helper scripts.
 * A test helper script is simply a js file that contains common test code that
 * is either not common-enough to be in head.js, or that is located in a
 * separate directory.
 * The script will be loaded synchronously and in the test's scope.
 * @param {String} filePath The file path, relative to the current directory.
 *                 Examples:
 *                 - "helper_attributes_test_runner.js"
 *                 - "../../../commandline/test/helpers.js"
 */
function loadHelperScript(filePath) {
  let testDir = gTestPath.substr(0, gTestPath.lastIndexOf("/"));
  Services.scriptloader.loadSubScript(testDir + "/" + filePath, this);
}

/**
 * Wait for a tick.
 * @return {Promise}
 */
function waitForTick() {
  let deferred = defer();
  executeSoon(deferred.resolve);
  return deferred.promise;
}

/**
 * This shouldn't be used in the tests, but is useful when writing new tests or
 * debugging existing tests in order to introduce delays in the test steps
 *
 * @param {Number} ms
 *        The time to wait
 * @return A promise that resolves when the time is passed
 */
function wait(ms) {
  return new promise(resolve => setTimeout(resolve, ms));
}

/**
 * Open the toolbox in a given tab.
 * @param {XULNode} tab The tab the toolbox should be opened in.
 * @param {String} toolId Optional. The ID of the tool to be selected.
 * @param {String} hostType Optional. The type of toolbox host to be used.
 * @return {Promise} Resolves with the toolbox, when it has been opened.
 */
var openToolboxForTab = Task.async(function* (tab, toolId, hostType) {
  info("Opening the toolbox");

  let toolbox;
  let target = TargetFactory.forTab(tab);
  yield target.makeRemote();

  // Check if the toolbox is already loaded.
  toolbox = gDevTools.getToolbox(target);
  if (toolbox) {
    if (!toolId || (toolId && toolbox.getPanel(toolId))) {
      info("Toolbox is already opened");
      return toolbox;
    }
  }

  // If not, load it now.
  toolbox = yield gDevTools.showToolbox(target, toolId, hostType);

  // Make sure that the toolbox frame is focused.
  yield new Promise(resolve => waitForFocus(resolve, toolbox.win));

  info("Toolbox opened and focused");

  return toolbox;
});

/**
 * Add a new tab and open the toolbox in it.
 * @param {String} url The URL for the tab to be opened.
 * @param {String} toolId Optional. The ID of the tool to be selected.
 * @param {String} hostType Optional. The type of toolbox host to be used.
 * @return {Promise} Resolves when the tab has been added, loaded and the
 * toolbox has been opened. Resolves to the toolbox.
 */
var openNewTabAndToolbox = Task.async(function* (url, toolId, hostType) {
  let tab = yield addTab(url);
  return openToolboxForTab(tab, toolId, hostType);
});

/**
 * Close a tab and if necessary, the toolbox that belongs to it
 * @param {Tab} tab The tab to close.
 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
 * closed.
 */
var closeTabAndToolbox = Task.async(function* (tab = gBrowser.selectedTab) {
  let target = TargetFactory.forTab(gBrowser.selectedTab);
  if (target) {
    yield gDevTools.closeToolbox(target);
  }

  yield removeTab(gBrowser.selectedTab);
});

/**
 * Close a toolbox and the current tab.
 * @param {Toolbox} toolbox The toolbox to close.
 * @return {Promise} Resolves when the toolbox and tab have been destroyed and
 * closed.
 */
var closeToolboxAndTab = Task.async(function* (toolbox) {
  yield toolbox.destroy();
  yield removeTab(gBrowser.selectedTab);
});

/**
 * 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, interval).then(() => resolve(true));
    }, interval);
  });
}

/**
 * Takes a string `script` and evaluates it directly in the content
 * in potentially a different process.
 */
let MM_INC_ID = 0;
function evalInDebuggee(mm, script) {
  return new Promise(function (resolve, reject) {
    let id = MM_INC_ID++;
    mm.sendAsyncMessage("devtools:test:eval", { script, id });
    mm.addMessageListener("devtools:test:eval:response", handler);

    function handler({ data }) {
      if (id !== data.id) {
        return;
      }

      info(`Successfully evaled in debuggee: ${script}`);
      mm.removeMessageListener("devtools:test:eval:response", handler);
      resolve(data.value);
    }
  });
}

/**
 * Wait for a context menu popup to open.
 *
 * @param nsIDOMElement popup
 *        The XUL popup you expect to open.
 * @param nsIDOMElement button
 *        The button/element that receives the contextmenu event. This is
 *        expected to open the popup.
 * @param function onShown
 *        Function to invoke on popupshown event.
 * @param function onHidden
 *        Function to invoke on popuphidden event.
 * @return object
 *         A Promise object that is resolved after the popuphidden event
 *         callback is invoked.
 */
function waitForContextMenu(popup, button, onShown, onHidden) {
  let deferred = defer();

  function onPopupShown() {
    info("onPopupShown");
    popup.removeEventListener("popupshown", onPopupShown);

    onShown && onShown();

    // Use executeSoon() to get out of the popupshown event.
    popup.addEventListener("popuphidden", onPopupHidden);
    executeSoon(() => popup.hidePopup());
  }
  function onPopupHidden() {
    info("onPopupHidden");
    popup.removeEventListener("popuphidden", onPopupHidden);

    onHidden && onHidden();

    deferred.resolve(popup);
  }

  popup.addEventListener("popupshown", onPopupShown);

  info("wait for the context menu to open");
  button.scrollIntoView();
  let eventDetails = {type: "contextmenu", button: 2};
  EventUtils.synthesizeMouse(button, 5, 2, eventDetails,
                             button.ownerDocument.defaultView);
  return deferred.promise;
}

/**
 * Promise wrapper around SimpleTest.waitForClipboard
 */
function waitForClipboardPromise(setup, expected) {
  return new Promise((resolve, reject) => {
    SimpleTest.waitForClipboard(expected, setup, resolve, reject);
  });
}

/**
 * Simple helper to push a temporary preference. Wrapper on SpecialPowers
 * pushPrefEnv that returns a promise resolving when the preferences have been
 * updated.
 *
 * @param {String} preferenceName
 *        The name of the preference to updated
 * @param {} value
 *        The preference value, type can vary
 * @return {Promise} resolves when the preferences have been updated
 */
function pushPref(preferenceName, value) {
  return new Promise(resolve => {
    let options = {"set": [[preferenceName, value]]};
    SpecialPowers.pushPrefEnv(options, resolve);
  });
}

/**
 * Lookup the provided dotted path ("prop1.subprop2.myProp") in the provided object.
 *
 * @param {Object} obj
 *        Object to expand.
 * @param {String} path
 *        Dotted path to use to expand the object.
 * @return {?} anything that is found at the provided path in the object.
 */
function lookupPath(obj, path) {
  let segments = path.split(".");
  return segments.reduce((prev, current) => prev[current], obj);
}

var closeToolbox = Task.async(function* () {
  let target = TargetFactory.forTab(gBrowser.selectedTab);
  yield gDevTools.closeToolbox(target);
});

/**
 * Load the Telemetry utils, then stub Telemetry.prototype.log and
 * Telemetry.prototype.logKeyed in order to record everything that's logged in
 * it.
 * Store all recordings in Telemetry.telemetryInfo.
 * @return {Telemetry}
 */
function loadTelemetryAndRecordLogs() {
  info("Mock the Telemetry log function to record logged information");

  let Telemetry = require("devtools/client/shared/telemetry");
  Telemetry.prototype.telemetryInfo = {};
  Telemetry.prototype._oldlog = Telemetry.prototype.log;
  Telemetry.prototype.log = function (histogramId, value) {
    if (!this.telemetryInfo) {
      // Telemetry instance still in use after stopRecordingTelemetryLogs
      return;
    }
    if (histogramId) {
      if (!this.telemetryInfo[histogramId]) {
        this.telemetryInfo[histogramId] = [];
      }
      this.telemetryInfo[histogramId].push(value);
    }
  };
  Telemetry.prototype._oldlogKeyed = Telemetry.prototype.logKeyed;
  Telemetry.prototype.logKeyed = function (histogramId, key, value) {
    this.log(`${histogramId}|${key}`, value);
  };

  return Telemetry;
}

/**
 * Stop recording the Telemetry logs and put back the utils as it was before.
 * @param {Telemetry} Required Telemetry
 *        Telemetry object that needs to be stopped.
 */
function stopRecordingTelemetryLogs(Telemetry) {
  info("Stopping Telemetry");
  Telemetry.prototype.log = Telemetry.prototype._oldlog;
  Telemetry.prototype.logKeyed = Telemetry.prototype._oldlogKeyed;
  delete Telemetry.prototype._oldlog;
  delete Telemetry.prototype._oldlogKeyed;
  delete Telemetry.prototype.telemetryInfo;
}

/**
 * Clean the logical clipboard content. This method only clears the OS clipboard on
 * Windows (see Bug 666254).
 */
function emptyClipboard() {
  let clipboard = Cc["@mozilla.org/widget/clipboard;1"]
    .getService(SpecialPowers.Ci.nsIClipboard);
  clipboard.emptyClipboard(clipboard.kGlobalClipboard);
}

/**
 * Check if the current operating system is Windows.
 */
function isWindows() {
  return Services.appinfo.OS === "WINNT";
}

/**
 * Wait for a given toolbox to get its title updated.
 */
function waitForTitleChange(toolbox) {
  let deferred = defer();
  toolbox.win.parent.addEventListener("message", function onmessage(event) {
    if (event.data.name == "set-host-title") {
      toolbox.win.parent.removeEventListener("message", onmessage);
      deferred.resolve();
    }
  });
  return deferred.promise;
}