Bug 1286908 Remove id allocation from SpecialPowers loadExtension() r?kmag draft
authorAndrew Swan <aswan@mozilla.com>
Tue, 02 Aug 2016 16:04:37 -0700
changeset 398704 c367d0d31ec077d0aaec47757d174ef6656706c8
parent 398252 720b5d2c84d5b253d4dfde4897e13384dc97a46a
child 527723 e12ad736eff0861e90f18f7dbc6adfd86a32419a
push id25602
push useraswan@mozilla.com
push dateTue, 09 Aug 2016 16:39:36 +0000
reviewerskmag
bugs1286908
milestone51.0a1
Bug 1286908 Remove id allocation from SpecialPowers loadExtension() r?kmag Prior to this change, SpecialPowers used the extension id to identiy extension instances in inter-process messaging. This required that an id be allocated from the content process side when loadExtension() was called, but that made it impossible to test code that exercises the code path in the AddonManager that allocates ids for extensions that do not include an id in the manifest (it also made the loadExtension() api clunky). With this change, SpecialPowers allocates an internal identifier for messaging, but this identifier is separate from extension ids. Confusingly, we still store the actual extension id in an id property on the object returned by loadExtension(), but there are enough tests that reference this that it would be unnecessarily disruptive to get rid of it so it stays for now... MozReview-Commit-ID: G6xk1mBJJL8
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
browser/components/extensions/test/browser/browser_ext_pageAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
testing/specialpowers/content/SpecialPowersObserverAPI.js
testing/specialpowers/content/specialpowersAPI.js
toolkit/components/extensions/Extension.jsm
toolkit/components/extensions/ExtensionXPCShellUtils.jsm
toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
toolkit/components/extensions/test/xpcshell/test_locale_data.js
toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
toolkit/mozapps/extensions/test/xpcshell/head_addons.js
toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -82,20 +82,22 @@ function* runTests(options) {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
 
     files: options.files || {},
 
     background: `(${background})(${options.getTests})`,
   });
 
+  let browserActionId;
+  function checkDetails(details) {
+    if (!browserActionId) {
+      browserActionId = `${makeWidgetId(extension.id)}-browser-action`;
+    }
 
-  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
-
-  function checkDetails(details) {
     let button = document.getElementById(browserActionId);
 
     ok(button, "button exists");
 
     let title = details.title || options.manifest.name;
 
     is(getListStyleImage(button), details.icon, "icon URL is correct");
     is(button.getAttribute("tooltiptext"), title, "image title is correct");
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_pageAction_icon.js
@@ -251,23 +251,22 @@ add_task(function* testDetailsObjects() 
       "data/100.png": imageBuffer,
       "data/a.png": imageBuffer,
       "data/a-x2.png": imageBuffer,
     },
   });
 
   const RESOLUTION_PREF = "layout.css.devPixelsPerPx";
 
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  yield extension.startup();
+
+  let pageActionId = `${makeWidgetId(extension.id)}-page-action`;
   let browserActionWidget = getBrowserActionWidget(extension);
 
-
-  yield extension.startup();
   let tests = yield extension.awaitMessage("ready");
-
   for (let test of tests) {
     extension.sendMessage("setIcon", test);
     yield extension.awaitMessage("iconSet");
 
     let browserActionButton = browserActionWidget.forWindow(window).node;
     let pageActionImage = document.getElementById(pageActionId);
 
 
--- a/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
+++ b/browser/components/extensions/test/browser/browser_ext_optionsPage_privileges.js
@@ -24,20 +24,22 @@ add_task(function* test_tab_options_priv
     }).then(tab => {
       browser.runtime.sendMessage({msgName: "removeTabId", tabId: tab.id});
     }).catch(error => {
       browser.test.log(`Error: ${error} :: ${error.stack}`);
       browser.test.notifyFail("options-ui-privileges");
     });
   }
 
+  const ID = "options_privileges@tests.mozilla.org";
   let extension = ExtensionTestUtils.loadExtension({
     useAddonManager: "temporary",
 
     manifest: {
+      applications: {gecko: {id: ID}},
       "permissions": ["tabs"],
       "options_ui": {
         "page": "options.html",
       },
     },
     files: {
       "options.html": `<!DOCTYPE html>
         <html>
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -82,17 +82,17 @@ function* runTests(options) {
   let extension = ExtensionTestUtils.loadExtension({
     manifest: options.manifest,
 
     files: options.files || {},
 
     background: `(${background})(${options.getTests})`,
   });
 
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  let pageActionId;
   let currentWindow = window;
   let windows = [];
 
   function checkDetails(details) {
     let image = currentWindow.document.getElementById(pageActionId);
     if (details == null) {
       ok(image == null || image.hidden, "image is hidden");
     } else {
@@ -106,16 +106,20 @@ function* runTests(options) {
       // TODO: Popup URL.
     }
   }
 
   let testNewWindows = 1;
 
   let awaitFinish = new Promise(resolve => {
     extension.onMessage("nextTest", (expecting, testsRemaining) => {
+      if (!pageActionId) {
+        pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+      }
+
       checkDetails(expecting);
 
       if (testsRemaining) {
         extension.sendMessage("runNextTest");
       } else if (testNewWindows) {
         testNewWindows--;
 
         BrowserTestUtils.openNewBrowserWindow().then(window => {
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -131,24 +131,24 @@ add_task(function* testPageActionPopup()
           browser.pageAction.show(tabId).then(() => {
             browser.test.sendMessage("next-test");
           });
         });
       },
     },
   });
 
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
-  let panelId = makeWidgetId(extension.id) + "-panel";
-
   extension.onMessage("send-click", () => {
     clickPageAction(extension);
   });
 
+  let pageActionId, panelId;
   extension.onMessage("next-test", Task.async(function* (expecting = {}) {
+    pageActionId = `${makeWidgetId(extension.id)}-page-action`;
+    panelId = `${makeWidgetId(extension.id)}-panel`;
     let panel = document.getElementById(panelId);
     if (expecting.expectClosed) {
       ok(panel, "Expect panel to exist");
       yield promisePopupShown(panel);
 
       extension.sendMessage("close-popup");
 
       yield promisePopupHidden(panel);
--- a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage.js
@@ -37,16 +37,17 @@ function* loadExtension(options) {
   return extension;
 }
 
 add_task(function* test_inline_options() {
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
 
   let extension = yield loadExtension({
     manifest: {
+      applications: {gecko: {id: "inline_options@tests.mozilla.org"}},
       "options_ui": {
         "page": "options.html",
       },
     },
 
     background: function() {
       let _optionsPromise;
       let awaitOptions = () => {
@@ -130,16 +131,17 @@ add_task(function* test_inline_options()
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_tab_options() {
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
 
   let extension = yield loadExtension({
     manifest: {
+      applications: {gecko: {id: "tab_options@tests.mozilla.org"}},
       "options_ui": {
         "page": "options.html",
         "open_in_tab": true,
       },
     },
 
     background: function() {
       let _optionsPromise;
@@ -224,17 +226,19 @@ add_task(function* test_tab_options() {
   yield extension.awaitFinish("options-ui-tab");
   yield extension.unload();
 
   yield BrowserTestUtils.removeTab(tab);
 });
 
 add_task(function* test_options_no_manifest() {
   let extension = yield loadExtension({
-    manifest: {},
+    manifest: {
+      applications: {gecko: {id: "no_options@tests.mozilla.org"}},
+    },
 
     background: function() {
       browser.test.log("Try to open options page when not specified in the manifest.");
 
       browser.runtime.openOptionsPage().then(
         () => {
           browser.test.fail("Opening options page without one specified in the manifest generated an error");
           browser.test.notifyFail("options-no-manifest");
--- a/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_openOptionsPage_uninstall.js
@@ -37,16 +37,17 @@ function* loadExtension(options) {
   return extension;
 }
 
 add_task(function* test_inline_options_uninstall() {
   let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, "http://example.com/");
 
   let extension = yield loadExtension({
     manifest: {
+      applications: {gecko: {id: "inline_options_uninstall@tests.mozilla.org"}},
       "options_ui": {
         "page": "options.html",
       },
     },
 
     background: function() {
       let _optionsPromise;
       let awaitOptions = () => {
--- a/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
+++ b/browser/components/extensions/test/browser/browser_ext_runtime_setUninstallURL.js
@@ -1,16 +1,17 @@
 "use strict";
 
 let {AddonManager} = Components.utils.import("resource://gre/modules/AddonManager.jsm", {});
 let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
 
 function* makeAndInstallXPI(id, backgroundScript, loadedURL) {
-  let xpi = Extension.generateXPI(id, {
-    background: "(" + backgroundScript.toString() + ")()",
+  let xpi = Extension.generateXPI({
+    manifest: {applications: {gecko: {id}}},
+    background: backgroundScript,
   });
   SimpleTest.registerCleanupFunction(function cleanupXPI() {
     Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
     xpi.remove(false);
   });
 
   let loadPromise = BrowserTestUtils.waitForNewTab(gBrowser, loadedURL);
 
--- a/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
+++ b/testing/mochitest/tests/SimpleTest/ExtensionTestUtils.js
@@ -1,11 +1,11 @@
 var ExtensionTestUtils = {};
 
-ExtensionTestUtils.loadExtension = function(ext, id = null)
+ExtensionTestUtils.loadExtension = function(ext)
 {
   // Cleanup functions need to be registered differently depending on
   // whether we're in browser chrome or plain mochitests.
   var registerCleanup;
   if (typeof registerCleanupFunction != "undefined") {
     registerCleanup = registerCleanupFunction;
   } else {
     registerCleanup = SimpleTest.registerCleanupFunction.bind(SimpleTest);
@@ -78,17 +78,17 @@ ExtensionTestUtils.loadExtension = funct
       } else {
         messageQueue.add([msg, ...args]);
         checkMessages();
       }
 
     },
   };
 
-  var extension = SpecialPowers.loadExtension(id, ext, handler);
+  var extension = SpecialPowers.loadExtension(ext, handler);
 
   registerCleanup(() => {
     if (extension.state == "pending" || extension.state == "running") {
       SimpleTest.ok(false, "Extension left running at test shutdown")
       return extension.unload();
     } else if (extension.state == "unloading") {
       SimpleTest.ok(false, "Extension not fully unloaded at test shutdown")
     }
--- a/testing/specialpowers/content/SpecialPowersObserverAPI.js
+++ b/testing/specialpowers/content/SpecialPowersObserverAPI.js
@@ -565,30 +565,17 @@ SpecialPowersObserverAPI.prototype = {
         sss.removeState(Ci.nsISiteSecurityService.HEADER_HSTS, uri, flags);
       }
 
       case "SPLoadExtension": {
         let {Extension} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
 
         let id = aMessage.data.id;
         let ext = aMessage.data.ext;
-        let extension;
-        if (typeof(ext) == "string") {
-          let target = "resource://testing-common/extensions/" + ext + "/";
-          let resourceHandler = Services.io.getProtocolHandler("resource")
-                                        .QueryInterface(Ci.nsISubstitutingProtocolHandler);
-          let resURI = Services.io.newURI(target, null, null);
-          let uri = Services.io.newURI(resourceHandler.resolveURI(resURI), null, null);
-          extension = new Extension({
-            id,
-            resourceURI: uri
-          });
-        } else {
-          extension = Extension.generate(id, ext);
-        }
+        let extension = Extension.generate(ext);
 
         let resultListener = (...args) => {
           this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testResult", args});
         };
 
         let messageListener = (...args) => {
           args.shift();
           this._sendReply(aMessage, "SPExtensionMessage", {id, type: "testMessage", args});
@@ -602,36 +589,44 @@ SpecialPowersObserverAPI.prototype = {
 
         extension.on("test-message", messageListener);
 
         this._extensions.set(id, extension);
         return undefined;
       }
 
       case "SPStartupExtension": {
-        let {ExtensionData} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
+        let {ExtensionData, Management} = Components.utils.import("resource://gre/modules/Extension.jsm", {});
 
         let id = aMessage.data.id;
         let extension = this._extensions.get(id);
+        let startupListener = (msg, ext) => {
+          if (ext == extension) {
+            this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionSetId", args: [extension.id]});
+            Management.off("startup", startupListener);
+          }
+        };
+        Management.on("startup", startupListener);
 
         // Make sure the extension passes the packaging checks when
         // they're run on a bare archive rather than a running instance,
         // as the add-on manager runs them.
         let extensionData = new ExtensionData(extension.rootURI);
         extensionData.readManifest().then(() => {
           return extensionData.initAllLocales();
         }).then(() => {
           if (extensionData.errors.length) {
             return Promise.reject("Extension contains packaging errors");
           }
           return extension.startup();
         }).then(() => {
           this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionStarted", args: []});
         }).catch(e => {
           dump(`Extension startup failed: ${e}\n${e.stack}`);
+          Management.off("startup", startupListener);
           this._sendReply(aMessage, "SPExtensionMessage", {id, type: "extensionFailed", args: []});
         });
         return undefined;
       }
 
       case "SPExtensionMessage": {
         let id = aMessage.data.id;
         let extension = this._extensions.get(id);
--- a/testing/specialpowers/content/specialpowersAPI.js
+++ b/testing/specialpowers/content/specialpowersAPI.js
@@ -1914,21 +1914,22 @@ SpecialPowersAPI.prototype = {
   removeServiceWorkerDataForExampleDomain: function() {
     this.notifyObserversInParentProcess(null, "browser:purge-domain-data", "example.com");
   },
 
   cleanUpSTSData: function(origin, flags) {
     return this._sendSyncMessage('SPCleanUpSTSData', {origin: origin, flags: flags || 0});
   },
 
-  loadExtension: function(id, ext, handler) {
-    if (!id) {
-      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-      id = uuidGenerator.generateUUID().number;
-    }
+  _nextExtensionID: 0,
+  loadExtension: function(ext, handler) {
+    // Note, this is not the addon is as used by the AddonManager etc,
+    // this is just an identifier used for specialpowers messaging
+    // between this content process and the chrome process.
+    let id = this._nextExtensionID++;
 
     let resolveStartup, resolveUnload, rejectStartup;
     let startupPromise = new Promise((resolve, reject) => {
       resolveStartup = resolve;
       rejectStartup = reject;
     });
     let unloadPromise = new Promise(resolve => { resolveUnload = resolve; });
 
@@ -1937,18 +1938,16 @@ SpecialPowersAPI.prototype = {
     });
 
     handler = Cu.waiveXrays(handler);
     ext = Cu.waiveXrays(ext);
 
     let sp = this;
     let state = "uninitialized";
     let extension = {
-      id,
-
       get state() { return state; },
 
       startup() {
         state = "pending";
         sp._sendAsyncMessage("SPStartupExtension", {id});
         return startupPromise;
       },
 
@@ -1965,16 +1964,18 @@ SpecialPowersAPI.prototype = {
 
     this._sendAsyncMessage("SPLoadExtension", {ext, id});
 
     let listener = (msg) => {
       if (msg.data.id == id) {
         if (msg.data.type == "extensionStarted") {
           state = "running";
           resolveStartup();
+        } else if (msg.data.type == "extensionSetId") {
+          extension.id = msg.data.args[0];
         } else if (msg.data.type == "extensionFailed") {
           state = "failed";
           rejectStartup("startup failed");
         } else if (msg.data.type == "extensionUnloaded") {
           this._removeMessageListener("SPExtensionMessage", listener);
           state = "unloaded";
           resolveUnload();
         } else if (msg.data.type in handler) {
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -63,16 +63,20 @@ XPCOMUtils.defineLazyGetter(this, "requi
   let obj = {};
   Cu.import("resource://devtools/shared/Loader.jsm", obj);
   return obj.require;
 });
 
 Cu.import("resource://gre/modules/ExtensionContent.jsm");
 Cu.import("resource://gre/modules/ExtensionManagement.jsm");
 
+XPCOMUtils.defineLazyServiceGetter(this, "uuidGen",
+                                   "@mozilla.org/uuid-generator;1",
+                                   "nsIUUIDGenerator");
+
 const BASE_SCHEMA = "chrome://extensions/content/schemas/manifest.json";
 const CATEGORY_EXTENSION_SCHEMAS = "webextension-schemas";
 const CATEGORY_EXTENSION_SCRIPTS = "webextension-scripts";
 
 let schemaURLs = new Set();
 
 if (!AppConstants.RELEASE_BUILD) {
   schemaURLs.add("chrome://extensions/content/schemas/experiments.json");
@@ -505,18 +509,17 @@ var UUIDMap = {
     let map = this._read();
 
     if (id in map) {
       return map[id];
     }
 
     let uuid = null;
     if (create) {
-      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-      uuid = uuidGenerator.generateUUID().number;
+      uuid = uuidGen.generateUUID().number;
       uuid = uuid.slice(1, -1); // Strip { and } off the UUID.
 
       map[id] = uuid;
       this._write(map);
     }
     return uuid;
   },
 
@@ -1156,27 +1159,29 @@ this.ExtensionData = class {
  * uninstalles it on shutdown().
  *
  * @param {string} id
  * @param {nsIFile} file
  * @param {nsIURI} rootURI
  * @param {string} installType
  */
 class MockExtension {
-  constructor(id, file, rootURI, installType) {
-    this.id = id;
+  constructor(file, rootURI, installType) {
+    this.id = null;
     this.file = file;
     this.rootURI = rootURI;
     this.installType = installType;
+    this.addon = null;
 
     let promiseEvent = eventName => new Promise(resolve => {
       let onstartup = (msg, extension) => {
-        if (extension.id == this.id) {
+        if (this.addon && extension.id == this.addon.id) {
           Management.off(eventName, onstartup);
 
+          this.id = extension.id;
           this._extension = extension;
           resolve(extension);
         }
       };
       Management.on(eventName, onstartup);
     });
 
     this._extension = null;
@@ -1291,21 +1296,20 @@ this.Extension = class extends Extension
    *     to file names)
    *
    * To make things easier, the value of "background" and "files"[] can
    * be a function, which is converted to source that is run.
    *
    * The generated extension is stored in the system temporary directory,
    * and an nsIFile object pointing to it is returned.
    *
-   * @param {string} id
    * @param {object} data
    * @returns {nsIFile}
    */
-  static generateXPI(id, data) {
+  static generateXPI(data) {
     let manifest = data.manifest;
     if (!manifest) {
       manifest = {};
     }
 
     let files = data.files;
     if (!files) {
       files = {};
@@ -1319,25 +1323,22 @@ this.Extension = class extends Extension
       } else {
         if (!(keys[0] in obj)) {
           obj[keys[0]] = {};
         }
         provide(obj[keys[0]], keys.slice(1), value, override);
       }
     }
 
-    provide(manifest, ["applications", "gecko", "id"], id);
-
     provide(manifest, ["name"], "Generated extension");
     provide(manifest, ["manifest_version"], 2);
     provide(manifest, ["version"], "1.0");
 
     if (data.background) {
-      let uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
-      let bgScript = uuidGenerator.generateUUID().number + ".js";
+      let bgScript = uuidGen.generateUUID().number + ".js";
 
       provide(manifest, ["background", "scripts"], [bgScript], true);
       files[bgScript] = data.background;
     }
 
     provide(files, ["manifest.json"], manifest);
 
     return this.generateZipFile(files);
@@ -1391,32 +1392,43 @@ this.Extension = class extends Extension
 
     return file;
   }
 
   /**
    * Generates a new extension using |Extension.generateXPI|, and initializes a
    * new |Extension| instance which will execute it.
    *
-   * @param {string} id
    * @param {object} data
    * @returns {Extension}
    */
-  static generate(id, data) {
-    let file = this.generateXPI(id, data);
+  static generate(data) {
+    let file = this.generateXPI(data);
 
     flushJarCache(file);
     Services.ppmm.broadcastAsyncMessage("Extension:FlushJarCache", {path: file.path});
 
     let fileURI = Services.io.newFileURI(file);
     let jarURI = Services.io.newURI("jar:" + fileURI.spec + "!/", null, null);
 
     // This may be "temporary" or "permanent".
     if (data.useAddonManager) {
-      return new MockExtension(id, file, jarURI, data.useAddonManager);
+      return new MockExtension(file, jarURI, data.useAddonManager);
+    }
+
+    let id;
+    if (data.manifest) {
+      if (data.manifest.applications && data.manifest.applications.gecko) {
+        id = data.manifest.applications.gecko.id;
+      } else if (data.manifest.browser_specific_settings && data.manifest.browser_specific_settings.gecko) {
+        id = data.manifest.browser_specific_settings.gecko.id;
+      }
+    }
+    if (!id) {
+      id = uuidGen.generateUUID().number;
     }
 
     return new Extension({
       id,
       resourceURI: jarURI,
       cleanupFile: file,
     });
   }
--- a/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
+++ b/toolkit/components/extensions/ExtensionXPCShellUtils.jsm
@@ -15,19 +15,16 @@ XPCOMUtils.defineLazyModuleGetter(this, 
                                   "resource://gre/modules/Extension.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "FileUtils",
                                   "resource://gre/modules/FileUtils.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Schemas",
                                   "resource://gre/modules/Schemas.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "Services",
                                   "resource://gre/modules/Services.jsm");
 
-XPCOMUtils.defineLazyServiceGetter(this, "uuidGenerator",
-                                   "@mozilla.org/uuid-generator;1", "nsIUUIDGenerator");
-
 /* exported ExtensionTestUtils */
 
 let BASE_MANIFEST = Object.freeze({
   "applications": Object.freeze({
     "gecko": Object.freeze({
       "id": "test@web.ext",
     }),
   }),
@@ -272,14 +269,14 @@ var ExtensionTestUtils = {
     });
 
 
     let manager = Cc["@mozilla.org/addons/integration;1"].getService(Ci.nsIObserver)
                                                          .QueryInterface(Ci.nsITimerCallback);
     manager.observe(null, "addons-startup", null);
   },
 
-  loadExtension(data, id = uuidGenerator.generateUUID().number) {
-    let extension = Extension.generate(id, data);
+  loadExtension(data) {
+    let extension = Extension.generate(data);
 
     return new ExtensionWrapper(extension, this.currentScope);
   },
 };
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_background_debug_global.html
@@ -29,45 +29,48 @@ const {
  * debugging is actually working correctly end-to-end.
  */
 
 function backgroundScript() {
   window.testThing = "test!";
   browser.test.notifyPass("background script ran");
 }
 
+const ID = "debug@tests.mozilla.org";
 let extensionData = {
   useAddonManager: "temporary",
   background: "(" + backgroundScript.toString() + ")()",
-  manifest: {},
+  manifest: {
+    applications: {gecko: {id: ID}},
+  },
   files: {},
 };
 
 add_task(function* () {
   let extension = ExtensionTestUtils.loadExtension(extensionData);
   yield extension.startup();
 
   yield extension.awaitFinish("background script ran");
 
   yield new Promise(function(resolve) {
     window.BrowserToolboxProcess.emit("connectionchange", "opened", {
       setAddonOptions(id, options) {
-        if (id === extension.id) {
+        if (id === ID) {
           let context = Cu.waiveXrays(options.global);
           ok(context.chrome, "global context has a chrome object");
           ok(context.browser, "global context has a browser object");
           is("test!", context.testThing, "global context is the background script context");
           resolve();
         }
       },
     });
   });
 
   let addon = yield new Promise((resolve, reject) => {
-    AddonManager.getAddonByID(extension.id, addon => addon ? resolve(addon) : reject());
+    AddonManager.getAddonByID(ID, addon => addon ? resolve(addon) : reject());
   });
 
   ok(addon, `Got the addon wrapper for ${addon.id}`);
 
   function waitForDebugGlobalChanges(times, initialAddonInstanceID) {
     return new Promise((resolve) => {
       AddonManager.addAddonListener({
         count: 0,
--- a/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
+++ b/toolkit/components/extensions/test/mochitest/test_chrome_ext_storage_cleanup.html
@@ -103,55 +103,58 @@ add_task(function* test_uninstall() {
   });
   yield SpecialPowers.pushPrefEnv({
     set: [["extensions.webextensions.keepStorageOnUninstall", true]],
   });
 
   let extension = ExtensionTestUtils.loadExtension({
     background: `(${writeData})()`,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
-  }, ID);
+  });
 
   yield extension.startup();
   yield extension.awaitMessage("finished");
   yield extension.unload();
 
   // Check that we can still see data we wrote to storage but clear the
   // "leave storage" flag so our storaged gets cleared on uninstall.
   // This effectively tests the keepUuidOnUninstall logic, which ensures
   // that when we read storage again and check that it is cleared, that
   // it is actually a meaningful test!
   yield SpecialPowers.popPrefEnv();
   extension = ExtensionTestUtils.loadExtension({
     background: `(${readData})()`,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
-  }, ID);
+  });
 
   yield extension.startup();
   let results = yield extension.awaitMessage("results");
   is(results.matchLocalStorage, true, "localStorage data is still present");
   is(results.matchIDB, true, "indexedDB data is still present");
   is(results.matchBrowserStorage, true, "browser.storage.local data is still present");
 
   yield extension.unload();
 
   // Read again.  This time, our data should be gone.
   extension = ExtensionTestUtils.loadExtension({
     background: `(${readData})()`,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["storage"],
     },
     useAddonManager: "temporary",
-  }, ID);
+  });
 
   yield extension.startup();
   results = yield extension.awaitMessage("results");
   is(results.matchLocalStorage, false, "localStorage data was cleared");
   is(results.matchIDB, false, "indexedDB data was cleared");
   is(results.matchBrowserStorage, false, "browser.storage.local data was cleared");
   yield extension.unload();
 });
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript.html
@@ -40,16 +40,17 @@ add_task(function* test_contentscript() 
   function contentScript() {
     let manifest = browser.runtime.getManifest();
     void manifest.applications.gecko.id;
     chrome.runtime.sendMessage(["chrome-namespace-ok"]);
   }
 
   let extensionData = {
     manifest: {
+      applications: {gecko: {id: "contentscript@tests.mozilla.org"}},
       content_scripts: [
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
           "js": ["content_script_start.js"],
           "run_at": "document_start",
         },
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
--- a/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_contentscript_create_iframe.html
@@ -76,18 +76,20 @@ add_task(function* test_contentscript_cr
     browser.runtime.sendMessage({
       name: "content-script-iframe-loaded",
       availableAPIs,
       manifest,
       testGetManifest,
     });
   }
 
+  const ID = "contentscript@tests.mozilla.org";
   let extensionData = {
     manifest: {
+      applications: {gecko: {id: ID}},
       content_scripts: [
         {
           "matches": ["http://mochi.test/*/file_sample.html"],
           "js": ["content_script.js"],
           "run_at": "document_idle",
         },
       ],
       web_accessible_resources: [
--- a/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
+++ b/toolkit/components/extensions/test/mochitest/test_ext_runtime_id.html
@@ -23,29 +23,30 @@ add_task(function* test_runtime_id() {
     browser.test.sendMessage("content-id", browser.runtime.id);
   }
 
   let uuidGenerator = SpecialPowers.Cc["@mozilla.org/uuid-generator;1"].getService(SpecialPowers.Ci.nsIUUIDGenerator);
   let id = uuidGenerator.generateUUID().number;
 
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
+      applications: {gecko: {id}},
       "content_scripts": [{
         "matches": ["http://mochi.test/*/file_sample.html"],
         "run_at": "document_start",
         "js": ["content_script.js"],
       }],
     },
 
     background: `(${background})()`,
 
     files: {
       "content_script.js": `(${content})()`,
     },
-  }, id);
+  });
 
   yield extension.startup();
 
   let backgroundId = yield extension.awaitMessage("background-id");
   is(backgroundId, id, "runtime.id from background script is correct");
   let win = window.open("file_sample.html");
   let contentId = yield extension.awaitMessage("content-id");
   is(contentId, id, "runtime.id from content script is correct");
--- a/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_experiments.js
@@ -77,27 +77,31 @@ add_task(function* test_experiments_api(
               {"type": "string", "name": "text"},
             ],
           },
         ],
       },
     ],
   });
 
-  let addonFile = Extension.generateXPI("meh@web.extension", {
+  let addonFile = Extension.generateXPI({
     manifest: {
+      applications: {gecko: {id: "meh@web.extension"}},
       permissions: ["experiments.meh"],
     },
 
     background() {
       browser.meh.hello("Here I am");
     },
   });
 
-  let boringAddonFile = Extension.generateXPI("boring@web.extension", {
+  let boringAddonFile = Extension.generateXPI({
+    manifest: {
+      applications: {gecko: {id: "boring@web.extension"}},
+    },
     background() {
       if (browser.meh) {
         browser.meh.hello("Here I should not be");
       }
     },
   });
 
   do_register_cleanup(() => {
--- a/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_json_parser.js
@@ -1,16 +1,16 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
 add_task(function* test_json_parser() {
   const ID = "json@test.web.extension";
 
-  let xpi = Extension.generateXPI(ID, {
+  let xpi = Extension.generateXPI({
     files: {
       "manifest.json": String.raw`{
         // This is a manifest.
         "applications": {"gecko": {"id": "${ID}"}},
         "name": "This \" is // not a comment",
         "version": "0.1\\" // , "description": "This is not a description"
       }`,
     },
--- a/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_localStorage.js
@@ -21,26 +21,27 @@ function backgroundScript() {
       localStorage.clear();
       result = "cleared";
     }
   }
   browser.test.sendMessage("result", result);
   browser.test.notifyPass("localStorage");
 }
 
+const ID = "test-webextension@mozilla.com";
 let extensionData = {
+  manifest: {applications: {gecko: {id: ID}}},
   background: backgroundScript,
 };
 
 add_task(function* test_localStorage() {
-  let id = "test-webextension@mozilla.com";
   const RESULTS = ["item1", "item2", "deleted", "cleared", "item1"];
 
   for (let expected of RESULTS) {
-    let extension = ExtensionTestUtils.loadExtension(extensionData, id);
+    let extension = ExtensionTestUtils.loadExtension(extensionData);
 
     yield extension.startup();
 
     let actual = yield extension.awaitMessage("result");
 
     yield extension.awaitFinish("localStorage");
     yield extension.unload();
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging.js
@@ -83,19 +83,20 @@ add_task(function* test_happy_path() {
       }
     });
     browser.test.sendMessage("ready");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
   const tests = [
     {
       data: "this is a string",
       what: "simple string",
     },
@@ -151,19 +152,20 @@ if (AppConstants.platform == "win") {
         browser.test.sendMessage("done");
       });
       port.postMessage(MSG);
     }
 
     let extension = ExtensionTestUtils.loadExtension({
       background,
       manifest: {
+        applications: {gecko: {id: ID}},
         permissions: ["nativeMessaging"],
       },
-    }, ID);
+    });
 
     yield extension.startup();
     yield extension.awaitMessage("done");
 
     let procCount = yield getSubprocessCount();
     equal(procCount, 1, "subprocess is still running");
     let exitPromise = waitForSubprocessExit();
     yield extension.unload();
@@ -190,19 +192,20 @@ add_task(function* test_sendNativeMessag
       browser.test.assertEq(expected, received, "Received echoed native message");
       browser.test.sendMessage("finished");
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
   yield extension.awaitMessage("finished");
 
   // With sendNativeMessage(), the subprocess should be disconnected
   // after exchanging a single message.
   yield waitForSubprocessExit();
 
@@ -237,19 +240,20 @@ add_task(function* test_disconnect() {
       }
     });
     browser.test.sendMessage("ready");
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
   extension.sendMessage("send", "test");
   let response = yield extension.awaitMessage("message");
   equal(response, "test", "Echoed a string");
 
@@ -290,19 +294,20 @@ add_task(function* test_write_limit() {
     } catch (ex) {
       browser.test.sendMessage("result", ex.message);
     }
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
 
   let errmsg = yield extension.awaitMessage("result");
   notEqual(errmsg, null, "native postMessage() failed for overly large message");
 
   yield extension.unload();
   yield waitForSubprocessExit();
@@ -328,19 +333,20 @@ add_task(function* test_read_limit() {
       browser.test.sendMessage("result", "message");
     });
     port.postMessage(PAYLOAD);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
 
   let result = yield extension.awaitMessage("result");
   equal(result, "disconnected", "native port disconnected on receiving large message");
 
   yield extension.unload();
   yield waitForSubprocessExit();
@@ -409,19 +415,20 @@ add_task(function* test_child_process() 
     port.onMessage.addListener(msg => {
       browser.test.sendMessage("result", msg);
     });
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
 
   let msg = yield extension.awaitMessage("result");
   equal(msg.args.length, 2, "Received one command line argument");
   equal(msg.args[1], getPath("info.json"), "Command line argument is the path to the native host manifest");
   equal(msg.cwd.replace(/^\/private\//, "/"), tmpDir.path,
         "Working directory is the directory containing the native appliation");
@@ -438,19 +445,20 @@ add_task(function* test_stderr() {
       browser.test.sendMessage("finished");
     });
   }
 
   let {messages} = yield promiseConsoleOutput(function* () {
     let extension = ExtensionTestUtils.loadExtension({
       background,
       manifest: {
+        applications: {gecko: {id: ID}},
         permissions: ["nativeMessaging"],
       },
-    }, ID);
+    });
 
     yield extension.startup();
     yield extension.awaitMessage("finished");
     yield extension.unload();
 
     yield waitForSubprocessExit();
   });
 
--- a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_perf.js
@@ -103,19 +103,20 @@ add_task(function* test_round_trip_perf(
             finish();
           }
         });
 
         next();
       });
     },
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
 
   let roundTripTime = Infinity;
   for (let i = 0; i < MAX_RETRIES && roundTripTime > MAX_ROUND_TRIP_TIME_MS; i++) {
     extension.sendMessage("run-tests");
     roundTripTime = yield extension.awaitMessage("result");
   }
--- a/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_native_messaging_unresponsive.js
@@ -57,19 +57,20 @@ add_task(function* test_unresponsive_nat
       browser.test.sendMessage("ready");
     });
     port.postMessage(MSG);
   }
 
   let extension = ExtensionTestUtils.loadExtension({
     background,
     manifest: {
+      applications: {gecko: {id: ID}},
       permissions: ["nativeMessaging"],
     },
-  }, ID);
+  });
 
   yield extension.startup();
   yield extension.awaitMessage("ready");
 
   let procCount = yield getSubprocessCount();
   equal(procCount, 1, "subprocess is running");
 
   let exitPromise = waitForSubprocessExit();
--- a/toolkit/components/extensions/test/xpcshell/test_locale_data.js
+++ b/toolkit/components/extensions/test/xpcshell/test_locale_data.js
@@ -4,17 +4,18 @@ Cu.import("resource://gre/modules/Extens
 
 /* globals ExtensionData */
 
 const uuidGenerator = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator);
 
 function* generateAddon(data) {
   let id = uuidGenerator.generateUUID().number;
 
-  let xpi = Extension.generateXPI(id, data);
+  data = Object.assign({applications: {gecko: {id}}}, data);
+  let xpi = Extension.generateXPI(data);
   do_register_cleanup(() => {
     Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
     xpi.remove(false);
   });
 
   let fileURI = Services.io.newFileURI(xpi);
   let jarURI = NetUtil.newURI(`jar:${fileURI.spec}!/`);
 
--- a/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
+++ b/toolkit/mozapps/extensions/test/browser/browser_inlinesettings_browser.js
@@ -11,17 +11,21 @@ var gOtherAddon;
 var gManagerWindow;
 var gCategoryUtilities;
 
 var installedAddons = [];
 
 function installAddon(details) {
   let id = Cc["@mozilla.org/uuid-generator;1"].getService(Ci.nsIUUIDGenerator)
                                               .generateUUID().number;
-  let xpi = Extension.generateXPI(id, details);
+  if (!details.manifest) {
+    details.manifest = {};
+  }
+  details.manifest.applications = {gecko: {id}};
+  let xpi = Extension.generateXPI(details);
 
   return AddonManager.installTemporaryAddon(xpi).then(addon => {
     SimpleTest.registerCleanupFunction(function() {
       addon.uninstall();
 
       Services.obs.notifyObservers(xpi, "flush-cache-entry", null);
       xpi.remove(false);
     });
--- a/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/head_addons.js
@@ -1280,22 +1280,17 @@ function createTempXPIFile(aData, aExtra
  * returns the nsIFile for it. The file will be deleted when the test completes.
  *
  * @param   aData
  *          The object holding data about the add-on, as expected by
  *          |Extension.generateXPI|.
  * @return  A file pointing to the created XPI file
  */
 function createTempWebExtensionFile(aData) {
-  if (!aData.id) {
-    const uuidGenerator = AM_Cc["@mozilla.org/uuid-generator;1"].getService(AM_Ci.nsIUUIDGenerator);
-    aData.id = uuidGenerator.generateUUID().number;
-  }
-
-  let file = Extension.generateXPI(aData.id, aData);
+  let file = Extension.generateXPI(aData);
   temp_xpis.push(file);
   return file;
 }
 
 /**
  * Sets the last modified time of the extension, usually to trigger an update
  * of its metadata. If the extension is unpacked, this function assumes that
  * the extension contains only the install.rdf file.
--- a/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_update_webextensions.js
@@ -56,18 +56,19 @@ var checkUpdates = Task.async(function* 
           obj[key] = {};
       obj = obj[key];
     }
 
     if (!(prop in obj))
       obj[prop] = value;
   }
 
-  provide(aData, "addon.id", uuidGenerator.generateUUID().number);
-  let id = aData.addon.id;
+  let id = uuidGenerator.generateUUID().number;
+  provide(aData, "addon.id", id);
+  provide(aData, "addon.manifest.applications.gecko.id", id);
 
   let updatePath = `/updates/${id}.json`.replace(/[{}]/g, "");
   let updateUrl = `http://localhost:${gPort}${updatePath}`
 
   let addonData = { updates: [] };
   let manifestJSON = {
     addons: { [id]: addonData }
   };
@@ -77,16 +78,17 @@ var checkUpdates = Task.async(function* 
   let awaitInstall = promiseInstallWebExtension(aData.addon);
 
 
   for (let version of Object.keys(aData.updates)) {
     let update = aData.updates[version];
     update.version = version;
 
     provide(update, "addon.id", id);
+    provide(update, "addon.manifest.applications.gecko.id", id);
     let addon = update.addon;
 
     delete update.addon;
 
     let file;
     if (addon.rdf) {
       provide(addon, "version", version);
       provide(addon, "targetApplications", [{id: TOOLKIT_ID,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_webextension.js
@@ -28,18 +28,16 @@ function promiseAddonStartup() {
 }
 
 function promiseInstallWebExtension(aData) {
   let addonFile = createTempWebExtensionFile(aData);
 
   return promiseInstallAllFiles([addonFile]).then(() => {
     Services.obs.notifyObservers(addonFile, "flush-cache-entry", null);
     return promiseAddonStartup();
-  }).then(() => {
-    return promiseAddonByID(aData.id);
   });
 }
 
 add_task(function*() {
   equal(GlobalManager.extensionMap.size, 0);
 
   yield Promise.all([
     promiseInstallAllFiles([do_get_addon("webextension_1")], true),
@@ -259,59 +257,65 @@ add_task(function*() {
 
   yield promiseRestartManager();
 });
 
 // Test that the "options_ui" manifest section is processed correctly.
 add_task(function* test_options_ui() {
   let OPTIONS_RE = /^moz-extension:\/\/[\da-f]{8}-[\da-f]{4}-[\da-f]{4}-[\da-f]{4}-[\da-f]{12}\/options\.html$/;
 
-  let addon = yield promiseInstallWebExtension({
+  const ID = "webextension@tests.mozilla.org";
+  yield promiseInstallWebExtension({
     manifest: {
+      applications: {gecko: {id: ID}},
       "options_ui": {
         "page": "options.html",
       },
     },
   });
 
+  let addon = yield promiseAddonByID(ID);
   equal(addon.optionsType, AddonManager.OPTIONS_TYPE_INLINE_BROWSER,
         "Addon should have an INLINE_BROWSER options type");
 
   ok(OPTIONS_RE.test(addon.optionsURL),
      "Addon should have a moz-extension: options URL for /options.html");
 
   addon.uninstall();
 
-  addon = yield promiseInstallWebExtension({
+  const ID2 = "webextension2@tests.mozilla.org";
+  yield promiseInstallWebExtension({
     manifest: {
+      applications: {gecko: {id: ID2}},
       "options_ui": {
         "page": "options.html",
         "open_in_tab": true,
       },
     },
   });
 
+  addon = yield promiseAddonByID(ID2);
   equal(addon.optionsType, AddonManager.OPTIONS_TYPE_TAB,
         "Addon should have a TAB options type");
 
   ok(OPTIONS_RE.test(addon.optionsURL),
      "Addon should have a moz-extension: options URL for /options.html");
 
   addon.uninstall();
 });
 
 // Test that experiments permissions add the appropriate dependencies.
 add_task(function* test_experiments_dependencies() {
   if (AppConstants.RELEASE_BUILD)
     // Experiments are not enabled on release builds.
     return;
 
   let addonFile = createTempWebExtensionFile({
-    id: "meh@experiment",
     manifest: {
+      applications: {gecko: {id: "meh@experiment"}},
       "permissions": ["experiments.meh"],
     },
   });
 
   yield promiseInstallAllFiles([addonFile]);
 
   let addon = yield new Promise(resolve => AddonManager.getAddonByID("meh@experiment", resolve));