Merge m-i to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 17 Dec 2016 20:12:25 -0800
changeset 326308 a3ce1fce4f15616f66ac328e4a562d0117c93a0d
parent 326303 5baae61449c43851c961a83babe18adaa28e3c6b (current diff)
parent 326307 f22c6dc53276d9cb1e7365e47dc52657facc5e48 (diff)
child 326309 9eaabf883c5e7c23307cbd3a547823c1d4214283
child 326334 23eeffe5a9e96ec6acb2c1d37a6a352d23f72447
child 326350 ad85a32325fb0867bd384965285bc71d92c38984
push id31096
push userphilringnalda@gmail.com
push dateSun, 18 Dec 2016 04:12:32 +0000
treeherdermozilla-central@a3ce1fce4f15 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone53.0a1
first release with
nightly linux32
a3ce1fce4f15 / 53.0a1 / 20161218030213 / files
nightly linux64
a3ce1fce4f15 / 53.0a1 / 20161218030213 / files
nightly mac
a3ce1fce4f15 / 53.0a1 / 20161218030213 / files
nightly win32
a3ce1fce4f15 / 53.0a1 / 20161218030213 / files
nightly win64
a3ce1fce4f15 / 53.0a1 / 20161218030213 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge m-i to m-c, a=merge MozReview-Commit-ID: 9jotkaJuE00
--- a/dom/base/test/mochitest.ini
+++ b/dom/base/test/mochitest.ini
@@ -650,17 +650,17 @@ skip-if = (toolkit == 'android') # Andro
 [test_iframe_referrer.html]
 [test_iframe_referrer_changing.html]
 [test_iframe_referrer_invalid.html]
 [test_Image_constructor.html]
 [test_img_referrer.html]
 [test_innersize_scrollport.html]
 [test_integer_attr_with_leading_zero.html]
 [test_intersectionobservers.html]
-skip-if = (os == "android") # Timing issues
+skip-if = true # Track Bug 1320704
 [test_link_prefetch.html]
 skip-if = !e10s # Track Bug 1281415
 [test_link_stylesheet.html]
 [test_messagemanager_targetchain.html]
 [test_meta_viewport0.html]
 skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
 [test_meta_viewport1.html]
 skip-if = (os != 'android')    # meta-viewport tag support is mobile-only
--- a/dom/tests/mochitest/general/test_interfaces.html
+++ b/dom/tests/mochitest/general/test_interfaces.html
@@ -610,19 +610,19 @@ var interfaceNamesInGlobalScope =
     {name: "ImageCaptureErrorEvent", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "ImageData",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InputEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "InstallTrigger",
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "IntersectionObserver",
+    {name: "IntersectionObserver", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
-    "IntersectionObserverEntry",
+    {name: "IntersectionObserverEntry", disabled: true},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     "KeyboardEvent",
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffectReadOnly", release: false},
 // IMPORTANT: Do not change this list without review from a DOM peer!
     {name: "KeyframeEffect", release: false},
--- a/modules/libpref/init/all.js
+++ b/modules/libpref/init/all.js
@@ -5548,9 +5548,8 @@ pref("dom.storageManager.enabled", false
 // be blocked (automatically canceled) for that page. The counter resets
 // when the page is reloaded. To turn this feature off, just set the limit to 0.
 pref("prompts.authentication_dialog_abuse_limit", 3);
 
 // Enable the Storage management in about:preferences and persistent-storage permission request
 // To enable the DOM implementation, turn on "dom.storageManager.enabled"
 pref("browser.storageManager.enabled", false);
 
-pref("dom.IntersectionObserver.enabled", true);
--- a/toolkit/components/extensions/.eslintrc.js
+++ b/toolkit/components/extensions/.eslintrc.js
@@ -11,17 +11,23 @@ module.exports = { // eslint-disable-lin
     "Cu": true,
     "dump": true,
     "TextDecoder": false,
     "TextEncoder": false,
     // Specific to WebExtensions:
     "Extension": true,
     "ExtensionManagement": true,
     "extensions": true,
+    "getContainerForCookieStoreId": true,
+    "getCookieStoreIdForContainer": true,
     "global": true,
+    "isContainerCookieStoreId": true,
+    "isDefaultCookieStoreId": true,
+    "isPrivateCookieStoreId": true,
+    "isValidCookieStoreId": true,
     "NetUtil": true,
     "openOptionsPage": true,
     "require": false,
     "runSafe": true,
     "runSafeSync": true,
     "runSafeSyncWithoutClone": true,
     "Services": true,
     "TabManager": true,
--- a/toolkit/components/extensions/Extension.jsm
+++ b/toolkit/components/extensions/Extension.jsm
@@ -424,20 +424,31 @@ this.ExtensionData = class {
         // Do not override the add-on id that has been already assigned.
         if (!this.id && this.manifest.applications.gecko.id) {
           this.id = this.manifest.applications.gecko.id;
         }
       } catch (e) {
         // Errors are handled by the type checks above.
       }
 
+      let containersEnabled = true;
+      try {
+        containersEnabled = Services.prefs.getBoolPref("privacy.userContext.enabled");
+      } catch (e) {
+        // If we fail here, we are in some xpcshell test.
+      }
+
       let permissions = this.manifest.permissions || [];
 
       let whitelist = [];
       for (let perm of permissions) {
+        if (perm == "contextualIdentities" && !containersEnabled) {
+          continue;
+        }
+
         this.permissions.add(perm);
 
         let match = /^(\w+)(?:\.(\w+)(?:\.\w+)*)?$/.exec(perm);
         if (!match) {
           whitelist.push(perm);
         } else if (match[1] == "experiments" && match[2]) {
           this.apiNames.add(match[2]);
         }
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -0,0 +1,109 @@
+"use strict";
+
+const {interfaces: Ci, utils: Cu} = Components;
+
+XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
+                                  "resource://gre/modules/ContextualIdentityService.jsm");
+
+function convert(identity) {
+  let result = {
+    name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
+    icon: identity.icon,
+    color: identity.color,
+    cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
+  };
+
+  return result;
+}
+
+extensions.registerSchemaAPI("contextualIdentities", "addon_parent", context => {
+  let self = {
+    contextualIdentities: {
+      get(cookieStoreId) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        return Promise.resolve(convert(identity));
+      },
+
+      query(details) {
+        let identities = [];
+        ContextualIdentityService.getIdentities().forEach(identity => {
+          if (details.name &&
+              ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
+            return;
+          }
+
+          identities.push(convert(identity));
+        });
+
+        return Promise.resolve(identities);
+      },
+
+      create(details) {
+        let identity = ContextualIdentityService.create(details.name,
+                                                        details.icon,
+                                                        details.color);
+        return Promise.resolve(convert(identity));
+      },
+
+      update(cookieStoreId, details) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        if (!identity) {
+          return Promise.resolve(null);
+        }
+
+        if (details.name !== null) {
+          identity.name = details.name;
+        }
+
+        if (details.color !== null) {
+          identity.color = details.color;
+        }
+
+        if (details.icon !== null) {
+          identity.icon = details.icon;
+        }
+
+        if (!ContextualIdentityService.update(identity.userContextId,
+                                              identity.name, identity.icon,
+                                              identity.color)) {
+          return Promise.resolve(null);
+        }
+
+        return Promise.resolve(convert(identity));
+      },
+
+      remove(cookieStoreId) {
+        let containerId = getContainerForCookieStoreId(cookieStoreId);
+        if (!containerId) {
+          return Promise.resolve(null);
+        }
+
+        let identity = ContextualIdentityService.getIdentityFromId(containerId);
+        if (!identity) {
+          return Promise.resolve(null);
+        }
+
+        // We have to create the identity object before removing it.
+        let convertedIdentity = convert(identity);
+
+        if (!ContextualIdentityService.remove(identity.userContextId)) {
+          return Promise.resolve(null);
+        }
+
+        return Promise.resolve(convertedIdentity);
+      },
+    },
+  };
+
+  return self;
+});
--- a/toolkit/components/extensions/ext-cookies.js
+++ b/toolkit/components/extensions/ext-cookies.js
@@ -17,17 +17,17 @@ var PRIVATE_STORE = "firefox-private";
 var CONTAINER_STORE = "firefox-container-";
 
 global.getCookieStoreIdForTab = function(data, tab) {
   if (data.incognito) {
     return PRIVATE_STORE;
   }
 
   if (tab.userContextId) {
-    return CONTAINER_STORE + tab.userContextId;
+    return getCookieStoreIdForContainer(tab.userContextId);
   }
 
   return DEFAULT_STORE;
 };
 
 global.isPrivateCookieStoreId = function(storeId) {
   return storeId == PRIVATE_STORE;
 };
@@ -35,33 +35,37 @@ global.isPrivateCookieStoreId = function
 global.isDefaultCookieStoreId = function(storeId) {
   return storeId == DEFAULT_STORE;
 };
 
 global.isContainerCookieStoreId = function(storeId) {
   return storeId !== null && storeId.startsWith(CONTAINER_STORE);
 };
 
+global.getCookieStoreIdForContainer = function(containerId) {
+  return CONTAINER_STORE + containerId;
+};
+
 global.getContainerForCookieStoreId = function(storeId) {
-  if (!global.isContainerCookieStoreId(storeId)) {
+  if (!isContainerCookieStoreId(storeId)) {
     return null;
   }
 
   let containerId = storeId.substring(CONTAINER_STORE.length);
   if (ContextualIdentityService.getIdentityFromId(containerId)) {
     return parseInt(containerId, 10);
   }
 
   return null;
 };
 
 global.isValidCookieStoreId = function(storeId) {
-  return global.isDefaultCookieStoreId(storeId) ||
-         global.isPrivateCookieStoreId(storeId) ||
-         global.isContainerCookieStoreId(storeId);
+  return isDefaultCookieStoreId(storeId) ||
+         isPrivateCookieStoreId(storeId) ||
+         isContainerCookieStoreId(storeId);
 };
 
 function convert({cookie, isPrivate}) {
   let result = {
     name: cookie.name,
     value: cookie.value,
     domain: cookie.host,
     hostOnly: !cookie.isDomain,
@@ -71,17 +75,17 @@ function convert({cookie, isPrivate}) {
     session: cookie.isSession,
   };
 
   if (!cookie.isSession) {
     result.expirationDate = cookie.expiry;
   }
 
   if (cookie.originAttributes.userContextId) {
-    result.storeId = CONTAINER_STORE + cookie.originAttributes.userContextId;
+    result.storeId = getCookieStoreIdForContainer(cookie.originAttributes.userContextId);
   } else if (cookie.originAttributes.privateBrowsingId || isPrivate) {
     result.storeId = PRIVATE_STORE;
   } else {
     result.storeId = DEFAULT_STORE;
   }
 
   return result;
 }
@@ -192,27 +196,27 @@ function* query(detailsIn, props, contex
 
   if ("domain" in details) {
     details.domain = details.domain.toLowerCase().replace(/^\./, "");
   }
 
   let userContextId = 0;
   let isPrivate = context.incognito;
   if (details.storeId) {
-    if (!global.isValidCookieStoreId(details.storeId)) {
+    if (!isValidCookieStoreId(details.storeId)) {
       return;
     }
 
-    if (global.isDefaultCookieStoreId(details.storeId)) {
+    if (isDefaultCookieStoreId(details.storeId)) {
       isPrivate = false;
-    } else if (global.isPrivateCookieStoreId(details.storeId)) {
+    } else if (isPrivateCookieStoreId(details.storeId)) {
       isPrivate = true;
-    } else if (global.isContainerCookieStoreId(details.storeId)) {
+    } else if (isContainerCookieStoreId(details.storeId)) {
       isPrivate = false;
-      userContextId = global.getContainerForCookieStoreId(details.storeId);
+      userContextId = getContainerForCookieStoreId(details.storeId);
       if (!userContextId) {
         return;
       }
     }
   }
 
   let storeId = DEFAULT_STORE;
   if (isPrivate) {
@@ -363,22 +367,22 @@ extensions.registerSchemaAPI("cookies", 
         let name = details.name !== null ? details.name : "";
         let value = details.value !== null ? details.value : "";
         let secure = details.secure !== null ? details.secure : false;
         let httpOnly = details.httpOnly !== null ? details.httpOnly : false;
         let isSession = details.expirationDate === null;
         let expiry = isSession ? Number.MAX_SAFE_INTEGER : details.expirationDate;
         let isPrivate = context.incognito;
         let userContextId = 0;
-        if (global.isDefaultCookieStoreId(details.storeId)) {
+        if (isDefaultCookieStoreId(details.storeId)) {
           isPrivate = false;
-        } else if (global.isPrivateCookieStoreId(details.storeId)) {
+        } else if (isPrivateCookieStoreId(details.storeId)) {
           isPrivate = true;
-        } else if (global.isContainerCookieStoreId(details.storeId)) {
-          let containerId = global.getContainerForCookieStoreId(details.storeId);
+        } else if (isContainerCookieStoreId(details.storeId)) {
+          let containerId = getContainerForCookieStoreId(details.storeId);
           if (containerId === null) {
             return Promise.reject({message: `Illegal storeId: ${details.storeId}`});
           }
           isPrivate = false;
           userContextId = containerId;
         } else if (details.storeId !== null) {
           return Promise.reject({message: "Unknown storeId"});
         }
--- a/toolkit/components/extensions/extensions-toolkit.manifest
+++ b/toolkit/components/extensions/extensions-toolkit.manifest
@@ -1,11 +1,12 @@
 # scripts
 category webextension-scripts alarms chrome://extensions/content/ext-alarms.js
 category webextension-scripts backgroundPage chrome://extensions/content/ext-backgroundPage.js
+category webextension-scripts contextualIdentities chrome://extensions/content/ext-contextualIdentities.js
 category webextension-scripts cookies chrome://extensions/content/ext-cookies.js
 category webextension-scripts downloads chrome://extensions/content/ext-downloads.js
 category webextension-scripts management chrome://extensions/content/ext-management.js
 category webextension-scripts notifications chrome://extensions/content/ext-notifications.js
 category webextension-scripts i18n chrome://extensions/content/ext-i18n.js
 category webextension-scripts idle chrome://extensions/content/ext-idle.js
 category webextension-scripts webRequest chrome://extensions/content/ext-webRequest.js
 category webextension-scripts webNavigation chrome://extensions/content/ext-webNavigation.js
@@ -36,16 +37,17 @@ category webextension-scripts-addon i18n
 category webextension-scripts-addon identity chrome://extensions/content/ext-c-identity.js
 #endif
 category webextension-scripts-addon runtime chrome://extensions/content/ext-c-runtime.js
 category webextension-scripts-addon test chrome://extensions/content/ext-c-test.js
 category webextension-scripts-addon storage chrome://extensions/content/ext-c-storage.js
 
 # schemas
 category webextension-schemas alarms chrome://extensions/content/schemas/alarms.json
+category webextension-schemas contextualIdentities chrome://extensions/content/schemas/contextual_identities.json
 category webextension-schemas cookies chrome://extensions/content/schemas/cookies.json
 category webextension-schemas downloads chrome://extensions/content/schemas/downloads.json
 category webextension-schemas events chrome://extensions/content/schemas/events.json
 category webextension-schemas extension chrome://extensions/content/schemas/extension.json
 category webextension-schemas extension_types chrome://extensions/content/schemas/extension_types.json
 category webextension-schemas i18n chrome://extensions/content/schemas/i18n.json
 #ifndef ANDROID
 category webextension-schemas identity chrome://extensions/content/schemas/identity.json
--- a/toolkit/components/extensions/jar.mn
+++ b/toolkit/components/extensions/jar.mn
@@ -2,16 +2,17 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/ext-alarms.js
     content/extensions/ext-backgroundPage.js
     content/extensions/ext-browser-content.js
+    content/extensions/ext-contextualIdentities.js
     content/extensions/ext-cookies.js
     content/extensions/ext-downloads.js
     content/extensions/ext-management.js
     content/extensions/ext-notifications.js
     content/extensions/ext-i18n.js
     content/extensions/ext-idle.js
     content/extensions/ext-webRequest.js
     content/extensions/ext-webNavigation.js
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+[
+  {
+    "namespace": "manifest",
+    "types": [
+      {
+        "$extend": "Permission",
+        "choices": [{
+          "type": "string",
+          "enum": [
+            "contextualIdentities"
+          ]
+        }]
+      }
+    ]
+  },
+  {
+    "namespace": "contextualIdentities",
+    "description": "Use the <code>browser.contextualIdentities</code> API to query and modify contextual identity, also called as containers.",
+    "permissions": ["contextualIdentities"],
+    "types": [
+      {
+        "id": "ContextualIdentity",
+        "type": "object",
+        "description": "Represents information about a contextual identity.",
+        "properties": {
+          "name": {"type": "string", "description": "The name of the contextual identity."},
+          "icon": {"type": "string", "description": "The icon of the contextual identity."},
+          "color": {"type": "string", "description": "The color of the contextual identity."},
+          "cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
+        }
+      }
+    ],
+    "functions": [
+      {
+        "name": "get",
+        "type": "function",
+        "description": "Retrieves information about a single contextual identity.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          }
+        ]
+      },
+      {
+        "name": "query",
+        "type": "function",
+        "description": "Retrieves all contextual identities",
+        "async": true,
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Information to filter the contextual identities being retrieved.",
+            "properties": {
+              "name": {"type": "string", "optional": true, "description": "Filters the contextual identity by name."}
+            }
+          }
+        ]
+      },
+      {
+        "name": "create",
+        "type": "function",
+        "description": "Creates a contextual identity with the given data.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Details about the contextual identity being created.",
+            "properties": {
+              "name": {"type": "string", "optional": false, "description": "The name of the contextual identity." },
+              "color": {"type": "string", "optional": false, "description": "The color of the contextual identity." },
+              "icon": {"type": "string", "optional": false, "description": "The icon of the contextual identity." }
+            }
+          }
+        ]
+      },
+      {
+        "name": "update",
+        "type": "function",
+        "description": "Updates a contextual identity with the given data.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          },
+          {
+            "type": "object",
+            "name": "details",
+            "description": "Details about the contextual identity being created.",
+            "properties": {
+              "name": {"type": "string", "optional": true, "description": "The name of the contextual identity." },
+              "color": {"type": "string", "optional": true, "description": "The color of the contextual identity." },
+              "icon": {"type": "string", "optional": true, "description": "The icon of the contextual identity." }
+            }
+          }
+        ]
+      },
+      {
+        "name": "remove",
+        "type": "function",
+        "description": "Deletes a contetual identity by its cookie Store ID.",
+        "async": true,
+        "parameters": [
+          {
+            "type": "string",
+            "name": "cookieStoreId",
+            "description": "The ID of the contextual identity cookie store. "
+          }
+        ]
+      }
+    ]
+  }
+]
--- a/toolkit/components/extensions/schemas/jar.mn
+++ b/toolkit/components/extensions/schemas/jar.mn
@@ -1,15 +1,16 @@
 # This Source Code Form is subject to the terms of the Mozilla Public
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 toolkit.jar:
 % content extensions %content/extensions/
     content/extensions/schemas/alarms.json
+    content/extensions/schemas/contextual_identities.json
     content/extensions/schemas/cookies.json
     content/extensions/schemas/downloads.json
     content/extensions/schemas/events.json
     content/extensions/schemas/experiments.json
     content/extensions/schemas/extension.json
     content/extensions/schemas/extension_types.json
     content/extensions/schemas/i18n.json
 #ifndef ANDROID
new file mode 100644
--- /dev/null
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -0,0 +1,124 @@
+"use strict";
+
+do_get_profile();
+
+add_task(function* test_contextualIdentities_without_permissions() {
+  function backgroundScript() {
+    browser.test.assertTrue(!browser.contextualIdentities,
+                            "contextualIdentities API is not available when the contextualIdentities permission is not required");
+    browser.test.notifyPass("contextualIdentities_permission");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: [],
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities_permission");
+  yield extension.unload();
+});
+
+
+add_task(function* test_contextualIdentity_no_containers() {
+  function backgroundScript() {
+    browser.test.assertTrue(!browser.contextualIdentities,
+                            "contextualIdentities API is not available when the containers are disabled");
+    browser.test.notifyPass("contextualIdentities_pref");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["contextualIdentities"],
+    },
+  });
+
+  Services.prefs.setBoolPref("privacy.userContext.enabled", false);
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities_pref");
+  yield extension.unload();
+
+  Services.prefs.clearUserPref("privacy.userContext.enabled");
+});
+
+add_task(function* test_contextualIdentity_with_permissions() {
+  async function backgroundScript() {
+    let ci = await browser.contextualIdentities.get("foobar");
+    browser.test.assertEq(null, ci, "No identity should be returned here");
+
+    ci = await browser.contextualIdentities.get("firefox-container-1");
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertTrue("name" in ci, "We have an identity.name");
+    browser.test.assertTrue("color" in ci, "We have an identity.color");
+    browser.test.assertTrue("icon" in ci, "We have an identity.icon");
+    browser.test.assertEq("Personal", ci.name, "identity.name is correct");
+    browser.test.assertEq("firefox-container-1", ci.cookieStoreId, "identity.cookieStoreId is correct");
+
+    let cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(4, cis.length, "by default we should have 4 containers");
+
+    cis = await browser.contextualIdentities.query({name: "Personal"});
+    browser.test.assertEq(1, cis.length, "by default we should have 1 container called Personal");
+
+    cis = await browser.contextualIdentities.query({name: "foobar"});
+    browser.test.assertEq(0, cis.length, "by default we should have 0 container called foobar");
+
+    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "icon"});
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("foobar", ci.name, "identity.name is correct");
+    browser.test.assertEq("red", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+    browser.test.assertTrue(!!ci.cookieStoreId, "identity.cookieStoreId is correct");
+
+    ci = await browser.contextualIdentities.get(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("foobar", ci.name, "identity.name is correct");
+    browser.test.assertEq("red", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon", ci.icon, "identity.icon is correct");
+
+    cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(5, cis.length, "now we have 5 identities");
+
+    ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "icon icon"});
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    browser.test.assertEq("blue", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+
+    ci = await browser.contextualIdentities.get(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    browser.test.assertEq("blue", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+
+    ci = await browser.contextualIdentities.remove(ci.cookieStoreId);
+    browser.test.assertTrue(!!ci, "We have an identity");
+    browser.test.assertEq("barfoo", ci.name, "identity.name is correct");
+    browser.test.assertEq("blue", ci.color, "identity.color is correct");
+    browser.test.assertEq("icon icon", ci.icon, "identity.icon is correct");
+
+    cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(4, cis.length, "we are back to 4 identities");
+
+    browser.test.notifyPass("contextualIdentities");
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    background: `(${backgroundScript})()`,
+    manifest: {
+      permissions: ["contextualIdentities"],
+    },
+  });
+
+  Services.prefs.setBoolPref("privacy.userContext.enabled", true);
+
+  yield extension.startup();
+  yield extension.awaitFinish("contextualIdentities");
+  yield extension.unload();
+
+  Services.prefs.clearUserPref("privacy.userContext.enabled");
+});
--- a/toolkit/components/extensions/test/xpcshell/xpcshell.ini
+++ b/toolkit/components/extensions/test/xpcshell/xpcshell.ini
@@ -20,16 +20,18 @@ tags = webextensions
 [test_ext_background_global_history.js]
 skip-if = os == "android" # Android does not use Places for history.
 [test_ext_background_private_browsing.js]
 [test_ext_background_runtime_connect_params.js]
 [test_ext_background_sub_windows.js]
 [test_ext_background_window_properties.js]
 skip-if = os == "android"
 [test_ext_contexts.js]
+[test_ext_contextual_identities.js]
+skip-if = os == "android" # Containers are not exposed to android.
 [test_ext_downloads.js]
 [test_ext_downloads_download.js]
 skip-if = os == "android"
 [test_ext_downloads_misc.js]
 skip-if = os == "android"
 [test_ext_downloads_search.js]
 skip-if = os == "android"
 [test_ext_experiments.js]