Bug 1253132: [webext] Support window states in browser.windows APIs. r?billm draft
authorKris Maglione <maglione.k@gmail.com>
Thu, 03 Mar 2016 17:34:42 -0800
changeset 336674 f7363283cd918f60328eb97ff33d052bc543f9df
parent 336673 d2a269c7c7bbfcca49eef2648b07a4feb7351b35
child 336680 c65d56378aa77f1c83af6ba67627b937f5486f67
push id12168
push usermaglione.k@gmail.com
push dateFri, 04 Mar 2016 01:37:46 +0000
reviewersbillm
bugs1253132
milestone47.0a1
Bug 1253132: [webext] Support window states in browser.windows APIs. r?billm MozReview-Commit-ID: LIw6swRAB3h
browser/components/extensions/ext-tabs.js
browser/components/extensions/ext-utils.js
browser/components/extensions/ext-windows.js
browser/components/extensions/schemas/windows.json
browser/components/extensions/test/browser/browser.ini
browser/components/extensions/test/browser/browser_ext_windows_create.js
browser/components/extensions/test/browser/browser_ext_windows_update.js
--- a/browser/components/extensions/ext-tabs.js
+++ b/browser/components/extensions/ext-tabs.js
@@ -440,17 +440,17 @@ extensions.registerSchemaAPI("tabs", nul
             if (createProperties.pinned) {
               window.gBrowser.pinTab(tab);
             }
 
             resolve(TabManager.convert(extension, tab));
           }
 
           let window = createProperties.windowId !== null ?
-            WindowManager.getWindow(createProperties.windowId) :
+            WindowManager.getWindow(createProperties.windowId, context) :
             WindowManager.topWindow;
           if (!window.gBrowser) {
             let obs = (finishedWindow, topic, data) => {
               if (finishedWindow != window) {
                 return;
               }
               Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
               createInWindow(window);
@@ -624,17 +624,17 @@ extensions.registerSchemaAPI("tabs", nul
 
       captureVisibleTab: function(windowId, options) {
         if (!extension.hasPermission("<all_urls>")) {
           return Promise.reject({message: "The <all_urls> permission is required to use the captureVisibleTab API"});
         }
 
         let window = windowId == null ?
           WindowManager.topWindow :
-          WindowManager.getWindow(windowId);
+          WindowManager.getWindow(windowId, context);
 
         let browser = window.gBrowser.selectedBrowser;
         let recipient = {
           innerWindowID: browser.innerWindowID,
         };
 
         if (!options) {
           options = {};
@@ -769,17 +769,17 @@ extensions.registerSchemaAPI("tabs", nul
         let index = moveProperties.index;
         let tabsMoved = [];
         if (!Array.isArray(tabIds)) {
           tabIds = [tabIds];
         }
 
         let destinationWindow = null;
         if (moveProperties.windowId !== null) {
-          destinationWindow = WindowManager.getWindow(moveProperties.windowId);
+          destinationWindow = WindowManager.getWindow(moveProperties.windowId, context);
           // Ignore invalid window.
           if (!destinationWindow) {
             return;
           }
         }
 
         /*
           Indexes are maintained on a per window basis so that a call to
--- a/browser/components/extensions/ext-utils.js
+++ b/browser/components/extensions/ext-utils.js
@@ -632,38 +632,80 @@ global.WindowManager = {
     if (this._windows.has(window)) {
       return this._windows.get(window);
     }
     let id = this._nextId++;
     this._windows.set(window, id);
     return id;
   },
 
-  getWindow(id) {
+  getWindow(id, context) {
+    if (id == this.WINDOW_ID_CURRENT) {
+      return currentWindow(context);
+    }
+
     for (let window of WindowListManager.browserWindows(true)) {
       if (this.getId(window) == id) {
         return window;
       }
     }
     return null;
   },
 
+  setState(window, state) {
+    if (state != "fullscreen" && window.fullScreen) {
+      window.fullScreen = false;
+    }
+
+    switch (state) {
+      case "maximized":
+        window.maximize();
+        break;
+
+      case "minimized":
+      case "docked":
+        window.minimize();
+        break;
+
+      case "normal":
+        window.restore();
+        break;
+
+      case "fullscreen":
+        window.fullScreen = true;
+        break;
+
+      default:
+        throw new Error(`Unexpected window state: ${state}`);
+    }
+  },
+
   convert(extension, window, getInfo) {
+    const STATES = {
+      [window.STATE_MAXIMIZED]: "maximized",
+      [window.STATE_MINIMIZED]: "minimized",
+      [window.STATE_NORMAL]: "normal",
+    };
+    let state = STATES[window.windowState];
+    if (window.fullScreen) {
+      state = "fullscreen";
+    }
+
     let result = {
       id: this.getId(window),
       focused: window.document.hasFocus(),
       top: window.screenY,
       left: window.screenX,
       width: window.outerWidth,
       height: window.outerHeight,
       incognito: PrivateBrowsingUtils.isWindowPrivate(window),
 
       // We fudge on these next two.
       type: this.windowType(window),
-      state: window.fullScreen ? "fullscreen" : "normal",
+      state,
     };
 
     if (getInfo && getInfo.populate) {
       result.tabs = TabManager.for(extension).getTabs(window);
     }
 
     return result;
   },
--- a/browser/components/extensions/ext-windows.js
+++ b/browser/components/extensions/ext-windows.js
@@ -37,17 +37,17 @@ extensions.registerSchemaAPI("windows", 
         AllWindowEvents.addListener("blur", listener);
         return () => {
           AllWindowEvents.removeListener("focus", listener);
           AllWindowEvents.removeListener("blur", listener);
         };
       }).api(),
 
       get: function(windowId, getInfo) {
-        let window = WindowManager.getWindow(windowId);
+        let window = WindowManager.getWindow(windowId, context);
         return Promise.resolve(WindowManager.convert(extension, window, getInfo));
       },
 
       getCurrent: function(getInfo) {
         let window = currentWindow(context);
         return Promise.resolve(WindowManager.convert(extension, window, getInfo));
       },
 
@@ -58,16 +58,23 @@ extensions.registerSchemaAPI("windows", 
 
       getAll: function(getInfo) {
         let windows = Array.from(WindowListManager.browserWindows(),
                                  window => WindowManager.convert(extension, window, getInfo));
         return Promise.resolve(windows);
       },
 
       create: function(createData) {
+        if (createData.state !== null && createData.state != "normal") {
+          if (createData.left !== null || createData.top !== null ||
+              createData.width !== null || createData.height !== null) {
+            return Promise.reject({message: `"state": "${createData.state}" may not be combined with "left", "top", "width", or "height"`});
+          }
+        }
+
         function mkstr(s) {
           let result = Cc["@mozilla.org/supports-string;1"].createInstance(Ci.nsISupportsString);
           result.data = s;
           return result;
         }
 
         let args = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
 
@@ -122,38 +129,71 @@ extensions.registerSchemaAPI("windows", 
           window.moveTo(left, top);
         }
         if (createData.width !== null || createData.height !== null) {
           let width = createData.width !== null ? createData.width : window.outerWidth;
           let height = createData.height !== null ? createData.height : window.outerHeight;
           window.resizeTo(width, height);
         }
 
-        // TODO: focused, type, state
+        // TODO: focused, type
 
         return new Promise(resolve => {
           window.addEventListener("load", function listener() {
             window.removeEventListener("load", listener);
-            resolve(WindowManager.convert(extension, window));
+
+            if (createData.state == "maximized" || createData.state == "normal" || createData.state == "fullscreen") {
+              window.document.documentElement.setAttribute("sizemode", createData.state);
+            } else if (createData.state !== null) {
+              // window.minimize() has no useful effect until the window has
+              // been shown.
+
+              let obs = doc => {
+                if (doc === window.document) {
+                  Services.obs.removeObserver(obs, "document-shown");
+                  WindowManager.setState(window, createData.state);
+                  resolve();
+                }
+              };
+              Services.obs.addObserver(obs, "document-shown", false);
+              return;
+            }
+
+            resolve();
           });
+        }).then(() => {
+          return WindowManager.convert(extension, window);
         });
       },
 
       update: function(windowId, updateInfo) {
-        let window = WindowManager.getWindow(windowId);
+        // TODO: When we support size/position updates:
+        // if (updateInfo.state !== null && updateInfo.state != "normal") {
+        //   if (updateInfo.left !== null || updateInfo.top !== null ||
+        //       updateInfo.width !== null || updateInfo.height !== null) {
+        //     return Promise.reject({message: `"state": "${updateInfo.state}" may not be combined with "left", "top", "width", or "height"`});
+        //   }
+        // }
+
+        let window = WindowManager.getWindow(windowId, context);
         if (updateInfo.focused) {
           Services.focus.activeWindow = window;
         }
-        // TODO: All the other properties...
+
+        if (updateInfo.state !== null) {
+          WindowManager.setState(window, updateInfo.state);
+        }
+
+        // TODO: All the other properties, focused=false...
 
         return Promise.resolve(WindowManager.convert(extension, window));
       },
 
       remove: function(windowId) {
-        let window = WindowManager.getWindow(windowId);
+        let window = WindowManager.getWindow(windowId, context);
         window.close();
 
         return new Promise(resolve => {
           let listener = () => {
             AllWindowEvents.removeListener("domwindowclosed", listener);
             resolve();
           };
           AllWindowEvents.addListener("domwindowclosed", listener);
--- a/browser/components/extensions/schemas/windows.json
+++ b/browser/components/extensions/schemas/windows.json
@@ -336,17 +336,16 @@
               },
               "type": {
                 "unsupported": true,
                 "$ref": "CreateType",
                 "optional": true,
                 "description": "Specifies what type of browser window to create. The 'panel' and 'detached_panel' types create a popup unless the '--enable-panels' flag is set."
               },
               "state": {
-                "unsupported": true,
                 "$ref": "WindowState",
                 "optional": true,
                 "description": "The initial state of the window. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'."
               }
             },
             "optional": true
           },
           {
@@ -412,17 +411,16 @@
               },
               "drawAttention": {
                 "unsupported": true,
                 "type": "boolean",
                 "optional": true,
                 "description": "If true, causes the window to be displayed in a manner that draws the user's attention to the window, without changing the focused window. The effect lasts until the user changes focus to the window. This option has no effect if the window already has focus. Set to false to cancel a previous draw attention request."
               },
               "state": {
-                "unsupported": true,
                 "$ref": "WindowState",
                 "optional": true,
                 "description": "The new state of the window. The 'minimized', 'maximized' and 'fullscreen' states cannot be combined with 'left', 'top', 'width' or 'height'."
               }
             }
           },
           {
             "type": "function",
--- a/browser/components/extensions/test/browser/browser.ini
+++ b/browser/components/extensions/test/browser/browser.ini
@@ -44,14 +44,15 @@ support-files =
 [browser_ext_tabs_duplicate.js]
 [browser_ext_tabs_update.js]
 [browser_ext_tabs_update_url.js]
 [browser_ext_tabs_onUpdated.js]
 [browser_ext_tabs_sendMessage.js]
 [browser_ext_tabs_move.js]
 [browser_ext_tabs_move_window.js]
 [browser_ext_tabs_onHighlighted.js]
+[browser_ext_windows_create.js]
 [browser_ext_windows_create_tabId.js]
 [browser_ext_windows_update.js]
 [browser_ext_contentscript_connect.js]
 [browser_ext_tab_runtimeConnect.js]
 [browser_ext_topwindowid.js]
 [browser_ext_webNavigation_getFrames.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/extensions/test/browser/browser_ext_windows_create.js
@@ -0,0 +1,114 @@
+/* -*- Mode: indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim: set sts=2 sw=2 et tw=80: */
+"use strict";
+
+add_task(function* testWindowCreate() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      let _checkWindowPromise;
+      browser.test.onMessage.addListener(msg => {
+        if (msg == "checked-window") {
+          _checkWindowPromise.resolve();
+          _checkWindowPromise = null;
+        }
+      });
+
+      function checkWindow(expected) {
+        return new Promise(resolve => {
+          _checkWindowPromise = {resolve};
+          browser.test.sendMessage("check-window", expected);
+        });
+      }
+
+      function createWindow(params, expected) {
+        return browser.windows.create(params).then(window => {
+          for (let key of Object.keys(params)) {
+            browser.test.assertEq(params[key], window[key], `Got expected value for window.${key}`);
+          }
+
+          return checkWindow(expected).then(() => {
+            return browser.windows.remove(window.id);
+          });
+        });
+      }
+
+      createWindow({state: "minimized"}, {state: "STATE_MINIMIZED"})
+      .then(() => createWindow({state: "maximized"}, {state: "STATE_MAXIMIZED"}))
+      .then(() => createWindow({state: "normal"}, {state: "STATE_NORMAL"}))
+      .then(() => createWindow({state: "fullscreen"}, {state: "STATE_FULLSCREEN"}))
+      .then(() => {
+        browser.test.notifyPass("window-create");
+      }).catch(e => {
+        browser.test.fail(`${e} :: ${e.stack}`);
+        browser.test.notifyFail("window-create");
+      });
+    },
+  });
+
+  let latestWindow;
+  let windowListener = (window, topic) => {
+    if (topic == "domwindowopened") {
+      latestWindow = window;
+    }
+  };
+  Services.ww.registerNotification(windowListener);
+
+  extension.onMessage("check-window", expected => {
+    if (expected.state != null) {
+      let {windowState} = latestWindow;
+      if (latestWindow.fullScreen) {
+        windowState = latestWindow.STATE_FULLSCREEN;
+      }
+
+      is(windowState, latestWindow[expected.state],
+         `Expected window state to be ${expected.state}`);
+    }
+
+    extension.sendMessage("checked-window");
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("window-create");
+  yield extension.unload();
+
+  Services.ww.unregisterNotification(windowListener);
+  latestWindow = null;
+});
+
+
+// Tests that incompatible parameters can't be used together.
+add_task(function* testWindowCreateParams() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      function* getCalls() {
+        for (let state of ["minimized", "maximized", "fullscreen"]) {
+          for (let param of ["left", "top", "width", "height"]) {
+            let expected = `"state": "${state}" may not be combined with "left", "top", "width", or "height"`;
+
+            yield browser.windows.create({state, [param]: 100}).then(
+              val => {
+                browser.test.fail(`Expected error but got "${val}" instead`);
+              },
+              error => {
+                browser.test.assertTrue(
+                  error.message.includes(expected),
+                  `Got expected error (got: '${error.message}', expected: '${expected}'`);
+              });
+          }
+        }
+      }
+
+      Promise.all(getCalls()).then(() => {
+        browser.test.notifyPass("window-create-params");
+      }).catch(e => {
+        browser.test.fail(`${e} :: ${e.stack}`);
+        browser.test.notifyFail("window-create-params");
+      });
+    },
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("window-create-params");
+  yield extension.unload();
+});
+
--- a/browser/components/extensions/test/browser/browser_ext_windows_update.js
+++ b/browser/components/extensions/test/browser/browser_ext_windows_update.js
@@ -14,20 +14,16 @@ add_task(function* () {
 
   let window1 = window;
   let window2 = yield BrowserTestUtils.openNewBrowserWindow();
 
   Services.focus.activeWindow = window2;
   yield promiseWaitForFocus(window2);
 
   let extension = ExtensionTestUtils.loadExtension({
-    manifest: {
-      "permissions": ["windows"],
-    },
-
     background: function() {
       browser.windows.getAll(undefined, function(wins) {
         browser.test.assertEq(wins.length, 2, "should have two windows");
 
         // Sort the unfocused window to the lower index.
         wins.sort(function(win1, win2) {
           if (win1.focused === win2.focused) {
             return 0;
@@ -46,8 +42,71 @@ add_task(function* () {
   yield Promise.all([extension.startup(), extension.awaitMessage("check")]);
 
   yield promiseWaitForFocus(window1);
 
   yield extension.unload();
 
   yield BrowserTestUtils.closeWindow(window2);
 });
+
+
+add_task(function* testWindowUpdate() {
+  let extension = ExtensionTestUtils.loadExtension({
+    background() {
+      let _checkWindowPromise;
+      browser.test.onMessage.addListener(msg => {
+        if (msg == "checked-window") {
+          _checkWindowPromise.resolve();
+          _checkWindowPromise = null;
+        }
+      });
+
+      function checkWindow(expected) {
+        return new Promise(resolve => {
+          _checkWindowPromise = {resolve};
+          browser.test.sendMessage("check-window", expected);
+        });
+      }
+
+      function updateWindow(windowId, params, expected) {
+        return browser.windows.update(windowId, params).then(window => {
+          for (let key of Object.keys(params)) {
+            browser.test.assertEq(params[key], window[key], `Got expected value for window.${key}`);
+          }
+
+          return checkWindow(expected);
+        });
+      }
+
+      let windowId = browser.windows.WINDOW_ID_CURRENT;
+
+      updateWindow(windowId, {state: "minimized"}, {state: "STATE_MINIMIZED"})
+      .then(() => updateWindow(windowId, {state: "maximized"}, {state: "STATE_MAXIMIZED"}))
+      .then(() => updateWindow(windowId, {state: "fullscreen"}, {state: "STATE_FULLSCREEN"}))
+      .then(() => updateWindow(windowId, {state: "normal"}, {state: "STATE_NORMAL"}))
+      .then(() => {
+        browser.test.notifyPass("window-update");
+      }).catch(e => {
+        browser.test.fail(`${e} :: ${e.stack}`);
+        browser.test.notifyFail("window-update");
+      });
+    },
+  });
+
+  extension.onMessage("check-window", expected => {
+    if (expected.state != null) {
+      let {windowState} = window;
+      if (window.fullScreen) {
+        windowState = window.STATE_FULLSCREEN;
+      }
+
+      is(windowState, window[expected.state],
+         `Expected window state to be ${expected.state}`);
+    }
+
+    extension.sendMessage("checked-window");
+  });
+
+  yield extension.startup();
+  yield extension.awaitFinish("window-update");
+  yield extension.unload();
+});