Bug 962490 - Add a search field to the new tab page (part 1: ContentSearch). r=felipe
authorDrew Willcoxon <adw@mozilla.com>
Thu, 24 Apr 2014 19:09:20 -0700
changeset 180540 eeafc69ebfb15c487e0b8cebedae42003c31fefb
parent 180539 36b040d46b79c229ee7c486185f9f0d8ef0aa269
child 180541 a36dd9f2573919bcbe04fafc802db6861273f419
push id272
push userpvanderbeken@mozilla.com
push dateMon, 05 May 2014 16:31:18 +0000
reviewersfelipe
bugs962490
milestone31.0a1
Bug 962490 - Add a search field to the new tab page (part 1: ContentSearch). r=felipe
browser/base/content/content.js
browser/components/nsBrowserGlue.js
browser/modules/ContentSearch.jsm
browser/modules/moz.build
browser/modules/test/browser.ini
browser/modules/test/browser_ContentSearch.js
browser/modules/test/contentSearch.js
--- a/browser/base/content/content.js
+++ b/browser/base/content/content.js
@@ -186,16 +186,70 @@ let AboutHomeListener = {
       case "settings":
         sendAsyncMessage("AboutHome:Settings");
         break;
     }
   },
 };
 AboutHomeListener.init(this);
 
+
+let ContentSearchMediator = {
+
+  whitelist: new Set([
+    "about:newtab",
+  ]),
+
+  init: function (chromeGlobal) {
+    chromeGlobal.addEventListener("ContentSearchClient", this, true, true);
+    addMessageListener("ContentSearch", this);
+  },
+
+  handleEvent: function (event) {
+    if (this._contentWhitelisted) {
+      this._sendMsg(event.detail.type, event.detail.data);
+    }
+  },
+
+  receiveMessage: function (msg) {
+    if (msg.data.type == "AddToWhitelist") {
+      for (let uri of msg.data.data) {
+        this.whitelist.add(uri);
+      }
+      this._sendMsg("AddToWhitelistAck");
+      return;
+    }
+    if (this._contentWhitelisted) {
+      this._fireEvent(msg.data.type, msg.data.data);
+    }
+  },
+
+  get _contentWhitelisted() {
+    return this.whitelist.has(content.document.documentURI.toLowerCase());
+  },
+
+  _sendMsg: function (type, data=null) {
+    sendAsyncMessage("ContentSearch", {
+      type: type,
+      data: data,
+    });
+  },
+
+  _fireEvent: function (type, data=null) {
+    content.dispatchEvent(new content.CustomEvent("ContentSearchService", {
+      detail: {
+        type: type,
+        data: data,
+      },
+    }));
+  },
+};
+ContentSearchMediator.init(this);
+
+
 var global = this;
 
 // Lazily load the finder code
 addMessageListener("Finder:Initialize", function () {
   let {RemoteFinderListener} = Cu.import("resource://gre/modules/RemoteFinder.jsm", {});
   new RemoteFinderListener(global);
 });
 
--- a/browser/components/nsBrowserGlue.js
+++ b/browser/components/nsBrowserGlue.js
@@ -93,16 +93,19 @@ XPCOMUtils.defineLazyModuleGetter(this, 
 XPCOMUtils.defineLazyModuleGetter(this, "AsyncShutdown",
                                   "resource://gre/modules/AsyncShutdown.jsm");
 
 #ifdef NIGHTLY_BUILD
 XPCOMUtils.defineLazyModuleGetter(this, "SignInToWebsiteUX",
                                   "resource:///modules/SignInToWebsite.jsm");
 #endif
 
+XPCOMUtils.defineLazyModuleGetter(this, "ContentSearch",
+                                  "resource:///modules/ContentSearch.jsm");
+
 const PREF_PLUGINS_NOTIFYUSER = "plugins.update.notifyUser";
 const PREF_PLUGINS_UPDATEURL  = "plugins.update.url";
 
 // Seconds of idle before trying to create a bookmarks backup.
 const BOOKMARKS_BACKUP_IDLE_TIME_SEC = 10 * 60;
 // Minimum interval between backups.  We try to not create more than one backup
 // per interval.
 const BOOKMARKS_BACKUP_MIN_INTERVAL_DAYS = 1;
@@ -492,16 +495,17 @@ BrowserGlue.prototype = {
     PdfJs.init();
 #ifdef NIGHTLY_BUILD
     ShumwayUtils.init();
 #endif
     webrtcUI.init();
     AboutHome.init();
     SessionStore.init();
     BrowserUITelemetry.init();
+    ContentSearch.init();
 
     if (Services.appinfo.browserTabsRemote) {
       ContentClick.init();
       RemotePrompt.init();
     }
 
     Services.obs.notifyObservers(null, "browser-ui-startup-complete", "");
   },
new file mode 100644
--- /dev/null
+++ b/browser/modules/ContentSearch.jsm
@@ -0,0 +1,149 @@
+/* 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/. */
+
+"use strict";
+
+this.EXPORTED_SYMBOLS = [
+  "ContentSearch",
+];
+
+const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
+
+Cu.import("resource://gre/modules/Services.jsm");
+
+const INBOUND_MESSAGE = "ContentSearch";
+const OUTBOUND_MESSAGE = INBOUND_MESSAGE;
+
+/**
+ * ContentSearch receives messages named INBOUND_MESSAGE and sends messages
+ * named OUTBOUND_MESSAGE.  The data of each message is expected to look like
+ * { type, data }.  type is the message's type (or subtype if you consider the
+ * type of the message itself to be INBOUND_MESSAGE), and data is data that is
+ * specific to the type.
+ *
+ * Inbound messages have the following types:
+ *
+ *   GetState
+ *      Retrieves the current search engine state.
+ *      data: null
+ *   ManageEngines
+ *      Opens the search engine management window.
+ *      data: null
+ *   Search
+ *      Performs a search.
+ *      data: an object { engineName, searchString, whence }
+ *   SetCurrentEngine
+ *      Sets the current engine.
+ *      data: the name of the engine
+ *
+ * Outbound messages have the following types:
+ *
+ *   CurrentEngine
+ *     Sent when the current engine changes.
+ *     data: see _currentEngineObj
+ *   State
+ *     Sent in reply to GetState and when the state changes.
+ *     data: see _currentStateObj
+ */
+
+this.ContentSearch = {
+
+  init: function () {
+    Cc["@mozilla.org/globalmessagemanager;1"].
+      getService(Ci.nsIMessageListenerManager).
+      addMessageListener(INBOUND_MESSAGE, this);
+    Services.obs.addObserver(this, "browser-search-engine-modified", false);
+  },
+
+  receiveMessage: function (msg) {
+    let methodName = "on" + msg.data.type;
+    if (methodName in this) {
+      this[methodName](msg, msg.data.data);
+    }
+  },
+
+  onGetState: function (msg, data) {
+    this._reply(msg, "State", this._currentStateObj());
+  },
+
+  onSearch: function (msg, data) {
+    let expectedDataProps = [
+      "engineName",
+      "searchString",
+      "whence",
+    ];
+    for (let prop of expectedDataProps) {
+      if (!(prop in data)) {
+        Cu.reportError("Message data missing required property: " + prop);
+        return;
+      }
+    }
+    let browserWin = msg.target.ownerDocument.defaultView;
+    let engine = Services.search.getEngineByName(data.engineName);
+    browserWin.BrowserSearch.recordSearchInHealthReport(engine, data.whence);
+    let submission = engine.getSubmission(data.searchString, "", data.whence);
+    browserWin.loadURI(submission.uri.spec, null, submission.postData);
+  },
+
+  onSetCurrentEngine: function (msg, data) {
+    Services.search.currentEngine = Services.search.getEngineByName(data);
+  },
+
+  onManageEngines: function (msg, data) {
+    let browserWin = msg.target.ownerDocument.defaultView;
+    browserWin.BrowserSearch.searchBar.openManager(null);
+  },
+
+  observe: function (subj, topic, data) {
+    switch (topic) {
+    case "browser-search-engine-modified":
+      if (data == "engine-current") {
+        this._broadcast("CurrentEngine", this._currentEngineObj());
+      }
+      else if (data != "engine-default") {
+        // engine-default is always sent with engine-current and isn't otherwise
+        // relevant to content searches.
+        this._broadcast("State", this._currentStateObj());
+      }
+      break;
+    }
+  },
+
+  _reply: function (msg, type, data) {
+    msg.target.messageManager.sendAsyncMessage(...this._msgArgs(type, data));
+  },
+
+  _broadcast: function (type, data) {
+    Cc["@mozilla.org/globalmessagemanager;1"].
+      getService(Ci.nsIMessageListenerManager).
+      broadcastAsyncMessage(...this._msgArgs(type, data));
+  },
+
+  _msgArgs: function (type, data) {
+    return [OUTBOUND_MESSAGE, {
+      type: type,
+      data: data,
+    }];
+  },
+
+  _currentStateObj: function () {
+    return {
+      engines: Services.search.getVisibleEngines().map(engine => {
+        return {
+          name: engine.name,
+          iconURI: engine.getIconURLBySize(16, 16),
+        };
+      }),
+      currentEngine: this._currentEngineObj(),
+    };
+  },
+
+  _currentEngineObj: function () {
+    return {
+      name: Services.search.currentEngine.name,
+      logoURI: Services.search.currentEngine.getIconURLBySize(65, 26),
+      logo2xURI: Services.search.currentEngine.getIconURLBySize(130, 52),
+    };
+  },
+};
--- a/browser/modules/moz.build
+++ b/browser/modules/moz.build
@@ -6,16 +6,17 @@
 
 TEST_DIRS += ['test']
 
 EXTRA_JS_MODULES += [
     'BrowserNewTabPreloader.jsm',
     'BrowserUITelemetry.jsm',
     'ContentClick.jsm',
     'ContentLinkHandler.jsm',
+    'ContentSearch.jsm',
     'CustomizationTabPreloader.jsm',
     'Feeds.jsm',
     'NetworkPrioritizer.jsm',
     'offlineAppCache.jsm',
     'RemotePrompt.jsm',
     'SharedFrame.jsm',
     'SitePermissions.jsm',
     'Social.jsm',
--- a/browser/modules/test/browser.ini
+++ b/browser/modules/test/browser.ini
@@ -1,15 +1,17 @@
 [DEFAULT]
 support-files =
   head.js
+  contentSearch.js
+  image.png
   uitour.*
-  image.png
 
 [browser_BrowserUITelemetry_buckets.js]
+[browser_ContentSearch.js]
 [browser_NetworkPrioritizer.js]
 skip-if = e10s # Bug 666804 - Support NetworkPrioritizer in e10s
 [browser_SignInToWebsite.js]
 skip-if = e10s # Bug 941426 - SignIntoWebsite.jsm not e10s friendly
 [browser_UITour.js]
 skip-if = os == "linux" || e10s # Intermittent failures, bug 951965
 [browser_UITour2.js]
 skip-if = e10s # Bug 941428 - UITour.jsm not e10s friendly
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/browser_ContentSearch.js
@@ -0,0 +1,240 @@
+/* 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/. */
+
+const TEST_MSG = "ContentSearchTest";
+const CONTENT_SEARCH_MSG = "ContentSearch";
+const TEST_CONTENT_SCRIPT_BASENAME = "contentSearch.js";
+
+function generatorTest() {
+  // nextStep() drives the iterator returned by this function.  This function's
+  // iterator in turn drives the iterator of each test below.
+  let currentTestIter = yield startNextTest();
+  let arg = undefined;
+  while (currentTestIter) {
+    try {
+      currentTestIter.send(arg);
+      arg = yield null;
+    }
+    catch (err if err instanceof StopIteration) {
+      currentTestIter = yield startNextTest();
+      arg = undefined;
+    }
+  }
+}
+
+function startNextTest() {
+  if (!gTests.length) {
+    setTimeout(() => nextStep(null), 0);
+    return;
+  }
+  let nextTestGen = gTests.shift();
+  let nextTestIter = nextTestGen();
+  addTab(() => {
+    info("Starting test " + nextTestGen.name);
+    nextStep(nextTestIter);
+  });
+}
+
+function addTest(testGen) {
+  gTests.push(testGen);
+}
+
+var gTests = [];
+var gMsgMan;
+
+addTest(function GetState() {
+  gMsgMan.sendAsyncMessage(TEST_MSG, {
+    type: "GetState",
+  });
+  let msg = yield waitForTestMsg("State");
+  checkMsg(msg, {
+    type: "State",
+    data: currentStateObj(),
+  });
+});
+
+addTest(function SetCurrentEngine() {
+  let newCurrentEngine = null;
+  let oldCurrentEngine = Services.search.currentEngine;
+  let engines = Services.search.getVisibleEngines();
+  for (let engine of engines) {
+    if (engine != oldCurrentEngine) {
+      newCurrentEngine = engine;
+      break;
+    }
+  }
+  if (!newCurrentEngine) {
+    info("Couldn't find a non-selected search engine, " +
+         "skipping this part of the test");
+    return;
+  }
+  gMsgMan.sendAsyncMessage(TEST_MSG, {
+    type: "SetCurrentEngine",
+    data: newCurrentEngine.name,
+  });
+  Services.obs.addObserver(function obs(subj, topic, data) {
+    info("Test observed " + data);
+    if (data == "engine-current") {
+      ok(true, "Test observed engine-current");
+      Services.obs.removeObserver(obs, "browser-search-engine-modified", false);
+      nextStep();
+    }
+  }, "browser-search-engine-modified", false);
+  info("Waiting for test to observe engine-current...");
+  waitForTestMsg("CurrentEngine");
+  let maybeMsg1 = yield null;
+  let maybeMsg2 = yield null;
+  let msg = maybeMsg1 || maybeMsg2;
+  ok(!!msg,
+     "Sanity check: One of the yields is for waitForTestMsg and should have " +
+     "therefore produced a message object");
+  checkMsg(msg, {
+    type: "CurrentEngine",
+    data: currentEngineObj(newCurrentEngine),
+  });
+
+  Services.search.currentEngine = oldCurrentEngine;
+  let msg = yield waitForTestMsg("CurrentEngine");
+  checkMsg(msg, {
+    type: "CurrentEngine",
+    data: currentEngineObj(oldCurrentEngine),
+  });
+});
+
+addTest(function ManageEngines() {
+  gMsgMan.sendAsyncMessage(TEST_MSG, {
+    type: "ManageEngines",
+  });
+  let winWatcher = Cc["@mozilla.org/embedcomp/window-watcher;1"].
+                   getService(Ci.nsIWindowWatcher);
+  winWatcher.registerNotification(function onOpen(subj, topic, data) {
+    if (topic == "domwindowopened" && subj instanceof Ci.nsIDOMWindow) {
+      subj.addEventListener("load", function onLoad() {
+        subj.removeEventListener("load", onLoad);
+        if (subj.document.documentURI ==
+            "chrome://browser/content/search/engineManager.xul") {
+          winWatcher.unregisterNotification(onOpen);
+          ok(true, "Observed search manager window open");
+          is(subj.opener, window,
+             "Search engine manager opener should be this chrome window");
+          subj.close();
+          nextStep();
+        }
+      });
+    }
+  });
+  info("Waiting for search engine manager window to open...");
+  yield null;
+});
+
+addTest(function modifyEngine() {
+  let engine = Services.search.currentEngine;
+  let oldAlias = engine.alias;
+  engine.alias = "ContentSearchTest";
+  let msg = yield waitForTestMsg("State");
+  checkMsg(msg, {
+    type: "State",
+    data: currentStateObj(),
+  });
+  engine.alias = oldAlias;
+  msg = yield waitForTestMsg("State");
+  checkMsg(msg, {
+    type: "State",
+    data: currentStateObj(),
+  });
+});
+
+addTest(function search() {
+  let engine = Services.search.currentEngine;
+  let data = {
+    engineName: engine.name,
+    searchString: "ContentSearchTest",
+    whence: "ContentSearchTest",
+  };
+  gMsgMan.sendAsyncMessage(TEST_MSG, {
+    type: "Search",
+    data: data,
+  });
+  let submissionURL =
+    engine.getSubmission(data.searchString, "", data.whence).uri.spec;
+  let listener = {
+    onStateChange: function (webProg, req, flags, status) {
+      let url = req.originalURI.spec;
+      info("onStateChange " + url);
+      let docStart = Ci.nsIWebProgressListener.STATE_IS_DOCUMENT |
+                     Ci.nsIWebProgressListener.STATE_START;
+      if ((flags & docStart) && webProg.isTopLevel && url == submissionURL) {
+        gBrowser.removeProgressListener(listener);
+        ok(true, "Search URL loaded");
+        req.cancel(Components.results.NS_ERROR_FAILURE);
+        nextStep();
+      }
+    }
+  };
+  gBrowser.addProgressListener(listener);
+  info("Waiting for search URL to load: " + submissionURL);
+  yield null;
+});
+
+function checkMsg(actualMsg, expectedMsgData) {
+  SimpleTest.isDeeply(actualMsg.data, expectedMsgData, "Checking message");
+}
+
+function waitForMsg(name, type, callback) {
+  info("Waiting for " + name + " message " + type + "...");
+  gMsgMan.addMessageListener(name, function onMsg(msg) {
+    info("Received " + name + " message " + msg.data.type + "\n");
+    if (msg.data.type == type) {
+      gMsgMan.removeMessageListener(name, onMsg);
+      (callback || nextStep)(msg);
+    }
+  });
+}
+
+function waitForTestMsg(type, callback) {
+  waitForMsg(TEST_MSG, type, callback);
+}
+
+function addTab(onLoad) {
+  let tab = gBrowser.addTab();
+  gBrowser.selectedTab = tab;
+  tab.linkedBrowser.addEventListener("load", function load() {
+    tab.removeEventListener("load", load, true);
+    let url = getRootDirectory(gTestPath) + TEST_CONTENT_SCRIPT_BASENAME;
+    gMsgMan = tab.linkedBrowser.messageManager;
+    gMsgMan.sendAsyncMessage(CONTENT_SEARCH_MSG, {
+      type: "AddToWhitelist",
+      data: ["about:blank"],
+    });
+    waitForMsg(CONTENT_SEARCH_MSG, "AddToWhitelistAck", () => {
+      gMsgMan.loadFrameScript(url, false);
+      onLoad();
+    });
+  }, true);
+  registerCleanupFunction(() => gBrowser.removeTab(tab));
+}
+
+function currentStateObj() {
+  return {
+    engines: Services.search.getVisibleEngines().map(engine => {
+      return {
+        name: engine.name,
+        iconURI: engine.getIconURLBySize(16, 16),
+      };
+    }),
+    currentEngine: currentEngineObj(),
+  };
+}
+
+function currentEngineObj(expectedCurrentEngine) {
+  if (expectedCurrentEngine) {
+    is(Services.search.currentEngine.name, expectedCurrentEngine.name,
+       "Sanity check: expected current engine");
+  }
+  return {
+    name: Services.search.currentEngine.name,
+    logoURI: Services.search.currentEngine.getIconURLBySize(65, 26),
+    logo2xURI: Services.search.currentEngine.getIconURLBySize(130, 52),
+  };
+}
new file mode 100644
--- /dev/null
+++ b/browser/modules/test/contentSearch.js
@@ -0,0 +1,21 @@
+/* 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/. */
+
+const TEST_MSG = "ContentSearchTest";
+const SERVICE_EVENT_TYPE = "ContentSearchService";
+const CLIENT_EVENT_TYPE = "ContentSearchClient";
+
+// Forward events from the in-content service to the test.
+content.addEventListener(SERVICE_EVENT_TYPE, event => {
+  sendAsyncMessage(TEST_MSG, event.detail);
+});
+
+// Forward messages from the test to the in-content service.
+addMessageListener(TEST_MSG, msg => {
+  content.dispatchEvent(
+    new content.CustomEvent(CLIENT_EVENT_TYPE, {
+      detail: msg.data,
+    })
+  );
+});