Bug 770695: land support for Social "service window" - chromeless window opened by social provider, r=gavin
authorShane Caraveo <mixedpuppy@gmail.com>
Tue, 24 Jul 2012 14:29:46 -0700
changeset 100556 9d8cc914ad36c644a37344b9b118916fc7e68d3d
parent 100555 20db7c6d82cc5bcb0e916bfe72d1ab04d3c3be2d
child 100557 677cdab1da6a1e6a8bcb1c56d2f65b10022c0575
push id23183
push usergsharp@mozilla.com
push dateThu, 26 Jul 2012 16:17:10 +0000
treeherdermozilla-central@98c2a42a3aef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin
bugs770695
milestone17.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 770695: land support for Social "service window" - chromeless window opened by social provider, r=gavin
browser/base/content/test/Makefile.in
browser/base/content/test/browser_social_mozSocial_API.js
browser/base/content/test/browser_social_toolbar.js
browser/base/content/test/head.js
browser/base/content/test/social_sidebar.html
browser/base/content/test/social_window.html
browser/base/content/test/social_worker.js
toolkit/components/social/MozSocialAPI.jsm
--- a/browser/base/content/test/Makefile.in
+++ b/browser/base/content/test/Makefile.in
@@ -256,16 +256,17 @@ endif
                  browser_tabDrop.js \
                  browser_lastAccessedTab.js \
                  browser_bug734076.js \
                  browser_social_toolbar.js \
                  browser_social_sidebar.js \
                  browser_social_mozSocial_API.js \
                  social_panel.html \
                  social_sidebar.html \
+                 social_window.html \
                  social_worker.js \
                  $(NULL)
 
 ifneq (cocoa,$(MOZ_WIDGET_TOOLKIT))
 _BROWSER_FILES += \
 		browser_bug462289.js \
 		$(NULL)
 else
--- a/browser/base/content/test/browser_social_mozSocial_API.js
+++ b/browser/base/content/test/browser_social_mozSocial_API.js
@@ -5,83 +5,130 @@
 let SocialService = Cu.import("resource://gre/modules/SocialService.jsm", {}).SocialService;
 
 function test() {
   // XXX Bug 775779
   if (Cc["@mozilla.org/xpcom/debug;1"].getService(Ci.nsIDebug2).isDebugBuild) {
     ok(true, "can't run social sidebar test in debug builds because they falsely report leaks");
     return;
   }
-
   waitForExplicitFinish();
 
   let manifest = { // normal provider
     name: "provider 1",
     origin: "http://example.com",
     sidebarURL: "http://example.com/browser/browser/base/content/test/social_sidebar.html",
     workerURL: "http://example.com/browser/browser/base/content/test/social_worker.js",
     iconURL: "chrome://branding/content/icon48.png"
   };
-  runSocialTestWithProvider(manifest, doTest);
+  runSocialTestWithProvider(manifest, function () {
+    runSocialTests(tests, undefined, undefined, function () {
+      SocialService.removeProvider(Social.provider.origin, finish);
+    });
+  });
 }
 
-function doTest() {
-  let iconsReady = false;
-  let gotSidebarMessage = false;
+var tests = {
+  testStatusIcons: function(next) {
+    let iconsReady = false;
+    let gotSidebarMessage = false;
+
+    function checkNext() {
+      if (iconsReady && gotSidebarMessage)
+        triggerIconPanel();
+    }
+
+    function triggerIconPanel() {
+      let statusIcons = document.getElementById("social-status-iconbox");
+      ok(!statusIcons.firstChild.collapsed, "status icon is visible");
+      // Click the button to trigger its contentPanel
+      let panel = document.getElementById("social-notification-panel");
+      EventUtils.synthesizeMouseAtCenter(statusIcons.firstChild, {});
+    }
 
-  function checkNext() {
-    if (iconsReady && gotSidebarMessage)
-      triggerIconPanel();
-  }
-
-  function triggerIconPanel() {
-    let statusIcons = document.getElementById("social-status-iconbox");
-    ok(!statusIcons.firstChild.collapsed, "status icon is visible");
-    // Click the button to trigger its contentPanel
-    let panel = document.getElementById("social-notification-panel");
-    EventUtils.synthesizeMouseAtCenter(statusIcons.firstChild, {});
-  }
+    let port = Social.provider.port;
+    ok(port, "provider has a port");
+    port.postMessage({topic: "test-init"});
+    Social.provider.port.onmessage = function (e) {
+      let topic = e.data.topic;
+      switch (topic) {
+        case "got-panel-message":
+          ok(true, "got panel message");
+          // Wait for the panel to close before ending the test
+          let panel = document.getElementById("social-notification-panel");
+          panel.addEventListener("popuphidden", function hiddenListener() {
+            panel.removeEventListener("popuphidden", hiddenListener);
+            next();
+          });
+          panel.hidePopup();
+          break;
+        case "got-sidebar-message":
+          // The sidebar message will always come first, since it loads by default
+          ok(true, "got sidebar message");
+          gotSidebarMessage = true;
+          checkNext();
+          break;
+      }
+    }
 
-  let port = Social.provider.port;
-  ok(port, "provider has a port");
-  port.postMessage({topic: "test-init"});
-  Social.provider.port.onmessage = function (e) {
-    let topic = e.data.topic;
-    switch (topic) {
-      case "got-panel-message":
-        ok(true, "got panel message");
-        // Wait for the panel to close before ending the test
-        let panel = document.getElementById("social-notification-panel");
-        panel.addEventListener("popuphidden", function hiddenListener() {
-          panel.removeEventListener("popuphidden", hiddenListener);
-          SocialService.removeProvider(Social.provider.origin, finish);
+    // Our worker sets up ambient notification at the same time as it responds to
+    // the workerAPI initialization. If it's already initialized, we can
+    // immediately check the icons, otherwise wait for initialization by
+    // observing the topic sent out by the social service.
+    if (Social.provider.workerAPI.initialized) {
+      iconsReady = true;
+      checkNext();
+    } else {
+      Services.obs.addObserver(function obs() {
+        Services.obs.removeObserver(obs, "social:ambient-notification-changed");
+        // Let the other observers (like the one that updates the UI) run before
+        // checking the icons.
+        executeSoon(function () {
+          iconsReady = true;
+          checkNext();
         });
-        panel.hidePopup();
-        break;
-      case "got-sidebar-message":
-        // The sidebar message will always come first, since it loads by default
-        ok(true, "got sidebar message");
-        info(topic);
-        gotSidebarMessage = true;
-        checkNext();
-        break;
+      }, "social:ambient-notification-changed", false);
+    }
+  },
+
+  testServiceWindow: function(next) {
+    // our test provider was initialized in the test above, we just
+    // initiate our specific test now.
+    let port = Social.provider.port;
+    ok(port, "provider has a port");
+    port.postMessage({topic: "test-service-window"});
+    port.onmessage = function (e) {
+      let topic = e.data.topic;
+      switch (topic) {
+        case "got-service-window-message":
+          // The sidebar message will always come first, since it loads by default
+          ok(true, "got service window message");
+          port.postMessage({topic: "test-close-service-window"});
+          break;
+        case "got-service-window-closed-message":
+          ok(true, "got service window closed message");
+          next();
+          break;
+      }
+    }
+  },
+
+  testServiceWindowTwice: function(next) {
+    let port = Social.provider.port;
+    port.postMessage({topic: "test-service-window-twice"});
+    Social.provider.port.onmessage = function (e) {
+      let topic = e.data.topic;
+      switch (topic) {
+        case "test-service-window-twice-result":
+          is(e.data.result, "ok", "only one window should open when name is reused");
+          break;
+        case "got-service-window-message":
+          ok(true, "got service window message");
+          port.postMessage({topic: "test-close-service-window"});
+          break;
+        case "got-service-window-closed-message":
+          ok(true, "got service window closed message");
+          next();
+          break;
+      }
     }
   }
-
-  // Our worker sets up ambient notification at the same time as it responds to
-  // the workerAPI initialization. If it's already initialized, we can
-  // immediately check the icons, otherwise wait for initialization by
-  // observing the topic sent out by the social service.
-  if (Social.provider.workerAPI.initialized) {
-    iconsReady = true;
-    checkNext();
-  } else {
-    Services.obs.addObserver(function obs() {
-      Services.obs.removeObserver(obs, "social:ambient-notification-changed");
-      // Let the other observers (like the one that updates the UI) run before
-      // checking the icons.
-      executeSoon(function () {
-        iconsReady = true;
-        checkNext();
-      });
-    }, "social:ambient-notification-changed", false);
-  }
 }
--- a/browser/base/content/test/browser_social_toolbar.js
+++ b/browser/base/content/test/browser_social_toolbar.js
@@ -9,17 +9,17 @@ function test() {
 
   let manifest = { // normal provider
     name: "provider 1",
     origin: "https://example1.com",
     workerURL: "https://example1.com/worker.js",
     iconURL: "chrome://branding/content/icon48.png"
   };
   runSocialTestWithProvider(manifest, function () {
-    runTests(tests, undefined, undefined, function () {
+    runSocialTests(tests, undefined, undefined, function () {
       SocialService.removeProvider(Social.provider.origin, finish);
     });
   });
 }
 
 var tests = {
   testProfileSet: function(next) {
     let profile = {
@@ -69,47 +69,8 @@ var tests = {
       ok(ambience.collapsed, "ambient icon is collapsed");
       ambience = ambience.nextSibling;
     }
     
     next();
   }
 }
 
-function runTests(tests, cbPreTest, cbPostTest, cbFinish) {
-  let testIter = Iterator(tests);
-
-  if (cbPreTest === undefined) {
-    cbPreTest = function(cb) {cb()};
-  }
-  if (cbPostTest === undefined) {
-    cbPostTest = function(cb) {cb()};
-  }
-
-  function runNextTest() {
-    let name, func;
-    try {
-      [name, func] = testIter.next();
-    } catch (err if err instanceof StopIteration) {
-      // out of items:
-      (cbFinish || finish)();
-      return;
-    }
-    // We run on a timeout as the frameworker also makes use of timeouts, so
-    // this helps keep the debug messages sane.
-    executeSoon(function() {
-      function cleanupAndRunNextTest() {
-        info("sub-test " + name + " complete");
-        cbPostTest(runNextTest);
-      }
-      cbPreTest(function() {
-        info("sub-test " + name + " starting");
-        try {
-          func.call(tests, cleanupAndRunNextTest);
-        } catch (ex) {
-          ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
-          cleanupAndRunNextTest();
-        }
-      })
-    });
-  }
-  runNextTest();
-}
--- a/browser/base/content/test/head.js
+++ b/browser/base/content/test/head.js
@@ -113,8 +113,49 @@ function runSocialTestWithProvider(manif
     } else {
       Services.obs.addObserver(function obs() {
         Services.obs.removeObserver(obs, "test-social-ui-ready");
         saveOldProviderAndStartTestWith(provider);
       }, "test-social-ui-ready", false);
     }
   });
 }
+
+
+function runSocialTests(tests, cbPreTest, cbPostTest, cbFinish) {
+  let testIter = Iterator(tests);
+
+  if (cbPreTest === undefined) {
+    cbPreTest = function(cb) {cb()};
+  }
+  if (cbPostTest === undefined) {
+    cbPostTest = function(cb) {cb()};
+  }
+
+  function runNextTest() {
+    let name, func;
+    try {
+      [name, func] = testIter.next();
+    } catch (err if err instanceof StopIteration) {
+      // out of items:
+      (cbFinish || finish)();
+      return;
+    }
+    // We run on a timeout as the frameworker also makes use of timeouts, so
+    // this helps keep the debug messages sane.
+    executeSoon(function() {
+      function cleanupAndRunNextTest() {
+        info("sub-test " + name + " complete");
+        cbPostTest(runNextTest);
+      }
+      cbPreTest(function() {
+        info("sub-test " + name + " starting");
+        try {
+          func.call(tests, cleanupAndRunNextTest);
+        } catch (ex) {
+          ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
+          cleanupAndRunNextTest();
+        }
+      })
+    });
+  }
+  runNextTest();
+}
--- a/browser/base/content/test/social_sidebar.html
+++ b/browser/base/content/test/social_sidebar.html
@@ -1,14 +1,40 @@
 <html>
   <head>
     <meta charset="utf-8">
     <script>
+      var win;
       function pingWorker() {
         var port = navigator.mozSocial.getWorker().port;
+        port.onmessage = function(e) {
+          var topic = e.data.topic;
+          switch (topic) {
+            case "test-service-window":
+              win = navigator.mozSocial.openServiceWindow("social_window.html", "test-service-window", "width=300,height=300");
+              break;
+            case "test-service-window-twice":
+              win = navigator.mozSocial.openServiceWindow("social_window.html", "test-service-window", "width=300,height=300");
+              var win2 = navigator.mozSocial.openServiceWindow("social_window.html", "test-service-window", "");
+              var result;
+              if (win == win2)
+                result = "ok";
+              else
+                result = "not ok: " + win2 + " != " + win;
+              port.postMessage({topic: "test-service-window-twice-result", result: result});
+              break;
+            case "test-close-service-window":
+              win.addEventListener("unload", function watchClose() {
+                win.removeEventListener("unload", watchClose);
+                port.postMessage({topic: "service-window-closed-message", result: "ok"});
+              }, false)
+              win.close();
+              break;
+          }
+        }
         port.postMessage({topic: "sidebar-message", result: "ok"});
       }
     </script>
   </head>
   <body onload="pingWorker();">
     <p>This is a test social sidebar.</p>
   </body>
 </html>
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/social_window.html
@@ -0,0 +1,14 @@
+<html>
+  <head>
+    <meta charset="utf-8">
+    <script>
+      function pingWorker() {
+        var port = navigator.mozSocial.getWorker().port;
+        port.postMessage({topic: "service-window-message", result: "ok"});
+      }
+    </script>
+  </head>
+  <body onload="pingWorker();">
+    <p>This is a test social service window.</p>
+  </body>
+</html>
--- a/browser/base/content/test/social_worker.js
+++ b/browser/base/content/test/social_worker.js
@@ -1,26 +1,45 @@
 /* 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/. */
 
-let testPort;
+let testPort, sidebarPort;
 
 onconnect = function(e) {
   let port = e.ports[0];
   port.onmessage = function onMessage(event) {
     let topic = event.data.topic;
     switch (topic) {
       case "test-init":
         testPort = port;
         break;
       case "sidebar-message":
+        sidebarPort = port;
         if (testPort && event.data.result == "ok")
           testPort.postMessage({topic:"got-sidebar-message"});
         break;
+      case "service-window-message":
+        testPort.postMessage({topic:"got-service-window-message"});
+        break;
+      case "service-window-closed-message":
+        testPort.postMessage({topic:"got-service-window-closed-message"});
+        break;
+      case "test-service-window":
+        sidebarPort.postMessage({topic:"test-service-window"});
+        break;
+      case "test-service-window-twice":
+        sidebarPort.postMessage({topic:"test-service-window-twice"});
+        break;
+      case "test-service-window-twice-result":
+        testPort.postMessage({topic: "test-service-window-twice-result", result: event.data.result })
+        break;
+      case "test-close-service-window":
+        sidebarPort.postMessage({topic:"test-close-service-window"});
+        break;
       case "panel-message":
         if (testPort && event.data.result == "ok")
           testPort.postMessage({topic:"got-panel-message"});
         break;
       case "social.initialize":
         // This is the workerAPI port, respond and set up a notification icon.
         port.postMessage({topic: "social.initialize-response"});
         let profile = {
--- a/toolkit/components/social/MozSocialAPI.jsm
+++ b/toolkit/components/social/MozSocialAPI.jsm
@@ -36,25 +36,16 @@ function injectController(doc, topic, da
     if (!window)
       return;
 
     var containingBrowser = window.QueryInterface(Ci.nsIInterfaceRequestor)
                                   .getInterface(Ci.nsIWebNavigation)
                                   .QueryInterface(Ci.nsIDocShell)
                                   .chromeEventHandler;
 
-    // If the containing browser isn't one of the social browsers, nothing to
-    // do here.
-    // XXX this is app-specific, might want some mechanism for consumers to
-    // whitelist other IDs/windowtypes  
-    if (!(containingBrowser.id == "social-sidebar-browser" ||
-          containingBrowser.id == "social-notification-browser")) {
-      return;
-    }
-
     let origin = containingBrowser.getAttribute("origin");
     if (!origin) {
       return;
     }
 
     SocialService.getProvider(origin, function(provider) {
       if (provider && provider.workerURL) {
         attachToWindow(provider, window);
@@ -87,16 +78,28 @@ function attachToWindow(provider, target
         port: port,
         __exposedProps__: {
           port: "r"
         }
       };
     },
     hasBeenIdleFor: function () {
       return false;
+    },
+    openServiceWindow: function(toURL, name, options) {
+      return openServiceWindow(provider, targetWindow, toURL, name, options);
+    },
+    getAttention: function() {
+      let mainWindow = targetWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                         .getInterface(Components.interfaces.nsIWebNavigation)
+                         .QueryInterface(Components.interfaces.nsIDocShellTreeItem)
+                         .rootTreeItem
+                         .QueryInterface(Components.interfaces.nsIInterfaceRequestor)
+                         .getInterface(Components.interfaces.nsIDOMWindow);
+      mainWindow.getAttention();
     }
   };
 
   let contentObj = Cu.createObjectIn(targetWindow);
   let propList = {};
   for (let prop in mozSocialObj) {
     propList[prop] = {
       enumerable: true,
@@ -123,8 +126,64 @@ function attachToWindow(provider, target
     // set a timer which will fire after the unload events have all fired.
     schedule(function () { port.close(); });
   });
 }
 
 function schedule(callback) {
   Services.tm.mainThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
 }
+
+function openServiceWindow(provider, contentWindow, url, name, options) {
+  // resolve partial URLs and check prePath matches
+  let uri;
+  let fullURL;
+  try {
+    fullURL = contentWindow.document.documentURIObject.resolve(url);
+    uri = Services.io.newURI(fullURL, null, null);
+  } catch (ex) {
+    Cu.reportError("openServiceWindow: failed to resolve window URL: " + url + "; " + ex);
+    return null;
+  }
+
+  if (provider.origin != uri.prePath) {
+    Cu.reportError("openServiceWindow: unable to load new location, " +
+                   provider.origin + " != " + uri.prePath);
+    return null;
+  }
+
+  function getChromeWindow(contentWin) {
+    return contentWin.QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIWebNavigation)
+                     .QueryInterface(Ci.nsIDocShellTreeItem)
+                     .rootTreeItem
+                     .QueryInterface(Ci.nsIInterfaceRequestor)
+                     .getInterface(Ci.nsIDOMWindow);
+
+  }
+  let chromeWindow = Services.ww.getWindowByName("social-service-window-" + name,
+                                                 getChromeWindow(contentWindow));
+  let tabbrowser = chromeWindow && chromeWindow.gBrowser;
+  if (tabbrowser &&
+      tabbrowser.selectedBrowser.getAttribute("origin") == provider.origin) {
+    return tabbrowser.contentWindow;
+  }
+
+  let serviceWindow = contentWindow.openDialog(fullURL, name,
+                                               "chrome=no,dialog=no" + options);
+
+  // Get the newly opened window's containing XUL window
+  chromeWindow = getChromeWindow(serviceWindow);
+
+  // set the window's name and origin attribute on its browser, so that it can
+  // be found via getWindowByName
+  chromeWindow.name = "social-service-window-" + name;
+  chromeWindow.gBrowser.selectedBrowser.setAttribute("origin", provider.origin);
+
+  // we dont want the default title the browser produces, we'll fixup whenever
+  // it changes.
+  serviceWindow.addEventListener("DOMTitleChanged", function() {
+    let sep = xulWindow.document.documentElement.getAttribute("titlemenuseparator");
+    xulWindow.document.title = provider.name + sep + serviceWindow.document.title;
+  });
+
+  return serviceWindow;
+}