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