mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 02:14:43 +00:00
Bug 1240900 - Connect primary browser UI to the viewport. r=ochameau
The primary browser navigational UI should now behave as if it's connected to the page content in the viewport, including things like: * Content page's URL is displayed in location bar * Content page's title is displayed on the tab * Back / forward navigates the viewport * Entering a location navigates the viewport * Page loading progress is displayed in the status bar as usual MozReview-Commit-ID: FzxWEwj13sJ
This commit is contained in:
parent
5345120a69
commit
10cd63b51b
@ -6,4 +6,6 @@
|
||||
|
||||
DevToolsModules(
|
||||
'swap.js',
|
||||
'tunnel.js',
|
||||
'web-navigation.js',
|
||||
)
|
||||
|
@ -6,6 +6,7 @@
|
||||
|
||||
const promise = require("promise");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const { tunnelToInnerBrowser } = require("./tunnel");
|
||||
|
||||
/**
|
||||
* Swap page content from an existing tab into a new browser within a container
|
||||
@ -32,10 +33,14 @@ const { Task } = require("devtools/shared/task");
|
||||
function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
|
||||
let gBrowser = tab.ownerDocument.defaultView.gBrowser;
|
||||
let innerBrowser;
|
||||
let tunnel;
|
||||
|
||||
return {
|
||||
|
||||
start: Task.async(function* () {
|
||||
// Freeze navigation temporarily to avoid "blinking" in the location bar.
|
||||
freezeNavigationState(tab);
|
||||
|
||||
// 1. Create a temporary, hidden tab to load the tool UI.
|
||||
let containerTab = gBrowser.addTab(containerURL, {
|
||||
skipAnimation: true,
|
||||
@ -78,32 +83,47 @@ function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
|
||||
// original browser tab and close the temporary tab used to load the
|
||||
// tool via `swapBrowsersAndCloseOther`.
|
||||
gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
|
||||
|
||||
// 7. Start a tunnel from the tool tab's browser to the viewport browser
|
||||
// so that some browser UI functions, like navigation, are connected to
|
||||
// the content in the viewport, instead of the tool page.
|
||||
tunnel = tunnelToInnerBrowser(tab.linkedBrowser, innerBrowser);
|
||||
yield tunnel.start();
|
||||
|
||||
// Force the browser UI to match the new state of the tab and browser.
|
||||
thawNavigationState(tab);
|
||||
gBrowser.setTabTitle(tab);
|
||||
gBrowser.updateCurrentBrowser(true);
|
||||
}),
|
||||
|
||||
stop() {
|
||||
// 1. Create a temporary, hidden tab to hold the content.
|
||||
// 1. Stop the tunnel between outer and inner browsers.
|
||||
tunnel.stop();
|
||||
tunnel = null;
|
||||
|
||||
// 2. Create a temporary, hidden tab to hold the content.
|
||||
let contentTab = gBrowser.addTab("about:blank", {
|
||||
skipAnimation: true,
|
||||
});
|
||||
gBrowser.hideTab(contentTab);
|
||||
let contentBrowser = contentTab.linkedBrowser;
|
||||
|
||||
// 2. Mark the content tab browser's docshell as active so the frame
|
||||
// 3. Mark the content tab browser's docshell as active so the frame
|
||||
// is created eagerly and will be ready to swap.
|
||||
contentBrowser.docShellIsActive = true;
|
||||
|
||||
// 3. Swap tab content from the browser within the viewport in the tool UI
|
||||
// 4. Swap tab content from the browser within the viewport in the tool UI
|
||||
// to the regular browser tab, preserving all state via
|
||||
// `gBrowser._swapBrowserDocShells`.
|
||||
gBrowser._swapBrowserDocShells(contentTab, innerBrowser);
|
||||
innerBrowser = null;
|
||||
|
||||
// 4. Force the original browser tab to be remote since web content is
|
||||
// 5. Force the original browser tab to be remote since web content is
|
||||
// loaded in the child process, and we're about to swap the content
|
||||
// into this tab.
|
||||
gBrowser.updateBrowserRemoteness(tab.linkedBrowser, true);
|
||||
|
||||
// 5. Swap the content into the original browser tab and close the
|
||||
// 6. Swap the content into the original browser tab and close the
|
||||
// temporary tab used to hold the content via
|
||||
// `swapBrowsersAndCloseOther`.
|
||||
gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
|
||||
@ -113,6 +133,40 @@ function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
|
||||
* location bar, etc. caused by the containerURL peeking through before the swap is
|
||||
* complete.
|
||||
*/
|
||||
const NAVIGATION_PROPERTIES = [
|
||||
"currentURI",
|
||||
"contentTitle",
|
||||
"securityUI",
|
||||
];
|
||||
|
||||
function freezeNavigationState(tab) {
|
||||
// Browser navigation properties we'll freeze temporarily to avoid "blinking" in the
|
||||
// location bar, etc. caused by the containerURL peeking through before the swap is
|
||||
// complete.
|
||||
for (let property of NAVIGATION_PROPERTIES) {
|
||||
let value = tab.linkedBrowser[property];
|
||||
Object.defineProperty(tab.linkedBrowser, property, {
|
||||
get() {
|
||||
return value;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function thawNavigationState(tab) {
|
||||
// Thaw out the properties we froze at the beginning now that the swap is complete.
|
||||
for (let property of NAVIGATION_PROPERTIES) {
|
||||
delete tab.linkedBrowser[property];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
|
||||
* expected to have certain properties that currently exist only on
|
||||
|
401
devtools/client/responsive.html/browser/tunnel.js
Normal file
401
devtools/client/responsive.html/browser/tunnel.js
Normal file
@ -0,0 +1,401 @@
|
||||
/* 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 { Ci } = require("chrome");
|
||||
const Services = require("Services");
|
||||
const { Task } = require("devtools/shared/task");
|
||||
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
|
||||
const { BrowserElementWebNavigation } = require("./web-navigation");
|
||||
|
||||
function debug(msg) {
|
||||
// console.log(msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Properties swapped between browsers by browser.xml's `swapDocShells`. See also the
|
||||
* list at /devtools/client/responsive.html/docs/browser-swap.md.
|
||||
*/
|
||||
const SWAPPED_BROWSER_STATE = [
|
||||
"_securityUI",
|
||||
"_documentURI",
|
||||
"_documentContentType",
|
||||
"_contentTitle",
|
||||
"_characterSet",
|
||||
"_contentPrincipal",
|
||||
"_imageDocument",
|
||||
"_fullZoom",
|
||||
"_textZoom",
|
||||
"_isSyntheticDocument",
|
||||
"_innerWindowID",
|
||||
"_manifestURI",
|
||||
];
|
||||
|
||||
/**
|
||||
* This module takes an "outer" <xul:browser> from a browser tab as described by
|
||||
* Firefox's tabbrowser.xml and wires it up to an "inner" <iframe mozbrowser>
|
||||
* browser element containing arbitrary page content of interest.
|
||||
*
|
||||
* The inner <iframe mozbrowser> element is _just_ the page content. It is not
|
||||
* enough to to replace <xul:browser> on its own. <xul:browser> comes along
|
||||
* with lots of associated functionality via XBL bindings defined for such
|
||||
* elements in browser.xml and remote-browser.xml, and the Firefox UI depends on
|
||||
* these various things to make the UI function.
|
||||
*
|
||||
* By mapping various methods, properties, and messages from the outer browser
|
||||
* to the inner browser, we can control the content inside the inner browser
|
||||
* using the standard Firefox UI elements for navigation, reloading, and more.
|
||||
*
|
||||
* The approaches used in this module were chosen to avoid needing changes to
|
||||
* the core browser for this specialized use case. If we start to increase
|
||||
* usage of <iframe mozbrowser> in the core browser, we should avoid this module
|
||||
* and instead refactor things to work with mozbrowser directly.
|
||||
*
|
||||
* For the moment though, this serves as a sufficient path to connect the
|
||||
* Firefox UI to a mozbrowser.
|
||||
*
|
||||
* @param outer
|
||||
* A <xul:browser> from a regular browser tab.
|
||||
* @param inner
|
||||
* A <iframe mozbrowser> containing page content to be wired up to the
|
||||
* primary browser UI via the outer browser.
|
||||
*/
|
||||
function tunnelToInnerBrowser(outer, inner) {
|
||||
let browserWindow = outer.ownerDocument.defaultView;
|
||||
let gBrowser = browserWindow.gBrowser;
|
||||
let mmTunnel;
|
||||
|
||||
return {
|
||||
|
||||
start: Task.async(function* () {
|
||||
if (outer.isRemoteBrowser) {
|
||||
throw new Error("The outer browser must be non-remote.");
|
||||
}
|
||||
if (!inner.isRemoteBrowser) {
|
||||
throw new Error("The inner browser must be remote.");
|
||||
}
|
||||
|
||||
// The `permanentKey` property on a <xul:browser> is used to index into various maps
|
||||
// held by the session store. When you swap content around with
|
||||
// `_swapBrowserDocShells`, these keys are also swapped so they follow the content.
|
||||
// This means the key that matches the content is on the inner browser. Since we
|
||||
// want the browser UI to believe the page content is part of the outer browser, we
|
||||
// copy the content's `permanentKey` up to the outer browser.
|
||||
copyPermanentKey(outer, inner);
|
||||
|
||||
// Replace the outer browser's native messageManager with a message manager tunnel
|
||||
// which we can use to route messages of interest to the inner browser instead.
|
||||
// Note: The _actual_ messageManager accessible from
|
||||
// `browser.frameLoader.messageManager` is not overridable and is left unchanged.
|
||||
// Only the XBL getter `browser.messageManager` is overridden. Browser UI code
|
||||
// always uses this getter instead of `browser.frameLoader.messageManager` directly,
|
||||
// so this has the effect of overriding the message manager for browser UI code.
|
||||
mmTunnel = new MessageManagerTunnel(outer, inner);
|
||||
Object.defineProperty(outer, "messageManager", {
|
||||
value: mmTunnel,
|
||||
writable: false,
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// We are tunneling to an inner browser with a specific remoteness, so it is simpler
|
||||
// for the logic of the browser UI to assume this tab has taken on that remoteness,
|
||||
// even though it's not true. Since the actions the browser UI performs are sent
|
||||
// down to the inner browser by this tunnel, the tab's remoteness effectively is the
|
||||
// remoteness of the inner browser.
|
||||
Object.defineProperty(outer, "isRemoteBrowser", {
|
||||
get() {
|
||||
return true;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Clear out any cached state that references the current non-remote XBL binding,
|
||||
// such as form fill controllers. Otherwise they will remain in place and leak the
|
||||
// outer docshell.
|
||||
outer.destroy();
|
||||
// The XBL binding for remote browsers uses the message manager for many actions in
|
||||
// the UI and that works well here, since it gives us one main thing we need to
|
||||
// route to the inner browser (the messages), instead of having to tweak many
|
||||
// different browser properties. It is safe to alter a XBL binding dynamically.
|
||||
// The content within is not reloaded.
|
||||
outer.style.MozBinding = "url(chrome://browser/content/tabbrowser.xml" +
|
||||
"#tabbrowser-remote-browser)";
|
||||
|
||||
// The constructor of the new XBL binding is run asynchronously and there is no
|
||||
// event to signal its completion. Spin an event loop to watch for properties that
|
||||
// are set by the contructor.
|
||||
while (!outer._remoteWebNavigation) {
|
||||
Services.tm.currentThread.processNextEvent(true);
|
||||
}
|
||||
|
||||
// Replace the `webNavigation` object with our own version which tries to use
|
||||
// mozbrowser APIs where possible. This replaces the webNavigation object that the
|
||||
// remote-browser.xml binding creates. We do not care about it's original value
|
||||
// because stop() will remove the remote-browser.xml binding and these will no
|
||||
// longer be used.
|
||||
let webNavigation = new BrowserElementWebNavigation(inner);
|
||||
webNavigation.copyStateFrom(inner._remoteWebNavigationImpl);
|
||||
outer._remoteWebNavigation = webNavigation;
|
||||
outer._remoteWebNavigationImpl = webNavigation;
|
||||
|
||||
// Now that we've flipped to the remote browser XBL binding, add `progressListener`
|
||||
// onto the remote version of `webProgress`. Normally tabbrowser.xml does this step
|
||||
// when it creates a new browser, etc. Since we manually changed the XBL binding
|
||||
// above, it caused a fresh webProgress object to be created which does not have any
|
||||
// listeners added. So, we get the listener that gBrowser is using for the tab and
|
||||
// reattach it here.
|
||||
let tab = gBrowser.getTabForBrowser(outer);
|
||||
let filteredProgressListener = gBrowser._tabFilters.get(tab);
|
||||
outer.webProgress.addProgressListener(filteredProgressListener);
|
||||
|
||||
// All of the browser state from content was swapped onto the inner browser. Pull
|
||||
// this state up to the outer browser.
|
||||
for (let property of SWAPPED_BROWSER_STATE) {
|
||||
outer[property] = inner[property];
|
||||
}
|
||||
|
||||
// Wants to access the content's `frameLoader`, so we'll redirect it to
|
||||
// inner browser.
|
||||
Object.defineProperty(outer, "hasContentOpener", {
|
||||
get() {
|
||||
return inner.frameLoader.tabParent.hasContentOpener;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Wants to access the content's `frameLoader`, so we'll redirect it to
|
||||
// inner browser.
|
||||
Object.defineProperty(outer, "docShellIsActive", {
|
||||
get() {
|
||||
return inner.frameLoader.tabParent.docShellIsActive;
|
||||
},
|
||||
set(value) {
|
||||
inner.frameLoader.tabParent.docShellIsActive = value;
|
||||
},
|
||||
configurable: true,
|
||||
enumerable: true,
|
||||
});
|
||||
|
||||
// Wants to access the content's `frameLoader`, so we'll redirect it to
|
||||
// inner browser.
|
||||
outer.setDocShellIsActiveAndForeground = value => {
|
||||
inner.frameLoader.tabParent.setDocShellIsActiveAndForeground(value);
|
||||
};
|
||||
}),
|
||||
|
||||
stop() {
|
||||
let tab = gBrowser.getTabForBrowser(outer);
|
||||
let filteredProgressListener = gBrowser._tabFilters.get(tab);
|
||||
browserWindow = null;
|
||||
gBrowser = null;
|
||||
|
||||
// The browser's state has changed over time while the tunnel was active. Push the
|
||||
// the current state down to the inner browser, so that it follows the content in
|
||||
// case that browser will be swapped elsewhere.
|
||||
for (let property of SWAPPED_BROWSER_STATE) {
|
||||
inner[property] = outer[property];
|
||||
}
|
||||
|
||||
// Remove the progress listener we added manually.
|
||||
outer.webProgress.removeProgressListener(filteredProgressListener);
|
||||
|
||||
// Reset the XBL binding back to the default.
|
||||
outer.destroy();
|
||||
outer.style.MozBinding = "";
|
||||
|
||||
// Reset overridden XBL properties and methods. Deleting the override
|
||||
// means it will fallback to the original XBL binding definitions which
|
||||
// are on the prototype.
|
||||
delete outer.messageManager;
|
||||
delete outer.isRemoteBrowser;
|
||||
delete outer.hasContentOpener;
|
||||
delete outer.docShellIsActive;
|
||||
delete outer.setDocShellIsActiveAndForeground;
|
||||
|
||||
mmTunnel.destroy();
|
||||
mmTunnel = null;
|
||||
|
||||
// Invalidate outer's permanentKey so that SessionStore stops associating
|
||||
// things that happen to the outer browser with the content inside in the
|
||||
// inner browser.
|
||||
outer.permanentKey = { id: "zombie" };
|
||||
},
|
||||
|
||||
};
|
||||
}
|
||||
|
||||
exports.tunnelToInnerBrowser = tunnelToInnerBrowser;
|
||||
|
||||
function copyPermanentKey(outer, inner) {
|
||||
// When we're in the process of swapping content around, we end up receiving a
|
||||
// SessionStore:update message which lists the container page that is loaded into the
|
||||
// outer browser (that we're hiding the inner browser within) as part of its history.
|
||||
// We want SessionStore's view of the history for our tab to only have the page content
|
||||
// of the inner browser, so we want to hide this message from SessionStore, but we have
|
||||
// no direct mechanism to do so. As a workaround, we wait until the one errant message
|
||||
// has gone by, and then we copy the permanentKey after that, since the permanentKey is
|
||||
// what SessionStore uses to identify each browser.
|
||||
let outerMM = outer.frameLoader.messageManager;
|
||||
let onHistoryEntry = message => {
|
||||
let history = message.data.data.history;
|
||||
if (!history || !history.entries) {
|
||||
// Wait for a message that contains history data
|
||||
return;
|
||||
}
|
||||
outerMM.removeMessageListener("SessionStore:update", onHistoryEntry);
|
||||
debug("Got session update for outer browser");
|
||||
DevToolsUtils.executeSoon(() => {
|
||||
debug("Copy inner permanentKey to outer browser");
|
||||
outer.permanentKey = inner.permanentKey;
|
||||
});
|
||||
};
|
||||
outerMM.addMessageListener("SessionStore:update", onHistoryEntry);
|
||||
}
|
||||
|
||||
/**
|
||||
* This module allows specific messages of interest to be directed from the
|
||||
* outer browser to the inner browser (and vice versa) in a targetted fashion
|
||||
* without having to touch the original code paths that use them.
|
||||
*/
|
||||
function MessageManagerTunnel(outer, inner) {
|
||||
if (outer.isRemoteBrowser) {
|
||||
throw new Error("The outer browser must be non-remote.");
|
||||
}
|
||||
this.outer = outer;
|
||||
this.inner = inner;
|
||||
this.init();
|
||||
}
|
||||
|
||||
MessageManagerTunnel.prototype = {
|
||||
|
||||
/**
|
||||
* Most message manager methods are left alone and are just passed along to
|
||||
* the outer browser's real message manager. `sendAsyncMessage` is only one
|
||||
* with special behavior.
|
||||
*/
|
||||
PASS_THROUGH_METHODS: [
|
||||
"addMessageListener",
|
||||
"loadFrameScript",
|
||||
"killChild",
|
||||
"assertPermission",
|
||||
"assertContainApp",
|
||||
"assertAppHasPermission",
|
||||
"assertAppHasStatus",
|
||||
"removeDelayedFrameScript",
|
||||
"getDelayedFrameScripts",
|
||||
"loadProcessScript",
|
||||
"removeDelayedProcessScript",
|
||||
"getDelayedProcessScripts",
|
||||
"removeMessageListener",
|
||||
"addWeakMessageListener",
|
||||
"removeWeakMessageListener",
|
||||
],
|
||||
|
||||
OUTER_TO_INNER_MESSAGES: [
|
||||
// Messages sent from remote-browser.xml
|
||||
"Browser:PurgeSessionHistory",
|
||||
"InPermitUnload",
|
||||
"PermitUnload",
|
||||
// Messages sent from browser.js
|
||||
"Browser:Reload",
|
||||
// Messages sent from SelectParentHelper.jsm
|
||||
"Forms:DismissedDropDown",
|
||||
"Forms:MouseOut",
|
||||
"Forms:MouseOver",
|
||||
"Forms:SelectDropDownItem",
|
||||
// Messages sent from SessionStore.jsm
|
||||
"SessionStore:flush",
|
||||
],
|
||||
|
||||
INNER_TO_OUTER_MESSAGES: [
|
||||
// Messages sent to RemoteWebProgress.jsm
|
||||
"Content:LoadURIResult",
|
||||
"Content:LocationChange",
|
||||
"Content:ProgressChange",
|
||||
"Content:SecurityChange",
|
||||
"Content:StateChange",
|
||||
"Content:StatusChange",
|
||||
// Messages sent to remote-browser.xml
|
||||
"DOMTitleChanged",
|
||||
"ImageDocumentLoaded",
|
||||
"Forms:ShowDropDown",
|
||||
"Forms:HideDropDown",
|
||||
"InPermitUnload",
|
||||
"PermitUnload",
|
||||
// Messages sent to SelectParentHelper.jsm
|
||||
"Forms:UpdateDropDown",
|
||||
// Messages sent to browser.js
|
||||
"PageVisibility:Hide",
|
||||
"PageVisibility:Show",
|
||||
// Messages sent to SessionStore.jsm
|
||||
"SessionStore:update",
|
||||
// Messages sent to BrowserTestUtils.jsm
|
||||
"browser-test-utils:loadEvent",
|
||||
],
|
||||
|
||||
get outerParentMM() {
|
||||
return this.outer.frameLoader.messageManager;
|
||||
},
|
||||
|
||||
get outerChildMM() {
|
||||
// This is only possible because we require the outer browser to be
|
||||
// non-remote, so we're able to reach into its window and use the child
|
||||
// side message manager there.
|
||||
let docShell = this.outer.frameLoader.docShell;
|
||||
return docShell.QueryInterface(Ci.nsIInterfaceRequestor)
|
||||
.getInterface(Ci.nsIContentFrameMessageManager);
|
||||
},
|
||||
|
||||
get innerParentMM() {
|
||||
return this.inner.frameLoader.messageManager;
|
||||
},
|
||||
|
||||
sendAsyncMessage(name, ...args) {
|
||||
debug(`Calling sendAsyncMessage for ${name}`);
|
||||
|
||||
if (!this.OUTER_TO_INNER_MESSAGES.includes(name)) {
|
||||
debug(`Should ${name} go to inner?`);
|
||||
this.outerParentMM.sendAsyncMessage(name, ...args);
|
||||
return;
|
||||
}
|
||||
|
||||
debug(`${name} outer -> inner`);
|
||||
this.innerParentMM.sendAsyncMessage(name, ...args);
|
||||
},
|
||||
|
||||
init() {
|
||||
for (let method of this.PASS_THROUGH_METHODS) {
|
||||
// Workaround bug 449811 to ensure a fresh binding each time through the loop
|
||||
let _method = method;
|
||||
this[_method] = (...args) => {
|
||||
return this.outerParentMM[_method](...args);
|
||||
};
|
||||
}
|
||||
|
||||
for (let message of this.INNER_TO_OUTER_MESSAGES) {
|
||||
this.innerParentMM.addMessageListener(message, this);
|
||||
}
|
||||
},
|
||||
|
||||
destroy() {
|
||||
for (let message of this.INNER_TO_OUTER_MESSAGES) {
|
||||
this.innerParentMM.removeMessageListener(message, this);
|
||||
}
|
||||
},
|
||||
|
||||
receiveMessage({ name, data, objects, principal }) {
|
||||
if (!this.INNER_TO_OUTER_MESSAGES.includes(name)) {
|
||||
debug(`Received unexpected message ${name}`);
|
||||
return;
|
||||
}
|
||||
|
||||
debug(`${name} inner -> outer`);
|
||||
this.outerChildMM.sendAsyncMessage(name, data, objects, principal);
|
||||
},
|
||||
|
||||
};
|
179
devtools/client/responsive.html/browser/web-navigation.js
Normal file
179
devtools/client/responsive.html/browser/web-navigation.js
Normal file
@ -0,0 +1,179 @@
|
||||
/* 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 { Ci, Cu, Cr } = require("chrome");
|
||||
const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
|
||||
const Services = require("Services");
|
||||
const { NetUtil } = require("resource://gre/modules/NetUtil.jsm");
|
||||
|
||||
function readInputStreamToString(stream) {
|
||||
return NetUtil.readInputStreamToString(stream, stream.available());
|
||||
}
|
||||
|
||||
/**
|
||||
* This object aims to provide the nsIWebNavigation interface for mozbrowser elements.
|
||||
* nsIWebNavigation is one of the interfaces expected on <xul:browser>s, so this wrapper
|
||||
* helps mozbrowser elements support this.
|
||||
*
|
||||
* It attempts to use the mozbrowser API wherever possible, however some methods don't
|
||||
* exist yet, so we fallback to the WebNavigation frame script messages in those cases.
|
||||
* Ideally the mozbrowser API would eventually be extended to cover all properties and
|
||||
* methods used here.
|
||||
*
|
||||
* This is largely copied from RemoteWebNavigation.js, which uses the message manager to
|
||||
* perform all actions.
|
||||
*/
|
||||
function BrowserElementWebNavigation(browser) {
|
||||
this._browser = browser;
|
||||
}
|
||||
|
||||
BrowserElementWebNavigation.prototype = {
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([
|
||||
Ci.nsIWebNavigation,
|
||||
Ci.nsISupports
|
||||
]),
|
||||
|
||||
get _mm() {
|
||||
return this._browser.frameLoader.messageManager;
|
||||
},
|
||||
|
||||
canGoBack: false,
|
||||
canGoForward: false,
|
||||
|
||||
goBack() {
|
||||
this._browser.goBack();
|
||||
},
|
||||
|
||||
goForward() {
|
||||
this._browser.goForward();
|
||||
},
|
||||
|
||||
gotoIndex(index) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this._sendMessage("WebNavigation:GotoIndex", { index });
|
||||
},
|
||||
|
||||
loadURI(uri, flags, referrer, postData, headers) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this.loadURIWithOptions(uri, flags, referrer,
|
||||
Ci.nsIHttpChannel.REFERRER_POLICY_DEFAULT,
|
||||
postData, headers, null);
|
||||
},
|
||||
|
||||
loadURIWithOptions(uri, flags, referrer, referrerPolicy, postData, headers,
|
||||
baseURI) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this._sendMessage("WebNavigation:LoadURI", {
|
||||
uri,
|
||||
flags,
|
||||
referrer: referrer ? referrer.spec : null,
|
||||
referrerPolicy: referrerPolicy,
|
||||
postData: postData ? readInputStreamToString(postData) : null,
|
||||
headers: headers ? readInputStreamToString(headers) : null,
|
||||
baseURI: baseURI ? baseURI.spec : null,
|
||||
});
|
||||
},
|
||||
|
||||
setOriginAttributesBeforeLoading(originAttributes) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this._sendMessage("WebNavigation:SetOriginAttributes", {
|
||||
originAttributes,
|
||||
});
|
||||
},
|
||||
|
||||
reload(flags) {
|
||||
let hardReload = false;
|
||||
if (flags & this.LOAD_FLAGS_BYPASS_PROXY ||
|
||||
flags & this.LOAD_FLAGS_BYPASS_CACHE) {
|
||||
hardReload = true;
|
||||
}
|
||||
this._browser.reload(hardReload);
|
||||
},
|
||||
|
||||
stop(flags) {
|
||||
// No equivalent in the current BrowserElement API
|
||||
this._sendMessage("WebNavigation:Stop", { flags });
|
||||
},
|
||||
|
||||
get document() {
|
||||
return this._browser.contentDocument;
|
||||
},
|
||||
|
||||
_currentURI: null,
|
||||
get currentURI() {
|
||||
if (!this._currentURI) {
|
||||
this._currentURI = Services.io.newURI("about:blank", null, null);
|
||||
}
|
||||
return this._currentURI;
|
||||
},
|
||||
set currentURI(uri) {
|
||||
this._browser.src = uri.spec;
|
||||
},
|
||||
|
||||
referringURI: null,
|
||||
|
||||
// Bug 1233803 - accessing the sessionHistory of remote browsers should be
|
||||
// done in content scripts.
|
||||
get sessionHistory() {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
set sessionHistory(value) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
|
||||
_sendMessage(message, data) {
|
||||
try {
|
||||
this._mm.sendAsyncMessage(message, data);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
},
|
||||
|
||||
swapBrowser(browser) {
|
||||
throw Cr.NS_ERROR_NOT_IMPLEMENTED;
|
||||
},
|
||||
|
||||
copyStateFrom(otherWebNavigation) {
|
||||
const state = [
|
||||
"canGoBack",
|
||||
"canGoForward",
|
||||
"_currentURI",
|
||||
];
|
||||
for (let property of state) {
|
||||
this[property] = otherWebNavigation[property];
|
||||
}
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
const FLAGS = [
|
||||
"LOAD_FLAGS_MASK",
|
||||
"LOAD_FLAGS_NONE",
|
||||
"LOAD_FLAGS_IS_REFRESH",
|
||||
"LOAD_FLAGS_IS_LINK",
|
||||
"LOAD_FLAGS_BYPASS_HISTORY",
|
||||
"LOAD_FLAGS_REPLACE_HISTORY",
|
||||
"LOAD_FLAGS_BYPASS_CACHE",
|
||||
"LOAD_FLAGS_BYPASS_PROXY",
|
||||
"LOAD_FLAGS_CHARSET_CHANGE",
|
||||
"LOAD_FLAGS_STOP_CONTENT",
|
||||
"LOAD_FLAGS_FROM_EXTERNAL",
|
||||
"LOAD_FLAGS_ALLOW_THIRD_PARTY_FIXUP",
|
||||
"LOAD_FLAGS_FIRST_LOAD",
|
||||
"LOAD_FLAGS_ALLOW_POPUPS",
|
||||
"LOAD_FLAGS_BYPASS_CLASSIFIER",
|
||||
"LOAD_FLAGS_FORCE_ALLOW_COOKIES",
|
||||
"STOP_NETWORK",
|
||||
"STOP_CONTENT",
|
||||
"STOP_ALL",
|
||||
];
|
||||
|
||||
for (let flag of FLAGS) {
|
||||
BrowserElementWebNavigation.prototype[flag] = Ci.nsIWebNavigation[flag];
|
||||
}
|
||||
|
||||
exports.BrowserElementWebNavigation = BrowserElementWebNavigation;
|
@ -19,6 +19,7 @@ support-files =
|
||||
[browser_menu_item_01.js]
|
||||
[browser_menu_item_02.js]
|
||||
[browser_mouse_resize.js]
|
||||
[browser_navigation.js]
|
||||
[browser_page_state.js]
|
||||
[browser_resize_cmd.js]
|
||||
skip-if = true # GCLI target confused after swap, will fix in bug 1240907
|
||||
|
@ -0,0 +1,98 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
// Test the primary browser navigation UI to verify it's connected to the viewport.
|
||||
|
||||
const DUMMY_1_URL = "http://example.com/";
|
||||
const TEST_URL = `${URL_ROOT}doc_page_state.html`;
|
||||
const DUMMY_2_URL = "http://example.com/browser/";
|
||||
const DUMMY_3_URL = "http://example.com/browser/devtools/";
|
||||
|
||||
add_task(function* () {
|
||||
// Load up a sequence of pages:
|
||||
// 0. DUMMY_1_URL
|
||||
// 1. TEST_URL
|
||||
// 2. DUMMY_2_URL
|
||||
let tab = yield addTab(DUMMY_1_URL);
|
||||
let browser = tab.linkedBrowser;
|
||||
yield load(browser, TEST_URL);
|
||||
yield load(browser, DUMMY_2_URL);
|
||||
|
||||
// Check session history state
|
||||
let history = yield getSessionHistory(browser);
|
||||
is(history.index, 2, "At page 2 in history");
|
||||
is(history.entries.length, 3, "3 pages in history");
|
||||
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
|
||||
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
|
||||
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
|
||||
|
||||
// Go back one so we're at the test page
|
||||
yield back(browser);
|
||||
|
||||
// Check session history state
|
||||
history = yield getSessionHistory(browser);
|
||||
is(history.index, 1, "At page 1 in history");
|
||||
is(history.entries.length, 3, "3 pages in history");
|
||||
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
|
||||
is(history.entries[1].uri, TEST_URL, "Page 1 URL matches");
|
||||
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
|
||||
|
||||
yield openRDM(tab);
|
||||
|
||||
ok(browser.webNavigation.canGoBack, "Going back is allowed");
|
||||
ok(browser.webNavigation.canGoForward, "Going forward is allowed");
|
||||
is(browser.documentURI.spec, TEST_URL, "documentURI matches page 1");
|
||||
is(browser.contentTitle, "Page State Test", "contentTitle matches page 1");
|
||||
|
||||
yield forward(browser);
|
||||
|
||||
ok(browser.webNavigation.canGoBack, "Going back is allowed");
|
||||
ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
|
||||
is(browser.documentURI.spec, DUMMY_2_URL, "documentURI matches page 2");
|
||||
is(browser.contentTitle, "mochitest index /browser/", "contentTitle matches page 2");
|
||||
|
||||
yield back(browser);
|
||||
yield back(browser);
|
||||
|
||||
ok(!browser.webNavigation.canGoBack, "Going back is not allowed");
|
||||
ok(browser.webNavigation.canGoForward, "Going forward is allowed");
|
||||
is(browser.documentURI.spec, DUMMY_1_URL, "documentURI matches page 0");
|
||||
is(browser.contentTitle, "mochitest index /", "contentTitle matches page 0");
|
||||
|
||||
let receivedStatusChanges = new Promise(resolve => {
|
||||
let statusChangesSeen = 0;
|
||||
let statusChangesExpected = 2;
|
||||
let progressListener = {
|
||||
onStatusChange(webProgress, request, status, message) {
|
||||
info(message);
|
||||
if (++statusChangesSeen == statusChangesExpected) {
|
||||
gBrowser.removeProgressListener(progressListener);
|
||||
ok(true, `${statusChangesExpected} status changes while loading`);
|
||||
resolve();
|
||||
}
|
||||
}
|
||||
};
|
||||
gBrowser.addProgressListener(progressListener);
|
||||
});
|
||||
yield load(browser, DUMMY_3_URL);
|
||||
yield receivedStatusChanges;
|
||||
|
||||
ok(browser.webNavigation.canGoBack, "Going back is allowed");
|
||||
ok(!browser.webNavigation.canGoForward, "Going forward is not allowed");
|
||||
is(browser.documentURI.spec, DUMMY_3_URL, "documentURI matches page 3");
|
||||
is(browser.contentTitle, "mochitest index /browser/devtools/",
|
||||
"contentTitle matches page 3");
|
||||
|
||||
yield closeRDM(tab);
|
||||
|
||||
// Check session history state
|
||||
history = yield getSessionHistory(browser);
|
||||
is(history.index, 1, "At page 1 in history");
|
||||
is(history.entries.length, 2, "2 pages in history");
|
||||
is(history.entries[0].uri, DUMMY_1_URL, "Page 0 URL matches");
|
||||
is(history.entries[1].uri, DUMMY_3_URL, "Page 1 URL matches");
|
||||
|
||||
yield removeTab(tab);
|
||||
});
|
@ -17,14 +17,8 @@ add_task(function* () {
|
||||
// 2. DUMMY_2_URL
|
||||
let tab = yield addTab(DUMMY_1_URL);
|
||||
let browser = tab.linkedBrowser;
|
||||
|
||||
let loaded = BrowserTestUtils.browserLoaded(browser, false, TEST_URL);
|
||||
browser.loadURI(TEST_URL, null, null);
|
||||
yield loaded;
|
||||
|
||||
loaded = BrowserTestUtils.browserLoaded(browser, false, DUMMY_2_URL);
|
||||
browser.loadURI(DUMMY_2_URL, null, null);
|
||||
yield loaded;
|
||||
yield load(browser, TEST_URL);
|
||||
yield load(browser, DUMMY_2_URL);
|
||||
|
||||
// Check session history state
|
||||
let history = yield getSessionHistory(browser);
|
||||
@ -35,9 +29,7 @@ add_task(function* () {
|
||||
is(history.entries[2].uri, DUMMY_2_URL, "Page 2 URL matches");
|
||||
|
||||
// Go back one so we're at the test page
|
||||
let shown = waitForPageShow(browser);
|
||||
browser.goBack();
|
||||
yield shown;
|
||||
yield back(browser);
|
||||
|
||||
// Check session history state
|
||||
history = yield getSessionHistory(browser);
|
||||
@ -82,41 +74,3 @@ add_task(function* () {
|
||||
|
||||
yield removeTab(tab);
|
||||
});
|
||||
|
||||
function getSessionHistory(browser) {
|
||||
return ContentTask.spawn(browser, {}, function* () {
|
||||
/* eslint-disable no-undef */
|
||||
let { interfaces: Ci } = Components;
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let sessionHistory = webNav.sessionHistory;
|
||||
let result = {
|
||||
index: sessionHistory.index,
|
||||
entries: []
|
||||
};
|
||||
|
||||
for (let i = 0; i < sessionHistory.count; i++) {
|
||||
let entry = sessionHistory.getEntryAtIndex(i, false);
|
||||
result.entries.push({
|
||||
uri: entry.URI.spec,
|
||||
title: entry.title
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
}
|
||||
|
||||
function waitForPageShow(browser) {
|
||||
let mm = browser.messageManager;
|
||||
return new Promise(resolve => {
|
||||
let onShow = message => {
|
||||
if (message.target != browser) {
|
||||
return;
|
||||
}
|
||||
mm.removeMessageListener("PageVisibility:Show", onShow);
|
||||
resolve();
|
||||
};
|
||||
mm.addMessageListener("PageVisibility:Show", onShow);
|
||||
});
|
||||
}
|
||||
|
@ -1,13 +1,16 @@
|
||||
<!doctype html>
|
||||
<html>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
background: red;
|
||||
}
|
||||
body.modified {
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
<head>
|
||||
<title>Page State Test</title>
|
||||
<style>
|
||||
body {
|
||||
height: 100vh;
|
||||
background: red;
|
||||
}
|
||||
body.modified {
|
||||
background: green;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body onclick="this.classList.add('modified')"/>
|
||||
</html>
|
||||
|
@ -153,3 +153,59 @@ function openDeviceModal(ui) {
|
||||
ok(!modal.classList.contains("hidden"),
|
||||
"The device modal is displayed.");
|
||||
}
|
||||
|
||||
function getSessionHistory(browser) {
|
||||
return ContentTask.spawn(browser, {}, function* () {
|
||||
/* eslint-disable no-undef */
|
||||
let { interfaces: Ci } = Components;
|
||||
let webNav = docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
let sessionHistory = webNav.sessionHistory;
|
||||
let result = {
|
||||
index: sessionHistory.index,
|
||||
entries: []
|
||||
};
|
||||
|
||||
for (let i = 0; i < sessionHistory.count; i++) {
|
||||
let entry = sessionHistory.getEntryAtIndex(i, false);
|
||||
result.entries.push({
|
||||
uri: entry.URI.spec,
|
||||
title: entry.title
|
||||
});
|
||||
}
|
||||
|
||||
return result;
|
||||
/* eslint-enable no-undef */
|
||||
});
|
||||
}
|
||||
|
||||
function waitForPageShow(browser) {
|
||||
let mm = browser.messageManager;
|
||||
return new Promise(resolve => {
|
||||
let onShow = message => {
|
||||
if (message.target != browser) {
|
||||
return;
|
||||
}
|
||||
mm.removeMessageListener("PageVisibility:Show", onShow);
|
||||
resolve();
|
||||
};
|
||||
mm.addMessageListener("PageVisibility:Show", onShow);
|
||||
});
|
||||
}
|
||||
|
||||
function load(browser, url) {
|
||||
let loaded = BrowserTestUtils.browserLoaded(browser, false, url);
|
||||
browser.loadURI(url, null, null);
|
||||
return loaded;
|
||||
}
|
||||
|
||||
function back(browser) {
|
||||
let shown = waitForPageShow(browser);
|
||||
browser.goBack();
|
||||
return shown;
|
||||
}
|
||||
|
||||
function forward(browser) {
|
||||
let shown = waitForPageShow(browser);
|
||||
browser.goForward();
|
||||
return shown;
|
||||
}
|
||||
|
@ -33,21 +33,25 @@ extra state that may be set on tab by add-ons or others.
|
||||
6. Swap the tool UI (with viewport showing the content) into the original
|
||||
browser tab and close the temporary tab used to load the tool via
|
||||
`swapBrowsersAndCloseOther`.
|
||||
7. Start a tunnel from the tool tab's browser to the viewport browser
|
||||
so that some browser UI functions, like navigation, are connected to
|
||||
the content in the viewport, instead of the tool page.
|
||||
|
||||
## Closing RDM During Current Firefox Session
|
||||
|
||||
To close RDM, we follow a similar process to the one from opening RDM so we can
|
||||
restore the content back to a normal tab.
|
||||
|
||||
1. Create a temporary, hidden tab to hold the content.
|
||||
2. Mark the content tab browser's docshell as active so the frame is created
|
||||
1. Stop the tunnel between outer and inner browsers.
|
||||
2. Create a temporary, hidden tab to hold the content.
|
||||
3. Mark the content tab browser's docshell as active so the frame is created
|
||||
eagerly and will be ready to swap.
|
||||
3. Swap tab content from the browser within the viewport in the tool UI to the
|
||||
4. Swap tab content from the browser within the viewport in the tool UI to the
|
||||
regular browser tab, preserving all state via
|
||||
`gBrowser._swapBrowserDocShells`.
|
||||
4. Force the original browser tab to be remote since web content is loaded in
|
||||
5. Force the original browser tab to be remote since web content is loaded in
|
||||
the child process, and we're about to swap the content into this tab.
|
||||
5. Swap the content into the original browser tab and close the temporary tab
|
||||
6. Swap the content into the original browser tab and close the temporary tab
|
||||
used to hold the content via `swapBrowsersAndCloseOther`.
|
||||
|
||||
## Session Restore
|
||||
|
Loading…
Reference in New Issue
Block a user