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