Bug 1115616 - Commit composition string forcibly when search suggestion list is clicked. r=gavin,adw a=sylvestre
authorTooru Fujisawa <arai_a@mac.com>
Fri, 23 Jan 2015 03:08:40 +0900
changeset 242975 2d629038c57b
parent 242974 787968dadb44
child 242976 a666c5c8d0ba
push id4352
push userarai_a@mac.com
push date2015-01-22 18:10 +0000
treeherdermozilla-beta@2d629038c57b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersgavin, adw, sylvestre
bugs1115616
milestone36.0
Bug 1115616 - Commit composition string forcibly when search suggestion list is clicked. r=gavin,adw a=sylvestre
browser/base/content/searchSuggestionUI.js
browser/base/content/test/general/browser_aboutHome.js
browser/base/content/test/general/browser_searchSuggestionUI.js
browser/base/content/test/general/searchSuggestionUI.js
--- a/browser/base/content/searchSuggestionUI.js
+++ b/browser/base/content/searchSuggestionUI.js
@@ -52,16 +52,18 @@ function SearchSuggestionUIController(in
   this.input.addEventListener("keypress", this);
   this.input.addEventListener("input", this);
   this.input.addEventListener("focus", this);
   this.input.addEventListener("blur", this);
   window.addEventListener("ContentSearchService", this);
 
   this._stickyInputValue = "";
   this._hideSuggestions();
+
+  this._ignoreInputEvent = false;
 }
 
 SearchSuggestionUIController.prototype = {
 
   // The timeout (ms) of the remote suggestions.  Corresponds to
   // SearchSuggestionController.remoteTimeout.  Uses
   // SearchSuggestionController's default timeout if falsey.
   remoteTimeout: undefined,
@@ -138,16 +140,20 @@ SearchSuggestionUIController.prototype =
     this._sendMsg("AddFormHistoryEntry", this.input.value);
   },
 
   handleEvent: function (event) {
     this["_on" + event.type[0].toUpperCase() + event.type.substr(1)](event);
   },
 
   _onInput: function () {
+    if (this._ignoreInputEvent) {
+      this._ignoreInputEvent = false;
+      return;
+    }
     if (this.input.value) {
       this._getSuggestions();
     }
     else {
       this._stickyInputValue = "";
       this._hideSuggestions();
     }
     this.selectAndUpdateInput(-1);
@@ -226,16 +232,25 @@ SearchSuggestionUIController.prototype =
   _onMousemove: function (event) {
     this.selectedIndex = this._indexOfTableRowOrDescendent(event.target);
   },
 
   _onMousedown: function (event) {
     let idx = this._indexOfTableRowOrDescendent(event.target);
     let suggestion = this.suggestionAtIndex(idx);
     this._stickyInputValue = suggestion;
+
+    // Commit composition string forcibly, because setting input value does not
+    // work if input has composition string (see bug 1115616 and bug 632744).
+    // Ignore input event for composition end to avoid getting suggestion again.
+    this._ignoreInputEvent = true;
+    this.input.blur();
+    this.input.focus();
+    this._ignoreInputEvent = false;
+
     this.input.value = suggestion;
     this.input.setAttribute("selection-index", idx);
     this.input.setAttribute("selection-kind", "mouse");
     this._hideSuggestions();
     if (this.onClick) {
       this.onClick.call(null);
     }
   },
--- a/browser/base/content/test/general/browser_aboutHome.js
+++ b/browser/base/content/test/general/browser_aboutHome.js
@@ -376,16 +376,66 @@ let gTests = [
       // Empty the search input, causing the suggestions to be hidden.
       EventUtils.synthesizeKey("a", { accelKey: true });
       EventUtils.synthesizeKey("VK_DELETE", {});
       ok(table.hidden, "Search suggestion table hidden");
     });
   }
 },
 {
+  desc: "Clicking suggestion list while composing",
+  setup: function() {},
+  run: function()
+  {
+    return Task.spawn(function* () {
+      // Start composition and type "x"
+      let input = gBrowser.contentDocument.getElementById("searchText");
+      input.focus();
+      EventUtils.synthesizeComposition({ type: "compositionstart", data: "" });
+      EventUtils.synthesizeComposition({ type: "compositionupdate", data: "x" });
+      EventUtils.synthesizeCompositionChange({
+        composition: {
+          string: "x",
+          clauses: [
+            { length: 1, attr: EventUtils.COMPOSITION_ATTR_RAWINPUT }
+          ]
+        },
+        caret: { start: 1, length: 0 }
+      });
+
+      // Wait for the search suggestions to become visible.
+      let table =
+        gBrowser.contentDocument.getElementById("searchSuggestionTable");
+      let deferred = Promise.defer();
+      let observer = new MutationObserver(() => {
+        if (input.getAttribute("aria-expanded") == "true") {
+          observer.disconnect();
+          ok(!table.hidden, "Search suggestion table unhidden");
+          deferred.resolve();
+        }
+      });
+      observer.observe(input, {
+        attributes: true,
+        attributeFilter: ["aria-expanded"],
+      });
+      yield deferred.promise;
+
+      // Click the second suggestion.
+      let expectedURL = Services.search.currentEngine.
+                        getSubmission("xbar", null, "homepage").
+                        uri.spec;
+      let loadPromise = waitForDocLoadAndStopIt(expectedURL);
+      let row = table.children[1];
+      EventUtils.sendMouseEvent({ type: "mousedown" }, row, gBrowser.contentWindow);
+      yield loadPromise;
+      ok(input.value == "xbar", "Suggestion is selected");
+    });
+  }
+},
+{
   desc: "Cmd+k should focus the search box in the page when the search box in the toolbar is absent",
   setup: function () {
     // Remove the search bar from toolbar
     CustomizableUI.removeWidgetFromArea("search-container");
   },
   run: Task.async(function* () {
     let doc = gBrowser.selectedTab.linkedBrowser.contentDocument;
     let logo = doc.getElementById("brandLogo");
--- a/browser/base/content/test/general/browser_searchSuggestionUI.js
+++ b/browser/base/content/test/general/browser_searchSuggestionUI.js
@@ -182,16 +182,56 @@ add_task(function* formHistory() {
 
   // Type an X again.  The form history entry should still be gone.
   state = yield msg("key", { key: "x", waitForSuggestions: true });
   checkState(state, "x", ["xfoo", "xbar"], -1);
 
   yield msg("reset");
 });
 
+add_task(function* composition() {
+  yield setUp();
+
+  let state = yield msg("startComposition", { data: "" });
+  checkState(state, "", [], -1);
+  state = yield msg("updateComposition", { data: "x" });
+  checkState(state, "", [], -1);
+  state = yield msg("changeComposition", { data: "x", waitForSuggestions: true });
+  checkState(state, "x", ["xfoo", "xbar"], -1);
+
+  // Mouse over the first suggestion.
+  state = yield msg("mousemove", 0);
+  checkState(state, "x", ["xfoo", "xbar"], 0);
+
+  // Mouse over the second suggestion.
+  state = yield msg("mousemove", 1);
+  checkState(state, "x", ["xfoo", "xbar"], 1);
+
+  // Click the second suggestion.  This should make it sticky.  To make sure it
+  // sticks, trigger suggestions again and cycle through them by pressing Down
+  // until nothing is selected again.
+  state = yield msg("mousedown", 1);
+
+  checkState(state, "xbar", [], -1);
+
+  state = yield msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbarfoo", ["xbarfoo", "xbarbar"], 0);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbarbar", ["xbarfoo", "xbarbar"], 1);
+
+  state = yield msg("key", "VK_DOWN");
+  checkState(state, "xbar", ["xbarfoo", "xbarbar"], -1);
+
+  yield msg("reset");
+});
+
 
 let gDidInitialSetUp = false;
 
 function setUp() {
   return Task.spawn(function* () {
     if (!gDidInitialSetUp) {
       yield promiseNewEngine(TEST_ENGINE_BASENAME);
       yield promiseTab();
--- a/browser/base/content/test/general/searchSuggestionUI.js
+++ b/browser/base/content/test/general/searchSuggestionUI.js
@@ -22,16 +22,43 @@ let messageHandlers = {
 
   key: function (arg) {
     let keyName = typeof(arg) == "string" ? arg : arg.key;
     content.synthesizeKey(keyName, {});
     let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
     wait(ack);
   },
 
+  startComposition: function (arg) {
+    let data = typeof(arg) == "string" ? arg : arg.data;
+    content.synthesizeComposition({ type: "compositionstart", data: data });
+    ack();
+  },
+
+  updateComposition: function (arg) {
+    let data = typeof(arg) == "string" ? arg : arg.data;
+    content.synthesizeComposition({ type: "compositionupdate", data: data });
+    ack();
+  },
+
+  changeComposition: function (arg) {
+    let data = typeof(arg) == "string" ? arg : arg.data;
+    content.synthesizeCompositionChange({
+      composition: {
+        string: data,
+        clauses: [
+          { length: data.length, attr: content.COMPOSITION_ATTR_RAWINPUT }
+        ]
+      },
+      caret: { start: data.length, length: 0 }
+    });
+    let wait = arg.waitForSuggestions ? waitForSuggestions : cb => cb();
+    wait(ack);
+  },
+
   focus: function () {
     gController.input.focus();
     ack();
   },
 
   blur: function () {
     gController.input.blur();
     ack();