Bug 1217129: Part 4b - [webext] Use the extension name as the default title value for browserAction/pageAction popups. r?billm draft
authorKris Maglione <maglione.k@gmail.com>
Sun, 10 Jan 2016 21:00:09 -0800
changeset 320568 7e413df414cbbb4af7df9aba1bb5733a687509b8
parent 320567 a0c403d89febb8b71846a8dd3238547290186b9d
child 320569 b65d62cf3d7a519084ccb04e4902450ff0350a84
push id9233
push usermaglione.k@gmail.com
push dateMon, 11 Jan 2016 20:43:12 +0000
reviewersbillm
bugs1217129
milestone46.0a1
Bug 1217129: Part 4b - [webext] Use the extension name as the default title value for browserAction/pageAction popups. r?billm This brings our behavior closer in line with Chrome's.
browser/components/extensions/ext-browserAction.js
browser/components/extensions/ext-pageAction.js
browser/components/extensions/test/browser/browser_ext_browserAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_context.js
browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
--- a/browser/components/extensions/ext-browserAction.js
+++ b/browser/components/extensions/ext-browserAction.js
@@ -32,17 +32,17 @@ function BrowserAction(options, extensio
   let title = extension.localize(options.default_title || "");
   let popup = extension.localize(options.default_popup || "");
   if (popup) {
     popup = extension.baseURI.resolve(popup);
   }
 
   this.defaults = {
     enabled: true,
-    title: title,
+    title: title || extension.name,
     badgeText: "",
     badgeBackgroundColor: null,
     icon: IconDetails.normalize({ path: options.default_icon }, extension,
                                 null, true),
     popup: popup,
   };
 
   this.tabContext = new TabContext(tab => Object.create(this.defaults),
@@ -91,25 +91,19 @@ BrowserAction.prototype = {
 
   togglePopup(node, popupResource) {
     openPanel(node, popupResource, this.extension);
   },
 
   // Update the toolbar button |node| with the tab context data
   // in |tabData|.
   updateButton(node, tabData) {
-    if (tabData.title) {
-      node.setAttribute("tooltiptext", tabData.title);
-      node.setAttribute("label", tabData.title);
-      node.setAttribute("aria-label", tabData.title);
-    } else {
-      node.removeAttribute("tooltiptext");
-      node.removeAttribute("label");
-      node.removeAttribute("aria-label");
-    }
+    let title = tabData.title || this.extension.name;
+    node.setAttribute("tooltiptext", title);
+    node.setAttribute("label", title);
 
     if (tabData.badgeText) {
       node.setAttribute("badge", tabData.badgeText);
     } else {
       node.removeAttribute("badge");
     }
 
     if (tabData.enabled) {
@@ -157,18 +151,20 @@ BrowserAction.prototype = {
     }
   },
 
   // tab is allowed to be null.
   // prop should be one of "icon", "title", "badgeText", "popup", or "badgeBackgroundColor".
   setProperty(tab, prop, value) {
     if (tab == null) {
       this.defaults[prop] = value;
+    } else if (value != null) {
+      this.tabContext.get(tab)[prop] = value;
     } else {
-      this.tabContext.get(tab)[prop] = value;
+      delete this.tabContext.get(tab)[prop];
     }
 
     this.updateOnChange(tab);
   },
 
   // tab is allowed to be null.
   // prop should be one of "title", "badgeText", "popup", or "badgeBackgroundColor".
   getProperty(tab, prop) {
@@ -221,17 +217,23 @@ extensions.registerSchemaAPI("browserAct
 
       disable: function(tabId) {
         let tab = tabId !== null ? TabManager.getTab(tabId) : null;
         browserActionOf(extension).setProperty(tab, "enabled", false);
       },
 
       setTitle: function(details) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
-        browserActionOf(extension).setProperty(tab, "title", details.title);
+
+        let title = details.title;
+        // Clear the tab-specific title when given a null string.
+        if (tab && title == "") {
+          title = null;
+        }
+        browserActionOf(extension).setProperty(tab, "title", title);
       },
 
       getTitle: function(details, callback) {
         let tab = details.tabId !== null ? TabManager.getTab(details.tabId) : null;
         let title = browserActionOf(extension).getProperty(tab, "title");
         runSafe(context, callback, title);
       },
 
--- a/browser/components/extensions/ext-pageAction.js
+++ b/browser/components/extensions/ext-pageAction.js
@@ -23,17 +23,17 @@ function PageAction(options, extension) 
   let title = extension.localize(options.default_title || "");
   let popup = extension.localize(options.default_popup || "");
   if (popup) {
     popup = extension.baseURI.resolve(popup);
   }
 
   this.defaults = {
     show: false,
-    title: title,
+    title: title || extension.name,
     icon: IconDetails.normalize({ path: options.default_icon }, extension,
                                 null, true),
     popup: popup && extension.baseURI.resolve(popup),
   };
 
   this.tabContext = new TabContext(tab => Object.create(this.defaults),
                                    extension);
 
@@ -53,17 +53,22 @@ PageAction.prototype = {
   },
 
   // Sets the value of the property |prop| for the given tab to the
   // given value, symmetrically to |getProperty|.
   //
   // If |tab| is currently selected, updates the page action button to
   // reflect the new value.
   setProperty(tab, prop, value) {
-    this.tabContext.get(tab)[prop] = value;
+    if (value != null) {
+      this.tabContext.get(tab)[prop] = value;
+    } else {
+      delete this.tabContext.get(tab)[prop];
+    }
+
     if (tab.selected) {
       this.updateButton(tab.ownerDocument.defaultView);
     }
   },
 
   // Updates the page action button in the given window to reflect the
   // properties of the currently selected tab:
   //
@@ -79,23 +84,19 @@ PageAction.prototype = {
       return;
     }
 
     let button = this.getButton(window);
 
     if (tabData.show) {
       // Update the title and icon only if the button is visible.
 
-      if (tabData.title) {
-        button.setAttribute("tooltiptext", tabData.title);
-        button.setAttribute("aria-label", tabData.title);
-      } else {
-        button.removeAttribute("tooltiptext");
-        button.removeAttribute("aria-label");
-      }
+      let title = tabData.title || this.extension.name;
+      button.setAttribute("tooltiptext", title);
+      button.setAttribute("aria-label", title);
 
       let icon = IconDetails.getURL(tabData.icon, window, this.extension);
       button.setAttribute("src", icon);
     }
 
     button.hidden = !tabData.show;
   },
 
@@ -208,17 +209,19 @@ extensions.registerSchemaAPI("pageAction
 
       hide(tabId) {
         let tab = TabManager.getTab(tabId);
         PageAction.for(extension).setProperty(tab, "show", false);
       },
 
       setTitle(details) {
         let tab = TabManager.getTab(details.tabId);
-        PageAction.for(extension).setProperty(tab, "title", details.title);
+
+        // Clear the tab-specific title when given a null string.
+        PageAction.for(extension).setProperty(tab, "title", details.title || null);
       },
 
       getTitle(details, callback) {
         let tab = TabManager.getTab(details.tabId);
         let title = PageAction.for(extension).getProperty(tab, "title");
         runSafe(context, callback, title);
       },
 
--- a/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_browserAction_context.js
@@ -1,24 +1,157 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
+function* runTests(options) {
+  function background(getTests) {
+    // Gets the current details of the browser action, and returns a
+    // promise that resolves to an object containing them.
+    function getDetails(tabId) {
+      return Promise.all([
+        new Promise(resolve => browser.browserAction.getTitle({tabId}, resolve)),
+        new Promise(resolve => browser.browserAction.getPopup({tabId}, resolve)),
+        new Promise(resolve => browser.browserAction.getBadgeText({tabId}, resolve)),
+        new Promise(resolve => browser.browserAction.getBadgeBackgroundColor({tabId}, resolve))]
+      ).then(details => {
+        return Promise.resolve({ title: details[0],
+                                 popup: details[1],
+                                 badge: details[2],
+                                 badgeBackgroundColor: details[3] });
+      });
+    }
+
+    function checkDetails(expecting, tabId) {
+      return getDetails(tabId).then(details => {
+        browser.test.assertEq(expecting.title, details.title,
+                              "expected value from getTitle");
+
+        browser.test.assertEq(expecting.popup, details.popup,
+                              "expected value from getPopup");
+
+        browser.test.assertEq(expecting.badge, details.badge,
+                              "expected value from getBadge");
+
+        browser.test.assertEq(String(expecting.badgeBackgroundColor),
+                              String(details.badgeBackgroundColor),
+                              "expected value from getBadgeBackgroundColor");
+      });
+    }
+
+    let expectDefaults = expecting => {
+      return checkDetails(expecting);
+    };
+
+    let tabs = [];
+    let tests = getTests(tabs, expectDefaults);
+
+    // Runs the next test in the `tests` array, checks the results,
+    // and passes control back to the outer test scope.
+    function nextTest() {
+      let test = tests.shift();
+
+      test(expecting => {
+        // Check that the API returns the expected values, and then
+        // run the next test.
+        new Promise(resolve => {
+          return browser.tabs.query({ active: true, currentWindow: true }, resolve);
+        }).then(tabs => {
+          return checkDetails(expecting, tabs[0].id);
+        }).then(() => {
+          // Check that the actual icon has the expected values, then
+          // run the next test.
+          browser.test.sendMessage("nextTest", expecting, tests.length);
+        });
+      });
+    }
+
+    browser.test.onMessage.addListener((msg) => {
+      if (msg != "runNextTest") {
+        browser.test.fail("Expecting 'runNextTest' message");
+      }
+
+      nextTest();
+    });
+
+    browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
+      tabs[0] = resultTabs[0].id;
+
+      nextTest();
+    });
+  }
+
+  let extension = ExtensionTestUtils.loadExtension({
+    manifest: options.manifest,
+
+    background: `(${background})(${options.getTests})`,
+  });
+
+
+  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(button.getAttribute("image"), details.icon, "icon URL is correct");
+    is(button.getAttribute("tooltiptext"), title, "image title is correct");
+    is(button.getAttribute("label"), title, "image label is correct");
+    is(button.getAttribute("badge"), details.badge, "badge text is correct");
+    is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
+
+    if (details.badge && details.badgeBackgroundColor) {
+      let badge = button.ownerDocument.getAnonymousElementByAttribute(
+        button, "class", "toolbarbutton-badge");
+
+      let badgeColor = window.getComputedStyle(badge).backgroundColor;
+      let color = details.badgeBackgroundColor;
+      let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
+
+      is(badgeColor, expectedColor, "badge color is correct");
+    }
+
+
+    // TODO: Popup URL.
+  }
+
+  let awaitFinish = new Promise(resolve => {
+    extension.onMessage("nextTest", (expecting, testsRemaining) => {
+      checkDetails(expecting);
+
+      if (testsRemaining) {
+        extension.sendMessage("runNextTest");
+      } else {
+        resolve();
+      }
+    });
+  });
+
+  yield extension.startup();
+
+  yield awaitFinish;
+
+  yield extension.unload();
+}
+
 add_task(function* testTabSwitchContext() {
-  let extension = ExtensionTestUtils.loadExtension({
+  yield runTests({
     manifest: {
       "browser_action": {
         "default_icon": "default.png",
         "default_popup": "default.html",
         "default_title": "Default Title",
       },
       "permissions": ["tabs"],
     },
 
-    background: function() {
+    getTests(tabs, expectDefaults) {
       let details = [
         { "icon": browser.runtime.getURL("default.png"),
           "popup": browser.runtime.getURL("default.html"),
           "title": "Default Title",
           "badge": "",
           "badgeBackgroundColor": null },
         { "icon": browser.runtime.getURL("1.png"),
           "popup": browser.runtime.getURL("default.html"),
@@ -45,20 +178,17 @@ add_task(function* testTabSwitchContext(
           "disabled": false },
         { "icon": browser.runtime.getURL("default-2.png"),
           "popup": browser.runtime.getURL("default-2.html"),
           "title": "Default Title 2",
           "badge": "d2",
           "badgeBackgroundColor": [0, 0xff, 0, 0xff] },
       ];
 
-      let tabs = [];
-
-      let expectDefaults;
-      let tests = [
+      return [
         expect => {
           browser.test.log("Initial state, expect default properties.");
           expectDefaults(details[0]).then(() => {
             expect(details[0]);
           });
         },
         expect => {
           browser.test.log("Change the icon in the current tab. Expect default properties excluding the icon.");
@@ -152,129 +282,87 @@ add_task(function* testTabSwitchContext(
         },
         expect => {
           browser.test.log("Delete tab.");
           browser.tabs.remove(tabs[2], () => {
             expect(details[4]);
           });
         },
       ];
-
-      // Gets the current details of the browser action, and returns a
-      // promise that resolves to an object containing them.
-      function getDetails(tabId) {
-        return Promise.all([
-          new Promise(resolve => browser.browserAction.getTitle({tabId}, resolve)),
-          new Promise(resolve => browser.browserAction.getPopup({tabId}, resolve)),
-          new Promise(resolve => browser.browserAction.getBadgeText({tabId}, resolve)),
-          new Promise(resolve => browser.browserAction.getBadgeBackgroundColor({tabId}, resolve))]
-        ).then(details => {
-          return Promise.resolve({ title: details[0],
-                                   popup: details[1],
-                                   badge: details[2],
-                                   badgeBackgroundColor: details[3] });
-        });
-      }
-
-      function checkDetails(expecting, tabId) {
-        return getDetails(tabId).then(details => {
-          browser.test.assertEq(expecting.title, details.title,
-                                "expected value from getTitle");
-
-          browser.test.assertEq(expecting.popup, details.popup,
-                                "expected value from getPopup");
-
-          browser.test.assertEq(expecting.badge, details.badge,
-                                "expected value from getBadge");
-
-          browser.test.assertEq(String(expecting.badgeBackgroundColor),
-                                String(details.badgeBackgroundColor),
-                                "expected value from getBadgeBackgroundColor");
-        });
-      }
-
-      expectDefaults = expecting => {
-        return checkDetails(expecting);
-      };
-
-      // Runs the next test in the `tests` array, checks the results,
-      // and passes control back to the outer test scope.
-      function nextTest() {
-        let test = tests.shift();
-
-        test(expecting => {
-          // Check that the API returns the expected values, and then
-          // run the next test.
-          new Promise(resolve => {
-            return browser.tabs.query({ active: true, currentWindow: true }, resolve);
-          }).then(tabs => {
-            return checkDetails(expecting, tabs[0].id);
-          }).then(() => {
-            // Check that the actual icon has the expected values, then
-            // run the next test.
-            browser.test.sendMessage("nextTest", expecting, tests.length);
-          });
-        });
-      }
-
-      browser.test.onMessage.addListener((msg) => {
-        if (msg != "runNextTest") {
-          browser.test.fail("Expecting 'runNextTest' message");
-        }
-
-        nextTest();
-      });
-
-      browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
-        tabs[0] = resultTabs[0].id;
-
-        nextTest();
-      });
     },
   });
+});
 
-  let browserActionId = makeWidgetId(extension.id) + "-browser-action";
+add_task(function* testDefaultTitle() {
+  yield runTests({
+    manifest: {
+      "name": "Foo Extension",
 
-  function checkDetails(details) {
-    let button = document.getElementById(browserActionId);
+      "browser_action": {
+        "default_icon": "icon.png",
+      },
 
-    ok(button, "button exists");
+      "permissions": ["tabs"],
+    },
 
-    is(button.getAttribute("image"), details.icon, "icon URL is correct");
-    is(button.getAttribute("tooltiptext"), details.title, "image title is correct");
-    is(button.getAttribute("label"), details.title, "image label is correct");
-    is(button.getAttribute("aria-label"), details.title, "image aria-label is correct");
-    is(button.getAttribute("badge"), details.badge, "badge text is correct");
-    is(button.getAttribute("disabled") == "true", Boolean(details.disabled), "disabled state is correct");
-
-    if (details.badge && details.badgeBackgroundColor) {
-      let badge = button.ownerDocument.getAnonymousElementByAttribute(
-        button, "class", "toolbarbutton-badge");
-
-      let badgeColor = window.getComputedStyle(badge).backgroundColor;
-      let color = details.badgeBackgroundColor;
-      let expectedColor = `rgb(${color[0]}, ${color[1]}, ${color[2]})`;
+    getTests(tabs, expectDefaults) {
+      let details = [
+        { "title": "Foo Extension",
+          "popup": "",
+          "badge": "",
+          "badgeBackgroundColor": null,
+          "icon": browser.runtime.getURL("icon.png") },
+        { "title": "Foo Title",
+          "popup": "",
+          "badge": "",
+          "badgeBackgroundColor": null,
+          "icon": browser.runtime.getURL("icon.png") },
+        { "title": "Bar Title",
+          "popup": "",
+          "badge": "",
+          "badgeBackgroundColor": null,
+          "icon": browser.runtime.getURL("icon.png") },
+        { "title": "",
+          "popup": "",
+          "badge": "",
+          "badgeBackgroundColor": null,
+          "icon": browser.runtime.getURL("icon.png") },
+      ];
 
-      is(badgeColor, expectedColor, "badge color is correct");
-    }
-
-
-    // TODO: Popup URL.
-  }
-
-  let awaitFinish = new Promise(resolve => {
-    extension.onMessage("nextTest", (expecting, testsRemaining) => {
-      checkDetails(expecting);
-
-      if (testsRemaining) {
-        extension.sendMessage("runNextTest");
-      } else {
-        resolve();
-      }
-    });
+      return [
+        expect => {
+          browser.test.log("Initial state. Expect extension title as default title.");
+          expectDefaults(details[0]).then(() => {
+            expect(details[0]);
+          });
+        },
+        expect => {
+          browser.test.log("Change the title. Expect new title.");
+          browser.browserAction.setTitle({ tabId: tabs[0], title: "Foo Title" });
+          expectDefaults(details[0]).then(() => {
+            expect(details[1]);
+          });
+        },
+        expect => {
+          browser.test.log("Change the default. Expect same properties.");
+          browser.browserAction.setTitle({ title: "Bar Title" });
+          expectDefaults(details[2]).then(() => {
+            expect(details[1]);
+          });
+        },
+        expect => {
+          browser.test.log("Clear the title. Expect new default title.");
+          browser.browserAction.setTitle({ tabId: tabs[0], title: "" });
+          expectDefaults(details[2]).then(() => {
+            expect(details[2]);
+          });
+        },
+        expect => {
+          browser.test.log("Set default title to null string. Expect null string from API, extension title in UI.");
+          browser.browserAction.setTitle({ title: "" });
+          expectDefaults(details[3]).then(() => {
+            expect(details[3]);
+          });
+        },
+      ];
+    },
   });
-
-  yield extension.startup();
-
-  yield awaitFinish;
-
-  yield extension.unload();
 });
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_context.js
@@ -1,39 +1,187 @@
 /* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
 /* vim: set sts=2 sw=2 et tw=80: */
 "use strict";
 
-add_task(function* testTabSwitchContext() {
+function* runTests(options) {
+  function background(getTests) {
+    let tabs;
+    let tests;
+
+    // Gets the current details of the page action, and returns a
+    // promise that resolves to an object containing them.
+    function getDetails() {
+      return new Promise(resolve => {
+        return browser.tabs.query({ active: true, currentWindow: true }, resolve);
+      }).then(tabs => {
+        let tabId = tabs[0].id;
+        return Promise.all([
+          new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
+          new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))]);
+      }).then(details => {
+        return Promise.resolve({ title: details[0],
+                                 popup: details[1] });
+      });
+    }
+
+
+    // Runs the next test in the `tests` array, checks the results,
+    // and passes control back to the outer test scope.
+    function nextTest() {
+      let test = tests.shift();
+
+      test(expecting => {
+        function finish() {
+          // Check that the actual icon has the expected values, then
+          // run the next test.
+          browser.test.sendMessage("nextTest", expecting, tests.length);
+        }
+
+        if (expecting) {
+          // Check that the API returns the expected values, and then
+          // run the next test.
+          getDetails().then(details => {
+            browser.test.assertEq(expecting.title, details.title,
+                                  "expected value from getTitle");
+
+            browser.test.assertEq(expecting.popup, details.popup,
+                                  "expected value from getPopup");
+
+            finish();
+          });
+        } else {
+          finish();
+        }
+      });
+    }
+
+    function runTests() {
+      tabs = [];
+      tests = getTests(tabs);
+
+      browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
+        tabs[0] = resultTabs[0].id;
+
+        nextTest();
+      });
+    }
+
+    browser.test.onMessage.addListener((msg) => {
+      if (msg == "runTests") {
+        runTests();
+      } else if (msg == "runNextTest") {
+        nextTest();
+      } else {
+        browser.test.fail(`Unexpected message: ${msg}`);
+      }
+    });
+
+    runTests();
+  }
+
   let extension = ExtensionTestUtils.loadExtension({
+    manifest: options.manifest,
+
+    background: `(${background})(${options.getTests})`,
+  });
+
+  let pageActionId = makeWidgetId(extension.id) + "-page-action";
+  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 {
+      ok(image, "image exists");
+
+      is(image.src, details.icon, "icon URL is correct");
+
+      let title = details.title || options.manifest.name;
+      is(image.getAttribute("tooltiptext"), title, "image title is correct");
+      is(image.getAttribute("aria-label"), title, "image aria-label is correct");
+      // TODO: Popup URL.
+    }
+  }
+
+  let testNewWindows = 1;
+
+  let awaitFinish = new Promise(resolve => {
+    extension.onMessage("nextTest", (expecting, testsRemaining) => {
+      checkDetails(expecting);
+
+      if (testsRemaining) {
+        extension.sendMessage("runNextTest");
+      } else if (testNewWindows) {
+        testNewWindows--;
+
+        BrowserTestUtils.openNewBrowserWindow().then(window => {
+          windows.push(window);
+          currentWindow = window;
+          return focusWindow(window);
+        }).then(() => {
+          extension.sendMessage("runTests");
+        });
+      } else {
+        resolve();
+      }
+    });
+  });
+
+  yield extension.startup();
+
+  yield awaitFinish;
+
+  yield extension.unload();
+
+  let node = document.getElementById(pageActionId);
+  is(node, null, "pageAction image removed from document");
+
+  currentWindow = null;
+  for (let win of windows.splice(0)) {
+    node = win.document.getElementById(pageActionId);
+    is(node, null, "pageAction image removed from second document");
+
+    yield BrowserTestUtils.closeWindow(win);
+  }
+}
+
+add_task(function* testTabSwitchContext() {
+  yield runTests({
     manifest: {
+      "name": "Foo Extension",
+
       "page_action": {
         "default_icon": "default.png",
         "default_popup": "default.html",
         "default_title": "Default Title",
       },
+
       "permissions": ["tabs"],
     },
 
-    background: function() {
+    getTests(tabs) {
       let details = [
         { "icon": browser.runtime.getURL("default.png"),
           "popup": browser.runtime.getURL("default.html"),
           "title": "Default Title" },
         { "icon": browser.runtime.getURL("1.png"),
           "popup": browser.runtime.getURL("default.html"),
           "title": "Default Title" },
         { "icon": browser.runtime.getURL("2.png"),
           "popup": browser.runtime.getURL("2.html"),
           "title": "Title 2" },
+        { "icon": browser.runtime.getURL("2.png"),
+          "popup": browser.runtime.getURL("2.html"),
+          "title": "Default Title" },
       ];
 
-      let tabs;
-      let tests;
-      let allTests = [
+      return [
         expect => {
           browser.test.log("Initial state. No icon visible.");
           expect(null);
         },
         expect => {
           browser.test.log("Show the icon on the first tab, expect default properties.");
           browser.pageAction.show(tabs[0]);
           expect(details[0]);
@@ -56,16 +204,22 @@ add_task(function* testTabSwitchContext(
           browser.pageAction.show(tabId);
           browser.pageAction.setIcon({ tabId, path: "2.png" });
           browser.pageAction.setPopup({ tabId, popup: "2.html" });
           browser.pageAction.setTitle({ tabId, title: "Title 2" });
 
           expect(details[2]);
         },
         expect => {
+          browser.test.log("Clear the title. Expect default title.");
+          browser.pageAction.setTitle({ tabId: tabs[1], title: "" });
+
+          expect(details[3]);
+        },
+        expect => {
           browser.test.log("Navigate to a new page. Expect icon hidden.");
 
           // TODO: This listener should not be necessary, but the |tabs.update|
           // callback currently fires too early in e10s windows.
           browser.tabs.onUpdated.addListener(function listener(tabId, changed) {
             if (tabId == tabs[1] && changed.url) {
               browser.tabs.onUpdated.removeListener(listener);
               expect(null);
@@ -99,140 +253,58 @@ add_task(function* testTabSwitchContext(
           });
         },
         expect => {
           browser.test.log("Hide the icon. Expect hidden.");
           browser.pageAction.hide(tabs[0]);
           expect(null);
         },
       ];
-
-      // Gets the current details of the page action, and returns a
-      // promise that resolves to an object containing them.
-      function getDetails() {
-        return new Promise(resolve => {
-          return browser.tabs.query({ active: true, currentWindow: true }, resolve);
-        }).then(tabs => {
-          let tabId = tabs[0].id;
-          return Promise.all([
-            new Promise(resolve => browser.pageAction.getTitle({tabId}, resolve)),
-            new Promise(resolve => browser.pageAction.getPopup({tabId}, resolve))]);
-        }).then(details => {
-          return Promise.resolve({ title: details[0],
-                                   popup: details[1] });
-        });
-      }
-
-
-      // Runs the next test in the `tests` array, checks the results,
-      // and passes control back to the outer test scope.
-      function nextTest() {
-        let test = tests.shift();
-
-        test(expecting => {
-          function finish() {
-            // Check that the actual icon has the expected values, then
-            // run the next test.
-            browser.test.sendMessage("nextTest", expecting, tests.length);
-          }
-
-          if (expecting) {
-            // Check that the API returns the expected values, and then
-            // run the next test.
-            getDetails().then(details => {
-              browser.test.assertEq(expecting.title, details.title,
-                                    "expected value from getTitle");
-
-              browser.test.assertEq(expecting.popup, details.popup,
-                                    "expected value from getPopup");
-
-              finish();
-            });
-          } else {
-            finish();
-          }
-        });
-      }
-
-      function runTests() {
-        tabs = [];
-        tests = allTests.slice();
-
-        browser.tabs.query({ active: true, currentWindow: true }, resultTabs => {
-          tabs[0] = resultTabs[0].id;
-
-          nextTest();
-        });
-      }
-
-      browser.test.onMessage.addListener((msg) => {
-        if (msg == "runTests") {
-          runTests();
-        } else if (msg == "runNextTest") {
-          nextTest();
-        } else {
-          browser.test.fail(`Unexpected message: ${msg}`);
-        }
-      });
-
-      runTests();
     },
   });
-
-  let pageActionId = makeWidgetId(extension.id) + "-page-action";
-  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 {
-      ok(image, "image exists");
+add_task(function* testDefaultTitle() {
+  yield runTests({
+    manifest: {
+      "name": "Foo Extension",
+
+      "page_action": {
+        "default_icon": "icon.png",
+      },
 
-      is(image.src, details.icon, "icon URL is correct");
-      is(image.getAttribute("tooltiptext"), details.title, "image title is correct");
-      is(image.getAttribute("aria-label"), details.title, "image aria-label is correct");
-      // TODO: Popup URL.
-    }
-  }
-
-  let testNewWindows = 1;
+      "permissions": ["tabs"],
+    },
 
-  let awaitFinish = new Promise(resolve => {
-    extension.onMessage("nextTest", (expecting, testsRemaining) => {
-      checkDetails(expecting);
-
-      if (testsRemaining) {
-        extension.sendMessage("runNextTest");
-      } else if (testNewWindows) {
-        testNewWindows--;
+    getTests(tabs) {
+      let details = [
+        { "title": "Foo Extension",
+          "popup": "",
+          "icon": browser.runtime.getURL("icon.png") },
+        { "title": "Foo Title",
+          "popup": "",
+          "icon": browser.runtime.getURL("icon.png") },
+      ];
 
-        BrowserTestUtils.openNewBrowserWindow().then(window => {
-          windows.push(window);
-          currentWindow = window;
-          return focusWindow(window);
-        }).then(() => {
-          extension.sendMessage("runTests");
-        });
-      } else {
-        resolve();
-      }
-    });
+      return [
+        expect => {
+          browser.test.log("Initial state. No icon visible.");
+          expect(null);
+        },
+        expect => {
+          browser.test.log("Show the icon on the first tab, expect extension title as default title.");
+          browser.pageAction.show(tabs[0]);
+          expect(details[0]);
+        },
+        expect => {
+          browser.test.log("Change the title. Expect new title.");
+          browser.pageAction.setTitle({ tabId: tabs[0], title: "Foo Title" });
+          expect(details[1]);
+        },
+        expect => {
+          browser.test.log("Clear the title. Expect extension title.");
+          browser.pageAction.setTitle({ tabId: tabs[0], title: "" });
+          expect(details[0]);
+        },
+      ];
+    },
   });
-
-  yield extension.startup();
-
-  yield awaitFinish;
-
-  yield extension.unload();
-
-  let node = document.getElementById(pageActionId);
-  is(node, null, "pageAction image removed from document");
-
-  currentWindow = null;
-  for (let win of windows.splice(0)) {
-    node = win.document.getElementById(pageActionId);
-    is(node, null, "pageAction image removed from second document");
-
-    yield BrowserTestUtils.closeWindow(win);
-  }
 });
--- a/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
+++ b/browser/components/extensions/test/browser/browser_ext_pageAction_popup.js
@@ -12,38 +12,40 @@ function promisePopupShown(popup) {
         resolve();
       };
       popup.addEventListener("popupshown", onPopupShown);
     }
   });
 }
 
 add_task(function* testPageActionPopup() {
+  let scriptPage = url => `<html><head><meta charset="utf-8"><script src="${url}"></script></head></html>`;
+
   let extension = ExtensionTestUtils.loadExtension({
     manifest: {
       "background": {
         "page": "data/background.html",
       },
       "page_action": {
         "default_popup": "popup-a.html",
       },
     },
 
     files: {
-      "popup-a.html": `<script src="popup-a.js"></script>`,
+      "popup-a.html": scriptPage("popup-a.js"),
       "popup-a.js": function() {
         browser.runtime.sendMessage("from-popup-a");
       },
 
-      "data/popup-b.html": `<script src="popup-b.js"></script>`,
+      "data/popup-b.html": scriptPage("popup-b.js"),
       "data/popup-b.js": function() {
         browser.runtime.sendMessage("from-popup-b");
       },
 
-      "data/background.html": `<script src="background.js"></script>`,
+      "data/background.html": scriptPage("background.js"),
 
       "data/background.js": function() {
         let tabId;
 
         let sendClick;
         let tests = [
           () => {
             sendClick({ expectEvent: false, expectPopup: "a" });