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/. */
7 import { ExtensionUtils } from "resource://gre/modules/ExtensionUtils.sys.mjs";
9 import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
11 // Windowless browsers can create documents that rely on XUL Custom Elements:
12 ChromeUtils.importESModule(
13 "resource://gre/modules/CustomElementsListener.sys.mjs"
16 // Need to import ActorManagerParent.sys.mjs so that the actors are initialized
17 // before running extension XPCShell tests.
18 ChromeUtils.importESModule("resource://gre/modules/ActorManagerParent.sys.mjs");
22 ChromeUtils.defineESModuleGetters(lazy, {
23 ContentTask: "resource://testing-common/ContentTask.sys.mjs",
24 HttpServer: "resource://testing-common/httpd.sys.mjs",
25 SpecialPowersParent: "resource://testing-common/SpecialPowersParent.sys.mjs",
26 TestUtils: "resource://testing-common/TestUtils.sys.mjs",
29 XPCOMUtils.defineLazyServiceGetters(lazy, {
31 "@mozilla.org/network/protocol-proxy-service;1",
32 "nsIProtocolProxyService",
36 const { promiseDocumentLoaded, promiseEvent, promiseObserved } = ExtensionUtils;
38 var gRemoteContentScripts = Services.appinfo.browserTabsRemoteAutostart;
39 const REMOTE_CONTENT_SUBFRAMES = Services.appinfo.fissionAutostart;
41 function frameScript() {
42 // We need to make sure that the ExtensionPolicy service has been initialized
43 // as it sets up the observers that inject extension content scripts.
44 Cc["@mozilla.org/addons/policy-service;1"].getService();
46 Services.obs.notifyObservers(this, "tab-content-frameloader-created");
48 // eslint-disable-next-line mozilla/balanced-listeners, no-undef
52 Services.obs.notifyObservers(null, "memory-pressure", "heap-minimize");
59 let kungFuDeathGrip = new Set();
60 function promiseBrowserLoaded(browser, url, redirectUrl) {
61 url = url && Services.io.newURI(url);
62 redirectUrl = redirectUrl && Services.io.newURI(redirectUrl);
64 return new Promise(resolve => {
66 QueryInterface: ChromeUtils.generateQI([
67 "nsISupportsWeakReference",
68 "nsIWebProgressListener",
71 onStateChange(webProgress, request, stateFlags) {
72 request.QueryInterface(Ci.nsIChannel);
75 request.originalURI ||
76 webProgress.DOMWindow.document.documentURIObject;
78 webProgress.isTopLevel &&
79 (url?.equals(requestURI) || redirectUrl?.equals(requestURI)) &&
80 stateFlags & Ci.nsIWebProgressListener.STATE_STOP
83 kungFuDeathGrip.delete(listener);
84 browser.removeProgressListener(listener);
89 // addProgressListener only supports weak references, so we need to
90 // use one. But we also need to make sure it stays alive until we're
91 // done with it, so thunk away a strong reference to keep it alive.
92 kungFuDeathGrip.add(listener);
93 browser.addProgressListener(
95 Ci.nsIWebProgress.NOTIFY_STATE_WINDOW
102 remote = gRemoteContentScripts,
103 remoteSubframes = REMOTE_CONTENT_SUBFRAMES,
105 privateBrowsing = false,
106 userContextId = undefined
108 this.remote = remote;
110 // If an extension has been passed, overwrite remote
111 // with extension.remote to be sure that the ContentPage
112 // will have the same remoteness of the extension.
114 this.remote = extension.remote;
117 this.remoteSubframes = this.remote && remoteSubframes;
118 this.extension = extension;
119 this.privateBrowsing = privateBrowsing;
120 this.userContextId = userContextId;
122 this.browserReady = this._initBrowser();
125 async _initBrowser() {
128 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_REMOTE_WINDOW;
130 if (this.remoteSubframes) {
131 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_FISSION_WINDOW;
133 if (this.privateBrowsing) {
134 chromeFlags |= Ci.nsIWebBrowserChrome.CHROME_PRIVATE_WINDOW;
136 this.windowlessBrowser = Services.appShell.createWindowlessBrowser(
141 let system = Services.scriptSecurityManager.getSystemPrincipal();
143 let chromeShell = this.windowlessBrowser.docShell.QueryInterface(
147 chromeShell.createAboutBlankDocumentViewer(system, system);
148 this.windowlessBrowser.browsingContext.useGlobalHistory = false;
149 let loadURIOptions = {
150 triggeringPrincipal: system,
153 Services.io.newURI("chrome://extensions/content/dummy.xhtml"),
157 await promiseObserved(
158 "chrome-document-global-created",
159 win => win.document == chromeShell.document
162 let chromeDoc = await promiseDocumentLoaded(chromeShell.document);
164 let { SpecialPowers } = chromeDoc.ownerGlobal;
165 SpecialPowers.xpcshellScope = XPCShellContentUtils.currentScope;
166 SpecialPowers.setAsDefaultAssertHandler();
168 let browser = chromeDoc.createXULElement("browser");
169 browser.setAttribute("type", "content");
170 browser.setAttribute("disableglobalhistory", "true");
171 browser.setAttribute("messagemanagergroup", "webext-browsers");
172 browser.setAttribute("nodefaultsrc", "true");
173 if (this.userContextId) {
174 browser.setAttribute("usercontextid", this.userContextId);
177 if (this.extension?.remote) {
178 browser.setAttribute("remote", "true");
179 browser.setAttribute("remoteType", "extension");
182 // Ensure that the extension is loaded into the correct
183 // BrowsingContextGroupID by default.
184 if (this.extension) {
185 browser.setAttribute(
186 "initialBrowsingContextGroupId",
187 this.extension.browsingContextGroupId
191 let awaitFrameLoader = Promise.resolve();
193 awaitFrameLoader = promiseEvent(browser, "XULFrameLoaderCreated");
194 browser.setAttribute("remote", "true");
196 browser.setAttribute("maychangeremoteness", "true");
197 browser.addEventListener(
198 "DidChangeBrowserRemoteness",
199 this.didChangeBrowserRemoteness.bind(this)
203 chromeDoc.documentElement.appendChild(browser);
205 // Forcibly flush layout so that we get a pres shell soon enough, see
207 browser.getBoundingClientRect();
209 await awaitFrameLoader;
211 this.browser = browser;
213 this.loadFrameScript(frameScript);
218 get browsingContext() {
219 return this.browser.browsingContext;
222 get SpecialPowers() {
223 return this.browser.ownerGlobal.SpecialPowers;
226 loadFrameScript(func) {
227 let frameScript = `data:text/javascript,(${encodeURI(func)}).call(this)`;
228 this.browser.messageManager.loadFrameScript(frameScript, true, true);
231 addFrameScriptHelper(func) {
232 let frameScript = `data:text/javascript,${encodeURI(func)}`;
233 this.browser.messageManager.loadFrameScript(frameScript, false, true);
236 didChangeBrowserRemoteness() {
237 // XXX: Tests can load their own additional frame scripts, so we may need to
238 // track all scripts that have been loaded, and reload them here?
239 this.loadFrameScript(frameScript);
242 async loadURL(url, redirectUrl = undefined) {
243 await this.browserReady;
245 this.browser.fixupAndLoadURIString(url, {
246 triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
248 return promiseBrowserLoaded(this.browser, url, redirectUrl);
251 async fetch(...args) {
252 return this.spawn(args, async (url, options) => {
253 let resp = await this.content.fetch(url, options);
258 spawn(params, task) {
259 return this.SpecialPowers.spawn(this.browser, params, task);
262 // Like spawn(), but uses the legacy ContentTask infrastructure rather than
263 // SpecialPowers. Exists only because the author of the SpecialPowers
264 // migration did not have the time to fix all of the legacy users who relied
265 // on the old semantics.
267 // DO NOT USE IN NEW CODE
268 legacySpawn(params, task) {
269 lazy.ContentTask.setTestScope(XPCShellContentUtils.currentScope);
271 return lazy.ContentTask.spawn(this.browser, params, task);
275 await this.browserReady;
277 let { messageManager } = this.browser;
279 this.browser.removeEventListener(
280 "DidChangeBrowserRemoteness",
281 this.didChangeBrowserRemoteness.bind(this)
285 this.windowlessBrowser.close();
286 this.windowlessBrowser = null;
288 await lazy.TestUtils.topicObserved(
289 "message-manager-disconnect",
290 subject => subject === messageManager
295 export var XPCShellContentUtils = {
297 fetchScopes: new Map(),
300 this.currentScope = scope;
302 // We need to load at least one frame script into every message
303 // manager to ensure that the scriptable wrapper for its global gets
304 // created before we try to access it externally. If we don't, we
305 // fail sanity checks on debug builds the first time we try to
306 // create a wrapper, because we should never have a global without a
308 Services.mm.loadFrameScript("data:text/javascript,//", true, true);
310 scope.registerCleanupFunction(() => {
311 this.currentScope = null;
314 Array.from(this.fetchScopes.values(), promise =>
315 promise.then(scope => scope.close())
322 // QuotaManager crashes if it doesn't have a profile.
323 scope.do_get_profile();
325 this.initCommon(scope);
327 lazy.SpecialPowersParent.registerActor();
330 initMochitest(scope) {
331 this.initCommon(scope);
334 ensureInitialized(scope) {
335 if (!this.currentScope) {
336 if (scope.do_get_profile) {
339 this.initMochitest(scope);
345 * Creates a new HttpServer for testing, and begins listening on the
346 * specified port. Automatically shuts down the server when the test
349 * @param {object} [options = {}]
350 * The options object.
351 * @param {integer} [options.port = -1]
352 * The port to listen on. If omitted, listen on a random
353 * port. The latter is the preferred behavior.
354 * @param {sequence<string>?} [options.hosts = null]
355 * A set of hosts to accept connections to. Support for this is
356 * implemented using a proxy filter.
358 * @returns {HttpServer}
359 * The HTTP server instance.
361 createHttpServer({ port = -1, hosts } = {}) {
362 let server = new lazy.HttpServer();
366 const hostsSet = new Set();
367 const serverHost = "localhost";
368 const serverPort = server.identity.primaryPort;
370 for (let host of hosts) {
371 if (host.startsWith("[") && host.endsWith("]")) {
372 // HttpServer expects IPv6 addresses in bracket notation, but the
373 // proxy filter uses nsIURI.host, which does not have brackets.
374 hostsSet.add(host.slice(1, -1));
378 server.identity.add("http", host, 80);
381 const proxyFilter = {
382 proxyInfo: lazy.proxyService.newProxyInfo(
393 applyFilter(channel, defaultProxyInfo, callback) {
394 if (hostsSet.has(channel.URI.host)) {
395 callback.onProxyFilterResult(this.proxyInfo);
397 callback.onProxyFilterResult(defaultProxyInfo);
402 lazy.proxyService.registerChannelFilter(proxyFilter, 0);
403 this.currentScope.registerCleanupFunction(() => {
404 lazy.proxyService.unregisterChannelFilter(proxyFilter);
408 this.currentScope.registerCleanupFunction(() => {
409 return new Promise(resolve => {
410 server.stop(resolve);
417 registerJSON(server, path, obj) {
418 server.registerPathHandler(path, (request, response) => {
419 response.setHeader("content-type", "application/json", true);
420 response.write(JSON.stringify(obj));
424 async fetch(origin, url, options) {
425 let fetchScopePromise = this.fetchScopes.get(origin);
426 if (!fetchScopePromise) {
427 fetchScopePromise = this.loadContentPage(origin);
428 this.fetchScopes.set(origin, fetchScopePromise);
431 let fetchScope = await fetchScopePromise;
432 return fetchScope.fetch(url, options);
436 * Loads a content page into a hidden docShell.
438 * @param {string} url
440 * @param {object} [options = {}]
441 * @param {ExtensionWrapper} [options.extension]
442 * If passed, load the URL as an extension page for the given
444 * @param {boolean} [options.remote]
445 * If true, load the URL in a content process. If false, load
446 * it in the parent process.
447 * @param {boolean} [options.remoteSubframes]
448 * If true, load cross-origin frames in separate content processes.
449 * This is ignored if |options.remote| is false.
450 * @param {string} [options.redirectUrl]
451 * An optional URL that the initial page is expected to
454 * @returns {ContentPage}
459 extension = undefined,
461 remoteSubframes = undefined,
462 redirectUrl = undefined,
463 privateBrowsing = false,
464 userContextId = undefined,
467 let contentPage = new ContentPage(
470 extension && extension.extension,
475 return contentPage.loadURL(url, redirectUrl).then(() => {