Bug 1306975 - Support containers in RDM. r=gl draft
authorJ. Ryan Stinnett <jryans@gmail.com>
Fri, 23 Feb 2018 17:33:55 -0600
changeset 805876 ebb056a493531ff56baf8c1c7538b8e9d36445d1
parent 805551 ea21bf3e665d10066b6dce39873de9b353a12e57
push id112794
push userbmo:jryans@gmail.com
push dateFri, 08 Jun 2018 17:03:31 +0000
reviewersgl
bugs1306975
milestone62.0a1
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
devtools/client/locales/en-US/responsive.properties
devtools/client/responsive.html/actions/viewports.js
devtools/client/responsive.html/browser/swap.js
devtools/client/responsive.html/components/Browser.js
devtools/client/responsive.html/components/ResizableViewport.js
devtools/client/responsive.html/index.js
devtools/client/responsive.html/manager.js
devtools/client/responsive.html/reducers/viewports.js
devtools/client/responsive.html/test/browser/browser.ini
devtools/client/responsive.html/test/browser/browser_contextual_identity.js
devtools/client/responsive.html/test/browser/contextual_identity.html
devtools/client/responsive.html/types.js
--- 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 = {