Bug 1306975 - Support containers in RDM. r=gl
This adds support for container tabs / contextual identity in Responsive Design
Mode. Tabs in non-default contexts can now be opened in RDM just like regular
tabs.
MozReview-Commit-ID: BofTgrowjGV
--- a/devtools/client/locales/en-US/responsive.properties
+++ b/devtools/client/locales/en-US/responsive.properties
@@ -57,21 +57,16 @@ responsive.screenshot=Take a screenshot
# second argument (%2$S) is the time string in HH.MM.SS format.
responsive.screenshotGeneratedFilename=Screen Shot %1$S at %2$S
# LOCALIZATION NOTE (responsive.remoteOnly): Message displayed in the tab's
# notification box if a user tries to open Responsive Design Mode in a
# non-remote tab.
responsive.remoteOnly=Responsive Design Mode is only available for remote browser tabs, such as those used for web content in multi-process Firefox.
-# LOCALIZATION NOTE (responsive.noContainerTabs): Message displayed in the tab's
-# notification box if a user tries to open Responsive Design Mode in a
-# container tab.
-responsive.noContainerTabs=Responsive Design Mode is currently unavailable in container tabs.
-
# LOCALIZATION NOTE (responsive.changeDevicePixelRatio): tooltip for the
# device pixel ratio dropdown when is enabled.
responsive.changeDevicePixelRatio=Change device pixel ratio of the viewport
# LOCALIZATION NOTE (responsive.devicePixelRatio.auto): tooltip for the device pixel ratio
# dropdown when it is disabled because a device is selected.
# The argument (%1$S) is the selected device (e.g. iPhone 6) that set
# automatically the device pixel ratio value.
--- a/devtools/client/responsive.html/actions/viewports.js
+++ b/devtools/client/responsive.html/actions/viewports.js
@@ -17,19 +17,20 @@ const {
const { post } = require("../utils/message");
module.exports = {
/**
* Add an additional viewport to display the document.
*/
- addViewport() {
+ addViewport(userContextId = 0) {
return {
type: ADD_VIEWPORT,
+ userContextId,
};
},
/**
* Change the viewport device.
*/
changeDevice(id, device, deviceType) {
return {
--- a/devtools/client/responsive.html/browser/swap.js
+++ b/devtools/client/responsive.html/browser/swap.js
@@ -125,16 +125,17 @@ function swapToInnerBrowser({ tab, conta
// Freeze navigation temporarily to avoid "blinking" in the location bar.
freezeNavigationState(tab);
// 1. Create a temporary, hidden tab to load the tool UI.
debug("Add blank tool tab");
const containerTab = addTabSilently("about:blank", {
skipAnimation: true,
forceNotRemote: true,
+ userContextId: tab.userContextId,
});
gBrowser.hideTab(containerTab);
const containerBrowser = containerTab.linkedBrowser;
// Even though we load the `containerURL` with `LOAD_FLAGS_BYPASS_HISTORY` below,
// `SessionHistory.jsm` has a fallback path for tabs with no history which
// fabricates a history entry by reading the current URL, and this can cause the
// container URL to be recorded in the session store. To avoid this, we send a
// bogus `epoch` value to our container tab, which causes all future history
@@ -236,16 +237,17 @@ function swapToInnerBrowser({ tab, conta
// 1. Stop the tunnel between outer and inner browsers.
tunnel.stop();
tunnel = null;
// 2. Create a temporary, hidden tab to hold the content.
const contentTab = addTabSilently("about:blank", {
skipAnimation: true,
+ userContextId: tab.userContextId,
});
gBrowser.hideTab(contentTab);
const contentBrowser = contentTab.linkedBrowser;
// 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;
--- a/devtools/client/responsive.html/components/Browser.js
+++ b/devtools/client/responsive.html/components/Browser.js
@@ -22,16 +22,17 @@ class Browser extends PureComponent {
/**
* This component is not allowed to depend directly on frequently changing data (width,
* height). Any changes in props would cause the <iframe> to be removed and added again,
* throwing away the current state of the page.
*/
static get propTypes() {
return {
swapAfterMount: PropTypes.bool.isRequired,
+ userContextId: PropTypes.number.isRequired,
onBrowserMounted: PropTypes.func.isRequired,
onContentResize: PropTypes.func.isRequired,
};
}
constructor(props) {
super(props);
this.onContentResize = this.onContentResize.bind(this);
@@ -132,16 +133,20 @@ class Browser extends PureComponent {
const mm = browser.frameLoader.messageManager;
e10s.off(mm, "OnContentResize", onContentResize);
await e10s.request(mm, "Stop");
message.post(window, "stop-frame-script:done");
}
render() {
+ const {
+ userContextId,
+ } = this.props;
+
// In the case of @remote and @remoteType, the attribute must be set before the
// element is added to the DOM to have any effect, which we are able to do with this
// approach.
//
// @noisolation and @allowfullscreen are needed so that these frames have the same
// access to browser features as regular browser tabs. The `swapFrameLoaders` platform
// API we use compares such features before allowing the swap to proceed.
return dom.iframe(
@@ -149,16 +154,17 @@ class Browser extends PureComponent {
allowFullScreen: "true",
className: "browser",
height: "100%",
mozbrowser: "true",
noisolation: "true",
remote: "true",
remotetype: "web",
src: "about:blank",
+ usercontextid: userContextId,
width: "100%",
ref: browser => {
this.browser = browser;
},
}
);
}
}
--- a/devtools/client/responsive.html/components/ResizableViewport.js
+++ b/devtools/client/responsive.html/components/ResizableViewport.js
@@ -169,16 +169,17 @@ class ResizableViewport extends Componen
className: contentClass,
style: {
width: viewport.width + "px",
height: viewport.height + "px",
},
},
Browser({
swapAfterMount,
+ userContextId: viewport.userContextId,
onBrowserMounted,
onContentResize,
})
),
dom.div({
className: resizeHandleClass,
onMouseDown: this.onResizeStart,
}),
--- a/devtools/client/responsive.html/index.js
+++ b/devtools/client/responsive.html/index.js
@@ -117,22 +117,22 @@ function onDevicePixelRatioChange() {
}
mql.addListener(listener);
}
/**
* Called by manager.js to add the initial viewport based on the original page.
*/
-window.addInitialViewport = contentURI => {
+window.addInitialViewport = ({ uri, userContextId }) => {
try {
onDevicePixelRatioChange();
- bootstrap.dispatch(changeLocation(contentURI));
+ bootstrap.dispatch(changeLocation(uri));
bootstrap.dispatch(changeDisplayPixelRatio(window.devicePixelRatio));
- bootstrap.dispatch(addViewport());
+ bootstrap.dispatch(addViewport(userContextId));
} catch (e) {
console.error(e);
}
};
/**
* Called by manager.js when tests want to check the viewport size.
*/
--- a/devtools/client/responsive.html/manager.js
+++ b/devtools/client/responsive.html/manager.js
@@ -83,21 +83,16 @@ const ResponsiveUIManager = exports.Resp
* Resolved to the ResponsiveUI instance for this tab when opening is
* complete.
*/
async openIfNeeded(window, tab, options = {}) {
if (!tab.linkedBrowser.isRemoteBrowser) {
this.showRemoteOnlyNotification(window, tab, options);
return promise.reject(new Error("RDM only available for remote tabs."));
}
- // Remove this once we support this case in bug 1306975.
- if (tab.linkedBrowser.hasAttribute("usercontextid")) {
- this.showNoContainerTabsNotification(window, tab, options);
- return promise.reject(new Error("RDM not available for container tabs."));
- }
if (!this.isActiveForTab(tab)) {
this.initMenuCheckListenerFor(window);
// Track whether a toolbox was opened before RDM was opened.
const toolbox = gDevTools.getToolbox(TargetFactory.forTab(tab));
const hostType = toolbox ? toolbox.hostType : "none";
const hasToolbox = !!toolbox;
const tel = this._telemetry;
@@ -264,24 +259,16 @@ const ResponsiveUIManager = exports.Resp
showRemoteOnlyNotification(window, tab, { trigger } = {}) {
showNotification(window, tab, {
command: trigger == "command",
msg: l10n.getStr("responsive.remoteOnly"),
priority: PriorityLevels.PRIORITY_CRITICAL_MEDIUM,
});
},
-
- showNoContainerTabsNotification(window, tab, { trigger } = {}) {
- showNotification(window, tab, {
- command: trigger == "command",
- msg: l10n.getStr("responsive.noContainerTabs"),
- priority: PriorityLevels.PRIORITY_CRITICAL_MEDIUM,
- });
- },
};
// GCLI commands in ./commands.js listen for events from this object to know
// when the UI for a tab has opened or closed.
EventEmitter.decorate(ResponsiveUIManager);
/**
* ResponsiveUI manages the responsive design tool for a specific tab. The
@@ -352,17 +339,20 @@ ResponsiveUI.prototype = {
this.swap = swapToInnerBrowser({
tab: this.tab,
containerURL: TOOL_URL,
async getInnerBrowser(containerBrowser) {
const toolWindow = ui.toolWindow = containerBrowser.contentWindow;
toolWindow.addEventListener("message", ui);
debug("Wait until init from inner");
await message.request(toolWindow, "init");
- toolWindow.addInitialViewport("about:blank");
+ toolWindow.addInitialViewport({
+ uri: "about:blank",
+ userContextId: ui.tab.userContextId,
+ });
debug("Wait until browser mounted");
await message.wait(toolWindow, "browser-mounted");
return ui.getViewportBrowser();
}
});
debug("Wait until swap start");
await this.swap.start();
--- a/devtools/client/responsive.html/reducers/viewports.js
+++ b/devtools/client/responsive.html/reducers/viewports.js
@@ -20,26 +20,29 @@ const INITIAL_VIEWPORT = {
id: nextViewportId++,
device: "",
deviceType: "",
width: 320,
height: 480,
pixelRatio: {
value: 0,
},
+ userContextId: 0,
};
const reducers = {
- [ADD_VIEWPORT](viewports) {
+ [ADD_VIEWPORT](viewports, { userContextId }) {
// For the moment, there can be at most one viewport.
if (viewports.length === 1) {
return viewports;
}
- return [...viewports, Object.assign({}, INITIAL_VIEWPORT)];
+ return [...viewports, Object.assign({}, INITIAL_VIEWPORT, {
+ userContextId,
+ })];
},
[CHANGE_DEVICE](viewports, { id, device, deviceType }) {
return viewports.map(viewport => {
if (viewport.id !== id) {
return viewport;
}
--- a/devtools/client/responsive.html/test/browser/browser.ini
+++ b/devtools/client/responsive.html/test/browser/browser.ini
@@ -1,29 +1,31 @@
[DEFAULT]
tags = devtools
subsuite = devtools
# !e10s: RDM only works for remote tabs
# Win: Bug 1319248
skip-if = !e10s || os == "win"
support-files =
+ contextual_identity.html
devices.json
doc_page_state.html
geolocation.html
head.js
touch.html
!/devtools/client/commandline/test/helpers.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/shared-head.js
!/devtools/client/shared/test/shared-redux-head.js
!/devtools/client/shared/test/telemetry-test-helpers.js
!/devtools/client/shared/test/test-actor.js
!/devtools/client/shared/test/test-actor-registry.js
[browser_cmd_click.js]
+[browser_contextual_identity.js]
[browser_device_change.js]
[browser_device_custom_remove.js]
[browser_device_custom.js]
[browser_device_modal_error.js]
[browser_device_modal_exit.js]
[browser_device_modal_submit.js]
[browser_device_pixel_ratio_change.js]
[browser_device_width.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/browser_contextual_identity.js
@@ -0,0 +1,99 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const TEST_URL = TEST_URI_ROOT + "contextual_identity.html";
+
+// Opens `uri' in a new tab with the provided userContextId.
+// Returns the newly opened tab and browser.
+async function addTabInUserContext(uri, userContextId) {
+ const tab = BrowserTestUtils.addTab(gBrowser, uri, { userContextId });
+ gBrowser.selectedTab = tab;
+ const browser = gBrowser.getBrowserForTab(tab);
+ await BrowserTestUtils.browserLoaded(browser);
+ return { tab, browser };
+}
+
+async function sendMessages(receiver) {
+ const channelName = "contextualidentity-broadcastchannel";
+
+ // reflect the received message on title
+ await ContentTask.spawn(receiver.browser, channelName, function(name) {
+ content.testPromise = new content.Promise(resolve => {
+ content.bc = new content.BroadcastChannel(name);
+ content.bc.onmessage = function(e) {
+ content.document.title += e.data;
+ resolve();
+ };
+ });
+ });
+
+ const sender1 = await addTabInUserContext(TEST_URL, 1);
+ const sender2 = await addTabInUserContext(TEST_URL, 2);
+ sender1.message = "Message from user context #1";
+ sender2.message = "Message from user context #2";
+
+ // send a message from a tab in different user context first
+ // then send a message from a tab in the same user context
+ for (const sender of [sender1, sender2]) {
+ await ContentTask.spawn(
+ sender.browser,
+ { name: channelName, message: sender.message },
+ function(opts) {
+ const bc = new content.BroadcastChannel(opts.name);
+ bc.postMessage(opts.message);
+ });
+ }
+
+ return {
+ sender1,
+ sender2,
+ receiver,
+ };
+}
+
+async function verifyResults({ sender1, sender2, receiver }) {
+ // Since sender1 sends before sender2, if the title is exactly
+ // sender2's message, sender1's message must've been blocked
+ await ContentTask.spawn(receiver.browser, sender2.message,
+ async function(message) {
+ await content.testPromise.then(function() {
+ is(content.document.title, message,
+ "should only receive messages from the same user context");
+ });
+ }
+ );
+
+ gBrowser.removeTab(sender1.tab);
+ gBrowser.removeTab(sender2.tab);
+}
+
+add_task(async function() {
+ // Make sure userContext is enabled.
+ await SpecialPowers.pushPrefEnv({
+ "set": [
+ ["privacy.userContext.enabled", true]
+ ]
+ });
+});
+
+add_task(async function() {
+ info("Checking broadcast channel, send outside RDM, verify inside RDM");
+ const receiver = await addTabInUserContext(TEST_URL, 2);
+ const tabs = await sendMessages(receiver);
+ const { ui } = await openRDM(receiver.tab);
+ receiver.browser = ui.getViewportBrowser();
+ await verifyResults(tabs);
+ await removeTab(receiver.tab);
+});
+
+add_task(async function() {
+ info("Checking broadcast channel, send inside RDM, verify inside RDM");
+ const receiver = await addTabInUserContext(TEST_URL, 2);
+ const { ui } = await openRDM(receiver.tab);
+ receiver.browser = ui.getViewportBrowser();
+ const tabs = await sendMessages(receiver);
+ await verifyResults(tabs);
+ await removeTab(receiver.tab);
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/responsive.html/test/browser/contextual_identity.html
@@ -0,0 +1,6 @@
+<html><body>
+<script>
+"use strict";
+document.title = window.location.search;
+</script>
+</body></html>
--- a/devtools/client/responsive.html/types.js
+++ b/devtools/client/responsive.html/types.js
@@ -168,16 +168,20 @@ exports.viewport = {
width: PropTypes.number,
// The height of the viewport
height: PropTypes.number,
// The device pixel ratio of the viewport
pixelRatio: PropTypes.shape(pixelRatio),
+ // The user context (container) ID for the viewport
+ // Defaults to 0 meaning the default context
+ userContextId: PropTypes.number,
+
};
/* ACTIONS IN PROGRESS */
/**
* The progression of the screenshot.
*/
exports.screenshot = {