new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/browser/moz.build
@@ -0,0 +1,9 @@
+# -*- Mode: python; c-basic-offset: 4; indent-tabs-mode: nil; tab-width: 40 -*-
+# vim: set filetype=python:
+# 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/.
+
+DevToolsModules(
+ 'swap.js',
+)
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -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 promise = require("promise");
+const { Task } = require("resource://gre/modules/Task.jsm");
+
+/**
+ * Swap page content from an existing tab into a new browser within a container
+ * page. Page state is preserved by using `swapFrameLoaders`, just like when
+ * you move a tab to a new window. This provides a seamless transition for the
+ * user since the page is not reloaded.
+ *
+ * See /devtools/docs/responsive-design-mode.md for a high level overview of how
+ * this is used in RDM. The steps described there are copied into the code
+ * below.
+ *
+ * For additional low level details about swapping browser content,
+ * see /devtools/client/responsive.html/docs/browser-swap.md.
+ *
+ * @param tab
+ * A browser tab with content to be swapped.
+ * @param containerURL
+ * URL to a page that holds an inner browser.
+ * @param getInnerBrowser
+ * Function that returns a Promise to the inner browser within the
+ * container page. It is called with the outer browser that loaded the
+ * container page.
+ */
+function swapToInnerBrowser({ tab, containerURL, getInnerBrowser }) {
+ let gBrowser = tab.ownerDocument.defaultView.gBrowser;
+ let innerBrowser;
+
+ return {
+
+ start: Task.async(function* () {
+ // 1. Create a temporary, hidden tab to load the tool UI.
+ let containerTab = gBrowser.addTab(containerURL, {
+ skipAnimation: true,
+ });
+ gBrowser.hideTab(containerTab);
+ let containerBrowser = containerTab.linkedBrowser;
+
+ // 2. Mark the tool tab browser's docshell as active so the viewport frame
+ // is created eagerly and will be ready to swap.
+ // This line is crucial when the tool UI is loaded into a background tab.
+ // Without it, the viewport browser's frame is created lazily, leading to
+ // a multi-second delay before it would be possible to `swapFrameLoaders`.
+ // Even worse than the delay, there appears to be no obvious event fired
+ // after the frame is set lazily, so it's unclear how to know that work
+ // has finished.
+ containerBrowser.docShellIsActive = true;
+
+ // 3. Create the initial viewport inside the tool UI.
+ // The calling application will use container page loaded into the tab to
+ // do whatever it needs to create the inner browser.
+ yield tabLoaded(containerTab);
+ innerBrowser = yield getInnerBrowser(containerBrowser);
+ addXULBrowserDecorations(innerBrowser);
+ if (innerBrowser.isRemoteBrowser != tab.linkedBrowser.isRemoteBrowser) {
+ throw new Error("The inner browser's remoteness must match the " +
+ "original tab.");
+ }
+
+ // 4. Swap tab content from the regular browser tab to the browser within
+ // the viewport in the tool UI, preserving all state via
+ // `gBrowser._swapBrowserDocShells`.
+ gBrowser._swapBrowserDocShells(tab, innerBrowser);
+
+ // 5. Force the original browser tab to be non-remote since the tool UI
+ // must be loaded in the parent process, and we're about to swap the
+ // tool UI into this tab.
+ gBrowser.updateBrowserRemoteness(tab.linkedBrowser, false);
+
+ // 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`.
+ gBrowser.swapBrowsersAndCloseOther(tab, containerTab);
+ }),
+
+ stop() {
+ // 1. 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
+ // 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
+ // 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
+ // 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
+ // temporary tab used to hold the content via
+ // `swapBrowsersAndCloseOther`.
+ gBrowser.swapBrowsersAndCloseOther(tab, contentTab);
+ gBrowser = null;
+ },
+
+ };
+}
+
+/**
+ * Browser elements that are passed to `gBrowser._swapBrowserDocShells` are
+ * expected to have certain properties that currently exist only on
+ * <xul:browser> elements. In particular, <iframe mozbrowser> elements don't
+ * have them.
+ *
+ * Rather than duplicate the swapping code used by the browser to work around
+ * this, we stub out the missing properties needed for the swap to complete.
+ */
+function addXULBrowserDecorations(browser) {
+ if (browser.isRemoteBrowser == undefined) {
+ Object.defineProperty(browser, "isRemoteBrowser", {
+ get() {
+ return this.getAttribute("remote") == "true";
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+ if (browser.messageManager == undefined) {
+ Object.defineProperty(browser, "messageManager", {
+ get() {
+ return this.frameLoader.messageManager;
+ },
+ configurable: true,
+ enumerable: true,
+ });
+ }
+
+ // It's not necessary for these to actually do anything. These properties are
+ // swapped between browsers in browser.xml's `swapDocShells`, and then their
+ // `swapBrowser` methods are called, so we define them here for that to work
+ // without errors. During the swap process above, these will move from the
+ // the new inner browser to the original tab's browser (step 4) and then to
+ // the temporary container tab's browser (step 7), which is then closed.
+ if (browser._remoteWebNavigationImpl == undefined) {
+ browser._remoteWebNavigationImpl = {
+ swapBrowser() {},
+ };
+ }
+ if (browser._remoteWebProgressManager == undefined) {
+ browser._remoteWebProgressManager = {
+ swapBrowser() {},
+ };
+ }
+}
+
+function tabLoaded(tab) {
+ let deferred = promise.defer();
+
+ function handle(event) {
+ if (event.originalTarget != tab.linkedBrowser.contentDocument ||
+ event.target.location.href == "about:blank") {
+ return;
+ }
+ tab.linkedBrowser.removeEventListener("load", handle, true);
+ deferred.resolve(event);
+ }
+
+ tab.linkedBrowser.addEventListener("load", handle, true);
+ return deferred.promise;
+}
+
+exports.swapToInnerBrowser = swapToInnerBrowser;
--- a/devtools/client/responsive.html/components/browser.js
+++ b/devtools/client/responsive.html/components/browser.js
@@ -9,39 +9,76 @@
const { Task } = require("resource://gre/modules/Task.jsm");
const DevToolsUtils = require("devtools/shared/DevToolsUtils");
const { getToplevelWindow } = require("sdk/window/utils");
const { DOM: dom, createClass, addons, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const e10s = require("../utils/e10s");
+const message = require("../utils/message");
module.exports = createClass({
+
/**
* This component is not allowed to depend directly on frequently changing
* data (width, height) due to the use of `dangerouslySetInnerHTML` below.
* Any changes in props will cause the <iframe> to be removed and added again,
* throwing away the current state of the page.
*/
propTypes: {
location: Types.location.isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
},
displayName: "Browser",
mixins: [ addons.PureRenderMixin ],
/**
* Once the browser element has mounted, load the frame script and enable
* various features, like floating scrollbars.
*/
componentDidMount: Task.async(function* () {
+ // If we are not swapping browsers after mount, it's safe to start the frame
+ // script now.
+ if (!this.props.swapAfterMount) {
+ yield this.startFrameScript();
+ }
+
+ // Notify manager.js that this browser has mounted, so that it can trigger
+ // a swap if needed and continue with the rest of its startup.
+ this.props.onBrowserMounted();
+
+ // If we are swapping browsers after mount, wait for the swap to complete
+ // and start the frame script after that.
+ if (this.props.swapAfterMount) {
+ yield message.wait(window, "start-frame-script");
+ yield this.startFrameScript();
+ message.post(window, "start-frame-script:done");
+ }
+
+ // Stop the frame script when requested in the future.
+ message.wait(window, "stop-frame-script").then(() => {
+ this.stopFrameScript();
+ });
+ }),
+
+ onContentResize(msg) {
+ let { onContentResize } = this.props;
+ let { width, height } = msg.data;
+ onContentResize({
+ width,
+ height,
+ });
+ },
+
+ startFrameScript: Task.async(function* () {
let { onContentResize } = this;
let browser = this.refs.browserContainer.querySelector("iframe.browser");
let mm = browser.frameLoader.messageManager;
// Notify tests when the content has received a resize event. This is not
// quite the same timing as when we _set_ a new size around the browser,
// since it still needs to do async work before the content is actually
// resized to match.
@@ -56,37 +93,26 @@ module.exports = createClass({
let requiresFloatingScrollbars =
!browserWindow.matchMedia("(-moz-overlay-scrollbars)").matches;
yield e10s.request(mm, "Start", {
requiresFloatingScrollbars,
// Tests expect events on resize to yield on various size changes
notifyOnResize: DevToolsUtils.testing,
});
-
- // manager.js waits for this signal before allowing browser tests to start
- this.props.onBrowserMounted();
}),
- componentWillUnmount() {
+ stopFrameScript: Task.async(function* () {
let { onContentResize } = this;
let browser = this.refs.browserContainer.querySelector("iframe.browser");
let mm = browser.frameLoader.messageManager;
e10s.off(mm, "OnContentResize", onContentResize);
- e10s.emit(mm, "Stop");
- },
-
- onContentResize(msg) {
- let { onContentResize } = this.props;
- let { width, height } = msg.data;
- onContentResize({
- width,
- height,
- });
- },
+ yield e10s.request(mm, "Stop");
+ message.post(window, "stop-frame-script:done");
+ }),
render() {
let {
location,
} = this.props;
// innerHTML expects & to be an HTML entity
location = location.replace(/&/g, "&");
--- a/devtools/client/responsive.html/components/resizable-viewport.js
+++ b/devtools/client/responsive.html/components/resizable-viewport.js
@@ -13,20 +13,22 @@ const Constants = require("../constants"
const Types = require("../types");
const Browser = createFactory(require("./browser"));
const ViewportToolbar = createFactory(require("./viewport-toolbar"));
const VIEWPORT_MIN_WIDTH = Constants.MIN_VIEWPORT_DIMENSION;
const VIEWPORT_MIN_HEIGHT = Constants.MIN_VIEWPORT_DIMENSION;
module.exports = createClass({
+
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeViewportDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
@@ -113,16 +115,17 @@ module.exports = createClass({
});
},
render() {
let {
devices,
location,
screenshot,
+ swapAfterMount,
viewport,
onBrowserMounted,
onChangeViewportDevice,
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
@@ -154,16 +157,17 @@ module.exports = createClass({
className: contentClass,
style: {
width: viewport.width + "px",
height: viewport.height + "px",
},
},
Browser({
location,
+ swapAfterMount,
onBrowserMounted,
onContentResize,
})
),
dom.div({
className: resizeHandleClass,
onMouseDown: this.onResizeStart,
}),
--- a/devtools/client/responsive.html/components/viewport.js
+++ b/devtools/client/responsive.html/components/viewport.js
@@ -7,20 +7,22 @@
const { DOM: dom, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const ResizableViewport = createFactory(require("./resizable-viewport"));
const ViewportDimension = createFactory(require("./viewport-dimension"));
module.exports = createClass({
+
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
+ swapAfterMount: PropTypes.bool.isRequired,
viewport: PropTypes.shape(Types.viewport).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeViewportDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
onResizeViewport: PropTypes.func.isRequired,
onRotateViewport: PropTypes.func.isRequired,
onUpdateDeviceModalOpen: PropTypes.func.isRequired,
},
@@ -54,16 +56,17 @@ module.exports = createClass({
onRotateViewport(viewport.id);
},
render() {
let {
devices,
location,
screenshot,
+ swapAfterMount,
viewport,
onBrowserMounted,
onContentResize,
onUpdateDeviceModalOpen,
} = this.props;
let {
onChangeViewportDevice,
@@ -74,16 +77,17 @@ module.exports = createClass({
return dom.div(
{
className: "viewport",
},
ResizableViewport({
devices,
location,
screenshot,
+ swapAfterMount,
viewport,
onBrowserMounted,
onChangeViewportDevice,
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
}),
--- a/devtools/client/responsive.html/components/viewports.js
+++ b/devtools/client/responsive.html/components/viewports.js
@@ -6,16 +6,17 @@
const { DOM: dom, createClass, createFactory, PropTypes } =
require("devtools/client/shared/vendor/react");
const Types = require("../types");
const Viewport = createFactory(require("./viewport"));
module.exports = createClass({
+
propTypes: {
devices: PropTypes.shape(Types.devices).isRequired,
location: Types.location.isRequired,
screenshot: PropTypes.shape(Types.screenshot).isRequired,
viewports: PropTypes.arrayOf(PropTypes.shape(Types.viewport)).isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onChangeViewportDevice: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
@@ -39,22 +40,23 @@ module.exports = createClass({
onRotateViewport,
onUpdateDeviceModalOpen,
} = this.props;
return dom.div(
{
id: "viewports",
},
- viewports.map(viewport => {
+ viewports.map((viewport, i) => {
return Viewport({
key: viewport.id,
devices,
location,
screenshot,
+ swapAfterMount: i == 0,
viewport,
onBrowserMounted,
onChangeViewportDevice,
onContentResize,
onResizeViewport,
onRotateViewport,
onUpdateDeviceModalOpen,
});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/docs/browser-swap.md
@@ -0,0 +1,145 @@
+# Overview
+
+The RDM tool uses several forms of tab and browser swapping to integrate the
+tool UI cleanly into the browser UI. The high level steps of this process are
+documented at `/devtools/docs/responsive-design-mode.md`.
+
+This document contains a random assortment of low level notes about the steps
+the browser goes through when swapping browsers between tabs.
+
+# Connections between Browsers and Tabs
+
+Link between tab and browser (`gBrowser._linkBrowserToTab`):
+
+```
+aTab.linkedBrowser = browser;
+gBrowser._tabForBrowser.set(browser, aTab);
+```
+
+# Swapping Browsers between Tabs
+
+## Legend
+
+* (R): remote browsers only
+* (!R): non-remote browsers only
+
+## Functions Called
+
+When you call `gBrowser.swapBrowsersAndCloseOther` to move tab content from a
+browser in one tab to a browser in another tab, here are all the code paths
+involved:
+
+* `gBrowser.swapBrowsersAndCloseOther`
+ * `gBrowser._beginRemoveTab`
+ * `gBrowser.tabContainer.updateVisibility`
+ * Emit `TabClose`
+ * `browser.webProgress.removeProgressListener`
+ * `filter.removeProgressListener`
+ * `listener.destroy`
+ * `gBrowser._swapBrowserDocShells`
+ * `ourBrowser.webProgress.removeProgressListener`
+ * `filter.removeProgressListener`
+ * `gBrowser._swapRegisteredOpenURIs`
+ * `ourBrowser.swapDocShells(aOtherBrowser)`
+ * Emit `SwapDocShells`
+ * `PopupNotifications._swapBrowserNotifications`
+ * `browser.detachFormFill` (!R)
+ * `browser.swapFrameLoaders`
+ * `browser.attachFormFill` (!R)
+ * `browser._remoteWebNavigationImpl.swapBrowser(browser)` (R)
+ * `browser._remoteWebProgressManager.swapBrowser(browser)` (R)
+ * `browser._remoteFinder.swapBrowser(browser)` (R)
+ * `gBrowser.mTabProgressListener`
+ * `filter.addProgressListener`
+ * `ourBrowser.webProgress.addProgressListener`
+ * `gBrowser._endRemoveTab`
+ * `gBrowser.tabContainer._fillTrailingGap`
+ * `gBrowser._blurTab`
+ * `gBrowser._tabFilters.delete`
+ * `gBrowser._tabListeners.delete`
+ * `gBrowser._outerWindowIDBrowserMap.delete`
+ * `browser.destroy`
+ * `gBrowser.tabContainer.removeChild`
+ * `gBrowser.tabContainer.adjustTabstrip`
+ * `gBrowser.tabContainer._setPositionalAttributes`
+ * `browser.parentNode.removeChild(browser)`
+ * `gBrowser._tabForBrowser.delete`
+ * `gBrowser.mPanelContainer.removeChild`
+ * `gBrowser.setTabTitle` / `gBrowser.setTabTitleLoading`
+ * `browser.currentURI.spec`
+ * `gBrowser._tabAttrModified`
+ * `gBrowser.updateTitlebar`
+ * `gBrowser.updateCurrentBrowser`
+ * `browser.docShellIsActive` (!R)
+ * `gBrowser.showTab`
+ * `gBrowser._appendStatusPanel`
+ * `gBrowser._callProgressListeners` with `onLocationChange`
+ * `gBrowser._callProgressListeners` with `onSecurityChange`
+ * `gBrowser._callProgressListeners` with `onUpdateCurrentBrowser`
+ * `gBrowser._recordTabAccess`
+ * `gBrowser.updateTitlebar`
+ * `gBrowser._callProgressListeners` with `onStateChange`
+ * `gBrowser._setCloseKeyState`
+ * Emit `TabSelect`
+ * `gBrowser._tabAttrModified`
+ * `browser.getInPermitUnload`
+ * `gBrowser.tabContainer._setPositionalAttributes`
+ * `gBrowser._tabAttrModified`
+
+## Browser State
+
+When calling `gBrowser.swapBrowsersAndCloseOther`, the browser is not actually
+moved from one tab to the other. Instead, various properties _on_ each of the
+browsers are swapped.
+
+Browser attributes `gBrowser.swapBrowsersAndCloseOther` transfers between
+browsers:
+
+* `usercontextid`
+
+Tab attributes `gBrowser.swapBrowsersAndCloseOther` transfers between tabs:
+
+* `usercontextid`
+* `muted`
+* `soundplaying`
+* `busy`
+
+Browser properties `gBrowser.swapBrowsersAndCloseOther` transfers between
+browsers:
+
+* `mIconURL`
+* `getFindBar(aOurTab)._findField.value`
+
+Browser properties `gBrowser._swapBrowserDocShells` transfers between browsers:
+
+* `outerWindowID` in `gBrowser._outerWindowIDBrowserMap`
+* `_outerWindowID` on the browser (R)
+* `docShellIsActive`
+* `permanentKey`
+* `registeredOpenURI`
+
+Browser properties `browser.swapDocShells` transfers between browsers:
+
+* `_docShell`
+* `_webBrowserFind`
+* `_contentWindow`
+* `_webNavigation`
+* `_remoteWebNavigation` (R)
+* `_remoteWebNavigationImpl` (R)
+* `_remoteWebProgressManager` (R)
+* `_remoteWebProgress` (R)
+* `_remoteFinder` (R)
+* `_securityUI` (R)
+* `_documentURI` (R)
+* `_documentContentType` (R)
+* `_contentTitle` (R)
+* `_characterSet` (R)
+* `_contentPrincipal` (R)
+* `_imageDocument` (R)
+* `_fullZoom` (R)
+* `_textZoom` (R)
+* `_isSyntheticDocument` (R)
+* `_innerWindowID` (R)
+* `_manifestURI` (R)
+
+`browser.swapFrameLoaders` swaps the actual page content.
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -17,16 +17,17 @@ const { Task } = require("resource://gre
const Telemetry = require("devtools/client/shared/telemetry");
const { loadSheet } = require("sdk/stylesheet/utils");
const { createFactory, createElement } =
require("devtools/client/shared/vendor/react");
const ReactDOM = require("devtools/client/shared/vendor/react-dom");
const { Provider } = require("devtools/client/shared/vendor/react-redux");
+const message = require("./utils/message");
const { initDevices } = require("./devices");
const App = createFactory(require("./app"));
const Store = require("./store");
const { changeLocation } = require("./actions/location");
const { addViewport, resizeViewport } = require("./actions/viewports");
let bootstrap = {
@@ -40,17 +41,17 @@ let bootstrap = {
loadSheet(window,
"resource://devtools/client/responsive.html/responsive-ua.css",
"agent");
this.telemetry.toolOpened("responsive");
let store = this.store = Store();
yield initDevices(this.dispatch.bind(this));
let provider = createElement(Provider, { store }, App());
ReactDOM.render(provider, document.querySelector("#root"));
- window.postMessage({ type: "init" }, "*");
+ message.post(window, "init:done");
}),
destroy() {
this.store = null;
this.telemetry.toolClosed("responsive");
this.telemetry = null;
},
@@ -66,20 +67,18 @@ let bootstrap = {
// initDevices() below are still pending.
return;
}
this.store.dispatch(action);
},
};
-window.addEventListener("load", function onLoad() {
- window.removeEventListener("load", onLoad);
- bootstrap.init();
-});
+// manager.js sends a message to signal init
+message.wait(window, "init").then(() => bootstrap.init());
window.addEventListener("unload", function onUnload() {
window.removeEventListener("unload", onUnload);
bootstrap.destroy();
});
// Allows quick testing of actions from the console
window.dispatch = action => bootstrap.dispatch(action);
@@ -117,16 +116,17 @@ window.setViewportSize = (width, height)
try {
bootstrap.dispatch(resizeViewport(0, width, height));
} catch (e) {
console.error(e);
}
};
/**
- * Called by manager.js when tests want to use the viewport's message manager.
- * It is packed into an object because this is the format most easily usable
- * with ContentTask.spawn().
+ * Called by manager.js when tests want to use the viewport's browser to access
+ * the content inside. We mock the format of a <xul:browser> to make this
+ * easily usable with ContentTask.spawn(), which expects an object with a
+ * `messageManager` property.
*/
-window.getViewportMessageManager = () => {
+window.getViewportBrowser = () => {
let { messageManager } = document.querySelector("iframe.browser").frameLoader;
return { messageManager };
};
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -1,23 +1,23 @@
/* 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, Cr } = require("chrome");
const promise = require("promise");
const { Task } = require("resource://gre/modules/Task.jsm");
-const { XPCOMUtils } = require("resource://gre/modules/XPCOMUtils.jsm");
const EventEmitter = require("devtools/shared/event-emitter");
const { getOwnerWindow } = require("sdk/tabs/utils");
const { on, off } = require("sdk/event/core");
const { startup } = require("sdk/window/helpers");
const events = require("./events");
+const message = require("./utils/message");
+const { swapToInnerBrowser } = require("./browser/swap");
const TOOL_URL = "chrome://devtools/content/responsive.html/index.xhtml";
/**
* ResponsiveUIManager is the external API for the browser UI, etc. to use when
* opening and closing the responsive UI.
*
* While the HTML UI is in an experimental stage, the older ResponsiveUIManager
@@ -53,16 +53,19 @@ const ResponsiveUIManager = exports.Resp
* The main browser chrome window.
* @param tab
* The browser tab.
* @return Promise
* Resolved to the ResponsiveUI instance for this tab when opening is
* complete.
*/
openIfNeeded: Task.async(function* (window, tab) {
+ if (!tab.linkedBrowser.isRemoteBrowser) {
+ return promise.reject(new Error("RDM only available for remote tabs."));
+ }
if (!this.isActiveForTab(tab)) {
if (!this.activeTabs.size) {
on(events.activate, "data", onActivate);
on(events.close, "data", onClose);
}
let ui = new ResponsiveUI(window, tab);
this.activeTabs.set(tab, ui);
yield setMenuCheckFor(tab, window);
@@ -80,26 +83,27 @@ const ResponsiveUIManager = exports.Resp
* @param tab
* The browser tab.
* @return Promise
* Resolved (with no value) when closing is complete.
*/
closeIfNeeded: Task.async(function* (window, tab) {
if (this.isActiveForTab(tab)) {
let ui = this.activeTabs.get(tab);
+ let destroyed = yield ui.destroy();
+ if (!destroyed) {
+ // Already in the process of destroying, abort.
+ return;
+ }
this.activeTabs.delete(tab);
-
if (!this.activeTabs.size) {
off(events.activate, "data", onActivate);
off(events.close, "data", onClose);
}
-
- yield ui.destroy();
this.emit("off", { tab });
-
yield setMenuCheckFor(tab, window);
}
}),
/**
* Returns true if responsive UI is active for a given tab.
*
* @param tab
@@ -185,64 +189,98 @@ ResponsiveUI.prototype = {
tab: null,
/**
* Promise resovled when the UI init has completed.
*/
inited: null,
/**
+ * Flag set when destruction has begun.
+ */
+ destroying: false,
+
+ /**
* A window reference for the chrome:// document that displays the responsive
* design tool. It is safe to reference this window directly even with e10s,
* as the tool UI is always loaded in the parent process. The web content
* contained *within* the tool UI on the other hand is loaded in the child
* process.
*/
toolWindow: null,
/**
- * For the moment, we open the tool by:
- * 1. Recording the tab's URL
- * 2. Navigating the tab to the tool
- * 3. Passing along the URL to the tool to open in the viewport
+ * Open RDM while preserving the state of the page. We use `swapFrameLoaders`
+ * to ensure all in-page state is preserved, just like when you move a tab to
+ * a new window.
*
- * This approach is simple, but it also discards the user's state on the page.
- * It's just like opening a fresh tab and pasting the URL.
- *
- * In the future, we can do better by using swapFrameLoaders to preserve the
- * state. Platform discussions are in progress to make this happen. See
- * bug 1238160 about <iframe mozbrowser> for more details.
+ * For more details, see /devtools/docs/responsive-design-mode.md.
*/
init: Task.async(function* () {
- let tabBrowser = this.tab.linkedBrowser;
- let contentURI = tabBrowser.documentURI.spec;
- tabBrowser.loadURI(TOOL_URL);
- yield tabLoaded(this.tab);
- let toolWindow = this.toolWindow = tabBrowser.contentWindow;
- toolWindow.addEventListener("message", this);
- yield waitForMessage(toolWindow, "init");
- toolWindow.addInitialViewport(contentURI);
- yield waitForMessage(toolWindow, "browser-mounted");
+ let ui = this;
+
+ // Swap page content from the current tab into a viewport within RDM
+ this.swap = swapToInnerBrowser({
+ tab: this.tab,
+ containerURL: TOOL_URL,
+ getInnerBrowser: Task.async(function* (containerBrowser) {
+ let toolWindow = ui.toolWindow = containerBrowser.contentWindow;
+ toolWindow.addEventListener("message", ui);
+ yield message.request(toolWindow, "init");
+ toolWindow.addInitialViewport("about:blank");
+ yield message.wait(toolWindow, "browser-mounted");
+ let toolViewportContentBrowser =
+ toolWindow.document.querySelector("iframe.browser");
+ return toolViewportContentBrowser;
+ })
+ });
+ yield this.swap.start();
+
+ // Notify the inner browser to start the frame script
+ yield message.request(this.toolWindow, "start-frame-script");
+
+ // TODO: Session restore continues to store the tool UI as the page's URL.
+ // Most likely related to browser UI's inability to show correct location.
}),
+ /**
+ * Close RDM and restore page content back into a regular tab.
+ *
+ * @return boolean
+ * Whether this call is actually destroying. False means destruction
+ * was already in progress.
+ */
destroy: Task.async(function* () {
- let tabBrowser = this.tab.linkedBrowser;
- let browserWindow = this.browserWindow;
+ if (this.destroying) {
+ return false;
+ }
+ this.destroying = true;
+
+ // Ensure init has finished before starting destroy
+ yield this.inited;
+
+ // Notify the inner browser to stop the frame script
+ yield message.request(this.toolWindow, "stop-frame-script");
+
+ // Destroy local state
+ let swap = this.swap;
this.browserWindow = null;
this.tab = null;
this.inited = null;
this.toolWindow = null;
- let loaded = waitForDocLoadComplete(browserWindow.gBrowser);
- tabBrowser.goBack();
- yield loaded;
+ this.swap = null;
+
+ // Undo the swap and return the content back to a normal tab
+ swap.stop();
+
+ return true;
}),
handleEvent(event) {
- let { tab, window } = this;
- let toolWindow = tab.linkedBrowser.contentWindow;
+ let { tab, window, toolWindow } = this;
if (event.origin !== "chrome://devtools") {
return;
}
switch (event.data.type) {
case "content-resize":
let { width, height } = event.data;
@@ -253,96 +291,52 @@ ResponsiveUI.prototype = {
break;
case "exit":
toolWindow.removeEventListener(event.type, this);
ResponsiveUIManager.closeIfNeeded(window, tab);
break;
}
},
+ /**
+ * Helper for tests. Assumes a single viewport for now.
+ */
getViewportSize() {
return this.toolWindow.getViewportSize();
},
+ /**
+ * Helper for tests. Assumes a single viewport for now.
+ */
setViewportSize: Task.async(function* (width, height) {
yield this.inited;
this.toolWindow.setViewportSize(width, height);
}),
- getViewportMessageManager() {
- return this.toolWindow.getViewportMessageManager();
+ /**
+ * Helper for tests. Assumes a single viewport for now.
+ */
+ getViewportBrowser() {
+ return this.toolWindow.getViewportBrowser();
},
};
EventEmitter.decorate(ResponsiveUI.prototype);
-function waitForMessage(win, type) {
- let deferred = promise.defer();
-
- let onMessage = event => {
- if (event.data.type !== type) {
- return;
- }
- win.removeEventListener("message", onMessage);
- deferred.resolve();
- };
- win.addEventListener("message", onMessage);
-
- return deferred.promise;
-}
-
-function tabLoaded(tab) {
- let deferred = promise.defer();
-
- function handle(event) {
- if (event.originalTarget != tab.linkedBrowser.contentDocument ||
- event.target.location.href == "about:blank") {
- return;
- }
- tab.linkedBrowser.removeEventListener("load", handle, true);
- deferred.resolve(event);
- }
-
- tab.linkedBrowser.addEventListener("load", handle, true);
- return deferred.promise;
-}
-
-/**
- * Waits for the next load to complete in the current browser.
- */
-function waitForDocLoadComplete(gBrowser) {
- let deferred = promise.defer();
- let progressListener = {
- onStateChange: function (webProgress, req, flags, status) {
- let docStop = Ci.nsIWebProgressListener.STATE_IS_NETWORK |
- Ci.nsIWebProgressListener.STATE_STOP;
-
- // When a load needs to be retargetted to a new process it is cancelled
- // with NS_BINDING_ABORTED so ignore that case
- if ((flags & docStop) == docStop && status != Cr.NS_BINDING_ABORTED) {
- gBrowser.removeProgressListener(progressListener);
- deferred.resolve();
- }
- },
- QueryInterface: XPCOMUtils.generateQI([Ci.nsIWebProgressListener,
- Ci.nsISupportsWeakReference])
- };
- gBrowser.addProgressListener(progressListener);
- return deferred.promise;
-}
-
const onActivate = (tab) => setMenuCheckFor(tab);
const onClose = ({ window, tabs }) => {
for (let tab of tabs) {
ResponsiveUIManager.closeIfNeeded(window, tab);
}
};
const setMenuCheckFor = Task.async(
function* (tab, window = getOwnerWindow(tab)) {
yield startup(window);
let menu = window.document.getElementById("menu_responsiveUI");
- menu.setAttribute("checked", ResponsiveUIManager.isActiveForTab(tab));
+ if (menu) {
+ menu.setAttribute("checked", ResponsiveUIManager.isActiveForTab(tab));
+ }
}
);
--- a/devtools/client/responsive.html/moz.build
+++ b/devtools/client/responsive.html/moz.build
@@ -2,16 +2,17 @@
# vim: set filetype=python:
# 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/.
DIRS += [
'actions',
'audio',
+ 'browser',
'components',
'images',
'reducers',
'utils',
]
DevToolsModules(
'app.js',
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -1,22 +1,26 @@
[DEFAULT]
tags = devtools
subsuite = devtools
-skip-if = (!e10s && debug) # Bug 1262416 - Intermittent crash at MessageLoop::DeletePendingTasks
+skip-if = !e10s # RDM only works for remote tabs
support-files =
devices.json
+ doc_page_state.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/framework/test/shared-redux-head.js
[browser_device_modal_exit.js]
[browser_device_modal_submit.js]
[browser_device_width.js]
[browser_exit_button.js]
+[browser_frame_script_active.js]
[browser_menu_item_01.js]
[browser_menu_item_02.js]
skip-if = (e10s && debug) # Bug 1267278: browser.xul leaks
[browser_mouse_resize.js]
+[browser_page_state.js]
[browser_resize_cmd.js]
+skip-if = true # GCLI target confused after swap, will fix in bug 1240907
[browser_screenshot_button.js]
[browser_viewport_basics.js]
--- a/devtools/client/responsive.html/test/browser/browser_device_width.js
+++ b/devtools/client/responsive.html/test/browser/browser_device_width.js
@@ -1,30 +1,30 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const TEST_URL = "about:logo";
+const TEST_URL = "data:text/html;charset=utf-8,";
addRDMTask(TEST_URL, function* ({ ui, manager }) {
ok(ui, "An instance of the RDM should be attached to the tab.");
yield setViewportSize(ui, manager, 110, 500);
info("Checking initial width/height properties.");
yield doInitialChecks(ui);
info("Changing the RDM size");
yield setViewportSize(ui, manager, 90, 500);
info("Checking for screen props");
yield checkScreenProps(ui);
info("Setting docShell.deviceSizeIsPageSize to false");
- yield ContentTask.spawn(ui.getViewportMessageManager(), {}, function* () {
+ yield ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
let docShell = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell);
docShell.deviceSizeIsPageSize = false;
});
info("Checking for screen props once again.");
yield checkScreenProps2(ui);
@@ -48,17 +48,17 @@ function* checkScreenProps(ui) {
function* checkScreenProps2(ui) {
let { matchesMedia, screen } = yield grabContentInfo(ui);
ok(!matchesMedia, "media query should be re-evaluated.");
is(window.screen.width, screen.width,
"screen.width should be the size of the screen.");
}
function grabContentInfo(ui) {
- return ContentTask.spawn(ui.getViewportMessageManager(), {}, function* () {
+ return ContentTask.spawn(ui.getViewportBrowser(), {}, function* () {
return {
screen: {
width: content.screen.width,
height: content.screen.height
},
innerWidth: content.innerWidth,
matchesMedia: content.matchMedia("(max-device-width:100px)").matches
};
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_frame_script_active.js
@@ -0,0 +1,46 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Verify frame script is active when expected.
+
+const e10s = require("devtools/client/responsive.html/utils/e10s");
+
+const TEST_URL = "http://example.com/";
+add_task(function* () {
+ let tab = yield addTab(TEST_URL);
+
+ let { ui } = yield openRDM(tab);
+
+ let mm = ui.getViewportBrowser().messageManager;
+ let { active } = yield e10s.request(mm, "IsActive");
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ // Try another round as well to be sure there is no state saved anywhere
+ ({ ui } = yield openRDM(tab));
+
+ mm = ui.getViewportBrowser().messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, true, "Frame script is active");
+
+ yield closeRDM(tab);
+
+ // Must re-get the messageManager on each run since it changes when RDM opens
+ // or closes due to the design of swapFrameLoaders. Also, we only have access
+ // to a valid `ui` instance while RDM is open.
+ mm = tab.linkedBrowser.messageManager;
+ ({ active } = yield e10s.request(mm, "IsActive"));
+ is(active, false, "Frame script is active");
+
+ yield removeTab(tab);
+});
--- a/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
+++ b/devtools/client/responsive.html/test/browser/browser_menu_item_02.js
@@ -15,20 +15,22 @@ const { partial } = require("sdk/lang/fu
const openBrowserWindow = partial(open, null, { features: { toolbar: true } });
const isMenuCheckedFor = ({document}) => {
let menu = document.getElementById("menu_responsiveUI");
return menu.getAttribute("checked") === "true";
};
add_task(function* () {
- const window1 = yield openBrowserWindow(TEST_URL);
+ const window1 = yield openBrowserWindow();
yield startup(window1);
+ yield BrowserTestUtils.openNewForegroundTab(window1.gBrowser, TEST_URL);
+
const tab1 = getActiveTab(window1);
is(window1, getMostRecentBrowserWindow(),
"The new window is the active one");
ok(!isMenuCheckedFor(window1),
"RDM menu item is unchecked by default");
--- a/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
+++ b/devtools/client/responsive.html/test/browser/browser_mouse_resize.js
@@ -1,14 +1,14 @@
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */
"use strict";
-const TEST_URL = "about:logo";
+const TEST_URL = "data:text/html;charset=utf-8,";
function getElRect(selector, win) {
let el = win.document.querySelector(selector);
return el.getBoundingClientRect();
}
/**
* Drag an element identified by 'selector' by [x,y] amount. Returns
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_page_state.js
@@ -0,0 +1,122 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test page state to ensure page is not reloaded and session history is not
+// modified.
+
+const DUMMY_1_URL = "http://example.com/";
+const TEST_URL = `${URL_ROOT}doc_page_state.html`;
+const DUMMY_2_URL = "http://example.com/browser/";
+
+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;
+
+ 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;
+
+ // 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
+ let shown = waitForPageShow(browser);
+ browser.goBack();
+ yield shown;
+
+ // 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");
+
+ // Click on content to set an altered state that would be lost on reload
+ yield BrowserTestUtils.synthesizeMouseAtCenter("body", {}, browser);
+
+ let { ui } = yield openRDM(tab);
+
+ // Check color inside the viewport
+ let color = yield spawnViewportTask(ui, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return content.getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(color, "rgb(0, 128, 0)",
+ "Content is still modified from click in viewport");
+
+ yield closeRDM(tab);
+
+ // Check color back in the browser tab
+ color = yield ContentTask.spawn(browser, {}, function* () {
+ // eslint-disable-next-line mozilla/no-cpows-in-tests
+ return content.getComputedStyle(content.document.body)
+ .getPropertyValue("background-color");
+ });
+ is(color, "rgb(0, 128, 0)",
+ "Content is still modified from click in browser tab");
+
+ // 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 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);
+ });
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/doc_page_state.html
@@ -0,0 +1,13 @@
+<!doctype html>
+<html>
+ <style>
+ body {
+ height: 100vh;
+ background: red;
+ }
+ body.modified {
+ background: green;
+ }
+ </style>
+ <body onclick="this.classList.add('modified')"/>
+</html>
--- a/devtools/client/responsive.html/test/browser/head.js
+++ b/devtools/client/responsive.html/test/browser/head.js
@@ -87,17 +87,17 @@ function addRDMTask(url, generator) {
}
yield closeRDM(tab);
yield removeTab(tab);
});
}
function spawnViewportTask(ui, args, task) {
- return ContentTask.spawn(ui.getViewportMessageManager(), args, task);
+ return ContentTask.spawn(ui.getViewportBrowser(), args, task);
}
function waitForFrameLoad(ui, targetURL) {
return spawnViewportTask(ui, { targetURL }, function* (args) {
if ((content.document.readyState == "complete" ||
content.document.readyState == "interactive") &&
content.location.href == args.targetURL) {
return;
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/utils/message.js
@@ -0,0 +1,38 @@
+/* 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 promise = require("promise");
+
+const REQUEST_DONE_SUFFIX = ":done";
+
+function wait(win, type) {
+ let deferred = promise.defer();
+
+ let onMessage = event => {
+ if (event.data.type !== type) {
+ return;
+ }
+ win.removeEventListener("message", onMessage);
+ deferred.resolve();
+ };
+ win.addEventListener("message", onMessage);
+
+ return deferred.promise;
+}
+
+function post(win, type) {
+ win.postMessage({ type }, "*");
+}
+
+function request(win, type) {
+ let done = wait(win, type + REQUEST_DONE_SUFFIX);
+ post(win, type);
+ return done;
+}
+
+exports.wait = wait;
+exports.post = post;
+exports.request = request;
--- a/devtools/client/responsive.html/utils/moz.build
+++ b/devtools/client/responsive.html/utils/moz.build
@@ -2,9 +2,10 @@
# vim: set filetype=python:
# 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/.
DevToolsModules(
'e10s.js',
'l10n.js',
+ 'message.js',
)
--- a/devtools/client/responsivedesign/responsivedesign-child.js
+++ b/devtools/client/responsivedesign/responsivedesign-child.js
@@ -20,24 +20,33 @@ var global = this;
const gFloatingScrollbarsStylesheet = Services.io.newURI("chrome://devtools/skin/floating-scrollbars-responsive-design.css", null, null);
var gRequiresFloatingScrollbars;
var active = false;
var resizeNotifications = false;
addMessageListener("ResponsiveMode:Start", startResponsiveMode);
addMessageListener("ResponsiveMode:Stop", stopResponsiveMode);
+ addMessageListener("ResponsiveMode:IsActive", isActive);
function debug(msg) {
// dump(`RDM CHILD: ${msg}\n`);
}
+ /**
+ * Used by tests to verify the state of responsive mode.
+ */
+ function isActive() {
+ sendAsyncMessage("ResponsiveMode:IsActive:Done", { active });
+ }
+
function startResponsiveMode({data:data}) {
debug("START");
if (active) {
+ debug("ALREADY STARTED, ABORT");
return;
}
addMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
webProgress.addProgressListener(WebProgressListener, Ci.nsIWebProgress.NOTIFY_ALL);
docShell.deviceSizeIsPageSize = true;
gRequiresFloatingScrollbars = data.requiresFloatingScrollbars;
if (data.notifyOnResize) {
@@ -84,16 +93,17 @@ var global = this;
resizeNotifications = false;
content.removeEventListener("resize", onResize, false);
removeEventListener("DOMWindowCreated", bindOnResize, false);
}
function stopResponsiveMode() {
debug("STOP");
if (!active) {
+ debug("ALREADY STOPPED, ABORT");
return;
}
active = false;
removeMessageListener("ResponsiveMode:RequestScreenshot", screenshot);
let webProgress = docShell.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIWebProgress);
webProgress.removeProgressListener(WebProgressListener);
docShell.deviceSizeIsPageSize = gDeviceSizeWasPageSize;
restoreScrollbars();
--- a/devtools/docs/SUMMARY.md
+++ b/devtools/docs/SUMMARY.md
@@ -1,18 +1,19 @@
# Summary
* [Tool Architectures](tools.md)
* [Inspector](inspector-panel.md)
* [Memory](memory-panel.md)
* [Debugger](debugger-panel.md)
+ * [Responsive Design Mode](responsive-design-mode.md)
* [Frontend](frontend.md)
* [Panel SVGs](svgs.md)
* [React](react.md)
* [Guidelines](react-guidelines.md)
* [Tips](react-tips.md)
* [Redux](redux.md)
* [Guidelines](redux-guidelines.md)
* [Tips](redux-tips.md)
* [Backend](backend.md)
* [Protocol](protocol.md)
- * [Debugger API](debugger-api.md)
\ No newline at end of file
+ * [Debugger API](debugger-api.md)
new file mode 100644
--- /dev/null
+++ b/devtools/docs/responsive-design-mode.md
@@ -0,0 +1,63 @@
+# Responsive Design Mode Architecture
+
+## Context
+
+You have a single browser tab that has visited several pages, and now has a
+history that looks like, in oldest to newest order:
+
+1. https://newsblur.com
+2. https://mozilla.org (← current page)
+3. https://convolv.es
+
+## Opening RDM During Current Firefox Session
+
+When opening RDM, the browser tab's history must preserved. Additionally, we
+strive to preserve the exact state of the currently displayed page (effectively
+any in-page state, which is important for single page apps where data can be
+lost if they are reloaded).
+
+This seems a bit convoluted, but one advantage of this technique is that it
+preserves tab state since the same tab is reused. This helps to maintain any
+extra state that may be set on tab by add-ons or others.
+
+1. Create a temporary, hidden tab to load the tool UI.
+2. Mark the tool tab browser's docshell as active so the viewport frame is
+ created eagerly and will be ready to swap.
+3. Create the initial viewport inside the tool UI.
+4. Swap tab content from the regular browser tab to the browser within the
+ viewport in the tool UI, preserving all state via
+ `gBrowser._swapBrowserDocShells`.
+5. Force the original browser tab to be non-remote since the tool UI must be
+ loaded in the parent process, and we're about to swap the tool UI into
+ this tab.
+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`.
+
+## 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
+ eagerly and will be ready to swap.
+3. 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
+ 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
+ used to hold the content via `swapBrowsersAndCloseOther`.
+
+## Session Restore
+
+When restarting Firefox and restoring a user's browsing session, we must
+correctly restore the tab history. If the RDM tool was opened when the session
+was captured, then it would be acceptable to either:
+
+* A: Restore the tab content without any RDM tool displayed **OR**
+* B: Restore the RDM tool the tab content inside, just as before the restart
+
+Option A (no RDM after session restore) seems more in line with how the rest of
+DevTools currently functions after restore.