Bug 1238310: Part 5 - Implement the browser.tabs.onZoomChange event. r?gabor f?aswan draft
authorKris Maglione <maglione.k@gmail.com>
Fri, 01 Apr 2016 11:45:01 -0700
changeset 353957 1c6ba6a0c85196fe56f07a5480380c4cdc6b563e
parent 353956 72e6f860df44b31c25940d0ea6b75242bf340851
child 518908 9e6a30311bc6beadc9453c930baeb45abb942c81
push id15974
push usermaglione.k@gmail.com
push dateWed, 20 Apr 2016 03:00:13 +0000
reviewersgabor
bugs1238310
milestone48.0a1
Bug 1238310: Part 5 - Implement the browser.tabs.onZoomChange event. r?gabor f?aswan MozReview-Commit-ID: JlxSM13SeYg
browser/components/extensions/ext-tabs.js
browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
toolkit/content/browser-child.js
toolkit/content/widgets/remote-browser.xml
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -953,12 +953,86 @@ extensions.registerSchemaAPI("tabs", nul
 
         let currentSettings = this._getZoomSettings(tab.id);
 
         if (!Object.keys(settings).every(key => settings[key] === currentSettings[key])) {
           return Promise.reject(`Unsupported zoom settings: ${JSON.stringify(settings)}`);
         }
         return Promise.resolve();
       },
+
+      onZoomChange: new EventManager(context, "tabs.onZoomChange", fire => {
+        let getZoomLevel = browser => {
+          let {ZoomManager} = browser.ownerDocument.defaultView;
+
+          return ZoomManager.getZoomForBrowser(browser);
+        };
+
+        // Stores the last known zoom level for each tab's browser.
+        // WeakMap[<browser> -> number]
+        let zoomLevels = new WeakMap();
+
+        // Store the zoom level for all existing tabs.
+        for (let window of WindowListManager.browserWindows()) {
+          for (let tab of window.gBrowser.tabs) {
+            let browser = tab.linkedBrowser;
+            zoomLevels.set(browser, getZoomLevel(browser));
+          }
+        }
+
+        let tabCreated = (eventName, event) => {
+          let browser = event.tab.linkedBrowser;
+          zoomLevels.set(browser, getZoomLevel(browser));
+        };
+
+
+        let zoomListener = event => {
+          let browser = event.originalTarget;
+
+          // For non-remote browsers, this event is dispatched on the document
+          // rather than on the <browser>.
+          if (browser instanceof Ci.nsIDOMDocument) {
+            browser = browser.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
+                             .getInterface(Ci.nsIDocShell)
+                             .chromeEventHandler;
+          }
+
+          let {gBrowser} = browser.ownerDocument.defaultView;
+          let tab = gBrowser.getTabForBrowser(browser);
+          if (!tab) {
+            // We only care about zoom events in the top-level browser of a tab.
+            return;
+          }
+
+          let oldZoomFactor = zoomLevels.get(browser);
+          let newZoomFactor = getZoomLevel(browser);
+
+          if (oldZoomFactor != newZoomFactor) {
+            zoomLevels.set(browser, newZoomFactor);
+
+            let tabId = TabManager.getId(tab);
+            fire({
+              tabId,
+              oldZoomFactor,
+              newZoomFactor,
+              zoomSettings: self.tabs._getZoomSettings(tabId),
+            });
+          }
+        };
+
+        tabListener.init();
+        tabListener.on("tab-attached", tabCreated);
+        tabListener.on("tab-created", tabCreated);
+
+        AllWindowEvents.addListener("FullZoomChange", zoomListener);
+        AllWindowEvents.addListener("TextZoomChange", zoomListener);
+        return () => {
+          tabListener.off("tab-attached", tabCreated);
+          tabListener.off("tab-created", tabCreated);
+
+          AllWindowEvents.removeListener("FullZoomChange", zoomListener);
+          AllWindowEvents.removeListener("TextZoomChange", zoomListener);
+        };
+      }).api(),
     },
   };
   return self;
 });
--- a/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
+++ b/browser/components/extensions/test/browser/browser_ext_tabs_zoom.js
@@ -35,38 +35,68 @@ add_task(function* () {
     function msg(...args) {
       return new Promise((resolve, reject) => {
         let id = ++_id;
         deferred[id] = {resolve, reject};
         browser.test.sendMessage("msg", id, ...args);
       });
     }
 
-    let checkZoom = (tabId, newValue) => {
+
+    let zoomEvents = [];
+    let eventPromises = [];
+    browser.tabs.onZoomChange.addListener(info => {
+      zoomEvents.push(info);
+      if (eventPromises.length) {
+        eventPromises.shift().resolve();
+      }
+    });
+
+    let checkZoom = (tabId, newValue, oldValue = null) => {
+      let awaitEvent;
+      if (oldValue != null && !zoomEvents.length) {
+        awaitEvent = new Promise(resolve => {
+          eventPromises.push({resolve});
+        });
+      }
+
       return Promise.all([
         browser.tabs.getZoom(tabId),
         msg("get-zoom", tabId),
+        awaitEvent,
       ]).then(([apiZoom, realZoom]) => {
         browser.test.assertEq(newValue, apiZoom, `Got expected zoom value from API`);
         browser.test.assertEq(newValue, realZoom, `Got expected zoom value from parent`);
+
+        if (oldValue != null) {
+          let event = zoomEvents.shift();
+          browser.test.assertEq(tabId, event.tabId, `Got expected zoom event tab ID`);
+          browser.test.assertEq(newValue, event.newZoomFactor, `Got expected zoom event zoom factor`);
+          browser.test.assertEq(oldValue, event.oldZoomFactor, `Got expected zoom event old zoom factor`);
+
+          browser.test.assertEq(3, Object.keys(event.zoomSettings).length, `Zoom settings should have 3 keys`);
+          browser.test.assertEq("automatic", event.zoomSettings.mode, `Mode should be "automatic"`);
+          browser.test.assertEq("per-origin", event.zoomSettings.scope, `Scope should be "per-origin"`);
+          browser.test.assertEq(1, event.zoomSettings.defaultZoomFactor, `Default zoom should be 1`);
+        }
       });
     };
 
     let tabIds;
 
     browser.tabs.query({lastFocusedWindow: true}).then(tabs => {
       browser.test.assertEq(tabs.length, 3, "We have three tabs");
 
       tabIds = [tabs[1].id, tabs[2].id];
 
       return checkZoom(tabIds[0], 1);
     }).then(() => {
       return browser.tabs.setZoom(tabIds[0], 2);
     }).then(() => {
-      return checkZoom(tabIds[0], 2);
+      return checkZoom(tabIds[0], 2, 1);
     }).then(() => {
       return browser.tabs.getZoomSettings(tabIds[0]);
     }).then(zoomSettings => {
       browser.test.assertEq(3, Object.keys(zoomSettings).length, `Zoom settings should have 3 keys`);
       browser.test.assertEq("automatic", zoomSettings.mode, `Mode should be "automatic"`);
       browser.test.assertEq("per-origin", zoomSettings.scope, `Scope should be "per-origin"`);
       browser.test.assertEq(1, zoomSettings.defaultZoomFactor, `Default zoom should be 1`);
 
@@ -75,48 +105,48 @@ add_task(function* () {
     }).then(() => {
       return checkZoom(tabIds[1], 1);
     }).then(() => {
       browser.test.log(`Navigate tab 2 to origin of tab 1`);
       browser.tabs.update(tabIds[1], {url: "http://example.com"});
 
       return promiseUpdated(tabIds[1], "url");
     }).then(() => {
-      return checkZoom(tabIds[1], 2);
+      return checkZoom(tabIds[1], 2, 1);
     }).then(() => {
       browser.test.log(`Update zoom in tab 2, expect changes in both tabs`);
       return browser.tabs.setZoom(tabIds[1], 1.5);
     }).then(() => {
-      return checkZoom(tabIds[1], 1.5);
+      return checkZoom(tabIds[1], 1.5, 2);
     }).then(() => {
       browser.test.log(`Switch to tab 1, expect asynchronous zoom change just after the switch`);
       return browser.tabs.update(tabIds[0], {active: true});
     }).then(() => {
       return new Promise(resolve => setTimeout(resolve, 0));
     }).then(() => {
-      return checkZoom(tabIds[0], 1.5);
+      return checkZoom(tabIds[0], 1.5, 2);
     }).then(() => {
       browser.test.log("Set zoom to 0, expect it set to 1");
       return browser.tabs.setZoom(tabIds[0], 0);
     }).then(() => {
-      return checkZoom(tabIds[0], 1);
+      return checkZoom(tabIds[0], 1, 1.5);
     }).then(() => {
       browser.test.log("Change zoom externally, expect changes reflected");
       return msg("enlarge");
     }).then(() => {
-      return checkZoom(tabIds[0], 1.1);
+      return checkZoom(tabIds[0], 1.1, 1);
     }).then(() => {
       return Promise.all([
         browser.tabs.setZoom(tabIds[0], 0),
         browser.tabs.setZoom(tabIds[1], 0),
       ]);
     }).then(() => {
       return Promise.all([
-        checkZoom(tabIds[0], 1),
-        checkZoom(tabIds[1], 1),
+        checkZoom(tabIds[0], 1, 1.1),
+        checkZoom(tabIds[1], 1, 1.5),
       ]);
     }).then(() => {
       browser.test.log("Check that invalid zoom values throw an error");
       return browser.tabs.setZoom(tabIds[0], 42).then(
         () => {
           browser.test.fail("Expected an error");
         },
         error => {
--- a/toolkit/content/browser-child.js
+++ b/toolkit/content/browser-child.js
@@ -454,23 +454,23 @@ addMessageListener("FullZoom", function 
 });
 
 addMessageListener("TextZoom", function (aMessage) {
   ZoomManager.textZoom = aMessage.data.value;
 });
 
 addEventListener("FullZoomChange", function () {
   if (ZoomManager.refreshFullZoom()) {
-    sendAsyncMessage("FullZoomChange", { value:  ZoomManager.fullZoom});
+    sendAsyncMessage("FullZoomChange", { value: ZoomManager.fullZoom });
   }
 }, false);
 
 addEventListener("TextZoomChange", function (aEvent) {
   if (ZoomManager.refreshTextZoom()) {
-    sendAsyncMessage("TextZoomChange", { value:  ZoomManager.textZoom});
+    sendAsyncMessage("TextZoomChange", { value: ZoomManager.textZoom });
   }
 }, false);
 
 addEventListener("ZoomChangeUsingMouseWheel", function () {
   sendAsyncMessage("ZoomChangeUsingMouseWheel", {});
 }, false);
 
 addMessageListener("UpdateCharacterSet", function (aMessage) {
--- a/toolkit/content/widgets/remote-browser.xml
+++ b/toolkit/content/widgets/remote-browser.xml
@@ -182,29 +182,43 @@
                 readonly="true"/>
 
       <field name="_fullZoom">1</field>
       <property name="fullZoom">
         <getter><![CDATA[
           return this._fullZoom;
         ]]></getter>
         <setter><![CDATA[
+          let changed = val.toFixed(2) != this._fullZoom.toFixed(2);
+
           this._fullZoom = val;
           this.messageManager.sendAsyncMessage("FullZoom", {value: val});
+
+          if (changed) {
+            let event = new Event("FullZoomChange", {bubbles: true});
+            this.dispatchEvent(event);
+          }
         ]]></setter>
       </property>
 
       <field name="_textZoom">1</field>
       <property name="textZoom">
         <getter><![CDATA[
           return this._textZoom;
         ]]></getter>
         <setter><![CDATA[
+          let changed = val.toFixed(2) != this._textZoom.toFixed(2);
+
           this._textZoom = val;
           this.messageManager.sendAsyncMessage("TextZoom", {value: val});
+
+          if (changed) {
+            let event = new Event("TextZoomChange", {bubbles: true});
+            this.dispatchEvent(event);
+          }
         ]]></setter>
       </property>
 
       <field name="_isSyntheticDocument">false</field>
       <property name="isSyntheticDocument">
         <getter><![CDATA[
           return this._isSyntheticDocument;
         ]]></getter>