Bug 1602173: Capture attempts to load pages and redirect back to the browser when needed. r=Gijs

Differential Revision: https://phabricator.services.mozilla.com/D56286

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Dave Townsend 2019-12-13 15:48:54 +00:00
Родитель 5577453668
Коммит e5bd7eee3c
13 изменённых файлов: 981 добавлений и 6 удалений

Просмотреть файл

@ -279,6 +279,17 @@ let ACTORS = {
allFrames: true,
},
SiteSpecificBrowser: {
parent: {
moduleURI: "resource:///actors/SiteSpecificBrowserParent.jsm",
},
child: {
moduleURI: "resource:///actors/SiteSpecificBrowserChild.jsm",
},
allFrames: true,
},
UITour: {
parent: {
moduleURI: "resource:///modules/UITourParent.jsm",

Просмотреть файл

@ -0,0 +1,174 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const EXPORTED_SYMBOLS = ["SiteSpecificBrowserChild"];
const { SiteSpecificBrowserBase } = ChromeUtils.import(
"resource:///modules/SiteSpecificBrowserService.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { E10SUtils } = ChromeUtils.import(
"resource://gre/modules/E10SUtils.jsm"
);
class SiteSpecificBrowserChild extends JSWindowActorChild {
receiveMessage(message) {
switch (message.name) {
case "SetSSB":
// Note that this sets the webbrowserchrome for the top-level browser
// child in this page. This means that any inner-frames loading in
// different processes will not be handled correctly. Fixing this will
// happen in bug 1602849.
this.docShell.browserChild.webBrowserChrome = new WebBrowserChrome(
message.data
);
break;
}
}
}
function getActor(docShell) {
return docShell.domWindow
.getWindowGlobalChild()
.getActor("SiteSpecificBrowser");
}
// JS actors can't generally be XPCOM objects so we must use a separate class.
class WebBrowserChrome {
constructor(id) {
this.id = id;
}
get ssb() {
return SiteSpecificBrowserBase.get(this.id);
}
// nsIWebBrowserChrome3
/**
* This gets called when a user clicks on a link or submits a form. We can use
* it to see where the resulting page load will occur and if needed redirect
* it to a different target.
*
* @param {string} originalTarget the target intended for the load.
* @param {nsIURI} linkURI the URI that will be loaded.
* @param {Node} linkNode the element causing the load.
* @param {boolean} isAppTab whether the source docshell is marked as an
* app tab.
* @return {string} the target to use for the load.
*/
onBeforeLinkTraversal(originalTarget, linkURI, linkNode, isAppTab) {
// Our actor is for the top-level frame in the page while this may be being
// called for a navigation in an inner frame. First we have to find the
// browsing context for the frame doing the load.
let docShell = linkNode.ownerGlobal.docShell;
let bc = docShell.browsingContext;
// Which browsing context is this link targetting?
let target = originalTarget ? bc.findWithName(originalTarget) : bc;
if (target) {
// If we found a target then it must be one of the frames within this
// frame tree since we don't support popup windows.
if (target.parent) {
// An inner frame, continue.
return originalTarget;
}
// A top-level load. If our SSB cannot load this URI then start the
// process of opening it into a new tab somewhere.
return this.ssb.canLoad(linkURI) ? originalTarget : "_blank";
}
// An attempt to open a new window/tab. If the new URI can be loaded by our
// SSB then load it at the top-level. Note that we override the requested
// target so that this page can't reach the new context.
return this.ssb.canLoad(linkURI) ? "_top" : "_blank";
}
/**
* A load is about to occur in a frame. This is an opportunity to stop it
* and redirect it somewhere.
*
* @param {nsIDocShell} docShell the current docshell.
* @param {nsIURI} uri the URI that will be loaded.
* @param {nsIReferrerInfo} referrerInfo the referrer info.
* @param {boolean} hasPostData whether there is POST data
* for the load.
* @param {nsIPrincipal} triggeringPrincipal the triggering principal.
* @param {nsIContentSecurityPolicy} csp the content security policy.
* @return {boolean} whether the load should proceed or not.
*/
shouldLoadURI(
docShell,
uri,
referrerInfo,
hasPostData,
triggeringPrincipal,
csp
) {
// As above, our actor is for the top-level frame in the page however we
// are passed the docshell potentially handling the load here so we can
// do the right thing.
// We only police loads at the top level.
if (docShell.browsingContext.parent) {
return true;
}
if (!this.ssb.canLoad(uri)) {
// Should only have got this far for a window.location manipulation.
getActor(docShell).sendAsyncMessage("RetargetOutOfScopeURIToBrowser", {
uri: uri.spec,
referrerInfo: E10SUtils.serializeReferrerInfo(referrerInfo),
triggeringPrincipal: E10SUtils.serializePrincipal(
triggeringPrincipal ||
Services.scriptSecurityManager.createNullPrincipal({})
),
csp: csp ? E10SUtils.serializeCSP(csp) : null,
});
return false;
}
return true;
}
/**
* A simple check for whether this is the correct process to load this URI.
*
* @param {nsIURI} uri the URI that will be loaded.
* @return {boolean} whether the load should proceed or not.
*/
shouldLoadURIInThisProcess(uri) {
return this.ssb.canLoad(uri);
}
/**
* Instructs us to start a fresh process to load this URI. Usually used for
* large allocation sites. SSB does not support this.
*
* @param {nsIDocShell} docShell the current docshell.
* @param {nsIURI} uri the URI that will be loaded.
* @param {nsIReferrerInfo} referrerInfo the referrer info.
* @param {nsIPrincipal} triggeringPrincipal the triggering principal.
* @param {Number} loadFlags the load flags.
* @param {nsIContentSecurityPolicy} csp the content security policy.
* @return {boolean} whether the load should proceed or not.
*/
reloadInFreshProcess(
docShell,
uri,
referrerInfo,
triggeringPrincipal,
loadFlags,
csp
) {
return false;
}
}

Просмотреть файл

@ -0,0 +1,75 @@
/* vim: set ts=2 sw=2 sts=2 et tw=80: */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
var EXPORTED_SYMBOLS = ["SiteSpecificBrowserParent"];
const { BrowserWindowTracker } = ChromeUtils.import(
"resource:///modules/BrowserWindowTracker.jsm"
);
const { E10SUtils } = ChromeUtils.import(
"resource://gre/modules/E10SUtils.jsm"
);
const { Services } = ChromeUtils.import("resource://gre/modules/Services.jsm");
const { AppConstants } = ChromeUtils.import(
"resource://gre/modules/AppConstants.jsm"
);
class SiteSpecificBrowserParent extends JSWindowActorParent {
receiveMessage(message) {
switch (message.name) {
case "RetargetOutOfScopeURIToBrowser":
// The content process found a URI that needs to be loaded in the main
// browser.
let triggeringPrincipal = E10SUtils.deserializePrincipal(
message.data.triggeringPrincipal
);
let referrerInfo = E10SUtils.deserializeReferrerInfo(
message.data.referrerInfo
);
let csp = E10SUtils.deserializeCSP(message.data.csp);
// Attempt to find an existing window to open it in.
let win = BrowserWindowTracker.getTopWindow();
if (win) {
win.gBrowser.selectedTab = win.gBrowser.addTab(message.data.uri, {
triggeringPrincipal,
csp,
referrerInfo,
});
} else {
let sa = Cc["@mozilla.org/array;1"].createInstance(
Ci.nsIMutableArray
);
let wuri = Cc["@mozilla.org/supports-string;1"].createInstance(
Ci.nsISupportsString
);
wuri.data = message.data.uri;
sa.appendElement(wuri);
sa.appendElement(null); // unused (bug 871161)
sa.appendElement(referrerInfo);
sa.appendElement(null); // postData
sa.appendElement(null); // allowThirdPartyFixup
sa.appendElement(null); // userContextId
sa.appendElement(null); // originPrincipal
sa.appendElement(null); // originStoragePrincipal
sa.appendElement(triggeringPrincipal);
sa.appendElement(null); // allowInheritPrincipal
sa.appendElement(csp);
Services.ww.openWindow(
null,
AppConstants.BROWSER_CHROME_URL,
null,
"chrome,dialog=no,all",
sa
);
}
break;
}
}
}

Просмотреть файл

@ -2,24 +2,194 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
ChromeUtils.defineModuleGetter(
this,
"SiteSpecificBrowser",
"resource:///modules/SiteSpecificBrowserService.jsm"
const { XPCOMUtils } = ChromeUtils.import(
"resource://gre/modules/XPCOMUtils.jsm"
);
XPCOMUtils.defineLazyModuleGetters(this, {
BrowserUtils: "resource://gre/modules/BrowserUtils.jsm",
Services: "resource://gre/modules/Services.jsm",
SiteSpecificBrowser: "resource:///modules/SiteSpecificBrowserService.jsm",
BrowserWindowTracker: "resource:///modules/BrowserWindowTracker.jsm",
});
let gSSBBrowser = null;
let gSSB = null;
function init() {
gSSB = SiteSpecificBrowser.get(window.arguments[0]);
window.browserDOMWindow = new BrowserDOMWindow();
gSSBBrowser = document.createXULElement("browser");
gSSBBrowser.setAttribute("id", "browser");
gSSBBrowser.setAttribute("type", "content");
gSSBBrowser.setAttribute("remote", "true");
gSSBBrowser.setAttribute("nodefaultsrc", "true");
document.getElementById("browser-container").appendChild(gSSBBrowser);
// Give our actor the SSB's ID.
let actor = gSSBBrowser.browsingContext.currentWindowGlobal.getActor(
"SiteSpecificBrowser"
);
actor.sendAsyncMessage("SetSSB", gSSB.id);
gSSBBrowser.src = gSSB.startURI.spec;
}
window.addEventListener("load", init, true);
class BrowserDOMWindow {
/**
* Called when a page in the main process needs a new window to display a new
* page in.
*
* @param {nsIURI?} uri
* @param {Window} opener
* @param {Number} where
* @param {Number} flags
* @param {nsIPrincipal} triggeringPrincipal
* @param {nsIContentSecurityPolicy?} csp
* @return {BrowsingContext} the BrowsingContext the URI should be loaded in.
*/
createContentWindow(uri, opener, where, flags, triggeringPrincipal, csp) {
console.error(
"createContentWindow should never be called from a remote browser"
);
throw Cr.NS_ERROR_FAILURE;
}
/**
* Called from a page in the main process to open a new URI.
*
* @param {nsIURI} uri
* @param {Window} opener
* @param {Number} where
* @param {Number} flags
* @param {nsIPrincipal} triggeringPrincipal
* @param {nsIContentSecurityPolicy?} csp
* @return {BrowsingContext} the BrowsingContext the URI should be loaded in.
*/
openURI(uri, opener, where, flags, triggeringPrincipal, csp) {
console.error("openURI should never be called from a remote browser");
throw Cr.NS_ERROR_FAILURE;
}
/**
* Finds a new frame to load some content in.
*
* @param {nsIURI?} uri
* @param {nsIOpenURIInFrameParams} params
* @param {Number} where
* @param {Number} flags
* @param {Number} nextRemoteTabId
* @param {string} name
* @param {boolean} shouldOpen should the load start or not.
* @return {Element} the frame element the URI should be loaded in.
*/
getContentWindowOrOpenURIInFrame(
uri,
params,
where,
flags,
nextRemoteTabId,
name,
shouldOpen
) {
// It's been determined that this load needs to happen in a new frame.
// Either onBeforeLinkTraversal set this correctly or this is the result
// of a window.open call.
// If this ssb can load the url then just load it internally.
if (gSSB.canLoad(uri)) {
return gSSBBrowser;
}
// Try and find a browser window to open in.
let win = BrowserWindowTracker.getTopWindow({
private: params.isPrivate,
allowPopups: false,
});
if (win) {
// Just hand off to the window's handler
win.focus();
return win.browserDOMWindow.openURIInFrame(
shouldOpen ? uri : null,
params,
where,
flags,
nextRemoteTabId,
name
);
}
// We need to open a new browser window and a tab in it. That's an
// asychronous operation but luckily if we return null here the platform
// handles doing that for us.
return null;
}
/**
* Gets an nsFrameLoaderOwner to load some new content in.
*
* @param {nsIURI?} uri
* @param {nsIOpenURIInFrameParams} params
* @param {Number} where
* @param {Number} flags
* @param {Number} nextRemoteTabId
* @param {string} name
* @return {Element} the frame element the URI should be loaded in.
*/
createContentWindowInFrame(uri, params, where, flags, nextRemoteTabId, name) {
return this.getContentWindowOrOpenURIInFrame(
uri,
params,
where,
flags,
nextRemoteTabId,
name,
false
);
}
/**
* Create a new nsFrameLoaderOwner and load some content into it.
*
* @param {nsIURI} uri
* @param {nsIOpenURIInFrameParams} params
* @param {Number} where
* @param {Number} flags
* @param {Number} nextRemoteTabId
* @param {string} name
* @return {Element} the frame element the URI is loading in.
*/
openURIInFrame(uri, params, where, flags, nextRemoteTabId, name) {
return this.getContentWindowOrOpenURIInFrame(
uri,
params,
where,
flags,
nextRemoteTabId,
name,
true
);
}
isTabContentWindow(window) {
// This method is probably not needed anymore: bug 1602915
return gSSBBrowser.contentWindow == window;
}
canClose() {
return BrowserUtils.canCloseWindow(window);
}
get tabCount() {
return 1;
}
}
BrowserDOMWindow.prototype.QueryInterface = ChromeUtils.generateQI([
Ci.nsIBrowserDOMWindow,
]);
window.addEventListener("DOMContentLoaded", init, true);

Просмотреть файл

@ -14,3 +14,8 @@ XPCOM_MANIFESTS += [
EXTRA_JS_MODULES += [
'SiteSpecificBrowserService.jsm',
]
FINAL_TARGET_FILES.actors += [
'SiteSpecificBrowserChild.jsm',
'SiteSpecificBrowserParent.jsm',
]

Просмотреть файл

@ -2,9 +2,16 @@
support-files =
head.js
test_page.html
empty_page.html
prefs =
browser.ssb.enabled=true
[browser_ssb_direct.js]
[browser_ssb_lasttab.js]
[browser_ssb_menu.js]
[browser_ssb_newtab.js]
[browser_ssb_newwindow.js]
[browser_ssb_open.js]
[browser_ssb_windowlocation.js]
[browser_ssb_windowopen.js]
skip-if = true # It is unclear what we want to do here.

Просмотреть файл

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
async function testDirectLoad(target, checker) {
let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
let promise = checker(ssb);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#direct",
{},
getBrowser(ssb)
);
await promise;
await BrowserTestUtils.closeWindow(ssb);
}
// A link that should load inside the ssb
add_task(async function local() {
await testDirectLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
try {
await expectSSBLoad(ssb);
Assert.equal(
getBrowser(ssb).currentURI.spec,
gHttpsTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to an insecure site should load outside the ssb
add_task(async function insecure() {
await testDirectLoad(gHttpTestRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to a different host should load outside the ssb
add_task(async function external() {
await testDirectLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpsOtherRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});

Просмотреть файл

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
async function testNewTabLoad(target, checker) {
let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
let promise = checker(ssb);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#new-tab",
{},
getBrowser(ssb)
);
await promise;
await BrowserTestUtils.closeWindow(ssb);
}
// A link that should load inside the ssb
add_task(async function local() {
await testNewTabLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
try {
await expectSSBLoad(ssb);
Assert.equal(
getBrowser(ssb).currentURI.spec,
gHttpsTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to an insecure site should load outside the ssb
add_task(async function insecure() {
await testNewTabLoad(gHttpTestRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to a different host should load outside the ssb
add_task(async function external() {
await testNewTabLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpsOtherRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});

Просмотреть файл

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
async function testNewWindowLoad(target, checker) {
let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
let promise = checker(ssb);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#new-window",
{},
getBrowser(ssb)
);
await promise;
await BrowserTestUtils.closeWindow(ssb);
}
// A link that should load inside the ssb
add_task(async function local() {
await testNewWindowLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
try {
await expectSSBLoad(ssb);
Assert.equal(
getBrowser(ssb).currentURI.spec,
gHttpsTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to an insecure site should load outside the ssb
add_task(async function insecure() {
await testNewWindowLoad(gHttpTestRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to a different host should load outside the ssb
add_task(async function external() {
await testNewWindowLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpsOtherRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});

Просмотреть файл

@ -0,0 +1,72 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
async function testWindowLocationLoad(target, checker) {
let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
let promise = checker(ssb);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#window-location",
{},
getBrowser(ssb)
);
await promise;
await BrowserTestUtils.closeWindow(ssb);
}
// A link that should load inside the ssb
add_task(async function local() {
await testWindowLocationLoad(
gHttpsTestRoot + "empty_page.html",
async ssb => {
try {
await expectSSBLoad(ssb);
Assert.equal(
getBrowser(ssb).currentURI.spec,
gHttpsTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
} catch (e) {
// Any error will already have logged a failure.
}
}
);
});
// A link to an insecure site should load outside the ssb
add_task(async function insecure() {
await testWindowLocationLoad(gHttpTestRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to a different host should load outside the ssb
add_task(async function external() {
await testWindowLocationLoad(
gHttpsOtherRoot + "empty_page.html",
async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpsOtherRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
}
);
});

Просмотреть файл

@ -0,0 +1,66 @@
/* Any copyright is dedicated to the Public Domain.
* http://creativecommons.org/publicdomain/zero/1.0/ */
async function testWindowOpenLoad(target, checker) {
let ssb = await openSSB(gHttpsTestRoot + "test_page.html#" + target);
let promise = checker(ssb);
await BrowserTestUtils.synthesizeMouseAtCenter(
"#window-open",
{},
getBrowser(ssb)
);
await promise;
await BrowserTestUtils.closeWindow(ssb);
}
// A link that should load inside the ssb
add_task(async function local() {
await testWindowOpenLoad(gHttpsTestRoot + "empty_page.html", async ssb => {
try {
await expectSSBLoad(ssb);
Assert.equal(
getBrowser(ssb).currentURI.spec,
gHttpsTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to an insecure site should load outside the ssb
add_task(async function insecure() {
await testWindowOpenLoad(gHttpTestRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpTestRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});
// A link to a different host should load outside the ssb
add_task(async function external() {
await testWindowOpenLoad(gHttpsOtherRoot + "empty_page.html", async ssb => {
try {
let tab = await expectTabLoad(ssb);
Assert.equal(
tab.linkedBrowser.currentURI.spec,
gHttpsOtherRoot + "empty_page.html",
"Should have loaded the right uri."
);
BrowserTestUtils.removeTab(tab);
} catch (e) {
// Any error will already have logged a failure.
}
});
});

Просмотреть файл

@ -13,9 +13,34 @@ const gHttpsTestRoot = getRootDirectory(gTestPath).replace(
"https://example.com/"
);
// A different secure site to use.
const gHttpsOtherRoot = getRootDirectory(gTestPath).replace(
"chrome://mochitests/content/",
"https://example.org/"
);
// The chrome url for the SSB UI.
const SSB_WINDOW = "chrome://browser/content/ssb/ssb.html";
// Directly opens an SSB for the given URI. Resolves to the SSB DOM window after
// the SSB content has loaded.
async function openSSB(uri) {
if (!(uri instanceof Ci.nsIURI)) {
uri = Services.io.newURI(uri);
}
let openPromise = BrowserTestUtils.domWindowOpened(null, async domwin => {
await BrowserTestUtils.waitForEvent(domwin, "load");
return domwin.location.toString() == SSB_WINDOW;
});
SiteSpecificBrowserService.launchFromURI(uri);
let ssbwin = await openPromise;
await BrowserTestUtils.browserLoaded(getBrowser(ssbwin), true, uri.spec);
return ssbwin;
}
// Simulates opening a SSB from the main browser window. Resolves to the SSB
// DOM window after the SSB content has loaded.
async function openSSBFromBrowserWindow(win = window) {
@ -34,7 +59,7 @@ async function openSSBFromBrowserWindow(win = window) {
Assert.ok(!openItem.hidden, "Open menu item should not be hidden");
let openPromise = BrowserTestUtils.domWindowOpened(null, async domwin => {
await BrowserTestUtils.waitForEvent(domwin, "load");
await BrowserTestUtils.waitForEvent(domwin, "DOMContentLoaded");
return domwin.location.toString() == SSB_WINDOW;
});
@ -49,3 +74,135 @@ async function openSSBFromBrowserWindow(win = window) {
function getBrowser(ssbwin) {
return ssbwin.document.getElementById("browser");
}
/**
* Waits for a load in response to an attempt to navigate from the SSB. It
* listens for new tab opens in the main window, new window opens and loads in
* the SSB itself. It returns a promise.
*
* The `where` argument is a string saying where the load is expected to
* happen, "ssb", "tab" or "window". The promise rejects if the load happens
* somewhere else and the offending new item (tab or window) get closed. When
* the load is seen in the correct location different things are returned
* depending on `where`. For "tab" the new tab is returned (it will have
* finished loading), for "window" the new window is returned (it will have
* finished loading). The "ssb" case doesn't return anything.
*
* Generally use the methods below this as they look more obvious.
*/
function expectLoadSomewhere(ssb, where, win = window) {
return new Promise((resolve, reject) => {
// Listens for a new tab opening in the main window.
const tabListener = async ({ target: tab }) => {
cleanup();
await BrowserTestUtils.browserLoaded(
tab.linkedBrowser,
true,
uri => uri != "about:blank"
);
if (where != "tab") {
Assert.ok(
false,
`Did not expect ${
tab.linkedBrowser.currentURI.spec
} to load in a new tab.`
);
BrowserTestUtils.removeTab(tab);
reject(new Error("Page unexpectedly loaded in a new tab."));
return;
}
Assert.ok(
true,
`${tab.linkedBrowser.currentURI.spec} loaded in a new tab as expected.`
);
resolve(tab);
};
win.gBrowser.tabContainer.addEventListener("TabOpen", tabListener);
// Listens for new top-level windows.
const winObserver = async (domwin, topic) => {
if (topic != "domwindowopened") {
return;
}
cleanup();
await BrowserTestUtils.waitForEvent(
domwin,
"load",
uri => uri != "about:blank"
);
if (where != "window") {
Assert.ok(false, `Did not expect a new ${domwin.location} to open.`);
await BrowserTestUtils.closeWindow(domwin);
reject(new Error("New window unexpectedly opened."));
return;
}
Assert.ok(true, `${domwin.location} opened as expected.`);
resolve(domwin);
};
Services.ww.registerNotification(winObserver);
BrowserTestUtils.browserLoaded(
getBrowser(ssb),
true,
uri => uri != "about:blank"
).then(() => {
cleanup();
if (where != "ssb") {
Assert.ok(
false,
`Did not expect ${
getBrowser(ssb).currentURI.spec
} to load in the ssb window.`
);
reject(new Error("Page unexpectedly loaded in the ssb window."));
return;
}
Assert.ok(
true,
`${
getBrowser(ssb).currentURI.spec
} loaded in the ssb window as expected.`
);
resolve();
}, reject);
// Makes sure that no notifications fire after the test is done. We assume
// that the SSB window will be closed between tests and so don't need to
// unregister the load listener for the SSB browser itself.
const cleanup = () => {
win.gBrowser.tabContainer.removeEventListener("TabOpen", tabListener);
Services.ww.unregisterNotification(winObserver);
};
});
}
/**
* Waits for a load to occur in the ssb window but rejects if a new tab or
* window get opened.
*/
function expectSSBLoad(ssb, win = window) {
return expectLoadSomewhere(ssb, "ssb", win);
}
/**
* Waits for a new tab to be opened and loaded. Rejects if a new window is
* opened or the ssb loads something before. Resolves with the new loaded tab.
*/
function expectTabLoad(ssb, win = window) {
return expectLoadSomewhere(ssb, "tab", win);
}
/**
* Waits for a new window to be opened and loaded. Rejects if a new tab is
* opened or the ssb loads something before. Resolves with the new loaded
* window.
*/
function expectWindowOpen(ssb, win = window) {
return expectLoadSomewhere(ssb, "window", win);
}

Просмотреть файл

@ -1,6 +1,46 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script type="text/javascript">
function initialize() {
let target = window.location.hash;
if (target.length < 2) {
return;
}
target = target.substring(1);
let anchor = document.getElementById("direct");
anchor.href = target;
anchor = document.getElementById("new-tab");
anchor.href = target;
anchor = document.getElementById("new-window");
anchor.href = target;
anchor = document.getElementById("window-open");
anchor.onclick = (e) => {
window.open(target, "foo", "height=300,width=400");
e.preventDefault();
};
anchor = document.getElementById("window-location");
anchor.onclick = (e) => {
window.location = target;
e.preventDefault();
};
}
window.addEventListener("load", initialize, true);
</script>
</head>
<body>
<p><a id="direct">Direct link</a></p>
<p><a id="new-tab" target="_blank">New tab link</a></p>
<p><a id="new-window" target="foo">New window link</a></p>
<p><a id="window-open" href="#">window.open call</a></p>
<p><a id="window-location" href="#">window.location manipulation</a></p>
</body>
</html>