Source code

Revision control

Other Tools

1
/* This Source Code Form is subject to the terms of the Mozilla Public
2
* License, v. 2.0. If a copy of the MPL was not distributed with this
3
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
4
5
/*
6
* This module implements a number of utilities useful for browser tests.
7
*
8
* All asynchronous helper methods should return promises, rather than being
9
* callback based.
10
*/
11
12
// This file uses ContentTask & frame scripts, where these are available.
13
/* global ContentTaskUtils */
14
15
"use strict";
16
17
var EXPORTED_SYMBOLS = ["BrowserTestUtils"];
18
19
const { AppConstants } = ChromeUtils.import(
21
);
22
const { XPCOMUtils } = ChromeUtils.import(
24
);
25
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
26
const { TestUtils } = ChromeUtils.import(
28
);
29
const { OS } = ChromeUtils.import("resource://gre/modules/osfile.jsm");
30
31
XPCOMUtils.defineLazyModuleGetters(this, {
32
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
35
});
36
37
XPCOMUtils.defineLazyServiceGetters(this, {
38
ProtocolProxyService: [
39
"@mozilla.org/network/protocol-proxy-service;1",
40
"nsIProtocolProxyService",
41
],
42
});
43
44
const PROCESSSELECTOR_CONTRACTID = "@mozilla.org/ipc/processselector;1";
45
const OUR_PROCESSSELECTOR_CID = Components.ID(
46
"{f9746211-3d53-4465-9aeb-ca0d96de0253}"
47
);
48
const EXISTING_JSID = Cc[PROCESSSELECTOR_CONTRACTID];
49
const DEFAULT_PROCESSSELECTOR_CID = EXISTING_JSID
50
? Components.ID(EXISTING_JSID.number)
51
: null;
52
53
let gListenerId = 0;
54
55
// A process selector that always asks for a new process.
56
function NewProcessSelector() {}
57
58
NewProcessSelector.prototype = {
59
classID: OUR_PROCESSSELECTOR_CID,
60
QueryInterface: ChromeUtils.generateQI([Ci.nsIContentProcessProvider]),
61
62
provideProcess() {
63
return Ci.nsIContentProcessProvider.NEW_PROCESS;
64
},
65
};
66
67
let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
68
let selectorFactory = XPCOMUtils._getFactory(NewProcessSelector);
69
registrar.registerFactory(OUR_PROCESSSELECTOR_CID, "", null, selectorFactory);
70
71
// For now, we'll allow tests to use CPOWs in this module for
72
// some cases.
73
Cu.permitCPOWsInScope(this);
74
75
const kAboutPageRegistrationContentScript =
77
78
/**
79
* Create and register the BrowserTestUtils and ContentEventListener window
80
* actors.
81
*/
82
function registerActors() {
83
ChromeUtils.registerWindowActor("BrowserTestUtils", {
86
},
87
child: {
89
events: {
90
DOMContentLoaded: { capture: true },
91
load: { capture: true },
92
},
93
},
94
allFrames: true,
95
includeChrome: true,
96
});
97
98
ChromeUtils.registerWindowActor("ContentEventListener", {
101
},
102
child: {
104
events: {
105
// We need to see the creation of all new windows, in case they have
106
// a browsing context we are interested in.
107
DOMWindowCreated: { capture: true },
108
},
109
},
110
allFrames: true,
111
});
112
}
113
114
registerActors();
115
116
var BrowserTestUtils = {
117
/**
118
* Loads a page in a new tab, executes a Task and closes the tab.
119
*
120
* @param options
121
* An object or string.
122
* If this is a string it is the url to open and will be opened in the
123
* currently active browser window.
124
* If an object it should have the following properties:
125
* {
126
* gBrowser:
127
* Reference to the "tabbrowser" element where the new tab should
128
* be opened.
129
* url:
130
* String with the URL of the page to load.
131
* }
132
* @param taskFn
133
* Generator function representing a Task that will be executed while
134
* the tab is loaded. The first argument passed to the function is a
135
* reference to the browser object for the new tab.
136
*
137
* @return {} Returns the value that is returned from taskFn.
138
* @resolves When the tab has been closed.
139
* @rejects Any exception from taskFn is propagated.
140
*/
141
async withNewTab(options, taskFn) {
142
if (typeof options == "string") {
143
options = {
144
gBrowser: Services.wm.getMostRecentWindow("navigator:browser").gBrowser,
145
url: options,
146
};
147
}
148
let tab = await BrowserTestUtils.openNewForegroundTab(options);
149
let originalWindow = tab.ownerGlobal;
150
let result = await taskFn(tab.linkedBrowser);
151
let finalWindow = tab.ownerGlobal;
152
if (originalWindow == finalWindow && !tab.closing && tab.linkedBrowser) {
153
// taskFn may resolve within a tick after opening a new tab.
154
// We shouldn't remove the newly opened tab in the same tick.
155
// Wait for the next tick here.
156
await TestUtils.waitForTick();
157
BrowserTestUtils.removeTab(tab);
158
} else {
159
Services.console.logStringMessage(
160
"BrowserTestUtils.withNewTab: Tab was already closed before " +
161
"removeTab would have been called"
162
);
163
}
164
return Promise.resolve(result);
165
},
166
167
/**
168
* Opens a new tab in the foreground.
169
*
170
* This function takes an options object (which is preferred) or actual
171
* parameters. The names of the options must correspond to the names below.
172
* gBrowser is required and all other options are optional.
173
*
174
* @param {tabbrowser} gBrowser
175
* The tabbrowser to open the tab new in.
176
* @param {string} opening (or url)
177
* May be either a string URL to load in the tab, or a function that
178
* will be called to open a foreground tab. Defaults to "about:blank".
179
* @param {boolean} waitForLoad
180
* True to wait for the page in the new tab to load. Defaults to true.
181
* @param {boolean} waitForStateStop
182
* True to wait for the web progress listener to send STATE_STOP for the
183
* document in the tab. Defaults to false.
184
* @param {boolean} forceNewProcess
185
* True to force the new tab to load in a new process. Defaults to
186
* false.
187
*
188
* @return {Promise}
189
* Resolves when the tab is ready and loaded as necessary.
190
* @resolves The new tab.
191
*/
192
openNewForegroundTab(tabbrowser, ...args) {
193
let options;
194
if (
195
tabbrowser.ownerGlobal &&
196
tabbrowser === tabbrowser.ownerGlobal.gBrowser
197
) {
198
// tabbrowser is a tabbrowser, read the rest of the arguments from args.
199
let [
200
opening = "about:blank",
201
waitForLoad = true,
202
waitForStateStop = false,
203
forceNewProcess = false,
204
] = args;
205
206
options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
207
} else {
208
if ("url" in tabbrowser && !("opening" in tabbrowser)) {
209
tabbrowser.opening = tabbrowser.url;
210
}
211
212
let {
213
opening = "about:blank",
214
waitForLoad = true,
215
waitForStateStop = false,
216
forceNewProcess = false,
217
} = tabbrowser;
218
219
tabbrowser = tabbrowser.gBrowser;
220
options = { opening, waitForLoad, waitForStateStop, forceNewProcess };
221
}
222
223
let {
224
opening: opening,
225
waitForLoad: aWaitForLoad,
226
waitForStateStop: aWaitForStateStop,
227
} = options;
228
229
let promises, tab;
230
try {
231
// If we're asked to force a new process, replace the normal process
232
// selector with one that always asks for a new process.
233
// If DEFAULT_PROCESSSELECTOR_CID is null, we're in non-e10s mode and we
234
// should skip this.
235
if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) {
236
Services.ppmm.releaseCachedProcesses();
237
registrar.registerFactory(
238
OUR_PROCESSSELECTOR_CID,
239
"",
240
PROCESSSELECTOR_CONTRACTID,
241
null
242
);
243
}
244
245
promises = [
246
BrowserTestUtils.switchTab(tabbrowser, function() {
247
if (typeof opening == "function") {
248
opening();
249
tab = tabbrowser.selectedTab;
250
} else {
251
tabbrowser.selectedTab = tab = BrowserTestUtils.addTab(
252
tabbrowser,
253
opening
254
);
255
}
256
}),
257
];
258
259
if (aWaitForLoad) {
260
promises.push(BrowserTestUtils.browserLoaded(tab.linkedBrowser));
261
}
262
if (aWaitForStateStop) {
263
promises.push(BrowserTestUtils.browserStopped(tab.linkedBrowser));
264
}
265
} finally {
266
// Restore the original process selector, if needed.
267
if (options.forceNewProcess && DEFAULT_PROCESSSELECTOR_CID) {
268
registrar.registerFactory(
269
DEFAULT_PROCESSSELECTOR_CID,
270
"",
271
PROCESSSELECTOR_CONTRACTID,
272
null
273
);
274
}
275
}
276
return Promise.all(promises).then(() => tab);
277
},
278
279
/**
280
* Checks if a DOM element is hidden.
281
*
282
* @param {Element} element
283
* The element which is to be checked.
284
*
285
* @return {boolean}
286
*/
287
is_hidden(element) {
288
var style = element.ownerGlobal.getComputedStyle(element);
289
if (style.display == "none") {
290
return true;
291
}
292
if (style.visibility != "visible") {
293
return true;
294
}
295
if (style.display == "-moz-popup") {
296
return ["hiding", "closed"].includes(element.state);
297
}
298
299
// Hiding a parent element will hide all its children
300
if (element.parentNode != element.ownerDocument) {
301
return BrowserTestUtils.is_hidden(element.parentNode);
302
}
303
304
return false;
305
},
306
307
/**
308
* Checks if a DOM element is visible.
309
*
310
* @param {Element} element
311
* The element which is to be checked.
312
*
313
* @return {boolean}
314
*/
315
is_visible(element) {
316
var style = element.ownerGlobal.getComputedStyle(element);
317
if (style.display == "none") {
318
return false;
319
}
320
if (style.visibility != "visible") {
321
return false;
322
}
323
if (style.display == "-moz-popup" && element.state != "open") {
324
return false;
325
}
326
327
// Hiding a parent element will hide all its children
328
if (element.parentNode != element.ownerDocument) {
329
return BrowserTestUtils.is_visible(element.parentNode);
330
}
331
332
return true;
333
},
334
335
/**
336
* If the argument is a browsingContext, return it. If the
337
* argument is a browser/frame, returns the browsing context for it.
338
*/
339
getBrowsingContextFrom(browser) {
340
if (Element.isInstance(browser)) {
341
return browser.browsingContext;
342
}
343
344
return browser;
345
},
346
347
/**
348
* Switches to a tab and resolves when it is ready.
349
*
350
* @param {tabbrowser} tabbrowser
351
* The tabbrowser.
352
* @param {tab} tab
353
* Either a tab element to switch to or a function to perform the switch.
354
*
355
* @return {Promise}
356
* Resolves when the tab has been switched to.
357
* @resolves The tab switched to.
358
*/
359
switchTab(tabbrowser, tab) {
360
let promise = new Promise(resolve => {
361
tabbrowser.addEventListener(
362
"TabSwitchDone",
363
function() {
364
TestUtils.executeSoon(() => resolve(tabbrowser.selectedTab));
365
},
366
{ once: true }
367
);
368
});
369
370
if (typeof tab == "function") {
371
tab();
372
} else {
373
tabbrowser.selectedTab = tab;
374
}
375
return promise;
376
},
377
378
/**
379
* Waits for an ongoing page load in a browser window to complete.
380
*
381
* This can be used in conjunction with any synchronous method for starting a
382
* load, like the "addTab" method on "tabbrowser", and must be called before
383
* yielding control to the event loop. This is guaranteed to work because the
384
* way we're listening for the load is in the content-utils.js frame script,
385
* and then sending an async message up, so we can't miss the message.
386
*
387
* @param {xul:browser} browser
388
* A xul:browser.
389
* @param {Boolean} [includeSubFrames = false]
390
* A boolean indicating if loads from subframes should be included.
391
* @param {string|function} [wantLoad = null]
392
* If a function, takes a URL and returns true if that's the load we're
393
* interested in. If a string, gives the URL of the load we're interested
394
* in. If not present, the first load resolves the promise.
395
* @param {boolean} [maybeErrorPage = false]
396
* If true, this uses DOMContentLoaded event instead of load event.
397
* Also wantLoad will be called with visible URL, instead of
398
* 'about:neterror?...' for error page.
399
*
400
* @return {Promise}
401
* @resolves When a load event is triggered for the browser.
402
*/
403
browserLoaded(
404
browser,
405
includeSubFrames = false,
406
wantLoad = null,
407
maybeErrorPage = false
408
) {
409
// Passing a url as second argument is a common mistake we should prevent.
410
if (includeSubFrames && typeof includeSubFrames != "boolean") {
411
throw new Error(
412
"The second argument to browserLoaded should be a boolean."
413
);
414
}
415
416
// If browser belongs to tabbrowser-tab, ensure it has been
417
// inserted into the document.
418
let tabbrowser = browser.ownerGlobal.gBrowser;
419
if (tabbrowser && tabbrowser.getTabForBrowser) {
420
tabbrowser._insertBrowser(tabbrowser.getTabForBrowser(browser));
421
}
422
423
function isWanted(url) {
424
if (!wantLoad) {
425
return true;
426
} else if (typeof wantLoad == "function") {
427
return wantLoad(url);
428
}
429
// It's a string.
430
return wantLoad == url;
431
}
432
433
// Error pages are loaded slightly differently, so listen for the
434
// DOMContentLoaded event for those instead.
435
let loadEvent = maybeErrorPage ? "DOMContentLoaded" : "load";
436
let eventName = `BrowserTestUtils:ContentEvent:${loadEvent}`;
437
438
return new Promise((resolve, reject) => {
439
function listener(event) {
440
switch (event.type) {
441
case eventName: {
442
let { browsingContext, internalURL, visibleURL } = event.detail;
443
444
// Sometimes we arrive here without an internalURL. If that's the
445
// case, just keep waiting until we get one.
446
if (!internalURL) {
447
return;
448
}
449
450
// Ignore subframes if we only care about the top-level load.
451
let subframe = browsingContext !== browsingContext.top;
452
if (subframe && !includeSubFrames) {
453
return;
454
}
455
456
// See testing/mochitest/BrowserTestUtils/content/BrowserTestUtilsChild.jsm
457
// for the difference between visibleURL and internalURL.
458
if (!isWanted(maybeErrorPage ? visibleURL : internalURL)) {
459
return;
460
}
461
462
resolve(internalURL);
463
break;
464
}
465
466
case "unload":
467
reject();
468
break;
469
470
default:
471
return;
472
}
473
474
browser.removeEventListener(eventName, listener, true);
475
browser.ownerGlobal.removeEventListener("unload", listener);
476
}
477
478
browser.addEventListener(eventName, listener, true);
479
browser.ownerGlobal.addEventListener("unload", listener);
480
});
481
},
482
483
/**
484
* Waits for the selected browser to load in a new window. This
485
* is most useful when you've got a window that might not have
486
* loaded its DOM yet, and where you can't easily use browserLoaded
487
* on gBrowser.selectedBrowser since gBrowser doesn't yet exist.
488
*
489
* @param {xul:window} window
490
* A newly opened window for which we're waiting for the
491
* first browser load.
492
* @param {Boolean} aboutBlank [optional]
493
* If false, about:blank loads are ignored and we continue
494
* to wait.
495
* @param {function or null} checkFn [optional]
496
* If checkFn(browser) returns false, the load is ignored
497
* and we continue to wait.
498
*
499
* @return {Promise}
500
* @resolves Once the selected browser fires its load event.
501
*/
502
firstBrowserLoaded(win, aboutBlank = true, checkFn = null) {
503
return this.waitForEvent(
504
win,
505
"BrowserTestUtils:ContentEvent:load",
506
true,
507
event => {
508
if (checkFn) {
509
return checkFn(event.target);
510
}
511
return (
512
win.gBrowser.selectedBrowser.currentURI.spec !== "about:blank" ||
513
aboutBlank
514
);
515
}
516
);
517
},
518
519
_webProgressListeners: new Set(),
520
521
_contentEventListenerSharedState: new Map(),
522
523
_contentEventListeners: new Map(),
524
525
/**
526
* Waits for the web progress listener associated with this tab to fire a
527
* STATE_STOP for the toplevel document.
528
*
529
* @param {xul:browser} browser
530
* A xul:browser.
531
* @param {String} expectedURI (optional)
532
* A specific URL to check the channel load against
533
* @param {Boolean} checkAborts (optional, defaults to false)
534
* Whether NS_BINDING_ABORTED stops 'count' as 'real' stops
535
* (e.g. caused by the stop button or equivalent APIs)
536
*
537
* @return {Promise}
538
* @resolves When STATE_STOP reaches the tab's progress listener
539
*/
540
browserStopped(browser, expectedURI, checkAborts = false) {
541
return new Promise(resolve => {
542
let wpl = {
543
onStateChange(aWebProgress, aRequest, aStateFlags, aStatus) {
544
dump(
545
"Saw state " +
546
aStateFlags.toString(16) +
547
" and status " +
548
aStatus.toString(16) +
549
"\n"
550
);
551
if (
552
aStateFlags & Ci.nsIWebProgressListener.STATE_IS_NETWORK &&
553
aStateFlags & Ci.nsIWebProgressListener.STATE_STOP &&
554
(checkAborts || aStatus != Cr.NS_BINDING_ABORTED) &&
555
aWebProgress.isTopLevel
556
) {
557
let chan = aRequest.QueryInterface(Ci.nsIChannel);
558
dump("Browser loaded " + chan.originalURI.spec + "\n");
559
if (!expectedURI || chan.originalURI.spec == expectedURI) {
560
browser.removeProgressListener(wpl);
561
BrowserTestUtils._webProgressListeners.delete(wpl);
562
resolve();
563
}
564
}
565
},
566
onSecurityChange() {},
567
onStatusChange() {},
568
onLocationChange() {},
569
onContentBlockingEvent() {},
570
QueryInterface: ChromeUtils.generateQI([
571
Ci.nsIWebProgressListener,
572
Ci.nsIWebProgressListener2,
573
Ci.nsISupportsWeakReference,
574
]),
575
};
576
browser.addProgressListener(wpl);
577
this._webProgressListeners.add(wpl);
578
dump(
579
"Waiting for browser load" +
580
(expectedURI ? " of " + expectedURI : "") +
581
"\n"
582
);
583
});
584
},
585
586
/**
587
* Waits for a tab to open and load a given URL.
588
*
589
* By default, the method doesn't wait for the tab contents to load.
590
*
591
* @param {tabbrowser} tabbrowser
592
* The tabbrowser to look for the next new tab in.
593
* @param {string|function} [wantLoad = null]
594
* If a function, takes a URL and returns true if that's the load we're
595
* interested in. If a string, gives the URL of the load we're interested
596
* in. If not present, the first non-about:blank load is used.
597
* @param {boolean} [waitForLoad = false]
598
* True to wait for the page in the new tab to load. Defaults to false.
599
* @param {boolean} [waitForAnyTab = false]
600
* True to wait for the url to be loaded in any new tab, not just the next
601
* one opened.
602
*
603
* @return {Promise}
604
* @resolves With the {xul:tab} when a tab is opened and its location changes
605
* to the given URL and optionally that browser has loaded.
606
*
607
* NB: this method will not work if you open a new tab with e.g. BrowserOpenTab
608
* and the tab does not load a URL, because no onLocationChange will fire.
609
*/
610
waitForNewTab(
611
tabbrowser,
612
wantLoad = null,
613
waitForLoad = false,
614
waitForAnyTab = false
615
) {
616
let urlMatches;
617
if (wantLoad && typeof wantLoad == "function") {
618
urlMatches = wantLoad;
619
} else if (wantLoad) {
620
urlMatches = urlToMatch => urlToMatch == wantLoad;
621
} else {
622
urlMatches = urlToMatch => urlToMatch != "about:blank";
623
}
624
return new Promise((resolve, reject) => {
625
tabbrowser.tabContainer.addEventListener(
626
"TabOpen",
627
function tabOpenListener(openEvent) {
628
if (!waitForAnyTab) {
629
tabbrowser.tabContainer.removeEventListener(
630
"TabOpen",
631
tabOpenListener
632
);
633
}
634
let newTab = openEvent.target;
635
let newBrowser = newTab.linkedBrowser;
636
let result;
637
if (waitForLoad) {
638
// If waiting for load, resolve with promise for that, which when load
639
// completes resolves to the new tab.
640
result = BrowserTestUtils.browserLoaded(
641
newBrowser,
642
false,
643
urlMatches
644
).then(() => newTab);
645
} else {
646
// If not waiting for load, just resolve with the new tab.
647
result = newTab;
648
}
649
650
let progressListener = {
651
onLocationChange(aBrowser) {
652
// Only interested in location changes on our browser.
653
if (aBrowser != newBrowser) {
654
return;
655
}
656
657
// Check that new location is the URL we want.
658
if (!urlMatches(aBrowser.currentURI.spec)) {
659
return;
660
}
661
if (waitForAnyTab) {
662
tabbrowser.tabContainer.removeEventListener(
663
"TabOpen",
664
tabOpenListener
665
);
666
}
667
tabbrowser.removeTabsProgressListener(progressListener);
668
TestUtils.executeSoon(() => resolve(result));
669
},
670
};
671
tabbrowser.addTabsProgressListener(progressListener);
672
}
673
);
674
});
675
},
676
677
/**
678
* Waits for onLocationChange.
679
*
680
* @param {tabbrowser} tabbrowser
681
* The tabbrowser to wait for the location change on.
682
* @param {string} url
683
* The string URL to look for. The URL must match the URL in the
684
* location bar exactly.
685
* @return {Promise}
686
* @resolves When onLocationChange fires.
687
*/
688
waitForLocationChange(tabbrowser, url) {
689
return new Promise((resolve, reject) => {
690
let progressListener = {
691
onLocationChange(aBrowser) {
692
if (
693
(url && aBrowser.currentURI.spec != url) ||
694
(!url && aBrowser.currentURI.spec == "about:blank")
695
) {
696
return;
697
}
698
699
tabbrowser.removeTabsProgressListener(progressListener);
700
resolve();
701
},
702
};
703
tabbrowser.addTabsProgressListener(progressListener);
704
});
705
},
706
707
/**
708
* Waits for the next browser window to open and be fully loaded.
709
*
710
* @param aParams
711
* {
712
* url: A string (optional). If set, we will wait until the initial
713
* browser in the new window has loaded a particular page.
714
* If unset, the initial browser may or may not have finished
715
* loading its first page when the resulting Promise resolves.
716
* anyWindow: True to wait for the url to be loaded in any new
717
* window, not just the next one opened.
718
* maybeErrorPage: See browserLoaded function.
719
* }
720
* @return {Promise}
721
* A Promise which resolves the next time that a DOM window
722
* opens and the delayed startup observer notification fires.
723
*/
724
waitForNewWindow(aParams = {}) {
725
let { url = null, anyWindow = false, maybeErrorPage = false } = aParams;
726
727
if (anyWindow && !url) {
728
throw new Error("url should be specified if anyWindow is true");
729
}
730
731
return new Promise((resolve, reject) => {
732
let observe = async (win, topic, data) => {
733
if (topic != "domwindowopened") {
734
return;
735
}
736
737
try {
738
if (!anyWindow) {
739
Services.ww.unregisterNotification(observe);
740
}
741
742
// Add these event listeners now since they may fire before the
743
// DOMContentLoaded event down below.
744
let promises = [
745
this.waitForEvent(win, "focus", true),
746
this.waitForEvent(win, "activate"),
747
];
748
749
if (url) {
750
await this.waitForEvent(win, "DOMContentLoaded");
751
752
if (win.document.documentURI != AppConstants.BROWSER_CHROME_URL) {
753
return;
754
}
755
}
756
757
promises.push(
758
TestUtils.topicObserved(
759
"browser-delayed-startup-finished",
760
subject => subject == win
761
)
762
);
763
764
if (url) {
765
let browser = win.gBrowser.selectedBrowser;
766
767
if (
768
win.gMultiProcessBrowser &&
769
!E10SUtils.canLoadURIInRemoteType(
770
url,
771
win.gFissionBrowser,
772
browser.remoteType
773
)
774
) {
775
await this.waitForEvent(browser, "XULFrameLoaderCreated");
776
}
777
778
let loadPromise = this.browserLoaded(
779
browser,
780
false,
781
url,
782
maybeErrorPage
783
);
784
promises.push(loadPromise);
785
}
786
787
await Promise.all(promises);
788
789
if (anyWindow) {
790
Services.ww.unregisterNotification(observe);
791
}
792
resolve(win);
793
} catch (err) {
794
// We failed to wait for the load in this URI. This is only an error
795
// if `anyWindow` is not set, as if it is we can just wait for another
796
// window.
797
if (!anyWindow) {
798
reject(err);
799
}
800
}
801
};
802
Services.ww.registerNotification(observe);
803
});
804
},
805
806
/**
807
* Loads a new URI in the given browser and waits until we really started
808
* loading. In e10s browser.loadURI() can be an asynchronous operation due
809
* to having to switch the browser's remoteness and keep its shistory data.
810
*
811
* @param {xul:browser} browser
812
* A xul:browser.
813
* @param {string} uri
814
* The URI to load.
815
*
816
* @return {Promise}
817
* @resolves When we started loading the given URI.
818
*/
819
async loadURI(browser, uri) {
820
// Load the new URI.
821
browser.loadURI(uri, {
822
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
823
});
824
825
// Nothing to do in non-e10s mode.
826
if (!browser.ownerGlobal.gMultiProcessBrowser) {
827
return;
828
}
829
830
// If the new URI can't load in the browser's current process then we
831
// should wait for the new frameLoader to be created. This will happen
832
// asynchronously when the browser's remoteness changes.
833
if (
834
!E10SUtils.canLoadURIInRemoteType(
835
uri,
836
browser.ownerGlobal.gFissionBrowser,
837
browser.remoteType
838
)
839
) {
840
await this.waitForEvent(browser, "XULFrameLoaderCreated");
841
}
842
},
843
844
/**
845
* Maybe create a preloaded browser and ensure it's finished loading.
846
*
847
* @param gBrowser (<xul:tabbrowser>)
848
* The tabbrowser in which to preload a browser.
849
*/
850
async maybeCreatePreloadedBrowser(gBrowser) {
851
let win = gBrowser.ownerGlobal;
852
win.NewTabPagePreloading.maybeCreatePreloadedBrowser(win);
853
854
// We cannot use the regular BrowserTestUtils helper for waiting here, since that
855
// would try to insert the preloaded browser, which would only break things.
856
await ContentTask.spawn(gBrowser.preloadedBrowser, [], async () => {
857
await ContentTaskUtils.waitForCondition(() => {
858
return (
859
this.content.document &&
860
this.content.document.readyState == "complete"
861
);
862
});
863
});
864
},
865
866
/**
867
* @param win (optional)
868
* The window we should wait to have "domwindowopened" sent through
869
* the observer service for. If this is not supplied, we'll just
870
* resolve when the first "domwindowopened" notification is seen.
871
* @param {function} checkFn [optional]
872
* Called with the nsIDOMWindow object as argument, should return true
873
* if the event is the expected one, or false if it should be ignored
874
* and observing should continue. If not specified, the first window
875
* resolves the returned promise.
876
* @return {Promise}
877
* A Promise which resolves when a "domwindowopened" notification
878
* has been fired by the window watcher.
879
*/
880
domWindowOpened(win, checkFn) {
881
return new Promise(resolve => {
882
async function observer(subject, topic, data) {
883
if (topic == "domwindowopened" && (!win || subject === win)) {
884
let observedWindow = subject;
885
if (checkFn && !(await checkFn(observedWindow))) {
886
return;
887
}
888
Services.ww.unregisterNotification(observer);
889
resolve(observedWindow);
890
}
891
}
892
Services.ww.registerNotification(observer);
893
});
894
},
895
896
/**
897
* @param win (optional)
898
* The window we should wait to have "domwindowclosed" sent through
899
* the observer service for. If this is not supplied, we'll just
900
* resolve when the first "domwindowclosed" notification is seen.
901
* @return {Promise}
902
* A Promise which resolves when a "domwindowclosed" notification
903
* has been fired by the window watcher.
904
*/
905
domWindowClosed(win) {
906
return new Promise(resolve => {
907
function observer(subject, topic, data) {
908
if (topic == "domwindowclosed" && (!win || subject === win)) {
909
Services.ww.unregisterNotification(observer);
910
resolve(subject);
911
}
912
}
913
Services.ww.registerNotification(observer);
914
});
915
},
916
917
/**
918
* Open a new browser window from an existing one.
919
* This relies on OpenBrowserWindow in browser.js, and waits for the window
920
* to be completely loaded before resolving.
921
*
922
* @param {Object}
923
* Options to pass to OpenBrowserWindow. Additionally, supports:
924
* - waitForTabURL
925
* Forces the initial browserLoaded check to wait for the tab to
926
* load the given URL (instead of about:blank)
927
*
928
* @return {Promise}
929
* Resolves with the new window once it is loaded.
930
*/
931
async openNewBrowserWindow(options = {}) {
932
let currentWin = BrowserWindowTracker.getTopWindow({ private: false });
933
if (!currentWin) {
934
throw new Error(
935
"Can't open a new browser window from this helper if no non-private window is open."
936
);
937
}
938
let win = currentWin.OpenBrowserWindow(options);
939
940
let promises = [
941
this.waitForEvent(win, "focus", true),
942
this.waitForEvent(win, "activate"),
943
];
944
945
// Wait for browser-delayed-startup-finished notification, it indicates
946
// that the window has loaded completely and is ready to be used for
947
// testing.
948
promises.push(
949
TestUtils.topicObserved(
950
"browser-delayed-startup-finished",
951
subject => subject == win
952
).then(() => win)
953
);
954
955
promises.push(
956
this.firstBrowserLoaded(win, !options.waitForTabURL, browser => {
957
return (
958
!options.waitForTabURL ||
959
options.waitForTabURL == browser.currentURI.spec
960
);
961
})
962
);
963
964
await Promise.all(promises);
965
966
return win;
967
},
968
969
/**
970
* Closes a window.
971
*
972
* @param {Window}
973
* A window to close.
974
*
975
* @return {Promise}
976
* Resolves when the provided window has been closed. For browser
977
* windows, the Promise will also wait until all final SessionStore
978
* messages have been sent up from all browser tabs.
979
*/
980
closeWindow(win) {
981
let closedPromise = BrowserTestUtils.windowClosed(win);
982
win.close();
983
return closedPromise;
984
},
985
986
/**
987
* Returns a Promise that resolves when a window has finished closing.
988
*
989
* @param {Window}
990
* The closing window.
991
*
992
* @return {Promise}
993
* Resolves when the provided window has been fully closed. For
994
* browser windows, the Promise will also wait until all final
995
* SessionStore messages have been sent up from all browser tabs.
996
*/
997
windowClosed(win) {
998
let domWinClosedPromise = BrowserTestUtils.domWindowClosed(win);
999
let promises = [domWinClosedPromise];
1000
let winType = win.document.documentElement.getAttribute("windowtype");
1001
1002
if (winType == "navigator:browser") {
1003
let finalMsgsPromise = new Promise(resolve => {
1004
let browserSet = new Set(win.gBrowser.browsers);
1005
// Ensure all browsers have been inserted or we won't get
1006
// messages back from them.
1007
browserSet.forEach(browser => {
1008
win.gBrowser._insertBrowser(win.gBrowser.getTabForBrowser(browser));
1009
});
1010
let mm = win.getGroupMessageManager("browsers");
1011
1012
mm.addMessageListener(
1013
"SessionStore:update",
1014
function onMessage(msg) {
1015
if (browserSet.has(msg.target) && msg.data.isFinal) {
1016
browserSet.delete(msg.target);
1017
if (!browserSet.size) {
1018
mm.removeMessageListener("SessionStore:update", onMessage);
1019
// Give the TabStateFlusher a chance to react to this final
1020
// update and for the TabStateFlusher.flushWindow promise
1021
// to resolve before we resolve.
1022
TestUtils.executeSoon(resolve);
1023
}
1024
}
1025
},
1026
true
1027
);
1028
});
1029
1030
promises.push(finalMsgsPromise);
1031
}
1032
1033
return Promise.all(promises);
1034
},
1035
1036
/**
1037
* Returns a Promise that resolves once the SessionStore information for the
1038
* given tab is updated and all listeners are called.
1039
*
1040
* @param (tab) tab
1041
* The tab that will be removed.
1042
* @returns (Promise)
1043
* @resolves When the SessionStore information is updated.
1044
*/
1045
waitForSessionStoreUpdate(tab) {
1046
return new Promise(resolve => {
1047
let { messageManager: mm, frameLoader } = tab.linkedBrowser;
1048
mm.addMessageListener(
1049
"SessionStore:update",
1050
function onMessage(msg) {
1051
if (msg.targetFrameLoader == frameLoader && msg.data.isFinal) {
1052
mm.removeMessageListener("SessionStore:update", onMessage);
1053
// Wait for the next event tick to make sure other listeners are
1054
// called.
1055
TestUtils.executeSoon(() => resolve());
1056
}
1057
},
1058
true
1059
);
1060
});
1061
},
1062
1063
/**
1064
* Waits for an event to be fired on a specified element.
1065
*
1066
* Usage:
1067
* let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
1068
* // Do some processing here that will cause the event to be fired
1069
* // ...
1070
* // Now wait until the Promise is fulfilled
1071
* let receivedEvent = await promiseEvent;
1072
*
1073
* The promise resolution/rejection handler for the returned promise is
1074
* guaranteed not to be called until the next event tick after the event
1075
* listener gets called, so that all other event listeners for the element
1076
* are executed before the handler is executed.
1077
*
1078
* let promiseEvent = BrowserTestUtils.waitForEvent(element, "eventName");
1079
* // Same event tick here.
1080
* await promiseEvent;
1081
* // Next event tick here.
1082
*
1083
* If some code, such like adding yet another event listener, needs to be
1084
* executed in the same event tick, use raw addEventListener instead and
1085
* place the code inside the event listener.
1086
*
1087
* element.addEventListener("load", () => {
1088
* // Add yet another event listener in the same event tick as the load
1089
* // event listener.
1090
* p = BrowserTestUtils.waitForEvent(element, "ready");
1091
* }, { once: true });
1092
*
1093
* @param {Element} subject
1094
* The element that should receive the event.
1095
* @param {string} eventName
1096
* Name of the event to listen to.
1097
* @param {bool} capture [optional]
1098
* True to use a capturing listener.
1099
* @param {function} checkFn [optional]
1100
* Called with the Event object as argument, should return true if the
1101
* event is the expected one, or false if it should be ignored and
1102
* listening should continue. If not specified, the first event with
1103
* the specified name resolves the returned promise.
1104
* @param {bool} wantsUntrusted [optional]
1105
* True to receive synthetic events dispatched by web content.
1106
*
1107
* @note Because this function is intended for testing, any error in checkFn
1108
* will cause the returned promise to be rejected instead of waiting for
1109
* the next event, since this is probably a bug in the test.
1110
*
1111
* @returns {Promise}
1112
* @resolves The Event object.
1113
*/
1114
waitForEvent(subject, eventName, capture, checkFn, wantsUntrusted) {
1115
return new Promise((resolve, reject) => {
1116
subject.addEventListener(
1117
eventName,
1118
function listener(event) {
1119
try {
1120
if (checkFn && !checkFn(event)) {
1121
return;
1122
}
1123
subject.removeEventListener(eventName, listener, capture);
1124
TestUtils.executeSoon(() => resolve(event));
1125
} catch (ex) {
1126
try {
1127
subject.removeEventListener(eventName, listener, capture);
1128
} catch (ex2) {
1129
// Maybe the provided object does not support removeEventListener.
1130
}
1131
TestUtils.executeSoon(() => reject(ex));
1132
}
1133
},
1134
capture,
1135
wantsUntrusted
1136
);
1137
});
1138
},
1139
1140
/**
1141
* Like waitForEvent, but adds the event listener to the message manager
1142
* global for browser.
1143
*
1144
* @param {string} eventName
1145
* Name of the event to listen to.
1146
* @param {bool} capture [optional]
1147
* Whether to use a capturing listener.
1148
* @param {function} checkFn [optional]
1149
* Called with the Event object as argument, should return true if the
1150
* event is the expected one, or false if it should be ignored and
1151
* listening should continue. If not specified, the first event with
1152
* the specified name resolves the returned promise.
1153
* @param {bool} wantUntrusted [optional]
1154
* Whether to accept untrusted events
1155
*
1156
* @note As of bug 1588193, this function no longer rejects the returned
1157
* promise in the case of a checkFn error. Instead, since checkFn is now
1158
* called through eval in the content process, the error is thrown in
1159
* the listener created by ContentEventListenerChild. Work to improve
1160
* error handling (eg. to reject the promise as before and to preserve
1161
* the filename/stack) is being tracked in bug 1593811.
1162
*
1163
* @returns {Promise}
1164
*/
1165
waitForContentEvent(
1166
browser,
1167
eventName,
1168
capture = false,
1169
checkFn,
1170
wantUntrusted = false
1171
) {
1172
return new Promise(resolve => {
1173
let removeEventListener = this.addContentEventListener(
1174
browser,
1175
eventName,
1176
() => {
1177
removeEventListener();
1178
resolve();
1179
},
1180
{ capture, wantUntrusted },
1181
checkFn
1182
);
1183
});
1184
},
1185
1186
/**
1187
* Like waitForEvent, but acts on a popup. It ensures the popup is not already
1188
* in the expected state.
1189
*
1190
* @param {Element} popup
1191
* The popup element that should receive the event.
1192
* @param {string} eventSuffix
1193
* The event suffix expected to be received, one of "shown" or "hidden".
1194
* @returns {Promise}
1195
*/
1196
waitForPopupEvent(popup, eventSuffix) {
1197
let endState = { shown: "open", hidden: "closed" }[eventSuffix];
1198
1199
if (popup.state == endState) {
1200
return Promise.resolve();
1201
}
1202
return this.waitForEvent(popup, "popup" + eventSuffix);
1203
},
1204
1205
/**
1206
* Adds a content event listener on the given browser
1207
* element. Similar to waitForContentEvent, but the listener will
1208
* fire until it is removed. A callable object is returned that,
1209
* when called, removes the event listener. Note that this function
1210
* works even if the browser's frameloader is swapped.
1211
*
1212
* @param {xul:browser} browser
1213
* The browser element to listen for events in.
1214
* @param {string} eventName
1215
* Name of the event to listen to.
1216
* @param {function} listener
1217
* Function to call in parent process when event fires.
1218
* Not passed any arguments.
1219
* @param {object} listenerOptions [optional]
1220
* Options to pass to the event listener.
1221
* @param {function} checkFn [optional]
1222
* Called with the Event object as argument, should return true if the
1223
* event is the expected one, or false if it should be ignored and
1224
* listening should continue. If not specified, the first event with
1225
* the specified name resolves the returned promise. This is called
1226
* within the content process and can have no closure environment.
1227
*
1228
* @returns function
1229
* If called, the return value will remove the event listener.
1230
*/
1231
addContentEventListener(
1232
browser,
1233
eventName,
1234
listener,
1235
listenerOptions = {},
1236
checkFn
1237
) {
1238
let id = gListenerId++;
1239
let contentEventListeners = this._contentEventListeners;
1240
contentEventListeners.set(id, {
1241
listener,
1242
browsingContext: browser.browsingContext,
1243
});
1244
1245
let eventListenerState = this._contentEventListenerSharedState;
1246
eventListenerState.set(id, {
1247
eventName,
1248
listenerOptions,
1249
checkFnSource: checkFn ? checkFn.toSource() : "",
1250
});
1251
1252
Services.ppmm.sharedData.set(
1253
"BrowserTestUtils:ContentEventListener",
1254
eventListenerState
1255
);
1256
Services.ppmm.sharedData.flush();
1257
1258
let unregisterFunction = function() {
1259
if (!eventListenerState.has(id)) {
1260
return;
1261
}
1262
eventListenerState.delete(id);
1263
contentEventListeners.delete(id);
1264
Services.ppmm.sharedData.set(
1265
"BrowserTestUtils:ContentEventListener",
1266
eventListenerState
1267
);
1268
Services.ppmm.sharedData.flush();
1269
};
1270
return unregisterFunction;
1271
},
1272
1273
/**
1274
* This is an internal method to be invoked by
1275
* BrowserTestUtilsParent.jsm when a content event we were listening for
1276
* happens.
1277
*/
1278
_receivedContentEventListener(listenerId, browsingContext) {
1279
let listenerData = this._contentEventListeners.get(listenerId);
1280
if (!listenerData) {
1281
return;
1282
}
1283
if (listenerData.browsingContext != browsingContext) {
1284
return;
1285
}
1286
listenerData.listener();
1287
},
1288
1289
/**
1290
* This is an internal method that cleans up any state from content event
1291
* listeners.
1292
*/
1293
_cleanupContentEventListeners() {
1294
this._contentEventListeners.clear();
1295
1296
if (this._contentEventListenerSharedState.size != 0) {
1297
this._contentEventListenerSharedState.clear();
1298
Services.ppmm.sharedData.set(
1299
"BrowserTestUtils:ContentEventListener",
1300
this._contentEventListenerSharedState
1301
);
1302
Services.ppmm.sharedData.flush();
1303
}
1304
1305
if (this._contentEventListenerActorRegistered) {
1306
this._contentEventListenerActorRegistered = false;
1307
ChromeUtils.unregisterWindowActor("ContentEventListener");
1308
}
1309
},
1310
1311
observe(subject, topic, data) {
1312
switch (topic) {
1313
case "test-complete":
1314
this._cleanupContentEventListeners();
1315
break;
1316
}
1317
},
1318
1319
/**
1320
* Like browserLoaded, but waits for an error page to appear.
1321
* This explicitly deals with cases where the browser is not currently remote and a
1322
* remoteness switch will occur before the error page is loaded, which is tricky
1323
* because error pages don't fire 'regular' load events that we can rely on.
1324
*
1325
* @param {xul:browser} browser
1326
* A xul:browser.
1327
*
1328
* @return {Promise}
1329
* @resolves When an error page has been loaded in the browser.
1330
*/
1331
waitForErrorPage(browser) {
1332
let waitForLoad = () =>
1333
this.waitForContentEvent(browser, "AboutNetErrorLoad", false, null, true);
1334
1335
let win = browser.ownerGlobal;
1336
let tab = win.gBrowser.getTabForBrowser(browser);
1337
if (!tab || browser.isRemoteBrowser || !win.gMultiProcessBrowser) {
1338
return waitForLoad();
1339
}
1340
1341
// We're going to switch remoteness when loading an error page. We need to be
1342
// quite careful in order to make sure we're adding the listener in time to
1343
// get this event:
1344
return new Promise((resolve, reject) => {
1345
tab.addEventListener(
1346
"TabRemotenessChange",
1347
function() {
1348
waitForLoad().then(resolve, reject);
1349
},
1350
{ once: true }
1351
);
1352
});
1353
},
1354
1355
/**
1356
* Waits for the next top-level document load in the current browser. The URI
1357
* of the document is compared against expectedURL. The load is then stopped
1358
* before it actually starts.
1359
*
1360
* @param {string} expectedURL
1361
* The URL of the document that is expected to load.
1362
* @param {object} browser
1363
* The browser to wait for.
1364
* @param {function} checkFn (optional)
1365
* Function to run on the channel before stopping it.
1366
* @returns {Promise}
1367
*/
1368
waitForDocLoadAndStopIt(expectedURL, browser, checkFn) {
1369
let isHttp = url => /^https?:/.test(url);
1370
1371
let stoppedDocLoadPromise = () => {
1372
return new Promise(resolve => {
1373
// Redirect non-http URIs to http://mochi.test:8888/, so we can still
1374
// use http-on-before-connect to listen for loads. Since we're
1375
// aborting the load as early as possible, it doesn't matter whether the
1376
// server handles it sensibly or not. However, this also means that this
1377
// helper shouldn't be used to load local URIs (about pages, chrome://
1378
// URIs, etc).
1379
let proxyFilter;
1380
if (!isHttp(expectedURL)) {
1381
proxyFilter = {
1382
proxyInfo: ProtocolProxyService.newProxyInfo(
1383
"http",
1384
"mochi.test",
1385
8888,
1386
"",
1387
"",
1388
0,
1389
4096,
1390
null
1391
),
1392
1393
applyFilter(channel, defaultProxyInfo, callback) {
1394
callback.onProxyFilterResult(
1395
isHttp(channel.URI.spec) ? defaultProxyInfo : this.proxyInfo
1396
);
1397
},
1398
};
1399
1400
ProtocolProxyService.registerChannelFilter(proxyFilter, 0);
1401
}
1402
1403
function observer(chan) {
1404
chan.QueryInterface(Ci.nsIHttpChannel);
1405
if (!chan.originalURI || chan.originalURI.spec !== expectedURL) {
1406
return;
1407
}
1408
if (checkFn && !checkFn(chan)) {
1409
return;
1410
}
1411
1412
// TODO: We should check that the channel's BrowsingContext matches
1413
// the browser's. See bug 1587114.
1414
1415
try {
1416
chan.cancel(Cr.NS_BINDING_ABORTED);
1417
} finally {
1418
if (proxyFilter) {
1419
ProtocolProxyService.unregisterChannelFilter(proxyFilter);
1420
}
1421
Services.obs.removeObserver(observer, "http-on-before-connect");
1422
resolve();
1423
}
1424
}
1425
1426
Services.obs.addObserver(observer, "http-on-before-connect");
1427
});
1428
};
1429
1430
let win = browser.ownerGlobal;
1431
let tab = win.gBrowser.getTabForBrowser(browser);
1432
let { mustChangeProcess } = E10SUtils.shouldLoadURIInBrowser(
1433
browser,
1434
expectedURL
1435
);
1436
if (!tab || !win.gMultiProcessBrowser || !mustChangeProcess) {
1437
return stoppedDocLoadPromise();
1438
}
1439
1440
return new Promise((resolve, reject) => {
1441
tab.addEventListener(
1442
"TabRemotenessChange",
1443
function() {
1444
stoppedDocLoadPromise().then(resolve, reject);
1445
},
1446
{ once: true }
1447
);
1448
});
1449
},
1450
1451
/**
1452
* Versions of EventUtils.jsm synthesizeMouse functions that synthesize a
1453
* mouse event in a child process and return promises that resolve when the
1454
* event has fired and completed. Instead of a window, a browser or
1455
* browsing context is required to be passed to this function.
1456
*
1457
* @param target
1458
* One of the following:
1459
* - a selector string that identifies the element to target. The syntax is as
1460
* for querySelector.
1461
* - a function to be run in the content process that returns the element to
1462
* target
1463
* - null, in which case the offset is from the content document's edge.
1464
* @param {integer} offsetX
1465
* x offset from target's left bounding edge
1466
* @param {integer} offsetY
1467
* y offset from target's top bounding edge
1468
* @param {Object} event object
1469
* Additional arguments, similar to the EventUtils.jsm version
1470
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1471
* Browsing context or browser element, must not be null
1472
*
1473
* @returns {Promise}
1474
* @resolves True if the mouse event was cancelled.
1475
*/
1476
synthesizeMouse(target, offsetX, offsetY, event, browsingContext) {
1477
let targetFn = null;
1478
if (typeof target == "function") {
1479
targetFn = target.toString();
1480
target = null;
1481
} else if (typeof target != "string" && !Array.isArray(target)) {
1482
target = null;
1483
}
1484
1485
browsingContext = this.getBrowsingContextFrom(browsingContext);
1486
return this.sendQuery(browsingContext, "Test:SynthesizeMouse", {
1487
target,
1488
targetFn,
1489
x: offsetX,
1490
y: offsetY,
1491
event,
1492
});
1493
},
1494
1495
/**
1496
* Versions of EventUtils.jsm synthesizeTouch functions that synthesize a
1497
* touch event in a child process and return promises that resolve when the
1498
* event has fired and completed. Instead of a window, a browser or
1499
* browsing context is required to be passed to this function.
1500
*
1501
* @param target
1502
* One of the following:
1503
* - a selector string that identifies the element to target. The syntax is as
1504
* for querySelector.
1505
* - a function to be run in the content process that returns the element to
1506
* target
1507
* - null, in which case the offset is from the content document's edge.
1508
* @param {integer} offsetX
1509
* x offset from target's left bounding edge
1510
* @param {integer} offsetY
1511
* y offset from target's top bounding edge
1512
* @param {Object} event object
1513
* Additional arguments, similar to the EventUtils.jsm version
1514
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1515
* Browsing context or browser element, must not be null
1516
*
1517
* @returns {Promise}
1518
* @resolves True if the touch event was cancelled.
1519
*/
1520
synthesizeTouch(target, offsetX, offsetY, event, browsingContext) {
1521
let targetFn = null;
1522
if (typeof target == "function") {
1523
targetFn = target.toString();
1524
target = null;
1525
} else if (typeof target != "string" && !Array.isArray(target)) {
1526
target = null;
1527
}
1528
1529
browsingContext = this.getBrowsingContextFrom(browsingContext);
1530
return this.sendQuery(browsingContext, "Test:SynthesizeTouch", {
1531
target,
1532
targetFn,
1533
x: offsetX,
1534
y: offsetY,
1535
event,
1536
});
1537
},
1538
1539
/**
1540
* Wait for a message to be fired from a particular message manager
1541
*
1542
* @param {nsIMessageManager} messageManager
1543
* The message manager that should be used.
1544
* @param {String} message
1545
* The message we're waiting for.
1546
* @param {Function} checkFn (optional)
1547
* Optional function to invoke to check the message.
1548
*/
1549
waitForMessage(messageManager, message, checkFn) {
1550
return new Promise(resolve => {
1551
messageManager.addMessageListener(message, function onMessage(msg) {
1552
if (!checkFn || checkFn(msg)) {
1553
messageManager.removeMessageListener(message, onMessage);
1554
resolve(msg.data);
1555
}
1556
});
1557
});
1558
},
1559
1560
/**
1561
* Version of synthesizeMouse that uses the center of the target as the mouse
1562
* location. Arguments and the return value are the same.
1563
*/
1564
synthesizeMouseAtCenter(target, event, browsingContext) {
1565
// Use a flag to indicate to center rather than having a separate message.
1566
event.centered = true;
1567
return BrowserTestUtils.synthesizeMouse(
1568
target,
1569
0,
1570
0,
1571
event,
1572
browsingContext
1573
);
1574
},
1575
1576
/**
1577
* Version of synthesizeMouse that uses a client point within the child
1578
* window instead of a target as the offset. Otherwise, the arguments and
1579
* return value are the same as synthesizeMouse.
1580
*/
1581
synthesizeMouseAtPoint(offsetX, offsetY, event, browsingContext) {
1582
return BrowserTestUtils.synthesizeMouse(
1583
null,
1584
offsetX,
1585
offsetY,
1586
event,
1587
browsingContext
1588
);
1589
},
1590
1591
/**
1592
* Removes the given tab from its parent tabbrowser.
1593
* This method doesn't SessionStore etc.
1594
*
1595
* @param (tab) tab
1596
* The tab to remove.
1597
* @param (Object) options
1598
* Extra options to pass to tabbrowser's removeTab method.
1599
*/
1600
removeTab(tab, options = {}) {
1601
tab.ownerGlobal.gBrowser.removeTab(tab, options);
1602
},
1603
1604
/**
1605
* Returns a Promise that resolves once the tab starts closing.
1606
*
1607
* @param (tab) tab
1608
* The tab that will be removed.
1609
* @returns (Promise)
1610
* @resolves When the tab starts closing. Does not get passed a value.
1611
*/
1612
waitForTabClosing(tab) {
1613
return this.waitForEvent(tab, "TabClose");
1614
},
1615
1616
/**
1617
* Crashes a remote frame tab and cleans up the generated minidumps.
1618
* Resolves with the data from the .extra file (the crash annotations).
1619
*
1620
* @param (Browser) browser
1621
* A remote <xul:browser> element. Must not be null.
1622
* @param (bool) shouldShowTabCrashPage
1623
* True if it is expected that the tab crashed page will be shown
1624
* for this browser. If so, the Promise will only resolve once the
1625
* tab crash page has loaded.
1626
* @param (bool) shouldClearMinidumps
1627
* True if the minidumps left behind by the crash should be removed.
1628
* @param (BrowsingContext) browsingContext
1629
* The context where the frame leaves. Default to
1630
* top level context if not supplied.
1631
* @param (object?) options
1632
* An object with any of the following fields:
1633
* crashType: "CRASH_INVALID_POINTER_DEREF" | "CRASH_OOM"
1634
* The type of crash. If unspecified, default to "CRASH_INVALID_POINTER_DEREF"
1635
*
1636
* @returns (Promise)
1637
* @resolves An Object with key-value pairs representing the data from the
1638
* crash report's extra file (if applicable).
1639
*/
1640
async crashFrame(
1641
browser,
1642
shouldShowTabCrashPage = true,
1643
shouldClearMinidumps = true,
1644
browsingContext,
1645
options = {}
1646
) {
1647
let extra = {};
1648
1649
if (!browser.isRemoteBrowser) {
1650
throw new Error("<xul:browser> needs to be remote in order to crash");
1651
}
1652
1653
/**
1654
* Returns the directory where crash dumps are stored.
1655
*
1656
* @return nsIFile
1657
*/
1658
function getMinidumpDirectory() {
1659
let dir = Services.dirsvc.get("ProfD", Ci.nsIFile);
1660
dir.append("minidumps");
1661
return dir;
1662
}
1663
1664
/**
1665
* Removes a file from a directory. This is a no-op if the file does not
1666
* exist.
1667
*
1668
* @param directory
1669
* The nsIFile representing the directory to remove from.
1670
* @param filename
1671
* A string for the file to remove from the directory.
1672
*/
1673
function removeFile(directory, filename) {
1674
let file = directory.clone();
1675
file.append(filename);
1676
if (file.exists()) {
1677
file.remove(false);
1678
}
1679
}
1680
1681
let expectedPromises = [];
1682
1683
let crashCleanupPromise = new Promise((resolve, reject) => {
1684
let observer = (subject, topic, data) => {
1685
if (topic != "ipc:content-shutdown") {
1686
reject("Received incorrect observer topic: " + topic);
1687
return;
1688
}
1689
if (!(subject instanceof Ci.nsIPropertyBag2)) {
1690
reject("Subject did not implement nsIPropertyBag2");
1691
return;
1692
}
1693
// we might see this called as the process terminates due to previous tests.
1694
// We are only looking for "abnormal" exits...
1695
if (!subject.hasKey("abnormal")) {
1696
dump(
1697
"\nThis is a normal termination and isn't the one we are looking for...\n"
1698
);
1699
return;
1700
}
1701
1702
let dumpID;
1703
if (AppConstants.MOZ_CRASHREPORTER) {
1704
dumpID = subject.getPropertyAsAString("dumpID");
1705
if (!dumpID) {
1706
reject(
1707
"dumpID was not present despite crash reporting being enabled"
1708
);
1709
return;
1710
}
1711
}
1712
1713
let removalPromise = Promise.resolve();
1714
1715
if (dumpID) {
1716
removalPromise = Services.crashmanager
1717
.ensureCrashIsPresent(dumpID)
1718
.then(async () => {
1719
let minidumpDirectory = getMinidumpDirectory();
1720
let extrafile = minidumpDirectory.clone();
1721
extrafile.append(dumpID + ".extra");
1722
if (extrafile.exists()) {
1723
if (AppConstants.MOZ_CRASHREPORTER) {
1724
let extradata = await OS.File.read(extrafile.path, {
1725
encoding: "utf-8",
1726
});
1727
extra = JSON.parse(extradata);
1728
} else {
1729
dump(
1730
"\nCrashReporter not enabled - will not return any extra data\n"
1731
);
1732
}
1733
} else {
1734
dump(`\nNo .extra file for dumpID: ${dumpID}\n`);
1735
}
1736
1737
if (shouldClearMinidumps) {
1738
removeFile(minidumpDirectory, dumpID + ".dmp");
1739
removeFile(minidumpDirectory, dumpID + ".extra");
1740
}
1741
});
1742
}
1743
1744
removalPromise.then(() => {
1745
Services.obs.removeObserver(observer, "ipc:content-shutdown");
1746
dump("\nCrash cleaned up\n");
1747
// There might be other ipc:content-shutdown handlers that need to
1748
// run before we want to continue, so we'll resolve on the next tick
1749
// of the event loop.
1750
TestUtils.executeSoon(() => resolve());
1751
});
1752
};
1753
1754
Services.obs.addObserver(observer, "ipc:content-shutdown");
1755
});
1756
1757
expectedPromises.push(crashCleanupPromise);
1758
1759
if (shouldShowTabCrashPage) {
1760
expectedPromises.push(
1761
new Promise((resolve, reject) => {
1762
browser.addEventListener(
1763
"AboutTabCrashedReady",
1764
function onCrash() {
1765
browser.removeEventListener("AboutTabCrashedReady", onCrash);
1766
dump("\nabout:tabcrashed loaded and ready\n");
1767
resolve();
1768
},
1769
false,
1770
true
1771
);
1772
})
1773
);
1774
}
1775
1776
// Trigger crash by sending a message to BrowserTestUtils actor.
1777
this.sendAsyncMessage(
1778
browsingContext || browser.browsingContext,
1779
"BrowserTestUtils:CrashFrame",
1780
{
1781
crashType: options.crashType || "",
1782
}
1783
);
1784
1785
await Promise.all(expectedPromises);
1786
1787
if (shouldShowTabCrashPage) {
1788
let gBrowser = browser.ownerGlobal.gBrowser;
1789
let tab = gBrowser.getTabForBrowser(browser);
1790
if (tab.getAttribute("crashed") != "true") {
1791
throw new Error("Tab should be marked as crashed");
1792
}
1793
}
1794
1795
return extra;
1796
},
1797
1798
/**
1799
* Attempts to simulate a launch fail by crashing a browser, but
1800
* stripping the browser of its childID so that the TabCrashHandler
1801
* thinks it was a launch fail.
1802
*
1803
* @param browser (<xul:browser>)
1804
* The browser to simulate a content process launch failure on.
1805
* @return Promise
1806
* @resolves undefined
1807
* Resolves when the TabCrashHandler should be done handling the
1808
* simulated crash.
1809
*/
1810
simulateProcessLaunchFail(browser, dueToBuildIDMismatch = false) {
1811
const NORMAL_CRASH_TOPIC = "ipc:content-shutdown";
1812
1813
Object.defineProperty(browser.frameLoader, "childID", {
1814
get: () => 0,
1815
});
1816
1817
let sawNormalCrash = false;
1818
let observer = (subject, topic, data) => {
1819
sawNormalCrash = true;
1820
};
1821
1822
Services.obs.addObserver(observer, NORMAL_CRASH_TOPIC);
1823
1824
Services.obs.notifyObservers(
1825
browser.frameLoader,
1826
"oop-frameloader-crashed"
1827
);
1828
1829
let eventType = dueToBuildIDMismatch
1830
? "oop-browser-buildid-mismatch"
1831
: "oop-browser-crashed";
1832
1833
let event = new browser.ownerGlobal.CustomEvent(eventType, {
1834
bubbles: true,
1835
});
1836
event.isTopFrame = true;
1837
browser.dispatchEvent(event);
1838
1839
Services.obs.removeObserver(observer, NORMAL_CRASH_TOPIC);
1840
1841
if (sawNormalCrash) {
1842
throw new Error(`Unexpectedly saw ${NORMAL_CRASH_TOPIC}`);
1843
}
1844
1845
return new Promise(resolve => TestUtils.executeSoon(resolve));
1846
},
1847
1848
/**
1849
* Returns a promise that is resolved when element gains attribute (or,
1850
* optionally, when it is set to value).
1851
* @param {String} attr
1852
* The attribute to wait for
1853
* @param {Element} element
1854
* The element which should gain the attribute
1855
* @param {String} value (optional)
1856
* Optional, the value the attribute should have.
1857
*
1858
* @returns {Promise}
1859
*/
1860
waitForAttribute(attr, element, value) {
1861
let MutationObserver = element.ownerGlobal.MutationObserver;
1862
return new Promise(resolve => {
1863
let mut = new MutationObserver(mutations => {
1864
if (
1865
(!value && element.hasAttribute(attr)) ||
1866
(value && element.getAttribute(attr) === value)
1867
) {
1868
resolve();
1869
mut.disconnect();
1870
}
1871
});
1872
1873
mut.observe(element, { attributeFilter: [attr] });
1874
});
1875
},
1876
1877
/**
1878
* Returns a promise that is resolved when element loses an attribute.
1879
* @param {String} attr
1880
* The attribute to wait for
1881
* @param {Element} element
1882
* The element which should lose the attribute
1883
*
1884
* @returns {Promise}
1885
*/
1886
waitForAttributeRemoval(attr, element) {
1887
if (!element.hasAttribute(attr)) {
1888
return Promise.resolve();
1889
}
1890
1891
let MutationObserver = element.ownerGlobal.MutationObserver;
1892
return new Promise(resolve => {
1893
dump("Waiting for removal\n");
1894
let mut = new MutationObserver(mutations => {
1895
if (!element.hasAttribute(attr)) {
1896
resolve();
1897
mut.disconnect();
1898
}
1899
});
1900
1901
mut.observe(element, { attributeFilter: [attr] });
1902
});
1903
},
1904
1905
/**
1906
* Version of EventUtils' `sendChar` function; it will synthesize a keypress
1907
* event in a child process and returns a Promise that will resolve when the
1908
* event was fired. Instead of a Window, a Browser or Browsing Context
1909
* is required to be passed to this function.
1910
*
1911
* @param {String} char
1912
* A character for the keypress event that is sent to the browser.
1913
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1914
* Browsing context or browser element, must not be null
1915
*
1916
* @returns {Promise}
1917
* @resolves True if the keypress event was synthesized.
1918
*/
1919
sendChar(char, browsingContext) {
1920
browsingContext = this.getBrowsingContextFrom(browsingContext);
1921
return this.sendQuery(browsingContext, "Test:SendChar", { char });
1922
},
1923
1924
/**
1925
* Version of EventUtils' `synthesizeKey` function; it will synthesize a key
1926
* event in a child process and returns a Promise that will resolve when the
1927
* event was fired. Instead of a Window, a Browser or Browsing Context
1928
* is required to be passed to this function.
1929
*
1930
* @param {String} key
1931
* See the documentation available for EventUtils#synthesizeKey.
1932
* @param {Object} event
1933
* See the documentation available for EventUtils#synthesizeKey.
1934
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1935
* Browsing context or browser element, must not be null
1936
*
1937
* @returns {Promise}
1938
*/
1939
synthesizeKey(key, event, browsingContext) {
1940
browsingContext = this.getBrowsingContextFrom(browsingContext);
1941
return this.sendQuery(browsingContext, "Test:SynthesizeKey", {
1942
key,
1943
event,
1944
});
1945
},
1946
1947
/**
1948
* Version of EventUtils' `synthesizeComposition` function; it will synthesize
1949
* a composition event in a child process and returns a Promise that will
1950
* resolve when the event was fired. Instead of a Window, a Browser or
1951
* Browsing Context is required to be passed to this function.
1952
*
1953
* @param {Object} event
1954
* See the documentation available for EventUtils#synthesizeComposition.
1955
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1956
* Browsing context or browser element, must not be null
1957
*
1958
* @returns {Promise}
1959
* @resolves False if the composition event could not be synthesized.
1960
*/
1961
synthesizeComposition(event, browsingContext) {
1962
browsingContext = this.getBrowsingContextFrom(browsingContext);
1963
return this.sendQuery(browsingContext, "Test:SynthesizeComposition", {
1964
event,
1965
});
1966
},
1967
1968
/**
1969
* Version of EventUtils' `synthesizeCompositionChange` function; it will
1970
* synthesize a compositionchange event in a child process and returns a
1971
* Promise that will resolve when the event was fired. Instead of a Window, a
1972
* Browser or Browsing Context object is required to be passed to this function.
1973
*
1974
* @param {Object} event
1975
* See the documentation available for EventUtils#synthesizeCompositionChange.
1976
* @param {BrowserContext|MozFrameLoaderOwner} browsingContext
1977
* Browsing context or browser element, must not be null
1978
*
1979
* @returns {Promise}
1980
*/
1981
synthesizeCompositionChange(event, browsingContext) {
1982
browsingContext = this.getBrowsingContextFrom(browsingContext);
1983
return this.sendQuery(browsingContext, "Test:SynthesizeCompositionChange", {
1984
event,
1985
});
1986
},
1987
1988
// TODO: Fix consumers and remove me.
1989
waitForCondition: TestUtils.waitForCondition,
1990
1991
/**
1992
* Waits for a <xul:notification> with a particular value to appear
1993
* for the <xul:notificationbox> of the passed in browser.
1994
*
1995
* @param tabbrowser (<xul:tabbrowser>)
1996
* The gBrowser that hosts the browser that should show
1997
* the notification. For most tests, this will probably be
1998
* gBrowser.
1999
* @param browser (<xul:browser>)
2000
* The browser that should be showing the notification.
2001
* @param notificationValue (string)
2002
* The "value" of the notification, which is often used as
2003
* a unique identifier. Example: "plugin-crashed".
2004
* @return Promise
2005
* Resolves to the <xul:notification> that is being shown.
2006
*/
2007
waitForNotificationBar(tabbrowser, browser, notificationValue) {
2008
let notificationBox = tabbrowser.getNotificationBox(browser);
2009
return this.waitForNotificationInNotificationBox(
2010
notificationBox,
2011
notificationValue
2012
);
2013
},
2014
2015
/**
2016
* Waits for a <xul:notification> with a particular value to appear
2017
* in the global <xul:notificationbox> of the given browser window.
2018
*
2019
* @param win (<xul:window>)
2020
* The browser window in whose global notificationbox the
2021
* notification is expected to appear.
2022
* @param notificationValue (string)
2023
* The "value" of the notification, which is often used as
2024
* a unique identifier. Example: "captive-portal-detected".
2025
* @return Promise
2026
* Resolves to the <xul:notification> that is being shown.
2027
*/
2028
waitForGlobalNotificationBar(win, notificationValue) {
2029
return this.waitForNotificationInNotificationBox(
2030
win.gHighPriorityNotificationBox,
2031
notificationValue
2032
);
2033
},
2034
2035
waitForNotificationInNotificationBox(notificationBox, notificationValue) {
2036
return new Promise(resolve => {
2037
let check = event => {
2038
return event.target.getAttribute("value") == notificationValue;
2039
};
2040
2041
BrowserTestUtils.waitForEvent(
2042
notificationBox.stack,
2043
"AlertActive",
2044
false,
2045
check
2046
).then(event => {
2047
// The originalTarget of the AlertActive on a notificationbox
2048
// will be the notification itself.
2049
resolve(event.originalTarget);
2050
});
2051
});
2052
},
2053
2054
_knownAboutPages: new Set(),
2055
_loadedAboutContentScript: false,
2056
/**
2057
* Registers an about: page with particular flags in both the parent
2058
* and any content processes. Returns a promise that resolves when
2059
* registration is complete.
2060
*
2061
* @param registerCleanupFunction (Function)
2062
* The test framework doesn't keep its cleanup stuff anywhere accessible,
2063
* so the first argument is a reference to your cleanup registration
2064
* function, allowing us to clean up after you if necessary.
2065
* @param aboutModule (String)
2066
* The name of the about page.
2067
* @param pageURI (String)
2068
* The URI the about: page should point to.
2069
* @param flags (Number)
2070
* The nsIAboutModule flags to use for registration.
2071
* @returns Promise that resolves when registration has finished.
2072
*/
2073
registerAboutPage(registerCleanupFunction, aboutModule, pageURI, flags) {
2074
// Return a promise that resolves when registration finished.
2075
const kRegistrationMsgId =
2076
"browser-test-utils:about-registration:registered";
2077
let rv = this.waitForMessage(Services.ppmm, kRegistrationMsgId, msg => {
2078
return msg.data == aboutModule;
2079
});
2080
// Load a script that registers our page, then send it a message to execute the registration.
2081
if (!this._loadedAboutContentScript) {
2082
Services.ppmm.loadProcessScript(
2083
kAboutPageRegistrationContentScript,
2084
true
2085
);
2086
this._loadedAboutContentScript = true;
2087
registerCleanupFunction(this._removeAboutPageRegistrations.bind(this));
2088
}
2089
Services.ppmm.broadcastAsyncMessage(
2090
"browser-test-utils:about-registration:register",
2091
{ aboutModule, pageURI, flags }
2092
);
2093
return rv.then(() => {
2094
this._knownAboutPages.add(aboutModule);
2095
});
2096
},
2097
2098
unregisterAboutPage(aboutModule) {
2099
if (!this._knownAboutPages.has(aboutModule)) {
2100
return Promise.reject(
2101
new Error("We don't think this about page exists!")
2102
);
2103
}
2104
const kUnregistrationMsgId =
2105
"browser-test-utils:about-registration:unregistered";
2106
let rv = this.waitForMessage(Services.ppmm, kUnregistrationMsgId, msg => {
2107
return msg.data == aboutModule;
2108
});
2109
Services.ppmm.broadcastAsyncMessage(
2110
"browser-test-utils:about-registration:unregister",
2111
aboutModule
2112
);
2113
return rv.then(() => this._knownAboutPages.delete(aboutModule));
2114
},
2115
2116
async _removeAboutPageRegistrations() {
2117
for (let aboutModule of this._knownAboutPages) {
2118
await this.unregisterAboutPage(aboutModule);
2119
}
2120
Services.ppmm.removeDelayedProcessScript(
2121
kAboutPageRegistrationContentScript
2122
);
2123
},
2124
2125
/**
2126
* Waits for the dialog to open, and clicks the specified button.
2127
*
2128
* @param {string} buttonAction
2129