Bug 1339610 - Web extension API for container icon and colors. r=baku,kmag
authorJonathan Kingston <jkt@mozilla.com>
Sun, 27 Aug 2017 00:47:02 +0100
changeset 428512 34d6906c255925a1b1e3a223b2b9f804f4ebf590
parent 428511 d08ea6b954941a493134160669bbaa384e6520e2
child 428513 d281f0c4644ec66af41d50825a830d14c543b603
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersbaku, kmag
bugs1339610
milestone57.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1339610 - Web extension API for container icon and colors. r=baku,kmag MozReview-Commit-ID: BosKoxM8FMZ
toolkit/components/extensions/ext-contextualIdentities.js
toolkit/components/extensions/schemas/contextual_identities.json
toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
--- a/toolkit/components/extensions/ext-contextualIdentities.js
+++ b/toolkit/components/extensions/ext-contextualIdentities.js
@@ -5,42 +5,98 @@
 
 XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService",
                                   "resource://gre/modules/ContextualIdentityService.jsm");
 XPCOMUtils.defineLazyPreferenceGetter(this, "containersEnabled",
                                       "privacy.userContext.enabled");
 
 Cu.import("resource://gre/modules/ExtensionPreferencesManager.jsm");
 
+var {
+  ExtensionError,
+} = ExtensionUtils;
+
 const CONTAINER_PREF_INSTALL_DEFAULTS = {
   "privacy.userContext.enabled": true,
   "privacy.userContext.longPressBehavior": 2,
   "privacy.userContext.ui.enabled": true,
   "privacy.usercontext.about_newtab_segregation.enabled": true,
 };
 
 const CONTAINERS_ENABLED_SETTING_NAME = "privacy.containers";
 
+const CONTAINER_COLORS = new Map([
+  ["blue", "#37adff"],
+  ["turquoise", "#00c79a"],
+  ["green", "#51cd00"],
+  ["yellow", "#ffcb00"],
+  ["orange", "#ff9f00"],
+  ["red", "#ff613d"],
+  ["pink", "#ff4bda"],
+  ["purple", "#af51f5"],
+]);
+
+const CONTAINER_ICONS = new Set([
+  "briefcase",
+  "cart",
+  "circle",
+  "dollar",
+  "fingerprint",
+  "gift",
+  "vacation",
+  "food",
+  "fruit",
+  "pet",
+  "tree",
+  "chill",
+]);
+
+function getContainerIcon(iconName) {
+  if (!CONTAINER_ICONS.has(iconName)) {
+    throw new ExtensionError(`Invalid icon ${iconName} for container`);
+  }
+  return `resource://usercontext-content/${iconName}.svg`;
+}
+
+function getContainerColor(colorName) {
+  if (!CONTAINER_COLORS.has(colorName)) {
+    throw new ExtensionError(`Invalid color name ${colorName} for container`);
+  }
+  return CONTAINER_COLORS.get(colorName);
+}
+
 const convertIdentity = identity => {
   let result = {
     name: ContextualIdentityService.getUserContextLabel(identity.userContextId),
     icon: identity.icon,
+    iconUrl: getContainerIcon(identity.icon),
     color: identity.color,
+    colorCode: getContainerColor(identity.color),
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
 const convertIdentityFromObserver = wrappedIdentity => {
   let identity = wrappedIdentity.wrappedJSObject;
+  let iconUrl, colorCode;
+  try {
+    iconUrl = getContainerIcon(identity.icon);
+    colorCode = getContainerColor(identity.color);
+  } catch (e) {
+    return null;
+  }
+
   let result = {
     name: identity.name,
     icon: identity.icon,
+    iconUrl,
     color: identity.color,
+    colorCode,
     cookieStoreId: getCookieStoreIdForContainer(identity.userContextId),
   };
 
   return result;
 };
 
 ExtensionPreferencesManager.addSetting(CONTAINERS_ENABLED_SETTING_NAME, {
   prefNames: Object.keys(CONTAINER_PREF_INSTALL_DEFAULTS),
@@ -65,47 +121,51 @@ this.contextualIdentities = class extend
     if (extension.hasPermission("contextualIdentities")) {
       ExtensionPreferencesManager.setSetting(extension, CONTAINERS_ENABLED_SETTING_NAME, true);
     }
   }
 
   getAPI(context) {
     let self = {
       contextualIdentities: {
-        get(cookieStoreId) {
+        async get(cookieStoreId) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
-        query(details) {
+        async query(details) {
           let identities = [];
           ContextualIdentityService.getPublicIdentities().forEach(identity => {
             if (details.name &&
                 ContextualIdentityService.getUserContextLabel(identity.userContextId) != details.name) {
               return;
             }
 
             identities.push(convertIdentity(identity));
           });
 
-          return Promise.resolve(identities);
+          return identities;
         },
 
-        create(details) {
+        async create(details) {
+          // Lets prevent making containers that are not valid
+          getContainerIcon(details.icon);
+          getContainerColor(details.color);
+
           let identity = ContextualIdentityService.create(details.name,
                                                           details.icon,
                                                           details.color);
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
         update(cookieStoreId, details) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
@@ -133,20 +193,20 @@ this.contextualIdentities = class extend
           if (!ContextualIdentityService.update(identity.userContextId,
                                                 identity.name, identity.icon,
                                                 identity.color)) {
             return Promise.reject({
               message: `Contextual identitiy failed to update: ${cookieStoreId}`,
             });
           }
 
-          return Promise.resolve(convertIdentity(identity));
+          return convertIdentity(identity);
         },
 
-        remove(cookieStoreId) {
+        async remove(cookieStoreId) {
           let containerId = getContainerForCookieStoreId(cookieStoreId);
           if (!containerId) {
             return Promise.reject({
               message: `Invalid contextual identitiy: ${cookieStoreId}`,
             });
           }
 
           let identity = ContextualIdentityService.getPublicIdentityFromId(containerId);
@@ -160,44 +220,53 @@ this.contextualIdentities = class extend
           let convertedIdentity = convertIdentity(identity);
 
           if (!ContextualIdentityService.remove(identity.userContextId)) {
             return Promise.reject({
               message: `Contextual identitiy failed to remove: ${cookieStoreId}`,
             });
           }
 
-          return Promise.resolve(convertedIdentity);
+          return convertedIdentity;
         },
 
         onCreated: new EventManager(context, "contextualIdentities.onCreated", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-created");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-created");
           };
         }).api(),
 
         onUpdated: new EventManager(context, "contextualIdentities.onUpdated", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-updated");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-updated");
           };
         }).api(),
 
         onRemoved: new EventManager(context, "contextualIdentities.onRemoved", fire => {
           let observer = (subject, topic) => {
-            fire.async({contextualIdentity: convertIdentityFromObserver(subject)});
+            let convertedIdentity = convertIdentityFromObserver(subject);
+            if (convertedIdentity) {
+              fire.async({contextualIdentity: convertedIdentity});
+            }
           };
 
           Services.obs.addObserver(observer, "contextual-identity-deleted");
           return () => {
             Services.obs.removeObserver(observer, "contextual-identity-deleted");
           };
         }).api(),
 
--- a/toolkit/components/extensions/schemas/contextual_identities.json
+++ b/toolkit/components/extensions/schemas/contextual_identities.json
@@ -23,18 +23,20 @@
     "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."},
+          "icon": {"type": "string", "description": "The icon name of the contextual identity."},
+          "iconUrl": {"type": "string", "description": "The icon url of the contextual identity."},
+          "color": {"type": "string", "description": "The color name of the contextual identity."},
+          "colorCode": {"type": "string", "description": "The color hash of the contextual identity."},
           "cookieStoreId": {"type": "string", "description": "The cookie store ID of the contextual identity."}
         }
       }
     ],
     "functions": [
       {
         "name": "get",
         "type": "function",
--- a/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
+++ b/toolkit/components/extensions/test/xpcshell/test_ext_contextual_identities.js
@@ -46,34 +46,40 @@ add_task(async function test_contextualI
           browser.contextualIdentities[type].addListener(listener);
         } catch (e) {
           reject(e);
         }
       });
     }
 
     function assertExpected(expected, container) {
+      // Number of keys that are added by the APIs
+      const createdCount = 2;
       for (let key of Object.keys(container)) {
         browser.test.assertTrue(key in expected, `found property ${key}`);
         browser.test.assertEq(expected[key], container[key], `property value for ${key} is correct`);
       }
-      browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length, "all expected properties found");
+      const hexMatch = /^#[0-9a-f]{6}$/;
+      browser.test.assertTrue(hexMatch.test(expected.colorCode), "Color code property was expected Hex shape");
+      const iconMatch = /^resource:\/\/usercontext-content\/[a-z]+[.]svg$/;
+      browser.test.assertTrue(iconMatch.test(expected.iconUrl), "Icon url property was expected shape");
+      browser.test.assertEq(Object.keys(expected).length, Object.keys(container).length + createdCount, "all expected properties found");
     }
 
     let onCreatePromise = createOneTimeListener("onCreated");
 
-    let containerObj = {name: "foobar", color: "red", icon: "icon"};
+    let containerObj = {name: "foobar", color: "red", icon: "circle"};
     let ci = await browser.contextualIdentities.create(containerObj);
     browser.test.assertTrue(!!ci, "We have an identity");
     const onCreateListenerResponse = await onCreatePromise;
     const cookieStoreId = ci.cookieStoreId;
     assertExpected(onCreateListenerResponse.contextualIdentity, Object.assign(containerObj, {cookieStoreId}));
 
     let onUpdatedPromise = createOneTimeListener("onUpdated");
-    let updateContainerObj = {name: "testing", color: "blue", icon: "thing"};
+    let updateContainerObj = {name: "testing", color: "blue", icon: "dollar"};
     ci = await browser.contextualIdentities.update(cookieStoreId, updateContainerObj);
     browser.test.assertTrue(!!ci, "We have an update identity");
     const onUpdatedListenerResponse = await onUpdatedPromise;
     assertExpected(onUpdatedListenerResponse.contextualIdentity, Object.assign(updateContainerObj, {cookieStoreId}));
 
     let onRemovePromise = createOneTimeListener("onRemoved");
     ci = await browser.contextualIdentities.remove(updateContainerObj.cookieStoreId);
     browser.test.assertTrue(!!ci, "We have an remove identity");
@@ -125,49 +131,64 @@ add_task(async function test_contextualI
     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"});
+    ci = await browser.contextualIdentities.create({name: "foobar", color: "red", icon: "gift"});
     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.assertEq("gift", ci.icon, "identity.icon is correct");
     browser.test.assertTrue(!!ci.cookieStoreId, "identity.cookieStoreId is correct");
 
+    browser.test.assertRejects(
+      browser.contextualIdentities.create({name: "foobar", color: "red", icon: "firefox"}),
+      "Invalid icon firefox for container",
+      "Create container called with an invalid icon"
+    );
+
+    browser.test.assertRejects(
+      browser.contextualIdentities.create({name: "foobar", color: "firefox-orange", icon: "gift"}),
+      "Invalid color name firefox-orange for container",
+      "Create container called with an invalid color"
+    );
+
+    cis = await browser.contextualIdentities.query({});
+    browser.test.assertEq(5, cis.length, "we should still have have 5 containers");
+
     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");
+    browser.test.assertEq("gift", 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"});
+    ci = await browser.contextualIdentities.update(ci.cookieStoreId, {name: "barfoo", color: "blue", icon: "cart"});
     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");
+    browser.test.assertEq("cart", 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");
+    browser.test.assertEq("cart", 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");
+    browser.test.assertEq("cart", 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");
   }
   function makeExtension(id) {
     return ExtensionTestUtils.loadExtension({