Bug 1258360: Implement onMessageExternal and onConnectExternal. r=mixedpuppy
authorKris Maglione <maglione.k@gmail.com>
Sat, 11 Feb 2017 13:28:18 -0800
changeset 374777 83e85e21d35404878a2e4bb9b26f66e8b817c99b
parent 374776 0598ed8d643b46e20172348ac597cd5629661aac
child 374778 0dd9e290afb6b03512f9cec7743e75d3d583c6f1
push id10863
push userjlorenzo@mozilla.com
push dateMon, 06 Mar 2017 23:02:23 +0000
treeherdermozilla-aurora@0931190cd725 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmixedpuppy
bugs1258360
milestone54.0a1
Bug 1258360: Implement onMessageExternal and onConnectExternal. r=mixedpuppy MozReview-Commit-ID: 7NTrgyWpXbv
browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js
toolkit/components/extensions/ExtensionChild.jsm
toolkit/components/extensions/ExtensionCommon.jsm
toolkit/components/extensions/ExtensionContent.jsm
toolkit/components/extensions/LegacyExtensionsUtils.jsm
toolkit/components/extensions/ext-c-runtime.js
toolkit/components/extensions/schemas/runtime.json
toolkit/components/extensions/test/mochitest/mochitest-common.ini
toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
--- a/browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js
+++ b/browser/components/extensions/test/browser/browser_ext_legacy_extension_context_contentscript.js
@@ -117,30 +117,30 @@ add_task(function* test_legacy_extension
 
   let {uuid, tab} = yield extension.awaitMessage("get-expected-sender-info");
 
   let {singleMsg, msgSender} = yield waitMessage;
   is(singleMsg, "webextension -> legacy_extension message",
      "Got the expected message");
   ok(msgSender, "Got a message sender object");
 
-  is(msgSender.id, uuid, "The sender has the expected id property");
+  is(msgSender.id, extension.id, "The sender has the expected id property");
   is(msgSender.url, "http://example.com/", "The sender has the expected url property");
   ok(msgSender.tab, "The sender has a tab property");
   is(msgSender.tab.id, tab.id, "The port sender has the expected tab.id");
 
   // Wait confirmation that the reply has been received.
   yield extension.awaitMessage("got-reply-message");
 
   let port = yield waitConnectPort;
 
   ok(port, "Got the Port API object");
   ok(port.sender, "The port has a sender property");
 
-  is(port.sender.id, uuid, "The port sender has an id property");
+  is(port.sender.id, extension.id, "The port sender has an id property");
   is(port.sender.url, "http://example.com/", "The port sender has the expected url property");
   ok(port.sender.tab, "The port sender has a tab property");
   is(port.sender.tab.id, tab.id, "The port sender has the expected tab.id");
 
   let waitPortMessage = new Promise(resolve => {
     port.onMessage.addListener((msg) => {
       resolve(msg);
     });
--- a/toolkit/components/extensions/ExtensionChild.jsm
+++ b/toolkit/components/extensions/ExtensionChild.jsm
@@ -324,25 +324,26 @@ class Messenger {
     return this.context.wrapPromise(promise, responseCallback);
   }
 
   sendNativeMessage(messageManager, msg, recipient, responseCallback) {
     msg = NativeApp.encodeMessage(this.context, msg);
     return this.sendMessage(messageManager, msg, recipient, responseCallback);
   }
 
-  onMessage(name) {
+  _onMessage(name, filter) {
     return new SingletonEventManager(this.context, name, fire => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the message if it was sent by this Messenger.
-          return sender.contextId !== this.context.contextId;
+          return (sender.contextId !== this.context.contextId &&
+                  filter(sender, recipient));
         },
 
         receiveMessage: ({target, data: message, sender, recipient}) => {
           if (!this.context.active) {
             return;
           }
 
           let sendResponse;
@@ -372,16 +373,24 @@ class Messenger {
 
       MessageChannel.addListener(this.messageManagers, "Extension:Message", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Message", listener);
       };
     }).api();
   }
 
+  onMessage(name) {
+    return this._onMessage(name, sender => sender.id === this.sender.id);
+  }
+
+  onMessageExternal(name) {
+    return this._onMessage(name, sender => sender.id !== this.sender.id);
+  }
+
   _connect(messageManager, port, recipient) {
     let msg = {
       name: port.name,
       portId: port.id,
     };
 
     this._sendMessage(messageManager, "Extension:Connect", msg, recipient).catch(error => {
       if (error.result === MessageChannel.RESULT_NO_HANDLER) {
@@ -406,25 +415,26 @@ class Messenger {
   connectNative(messageManager, name, recipient) {
     let portId = getUniqueId();
 
     let port = new NativePort(this.context, messageManager, this.messageManagers, name, portId, null, recipient);
 
     return this._connect(messageManager, port, recipient);
   }
 
-  onConnect(name) {
+  _onConnect(name, filter) {
     return new SingletonEventManager(this.context, name, fire => {
       let listener = {
         messageFilterPermissive: this.optionalFilter,
         messageFilterStrict: this.filter,
 
         filterMessage: (sender, recipient) => {
           // Ignore the port if it was created by this Messenger.
-          return sender.contextId !== this.context.contextId;
+          return (sender.contextId !== this.context.contextId &&
+                  filter(sender, recipient));
         },
 
         receiveMessage: ({target, data: message, sender}) => {
           let {name, portId} = message;
           let mm = getMessageManager(target);
           let recipient = Object.assign({}, sender);
           if (recipient.tab) {
             recipient.tabId = recipient.tab.id;
@@ -437,16 +447,24 @@ class Messenger {
       };
 
       MessageChannel.addListener(this.messageManagers, "Extension:Connect", listener);
       return () => {
         MessageChannel.removeListener(this.messageManagers, "Extension:Connect", listener);
       };
     }).api();
   }
+
+  onConnect(name) {
+    return this._onConnect(name, sender => sender.id === this.sender.id);
+  }
+
+  onConnectExternal(name) {
+    return this._onConnect(name, sender => sender.id !== this.sender.id);
+  }
 }
 
 var apiManager = new class extends SchemaAPIManager {
   constructor() {
     super("addon");
     this.initialized = false;
   }
 
@@ -777,17 +795,17 @@ class ExtensionBaseContextChild extends 
     let {viewType, uri, contentWindow, tabId} = params;
     this.viewType = viewType;
     this.uri = uri || extension.baseURI;
 
     this.setContentWindow(contentWindow);
 
     // This is the MessageSender property passed to extension.
     // It can be augmented by the "page-open" hook.
-    let sender = {id: extension.uuid};
+    let sender = {id: extension.id};
     if (viewType == "tab") {
       sender.tabId = tabId;
       this.tabId = tabId;
     }
     if (uri) {
       sender.url = uri.spec;
     }
     this.sender = sender;
--- a/toolkit/components/extensions/ExtensionCommon.jsm
+++ b/toolkit/components/extensions/ExtensionCommon.jsm
@@ -192,20 +192,19 @@ class BaseContext {
    * @param {object} data
    * @param {object} [options]
    * @param {object} [options.sender]
    * @param {object} [options.recipient]
    *
    * @returns {Promise}
    */
   sendMessage(target, messageName, data, options = {}) {
-    options.recipient = options.recipient || {};
+    options.recipient = Object.assign({extensionId: this.extension.id}, options.recipient);
     options.sender = options.sender || {};
 
-    options.recipient.extensionId = this.extension.id;
     options.sender.extensionId = this.extension.id;
     options.sender.contextId = this.contextId;
 
     return MessageChannel.sendMessage(target, messageName, data, options);
   }
 
   get lastError() {
     this.checkedLastError = true;
--- a/toolkit/components/extensions/ExtensionContent.jsm
+++ b/toolkit/components/extensions/ExtensionContent.jsm
@@ -462,17 +462,17 @@ class ContentScriptContextChild extends 
     }
     Cu.nukeSandbox(this.sandbox);
     this.sandbox = null;
   }
 }
 
 defineLazyGetter(ContentScriptContextChild.prototype, "messenger", function() {
   // The |sender| parameter is passed directly to the extension.
-  let sender = {id: this.extension.uuid, frameId: this.frameId, url: this.url};
+  let sender = {id: this.extension.id, frameId: this.frameId, url: this.url};
   let filter = {extensionId: this.extension.id};
   let optionalFilter = {frameId: this.frameId};
 
   return new Messenger(this, [this.messageManager], sender, filter, optionalFilter);
 });
 
 defineLazyGetter(ContentScriptContextChild.prototype, "childManager", function() {
   let localApis = {};
--- a/toolkit/components/extensions/LegacyExtensionsUtils.jsm
+++ b/toolkit/components/extensions/LegacyExtensionsUtils.jsm
@@ -59,17 +59,17 @@ var LegacyExtensionContext = class exten
 
     let cloneScope = Cu.Sandbox(this.principal, {});
     Cu.setSandboxMetadata(cloneScope, {addonId: targetExtension.id});
     Object.defineProperty(
       this, "cloneScope",
       {value: cloneScope, enumerable: true, configurable: true, writable: true}
     );
 
-    let sender = {id: targetExtension.uuid};
+    let sender = {id: targetExtension.id};
     let filter = {extensionId: targetExtension.id};
     // Legacy addons live in the main process. Messages from other addons are
     // Messages from WebExtensions are sent to the main process and forwarded via
     // the parent process manager to the legacy extension.
     this.messenger = new Messenger(this, [Services.cpmm], sender, filter);
 
     this.api = {
       browser: {
--- a/toolkit/components/extensions/ext-c-runtime.js
+++ b/toolkit/components/extensions/ext-c-runtime.js
@@ -4,16 +4,20 @@ function runtimeApiFactory(context) {
   let {extension} = context;
 
   return {
     runtime: {
       onConnect: context.messenger.onConnect("runtime.onConnect"),
 
       onMessage: context.messenger.onMessage("runtime.onMessage"),
 
+      onConnectExternal: context.messenger.onConnectExternal("runtime.onConnectExternal"),
+
+      onMessageExternal: context.messenger.onMessageExternal("runtime.onMessageExternal"),
+
       connect: function(extensionId, connectInfo) {
         let name = connectInfo !== null && connectInfo.name || "";
         extensionId = extensionId || extension.id;
         let recipient = {extensionId};
 
         return context.messenger.connect(context.messageManager, name, recipient);
       },
 
@@ -42,17 +46,16 @@ function runtimeApiFactory(context) {
         }
 
         if (extensionId != null && typeof extensionId != "string") {
           return Promise.reject({message: "runtime.sendMessage's extensionId argument is invalid"});
         }
         if (options != null && typeof options != "object") {
           return Promise.reject({message: "runtime.sendMessage's options argument is invalid"});
         }
-        // TODO(robwu): Validate option keys and values when we support it.
 
         extensionId = extensionId || extension.id;
         let recipient = {extensionId};
 
         return context.messenger.sendMessage(context.messageManager, message, recipient, responseCallback);
       },
 
       connectNative(application) {
--- a/toolkit/components/extensions/schemas/runtime.json
+++ b/toolkit/components/extensions/schemas/runtime.json
@@ -530,17 +530,16 @@
         "allowedContexts": ["content", "devtools"],
         "description": "Fired when a connection is made from either an extension process or a content script.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onConnectExternal",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a connection is made from another extension.",
         "parameters": [
           {"$ref": "Port", "name": "port"}
         ]
       },
       {
         "name": "onMessage",
@@ -555,17 +554,16 @@
         "returns": {
           "type": "boolean",
           "optional": true,
           "description": "Return true from the event listener if you wish to call <code>sendResponse</code> after the event listener returns."
         }
       },
       {
         "name": "onMessageExternal",
-        "unsupported": true,
         "type": "function",
         "description": "Fired when a message is sent from another extension/app. Cannot be used in a content script.",
         "parameters": [
           {"name": "message", "type": "any", "optional": true, "description": "The message sent by the calling script."},
           {"name": "sender", "$ref": "MessageSender" },
           {"name": "sendResponse", "type": "function", "description": "Function to call (at most once) when you have a response. The argument should be any JSON-ifiable object. If you have more than one <code>onMessage</code> listener in the same document, then only one may send a response. This function becomes invalid when the event listener returns, unless you return true from the event listener to indicate you wish to send a response asynchronously (this will keep the message channel open to the other end until <code>sendResponse</code> is called)." }
         ],
         "returns": {
--- a/toolkit/components/extensions/test/mochitest/mochitest-common.ini
+++ b/toolkit/components/extensions/test/mochitest/mochitest-common.ini
@@ -62,16 +62,17 @@ skip-if = os == 'android' # Android does
 [test_ext_contentscript_exporthelpers.html]
 [test_ext_contentscript_incognito.html]
 skip-if = os == 'android' # Android does not support multiple windows.
 [test_ext_contentscript_css.html]
 [test_ext_contentscript_about_blank.html]
 [test_ext_contentscript_permission.html]
 [test_ext_contentscript_teardown.html]
 [test_ext_exclude_include_globs.html]
+[test_ext_external_messaging.html]
 [test_ext_i18n_css.html]
 [test_ext_generate.html]
 [test_ext_geolocation.html]
 skip-if = os == 'android' # Android support Bug 1336194
 [test_ext_notifications.html]
 [test_ext_permission_xhr.html]
 [test_ext_runtime_connect.html]
 [test_ext_runtime_connect_twoway.html]
--- a/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
+++ b/toolkit/components/extensions/test/mochitest/test_ext_all_apis.js
@@ -71,17 +71,19 @@ let expectedBackgroundApis = [
   "management.ExtensionDisabledReason",
   "management.ExtensionInstallType",
   "management.ExtensionType",
   "management.getSelf",
   "management.uninstallSelf",
   "runtime.getBackgroundPage",
   "runtime.getBrowserInfo",
   "runtime.getPlatformInfo",
+  "runtime.onConnectExternal",
   "runtime.onInstalled",
+  "runtime.onMessageExternal",
   "runtime.onStartup",
   "runtime.onUpdateAvailable",
   "runtime.openOptionsPage",
   "runtime.reload",
   "runtime.setUninstallURL",
   "types.LevelOfControl",
   "types.SettingScope",
 ];
copy from toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
copy to toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_external_messaging.html
@@ -1,93 +1,111 @@
 <!DOCTYPE HTML>
 <html>
 <head>
-  <title>WebExtension test</title>
+  <title>WebExtension external messaging</title>
   <script type="text/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/SpawnTask.js"></script>
   <script type="text/javascript" src="/tests/SimpleTest/ExtensionTestUtils.js"></script>
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript(token) {
-  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
-
-    if (msg == "done") {
-      browser.test.notifyPass("sendmessage_reply");
-      return;
-    }
-
-    let tabId = sender.tab.id;
-    browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+function backgroundScript(id, otherId) {
+  browser.runtime.onMessage.addListener((msg, sender) => {
+    browser.test.fail(`Got unexpected message: ${uneval(msg)} ${uneval(sender)}`);
+  });
 
-    browser.test.assertEq(msg, token, "token matches");
-    sendReply(`${token}-done`);
+  browser.runtime.onConnect.addListener(port => {
+    browser.test.fail(`Got unexpected connection: ${uneval(port.sender)}`);
   });
-}
 
-function contentScript(token) {
-  let gotTabMessage = false;
-  let badTabMessage = false;
-  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    if (msg == `${token}-tabMessage`) {
-      gotTabMessage = true;
-    } else {
-      badTabMessage = true;
-    }
+  browser.runtime.onMessageExternal.addListener((msg, sender) => {
+    browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
+    browser.test.assertEq(`helo-${id}`, msg, "Got expected message");
+
+    browser.test.sendMessage("onMessage-done");
+
+    return Promise.resolve(`ehlo-${otherId}`);
   });
 
-  browser.runtime.sendMessage(token, function(resp) {
-    if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
-      return; // test failed
+  browser.runtime.onConnectExternal.addListener(port => {
+    browser.test.assertEq(otherId, port.sender.id, `${id}: Got expected external connecter ID`);
+
+    port.onMessage.addListener(msg => {
+      browser.test.assertEq(`helo-${id}`, msg, "Got expected port message");
+
+      port.postMessage(`ehlo-${otherId}`);
+
+      browser.test.sendMessage("onConnect-done");
+    });
+  });
+
+  browser.test.onMessage.addListener(msg => {
+    if (msg === "go") {
+      browser.runtime.sendMessage(otherId, `helo-${otherId}`).then(result => {
+        browser.test.assertEq(`ehlo-${id}`, result, "Got expected reply");
+        browser.test.sendMessage("sendMessage-done");
+      });
+
+      let port = browser.runtime.connect(otherId);
+      port.postMessage(`helo-${otherId}`);
+
+      port.onMessage.addListener(msg => {
+        port.disconnect();
+
+        browser.test.assertEq(msg, `ehlo-${id}`, "Got expected port reply");
+        browser.test.sendMessage("connect-done");
+      });
     }
-    browser.runtime.sendMessage("done");
   });
 }
 
-function makeExtension() {
-  let token = Math.random();
+function makeExtension(id, otherId) {
+  let args = `${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
   let extensionData = {
-    background: `(${backgroundScript})(${token})`,
+    background: `(${backgroundScript})(${args})`,
     manifest: {
-      "permissions": ["tabs"],
-      "content_scripts": [{
-        "matches": ["http://mochi.test/*/file_sample.html"],
-        "js": ["content_script.js"],
-        "run_at": "document_start",
-      }],
-    },
-
-    files: {
-      "content_script.js": `(${contentScript})(${token})`,
+      "applications": {"gecko": {id}},
     },
   };
-  return extensionData;
+
+  return ExtensionTestUtils.loadExtension(extensionData);
 }
 
 add_task(function* test_contentscript() {
-  let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
-  let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+  const ID1 = "foo-message@mochitest.mozilla.org";
+  const ID2 = "bar-message@mochitest.mozilla.org";
+
+  let extension1 = makeExtension(ID1, ID2);
+  let extension2 = makeExtension(ID2, ID1);
 
   yield Promise.all([extension1.startup(), extension2.startup()]);
 
-  let win = window.open("file_sample.html");
+  extension1.sendMessage("go");
+  extension2.sendMessage("go");
+
+  yield Promise.all([
+    extension1.awaitMessage("sendMessage-done"),
+    extension2.awaitMessage("sendMessage-done"),
 
-  yield Promise.all([waitForLoad(win),
-                     extension1.awaitFinish("sendmessage_reply"),
-                     extension2.awaitFinish("sendmessage_reply")]);
+    extension1.awaitMessage("onMessage-done"),
+    extension2.awaitMessage("onMessage-done"),
 
-  win.close();
+    extension1.awaitMessage("connect-done"),
+    extension2.awaitMessage("connect-done"),
+
+    extension1.awaitMessage("onConnect-done"),
+    extension2.awaitMessage("onConnect-done"),
+  ]);
 
   yield extension1.unload();
   yield extension2.unload();
-  info("extensions unloaded");
 });
 </script>
 
 </body>
 </html>
--- a/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_sendmessage_reply2.html
@@ -8,86 +8,174 @@
   <script type="text/javascript" src="head.js"></script>
   <link rel="stylesheet" type="text/css" href="/tests/SimpleTest/test.css"/>
 </head>
 <body>
 
 <script type="text/javascript">
 "use strict";
 
-function backgroundScript(token) {
+function backgroundScript(token, id, otherId) {
+  browser.tabs.create({url: "tab.html"});
+
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"), "sender url correct");
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    if (msg === `content-${token}`) {
+      browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+                              `${id}: sender url correct`);
+
+      let tabId = sender.tab.id;
+      browser.tabs.sendMessage(tabId, `${token}-contentMessage`);
+
+      sendReply(`${token}-done`);
+    } else if (msg === `tab-${token}`) {
+      browser.runtime.sendMessage(otherId, `${otherId}-tabMessage`);
+      browser.runtime.sendMessage(`${token}-tabMessage`);
 
-    if (msg == "done") {
-      browser.test.notifyPass("sendmessage_reply");
-      return;
+      sendReply(`${token}-done`);
+    } else {
+      browser.test.fail(`${id}: Unexpected runtime message received: ${msg} ${uneval(sender)}`);
     }
+  });
+
+  browser.runtime.onMessageExternal.addListener((msg, sender, sendReply) => {
+    browser.test.assertEq(otherId, sender.id, `${id}: Got expected external sender ID`);
 
-    let tabId = sender.tab.id;
-    browser.tabs.sendMessage(tabId, `${token}-tabMessage`);
+    if (msg === `content-${id}`) {
+      browser.test.assertTrue(sender.tab.url.endsWith("file_sample.html"),
+                              `${id}: external sender url correct`);
 
-    browser.test.assertEq(msg, token, "token matches");
-    sendReply(`${token}-done`);
+      sendReply(`${otherId}-done`);
+    } else if (msg === `tab-${id}`) {
+      sendReply(`${otherId}-done`);
+    } else if (msg !== `${id}-tabMessage`) {
+      browser.test.fail(`${id}: Unexpected runtime external message received: ${msg} ${uneval(sender)}`);
+    }
   });
 }
 
-function contentScript(token) {
-  let gotTabMessage = false;
-  let badTabMessage = false;
+function contentScript(token, id, otherId) {
+  let gotContentMessage = false;
   browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
-    if (msg == `${token}-tabMessage`) {
-      gotTabMessage = true;
-    } else {
-      badTabMessage = true;
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    browser.test.assertEq(`${token}-contentMessage`, msg,
+                          `${id}: Correct content script message`);
+    if (msg === `${token}-contentMessage`) {
+      gotContentMessage = true;
     }
   });
 
-  browser.runtime.sendMessage(token, function(resp) {
-    if (resp != `${token}-done` || !gotTabMessage || badTabMessage) {
-      return; // test failed
-    }
-    browser.runtime.sendMessage("done");
+  Promise.all([
+    browser.runtime.sendMessage(otherId, `content-${otherId}`).then(resp => {
+      browser.test.assertEq(`${id}-done`, resp, `${id}: Correct content script external response token`);
+    }),
+
+    browser.runtime.sendMessage(`content-${token}`).then(resp => {
+      browser.test.assertEq(`${token}-done`, resp, `${id}: Correct content script response token`);
+    }),
+  ]).then(() => {
+    browser.test.assertTrue(gotContentMessage, `${id}: Got content script message`);
+
+    browser.test.sendMessage("content-script-done");
   });
 }
 
-function makeExtension() {
+function tabScript(token, id, otherId) {
+  let gotTabMessage = false;
+  browser.runtime.onMessage.addListener((msg, sender, sendReply) => {
+    browser.test.assertEq(id, sender.id, `${id}: Got expected sender ID`);
+
+    if (String(msg).startsWith("content-")) {
+      return;
+    }
+
+    browser.test.assertEq(`${token}-tabMessage`, msg,
+                          `${id}: Correct tab script message`);
+    if (msg === `${token}-tabMessage`) {
+      gotTabMessage = true;
+    }
+  });
+
+  Promise.all([
+    browser.runtime.sendMessage(otherId, `tab-${otherId}`).then(resp => {
+      browser.test.assertEq(`${id}-done`, resp, `${id}: Correct tab script external response token`);
+    }),
+
+    browser.runtime.sendMessage(`tab-${token}`).then(resp => {
+      browser.test.assertEq(`${token}-done`, resp, `${id}: Correct tab script response token`);
+    }),
+  ]).then(() => {
+    browser.test.assertTrue(gotTabMessage, `${id}: Got tab script message`);
+
+    window.close();
+
+    browser.test.sendMessage("tab-script-done");
+  });
+}
+
+function makeExtension(id, otherId) {
   let token = Math.random();
+
+  let args = `${token}, ${JSON.stringify(id)}, ${JSON.stringify(otherId)}`;
+
   let extensionData = {
-    background: `(${backgroundScript})(${token})`,
+    background: `(${backgroundScript})(${args})`,
     manifest: {
+      "applications": {"gecko": {id}},
+
       "permissions": ["tabs"],
+
+
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "js": ["content_script.js"],
         "run_at": "document_start",
       }],
     },
 
     files: {
-      "content_script.js": `(${contentScript})(${token})`,
+      "tab.html": `<!DOCTYPE html>
+        <html>
+          <head>
+            <meta charset="utf-8">
+            <script src="tab.js"><\/script>
+          </head>
+        </html>`,
+
+      "tab.js": `(${tabScript})(${args})`,
+
+      "content_script.js": `(${contentScript})(${args})`,
     },
   };
   return extensionData;
 }
 
 add_task(function* test_contentscript() {
-  let extension1 = ExtensionTestUtils.loadExtension(makeExtension());
-  let extension2 = ExtensionTestUtils.loadExtension(makeExtension());
+  const ID1 = "sendmessage1@mochitest.mozilla.org";
+  const ID2 = "sendmessage2@mochitest.mozilla.org";
+
+  let extension1 = ExtensionTestUtils.loadExtension(makeExtension(ID1, ID2));
+  let extension2 = ExtensionTestUtils.loadExtension(makeExtension(ID2, ID1));
 
   yield Promise.all([extension1.startup(), extension2.startup()]);
 
   let win = window.open("file_sample.html");
 
-  yield Promise.all([waitForLoad(win),
-                     extension1.awaitFinish("sendmessage_reply"),
-                     extension2.awaitFinish("sendmessage_reply")]);
+  yield waitForLoad(win);
+
+  yield Promise.all([
+    extension1.awaitMessage("content-script-done"),
+    extension2.awaitMessage("content-script-done"),
+    extension1.awaitMessage("tab-script-done"),
+    extension2.awaitMessage("tab-script-done"),
+  ]);
 
   win.close();
 
   yield extension1.unload();
   yield extension2.unload();
-  info("extensions unloaded");
 });
 </script>
 
 </body>
 </html>
--- a/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_legacy_extension_context.js
@@ -103,17 +103,17 @@ add_task(function* test_legacy_extension
 
   extension.testMessage("do-send-message");
 
   let {singleMsg, msgSender} = yield waitMessage;
   equal(singleMsg, "webextension -> legacy_extension message",
      "Got the expected message");
   ok(msgSender, "Got a message sender object");
 
-  equal(msgSender.id, extensionInfo.uuid, "The sender has the expected id property");
+  equal(msgSender.id, extension.id, "The sender has the expected id property");
   equal(msgSender.url, extensionInfo.bgURL, "The sender has the expected url property");
 
   // Wait confirmation that the reply has been received.
   yield new Promise((resolve, reject) => {
     extension.on("test-message", function testMessageListener(kind, msg, ...args) {
       if (msg != "got-reply-message") {
         reject(new Error(`Got an unexpected test-message: ${msg}`));
       } else {
@@ -131,17 +131,17 @@ add_task(function* test_legacy_extension
   });
 
   extension.testMessage("do-connect");
 
   let port = yield waitConnectPort;
 
   ok(port, "Got the Port API object");
   ok(port.sender, "The port has a sender property");
-  equal(port.sender.id, extensionInfo.uuid,
+  equal(port.sender.id, extension.id,
      "The port sender has the expected id property");
   equal(port.sender.url, extensionInfo.bgURL,
      "The port sender has the expected url property");
 
   let waitPortMessage = new Promise(resolve => {
     port.onMessage.addListener((msg) => {
       resolve(msg);
     });