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