1 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
2 /* vim: set sts=2 sw=2 et tw=80: */
3 /* This Source Code Form is subject to the terms of the Mozilla Public
4 * License, v. 2.0. If a copy of the MPL was not distributed with this
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
8 var EXPORTED_SYMBOLS = ["XPCShellContentUtils"];
10 const { ExtensionUtils } = ChromeUtils.import(
11 "resource://gre/modules/ExtensionUtils.jsm"
13 const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
14 const { XPCOMUtils } = ChromeUtils.import(
15 "resource://gre/modules/XPCOMUtils.jsm"
18 // Windowless browsers can create documents that rely on XUL Custom Elements:
19 // eslint-disable-next-line mozilla/reject-chromeutils-import-params
20 ChromeUtils.import("resource://gre/modules/CustomElementsListener.jsm", null);
22 // Need to import ActorManagerParent.jsm so that the actors are initialized before
23 // running extension XPCShell tests.
24 ChromeUtils.import("resource://gre/modules/ActorManagerParent.jsm");
26 XPCOMUtils.defineLazyModuleGetters(this, {
27 ContentTask: "resource://testing-common/ContentTask.jsm",
28 HttpServer: "resource://testing-common/httpd.js",
29 MessageChannel: "resource://testing-common/MessageChannel.jsm",
30 TestUtils: "resource://testing-common/TestUtils.jsm",
33 XPCOMUtils.defineLazyServiceGetters(this, {
35 "@mozilla.org/network/protocol-proxy-service;1",
36 "nsIProtocolProxyService",
40 const { promiseDocumentLoaded, promiseEvent, promiseObserved } = ExtensionUtils;
42 var gRemoteContentScripts = Services.appinfo.browserTabsRemoteAutostart;
43 const REMOTE_CONTENT_SUBFRAMES = Services.appinfo.fissionAutostart;
45 function frameScript() {
46 const { MessageChannel } = ChromeUtils.import(
47 "resource://testing-common/MessageChannel.jsm"
49 const { Services } = ChromeUtils.import(
50 "resource://gre/modules/Services.jsm"
53 // We need to make sure that the ExtensionPolicy service has been initialized
54 // as it sets up the observers that inject extension content scripts.
55 Cc["@mozilla.org/addons/policy-service;1"].getService();
57 const messageListener = {
58 async receiveMessage({ target, messageName, recipient, data, name }) {
60 let resp = await content.fetch(data.url, data.options);
64 MessageChannel.addListener(this, "Test:Fetch", messageListener);
66 // eslint-disable-next-line mozilla/balanced-listeners, no-undef
70 Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
77 let kungFuDeathGrip = new Set();
78 function promiseBrowserLoaded(browser, url, redirectUrl) {
79 url = url && Services.io.newURI(url);
80 redirectUrl = redirectUrl && Services.io.newURI(redirectUrl);
82 return new Promise(resolve => {
84 QueryInterface: ChromeUtils.generateQI([
85 "nsISupportsWeakReference",
86 "nsIWebProgressListener",
89 onStateChange(webProgress, request, stateFlags, statusCode) {
90 request.QueryInterface(Ci.nsIChannel);
93 request.originalURI ||
94 webProgress.DOMWindow.document.documentURIObject;
96 webProgress.isTopLevel &&
97 (url?.equals(requestURI) || redirectUrl?.equals(requestURI)) &&
98 stateFlags & Ci.nsIWebProgressListener.STATE_STOP
101 kungFuDeathGrip.delete(listener);
102 browser.removeProgressListener(listener);
107 // addProgressListener only supports weak references, so we need to
108 // use one. But we also need to make sure it stays alive until we're
109 // done with it, so thunk away a strong reference to keep it alive.
110 kungFuDeathGrip.add(listener);
111 browser.addProgressListener(
113 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
120 remote = gRemoteContentScripts,
121 remoteSubframes = REMOTE_CONTENT_SUBFRAMES,
123 privateBrowsing = false,
124 userContextId = undefined
126 this.remote = remote;
128 // If an extension has been passed, overwrite remote
129 // with extension.remote to be sure that the ContentPage
130 // will have the same remoteness of the extension.
132 this.remote = extension.remote;
135 this.remoteSubframes = this.remote && remoteSubframes;
136 this.extension = extension;
137 this.privateBrowsing = privateBrowsing;
138 this.userContextId = userContextId;
140 this.browserReady = this._initBrowser();
143 async _initBrowser() {
146 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW;
148 if (this.remoteSubframes) {
149 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW;
151 if (this.privateBrowsing) {
152 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW;
154 this.windowlessBrowser = Services.appShell.createWindowlessBrowser(
159 let system = Services.scriptSecurityManager.getSystemPrincipal();
161 let chromeShell = this.windowlessBrowser.docShell.QueryInterface(
165 chromeShell.createAboutBlankContentViewer(system, system);
166 this.windowlessBrowser.browsingContext.useGlobalHistory = false;
167 let loadURIOptions = {
168 triggeringPrincipal: system,
171 "chrome://extensions/content/dummy.xhtml",
175 await promiseObserved(
176 "chrome-document-global-created",
177 win => win.document == chromeShell.document
180 let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
182 let browser = chromeDoc.createXULElement("browser");
183 browser.setAttribute("type", "content");
184 browser.setAttribute("disableglobalhistory", "true");
185 browser.setAttribute("messagemanagergroup", "webext-browsers");
186 browser.setAttribute("nodefaultsrc", "true");
187 if (this.userContextId) {
188 browser.setAttribute("usercontextid", this.userContextId);
191 if (this.extension?.remote) {
192 browser.setAttribute("remote", "true");
193 browser.setAttribute("remoteType", "extension");
196 // Ensure that the extension is loaded into the correct
197 // BrowsingContextGroupID by default.
198 if (this.extension) {
199 browser.setAttribute(
200 "initialBrowsingContextGroupId",
201 this.extension.browsingContextGroupId
205 let awaitFrameLoader = Promise.resolve();
207 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
208 browser.setAttribute("remote", "true");
210 browser.setAttribute("maychangeremoteness", "true");
211 browser.addEventListener(
212 "DidChangeBrowserRemoteness",
213 this.didChangeBrowserRemoteness.bind(this)
217 chromeDoc.documentElement.appendChild(browser);
219 // Forcibly flush layout so that we get a pres shell soon enough, see
221 browser.getBoundingClientRect();
223 await awaitFrameLoader;
225 this.browser = browser;
227 this.loadFrameScript(frameScript);
232 get browsingContext() {
233 return this.browser.browsingContext;
236 sendMessage(msg, data) {
237 return MessageChannel.sendMessage(this.browser.messageManager, msg, data);
240 loadFrameScript(func) {
241 let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`;
242 this.browser.messageManager.loadFrameScript(frameScript, true, true);
245 addFrameScriptHelper(func) {
246 let frameScript = `data:text/javascript,${encodeURI(func)}`;
247 this.browser.messageManager.loadFrameScript(frameScript, false, true);
250 didChangeBrowserRemoteness(event) {
251 // XXX: Tests can load their own additional frame scripts, so we may need to
252 // track all scripts that have been loaded, and reload them here?
253 this.loadFrameScript(frameScript);
256 async loadURL(url, redirectUrl = undefined) {
257 await this.browserReady;
259 this.browser.loadURI(url, {
260 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
262 return promiseBrowserLoaded(this.browser, url, redirectUrl);
265 async fetch(url, options) {
266 return this.sendMessage("Test:Fetch", { url, options });
269 spawn(params, task) {
270 return ContentTask.spawn(this.browser, params, task);
274 await this.browserReady;
276 let { messageManager } = this.browser;
278 this.browser.removeEventListener(
279 "DidChangeBrowserRemoteness",
280 this.didChangeBrowserRemoteness.bind(this)
284 this.windowlessBrowser.close();
285 this.windowlessBrowser = null;
287 await TestUtils.topicObserved(
288 "message-manager-disconnect",
289 subject => subject === messageManager
294 var XPCShellContentUtils = {
296 fetchScopes: new Map(),
299 this.currentScope = scope;
301 // We need to load at least one frame script into every message
302 // manager to ensure that the scriptable wrapper for its global gets
303 // created before we try to access it externally. If we don't, we
304 // fail sanity checks on debug builds the first time we try to
305 // create a wrapper, because we should never have a global without a
307 Services.mm.loadFrameScript("data:text/javascript,//", true, true);
309 scope.registerCleanupFunction(() => {
310 this.currentScope = null;
313 Array.from(this.fetchScopes.values(), promise =>
314 promise.then(scope => scope.close())
321 // QuotaManager crashes if it doesn't have a profile.
322 scope.do_get_profile();
324 this.initCommon(scope);
327 initMochitest(scope) {
328 this.initCommon(scope);
331 ensureInitialized(scope) {
332 if (!this.currentScope) {
333 if (scope.do_get_profile) {
336 this.initMochitest(scope);
342 * Creates a new HttpServer for testing, and begins listening on the
343 * specified port. Automatically shuts down the server when the test
346 * @param {object} [options = {}]
347 * The options object.
348 * @param {integer} [options.port = -1]
349 * The port to listen on. If omitted, listen on a random
350 * port. The latter is the preferred behavior.
351 * @param {sequence<string>?} [options.hosts = null]
352 * A set of hosts to accept connections to. Support for this is
353 * implemented using a proxy filter.
355 * @returns {HttpServer}
356 * The HTTP server instance.
358 createHttpServer({ port = -1, hosts } = {}) {
359 let server = new HttpServer();
363 hosts = new Set(hosts);
364 const serverHost = "localhost";
365 const serverPort = server.identity.primaryPort;
367 for (let host of hosts) {
368 server.identity.add("http", host, 80);
371 const proxyFilter = {
372 proxyInfo: proxyService.newProxyInfo(
383 applyFilter(channel, defaultProxyInfo, callback) {
384 if (hosts.has(channel.URI.host)) {
385 callback.onProxyFilterResult(this.proxyInfo);
387 callback.onProxyFilterResult(defaultProxyInfo);
392 proxyService.registerChannelFilter(proxyFilter, 0);
393 this.currentScope.registerCleanupFunction(() => {
394 proxyService.unregisterChannelFilter(proxyFilter);
398 this.currentScope.registerCleanupFunction(() => {
399 return new Promise(resolve => {
400 server.stop(resolve);
407 registerJSON(server, path, obj) {
408 server.registerPathHandler(path, (request, response) => {
409 response.setHeader("content-type", "application/json", true);
410 response.write(JSON.stringify(obj));
414 get remoteContentScripts() {
415 return gRemoteContentScripts;
418 set remoteContentScripts(val) {
419 gRemoteContentScripts = !!val;
422 async fetch(origin, url, options) {
423 let fetchScopePromise = this.fetchScopes.get(origin);
424 if (!fetchScopePromise) {
425 fetchScopePromise = this.loadContentPage(origin);
426 this.fetchScopes.set(origin, fetchScopePromise);
429 let fetchScope = await fetchScopePromise;
430 return fetchScope.sendMessage("Test:Fetch", { url, options });
434 * Loads a content page into a hidden docShell.
436 * @param {string} url
438 * @param {object} [options = {}]
439 * @param {ExtensionWrapper} [options.extension]
440 * If passed, load the URL as an extension page for the given
442 * @param {boolean} [options.remote]
443 * If true, load the URL in a content process. If false, load
444 * it in the parent process.
445 * @param {boolean} [options.remoteSubframes]
446 * If true, load cross-origin frames in separate content processes.
447 * This is ignored if |options.remote| is false.
448 * @param {string} [options.redirectUrl]
449 * An optional URL that the initial page is expected to
452 * @returns {ContentPage}
457 extension = undefined,
459 remoteSubframes = undefined,
460 redirectUrl = undefined,
461 privateBrowsing = false,
462 userContextId = undefined,
465 ContentTask.setTestScope(this.currentScope);
467 let contentPage = new ContentPage(
470 extension && extension.extension,
475 return contentPage.loadURL(url, redirectUrl).then(() => {