Bug 1172937 - Action row doesn't always update correctly with unified autocomplete. r=adw
authorMarco Bonardo <mbonardo@mozilla.com>
Thu, 30 Jul 2015 16:54:27 +0200
changeset 287584 397afd7d1e6bd7e6510502af3fe921244a863b5e
parent 287583 d191709cd9331027bd000b77c7db93f85a12392c
child 287585 4062c2452aad2ccdbf8c82f8dea1939f76a55ae7
push id5067
push userraliiev@mozilla.com
push dateMon, 21 Sep 2015 14:04:52 +0000
treeherdermozilla-beta@14221ffe5b2f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersadw
bugs1172937
milestone42.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 1172937 - Action row doesn't always update correctly with unified autocomplete. r=adw Original patch by Felipe Gomes <felipc@gmail.com>
browser/base/content/test/general/browser.ini
browser/base/content/test/general/browser_urlbar_autoFill_backspaced.js
toolkit/components/autocomplete/nsAutoCompleteController.cpp
toolkit/components/autocomplete/nsAutoCompleteController.h
toolkit/components/autocomplete/nsIAutoCompleteSearch.idl
toolkit/components/places/UnifiedComplete.js
toolkit/components/places/nsPlacesAutoComplete.js
toolkit/content/tests/chrome/file_autocomplete_with_composition.js
--- a/browser/base/content/test/general/browser.ini
+++ b/browser/base/content/test/general/browser.ini
@@ -449,16 +449,17 @@ skip-if = e10s # Bug 1100700 - test reli
 [browser_urlbarDelete.js]
 [browser_urlbarEnter.js]
 [browser_urlbarEnterAfterMouseOver.js]
 skip-if = os == "linux" || e10s # Bug 1073339 - Investigate autocomplete test unreliability on Linux/e10s
 [browser_urlbarRevert.js]
 [browser_urlbarSearchSingleWordNotification.js]
 [browser_urlbarStop.js]
 [browser_urlbarTrimURLs.js]
+[browser_urlbar_autoFill_backspaced.js]
 [browser_urlbar_search_healthreport.js]
 [browser_urlbar_searchsettings.js]
 [browser_utilityOverlay.js]
 [browser_visibleFindSelection.js]
 [browser_visibleLabel.js]
 [browser_visibleTabs.js]
 [browser_visibleTabs_bookmarkAllPages.js]
 skip-if = true # Bug 1005420 - fails intermittently. also with e10s enabled: bizarre problem with hidden tab having _mouseenter called, via _setPositionalAttributes, and tab not being found resulting in 'candidate is undefined'
new file mode 100644
--- /dev/null
+++ b/browser/base/content/test/general/browser_urlbar_autoFill_backspaced.js
@@ -0,0 +1,148 @@
+/* This test ensures that backspacing autoFilled values still allows to
+ * confirm the remaining value.
+ */
+
+function* test_autocomplete(data) {
+  let {desc, typed, autofilled, modified, keys, action, onAutoFill} = data;
+  info(desc);
+
+  yield promiseAutocompleteResultPopup(typed);
+  is(gURLBar.value, autofilled, "autofilled value is as expected");
+  if (onAutoFill)
+    onAutoFill()
+
+  keys.forEach(key => EventUtils.synthesizeKey(key, {}));
+
+  is(gURLBar.value, modified, "backspaced value is as expected");
+
+  yield promiseSearchComplete();
+
+  ok(gURLBar.popup.richlistbox.children.length > 0, "Should get at least 1 result");
+  let result = gURLBar.popup.richlistbox.children[0];
+  let type = result.getAttribute("type");
+  let types = type.split(/\s+/);
+  ok(types.includes(action), `The type attribute "${type}" includes the expected action "${action}"`);
+
+  gURLBar.popup.hidePopup();
+  yield promisePopupHidden(gURLBar.popup);
+  gURLBar.blur();
+};
+
+add_task(function* () {
+  registerCleanupFunction(function* () {
+    Services.prefs.clearUserPref("browser.urlbar.unifiedcomplete");
+    Services.prefs.clearUserPref("browser.urlbar.autoFill");
+    gURLBar.handleRevert();
+    yield PlacesTestUtils.clearHistory();
+  });
+  Services.prefs.setBoolPref("browser.urlbar.unifiedcomplete", true);
+  Services.prefs.setBoolPref("browser.urlbar.autoFill", true);
+
+  // Add a typed visit, so it will be autofilled.
+  yield PlacesTestUtils.addVisits({
+    uri: NetUtil.newURI("http://example.com/"),
+    transition: Ci.nsINavHistoryService.TRANSITION_TYPED
+  });
+
+  yield test_autocomplete({ desc: "DELETE the autofilled part should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exam",
+                            keys: ["VK_DELETE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "DELETE the final slash should visit",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.com",
+                            keys: ["VK_DELETE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE the autofilled part should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exam",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "BACK_SPACE the final slash should visit",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.com",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "DELETE the autofilled part, then BACK_SPACE, should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exa",
+                            keys: ["VK_DELETE", "VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "DELETE the final slash, then BACK_SPACE, should search",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.co",
+                            keys: ["VK_DELETE", "VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE the autofilled part, then BACK_SPACE, should search",
+                            typed: "exam",
+                            autofilled: "example.com/",
+                            modified: "exa",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "searchengine"
+                          });
+  yield test_autocomplete({ desc: "BACK_SPACE the final slash, then BACK_SPACE, should search",
+                            typed: "example.com",
+                            autofilled: "example.com/",
+                            modified: "example.co",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "visiturl"
+                          });
+
+  yield test_autocomplete({ desc: "BACK_SPACE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_BACK_SPACE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 1;
+                              gURLBar.selectionEnd = 12;
+                            }
+                         });
+  yield test_autocomplete({ desc: "DELETE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_DELETE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 1;
+                              gURLBar.selectionEnd = 12;
+                            }
+                          });
+  yield test_autocomplete({ desc: "double BACK_SPACE after blur should search",
+                            typed: "ex",
+                            autofilled: "example.com/",
+                            modified: "e",
+                            keys: ["VK_BACK_SPACE", "VK_BACK_SPACE"],
+                            action: "searchengine",
+                            onAutoFill: () => {
+                              gURLBar.blur();
+                              gURLBar.focus();
+                              gURLBar.selectionStart = 2;
+                              gURLBar.selectionEnd = 12;
+                            }
+                         });
+
+  yield PlacesTestUtils.clearHistory();
+});
--- a/toolkit/components/autocomplete/nsAutoCompleteController.cpp
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.cpp
@@ -39,18 +39,20 @@ NS_IMPL_CYCLE_COLLECTING_RELEASE(nsAutoC
 NS_INTERFACE_TABLE_HEAD(nsAutoCompleteController)
   NS_INTERFACE_TABLE(nsAutoCompleteController, nsIAutoCompleteController,
                      nsIAutoCompleteObserver, nsITimerCallback, nsITreeView)
   NS_INTERFACE_TABLE_TO_MAP_SEGUE_CYCLE_COLLECTION(nsAutoCompleteController)
 NS_INTERFACE_MAP_END
 
 nsAutoCompleteController::nsAutoCompleteController() :
   mDefaultIndexCompleted(false),
-  mBackspaced(false),
   mPopupClosedByCompositionStart(false),
+  mProhibitAutoFill(false),
+  mUserClearedAutoFill(false),
+  mClearingAutoFillSearchesAgain(false),
   mCompositionState(eCompositionState_None),
   mSearchStatus(nsAutoCompleteController::STATUS_NONE),
   mRowCount(0),
   mSearchesOngoing(0),
   mSearchesFailed(0),
   mFirstSearchResult(false),
   mImmediateSearchesCount(0),
   mCompletedSelectionIndex(-1)
@@ -114,51 +116,61 @@ nsAutoCompleteController::SetInput(nsIAu
 
   // Clear out this reference in case the new input's popup has no tree
   mTree = nullptr;
 
   // Reset all search state members to default values
   mSearchString = newValue;
   mPlaceholderCompletionString.Truncate();
   mDefaultIndexCompleted = false;
-  mBackspaced = false;
+  mProhibitAutoFill = false;
   mSearchStatus = nsIAutoCompleteController::STATUS_NONE;
   mRowCount = 0;
   mSearchesOngoing = 0;
   mCompletedSelectionIndex = -1;
 
   // Initialize our list of search objects
   uint32_t searchCount;
   aInput->GetSearchCount(&searchCount);
   mResults.SetCapacity(searchCount);
   mSearches.SetCapacity(searchCount);
   mMatchCounts.SetLength(searchCount);
   mImmediateSearchesCount = 0;
 
   const char *searchCID = kAutoCompleteSearchCID;
 
+  // Since the controller can be used as a service it's important to reset this.
+  mClearingAutoFillSearchesAgain = false;
+
   for (uint32_t i = 0; i < searchCount; ++i) {
     // Use the search name to create the contract id string for the search service
     nsAutoCString searchName;
     aInput->GetSearchAt(i, searchName);
     nsAutoCString cid(searchCID);
     cid.Append(searchName);
 
     // Use the created cid to get a pointer to the search service and store it for later
     nsCOMPtr<nsIAutoCompleteSearch> search = do_GetService(cid.get());
     if (search) {
       mSearches.AppendObject(search);
 
       // Count immediate searches.
-      uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
       nsCOMPtr<nsIAutoCompleteSearchDescriptor> searchDesc =
         do_QueryInterface(search);
-      if (searchDesc && NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) &&
-          searchType == nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE)
-        mImmediateSearchesCount++;
+      if (searchDesc) {
+        uint16_t searchType = nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_DELAYED;
+        if (NS_SUCCEEDED(searchDesc->GetSearchType(&searchType)) &&
+            searchType == nsIAutoCompleteSearchDescriptor::SEARCH_TYPE_IMMEDIATE) {
+          mImmediateSearchesCount++;
+        }
+
+        if (!mClearingAutoFillSearchesAgain) {
+          searchDesc->GetClearingAutoFillSearchesAgain(&mClearingAutoFillSearchesAgain);
+        }
+      }
     }
   }
 
   return NS_OK;
 }
 
 NS_IMETHODIMP
 nsAutoCompleteController::StartSearch(const nsAString &aSearchString)
@@ -194,53 +206,73 @@ nsAutoCompleteController::HandleText()
     StopSearch();
     // Note: if now is after blur and IME end composition,
     // check mInput before calling.
     // See https://bugzilla.mozilla.org/show_bug.cgi?id=193544#c31
     NS_ERROR("Called before attaching to the control or after detaching from the control");
     return NS_OK;
   }
 
+  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
   nsAutoString newValue;
-  nsCOMPtr<nsIAutoCompleteInput> input(mInput);
   input->GetTextValue(newValue);
 
   // Stop all searches in case they are async.
   StopSearch();
 
   if (!mInput) {
     // StopSearch() can call PostSearchCleanup() which might result
     // in a blur event, which could null out mInput, so we need to check it
     // again.  See bug #395344 for more details
     return NS_OK;
   }
 
   bool disabled;
   input->GetDisableAutoComplete(&disabled);
   NS_ENSURE_TRUE(!disabled, NS_OK);
 
-  // Don't search again if the new string is the same as the last search
+  // Usually we don't search again if the new string is the same as the last one.
   // However, if this is called immediately after compositionend event,
   // we need to search the same value again since the search was canceled
   // at compositionstart event handler.
-  if (!handlingCompositionCommit && newValue.Length() > 0 &&
-      newValue.Equals(mSearchString)) {
+  // The new string might also be the same as the last search if the autofilled
+  // portion was cleared. In this case, we may want to search again.
+
+  // Whether the user removed some text at the end.
+  bool userRemovedText =
+    newValue.Length() < mSearchString.Length() &&
+    Substring(mSearchString, 0, newValue.Length()).Equals(newValue);
+
+  // Whether the user is repeating the previous search.
+  bool repeatingPreviousSearch = !userRemovedText &&
+                                 newValue.Equals(mSearchString);
+
+  mUserClearedAutoFill =
+    repeatingPreviousSearch &&
+    newValue.Length() < mPlaceholderCompletionString.Length() &&
+    Substring(mPlaceholderCompletionString, 0, newValue.Length()).Equals(newValue);
+  bool searchAgainOnAutoFillClear = mUserClearedAutoFill && mClearingAutoFillSearchesAgain;
+
+  if (!handlingCompositionCommit &&
+      !searchAgainOnAutoFillClear &&
+      newValue.Length() > 0 &&
+      repeatingPreviousSearch) {
     return NS_OK;
   }
 
-  // Determine if the user has removed text from the end (probably by backspacing)
-  if (newValue.Length() < mSearchString.Length() &&
-      Substring(mSearchString, 0, newValue.Length()).Equals(newValue))
-  {
-    // We need to throw away previous results so we don't try to search through them again
-    ClearResults();
-    mBackspaced = true;
+  if (userRemovedText || searchAgainOnAutoFillClear) {
+    if (userRemovedText) {
+      // We need to throw away previous results so we don't try to search
+      // through them again.
+      ClearResults();
+    }
+    mProhibitAutoFill = true;
     mPlaceholderCompletionString.Truncate();
   } else {
-    mBackspaced = false;
+    mProhibitAutoFill = false;
   }
 
   mSearchString = newValue;
 
   // Don't search if the value is empty
   if (newValue.Length() == 0) {
     // If autocomplete popup was closed by compositionstart event handler,
     // we should reopen it forcibly even if the value is empty.
@@ -1146,16 +1178,23 @@ nsAutoCompleteController::StartSearch(ui
         result = nullptr;
     }
 
     nsAutoString searchParam;
     nsresult rv = input->GetSearchParam(searchParam);
     if (NS_FAILED(rv))
         return rv;
 
+    // FormFill expects the searchParam to only contain the input element id,
+    // other consumers may have other expectations, so this modifies it only
+    // for new consumers handling autoFill by themselves.
+    if (mProhibitAutoFill && mClearingAutoFillSearchesAgain) {
+      searchParam.AppendLiteral(" prohibit-autofill");
+    }
+
     rv = search->StartSearch(mSearchString, searchParam, result, static_cast<nsIAutoCompleteObserver *>(this));
     if (NS_FAILED(rv)) {
       ++mSearchesFailed;
       MOZ_ASSERT(mSearchesOngoing > 0);
       --mSearchesOngoing;
     }
     // Because of the joy of nested event loops (which can easily happen when some
     // code uses a generator for an asynchronous AutoComplete search),
@@ -1220,16 +1259,17 @@ nsAutoCompleteController::MaybeCompleteP
   // E.g. if the new value is "fob", but the last completion was "foobar",
   // then the last completion is incompatible.
   // If the search string is the same as the last completion value, then don't
   // complete the value again (this prevents completion to happen e.g. if the
   // cursor is moved and StartSeaches() is invoked).
   // In addition, the selection must be at the end of the current input to
   // trigger the placeholder completion.
   bool usePlaceholderCompletion =
+    !mUserClearedAutoFill &&
     !mPlaceholderCompletionString.IsEmpty() &&
     mPlaceholderCompletionString.Length() > mSearchString.Length() &&
     selectionEnd == selectionStart &&
     selectionEnd == (int32_t)mSearchString.Length() &&
     StringBeginsWith(mPlaceholderCompletionString, mSearchString,
                     nsCaseInsensitiveStringComparator());
 
   if (usePlaceholderCompletion) {
@@ -1608,17 +1648,17 @@ nsAutoCompleteController::ClearResults()
     }
   }
   return NS_OK;
 }
 
 nsresult
 nsAutoCompleteController::CompleteDefaultIndex(int32_t aResultIndex)
 {
-  if (mDefaultIndexCompleted || mBackspaced || mSearchString.Length() == 0 || !mInput)
+  if (mDefaultIndexCompleted || mProhibitAutoFill || mSearchString.Length() == 0 || !mInput)
     return NS_OK;
 
   nsCOMPtr<nsIAutoCompleteInput> input(mInput);
 
   int32_t selectionStart;
   input->GetSelectionStart(&selectionStart);
   int32_t selectionEnd;
   input->GetSelectionEnd(&selectionEnd);
--- a/toolkit/components/autocomplete/nsAutoCompleteController.h
+++ b/toolkit/components/autocomplete/nsAutoCompleteController.h
@@ -134,18 +134,29 @@ protected:
 
   nsCOMPtr<nsITimer> mTimer;
   nsCOMPtr<nsITreeSelection> mSelection;
   nsCOMPtr<nsITreeBoxObject> mTree;
 
   nsString mSearchString;
   nsString mPlaceholderCompletionString;
   bool mDefaultIndexCompleted;
-  bool mBackspaced;
   bool mPopupClosedByCompositionStart;
+
+  // Whether autofill is allowed for the next search. May be retrieved by the
+  // search through the "prohibit-autofill" searchParam.
+  bool mProhibitAutoFill;
+
+  // Indicates whether the user cleared the autofilled part, returning to the
+  // originally entered search string.
+  bool mUserClearedAutoFill;
+
+  // Indicates whether clearing the autofilled string should issue a new search.
+  bool mClearingAutoFillSearchesAgain;
+
   enum CompositionState {
     eCompositionState_None,
     eCompositionState_Composing,
     eCompositionState_Committing
   };
   CompositionState mCompositionState;
   uint16_t mSearchStatus;
   uint32_t mRowCount;
--- a/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl
+++ b/toolkit/components/autocomplete/nsIAutoCompleteSearch.idl
@@ -45,24 +45,30 @@ interface nsIAutoCompleteObserver : nsIS
    * Called to update with new results
    *
    * @param search - The search object that processed this search
    * @param result - The search result object
    */
   void onUpdateSearchResult(in nsIAutoCompleteSearch search, in nsIAutoCompleteResult result);
 };
 
-[scriptable, uuid(02314d6e-b730-40cc-a215-221554d77064)]
+[scriptable, uuid(4c3e7462-fbfb-4310-8f4b-239238392b75)]
 interface nsIAutoCompleteSearchDescriptor : nsISupports
 {
   // The search is started after the timeout specified by the corresponding
   // nsIAutoCompleteInput implementation.
   const unsigned short SEARCH_TYPE_DELAYED = 0;
   // The search is started synchronously, before any delayed searches.
   const unsigned short SEARCH_TYPE_IMMEDIATE = 1;
 
   /**
    * Identifies the search behavior.
    * Should be one of the SEARCH_TYPE_* constants above.
    * Defaults to SEARCH_TYPE_DELAYED.
    */
   readonly attribute unsigned short searchType;
+
+  /*
+   * Whether a new search should be triggered when the user deletes the
+   * autofilled part.
+   */
+  readonly attribute boolean clearingAutoFillSearchesAgain;
 };
--- a/toolkit/components/places/UnifiedComplete.js
+++ b/toolkit/components/places/UnifiedComplete.js
@@ -622,16 +622,17 @@ function Search(searchString, searchPara
   // Set the default behavior for this search.
   this._behavior = this._searchString ? Prefs.defaultBehavior
                                       : Prefs.emptySearchDefaultBehavior;
 
   let params = new Set(searchParam.split(" "));
   this._enableActions = params.has("enable-actions");
   this._disablePrivateActions = params.has("disable-private-actions");
   this._inPrivateWindow = params.has("private-window");
+  this._prohibitAutoFill = params.has("prohibit-autofill");
 
   this._searchTokens =
     this.filterTokens(getUnfilteredSearchTokens(this._searchString));
   // The protocol and the host are lowercased by nsIURI, so it's fine to
   // lowercase the typed prefix, to add it back to the results later.
   this._strippedPrefix = this._trimmedOriginalSearchString.slice(
     0, this._trimmedOriginalSearchString.length - this._searchString.length
   ).toLowerCase();
@@ -1589,16 +1590,19 @@ Search.prototype = {
     // tokenizer ends up trimming the search string and returning a value
     // that doesn't match it, or is even shorter.
     if (/\s/.test(this._originalSearchString))
       return false;
 
     if (this._searchString.length == 0)
       return false;
 
+    if (this._prohibitAutoFill)
+      return false;
+
     return true;
   },
 
   /**
    * Obtains the query to search for autoFill host results.
    *
    * @return an array consisting of the correctly optimized query to search the
    *         database with and an object containing the params to bound.
@@ -1829,17 +1833,23 @@ UnifiedComplete.prototype = {
     if (removeFromDB) {
       PlacesUtils.history.removePage(NetUtil.newURI(spec));
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIAutoCompleteSearchDescriptor
 
-  get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
+  get searchType() {
+    return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+  },
+
+  get clearingAutoFillSearchesAgain() {
+    return true;
+  },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsISupports
 
   classID: Components.ID("f964a319-397a-4d21-8be6-5cdd1ee3e3ae"),
 
   _xpcom_factory: XPCOMUtils.generateSingletonFactory(UnifiedComplete),
 
--- a/toolkit/components/places/nsPlacesAutoComplete.js
+++ b/toolkit/components/places/nsPlacesAutoComplete.js
@@ -1653,17 +1653,24 @@ urlInlineComplete.prototype = {
                                          true);
     if (aRegisterObserver) {
       Services.prefs.addObserver(kBrowserUrlbarBranch, this, true);
     }
   },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIAutoCompleteSearchDescriptor
-  get searchType() Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE,
+
+  get searchType() {
+    return Ci.nsIAutoCompleteSearchDescriptor.SEARCH_TYPE_IMMEDIATE;
+  },
+
+  get clearingAutoFillSearchesAgain() {
+    return false;
+  },
 
   //////////////////////////////////////////////////////////////////////////////
   //// nsIObserver
 
   observe: function UIC_observe(aSubject, aTopic, aData)
   {
     if (aTopic == kTopicShutdown) {
       this._closeDatabase();
--- a/toolkit/content/tests/chrome/file_autocomplete_with_composition.js
+++ b/toolkit/content/tests/chrome/file_autocomplete_with_composition.js
@@ -1,11 +1,22 @@
 // nsDoTestsForAutoCompleteWithComposition tests autocomplete with composition.
 // Users must include SimpleTest.js and EventUtils.js.
 
+function waitForCondition(condition, nextTest) {
+  var tries = 0;
+  var interval = setInterval(function() {
+    if (condition() || tries >= 30) {
+      moveOn();
+    }
+    tries++;
+  }, 100);
+  var moveOn = function() { clearInterval(interval); nextTest(); };
+}
+
 function nsDoTestsForAutoCompleteWithComposition(aDescription,
                                                  aWindow,
                                                  aTarget,
                                                  aAutoCompleteController,
                                                  aIsFunc,
                                                  aGetTargetValueFunc,
                                                  aOnFinishFunc)
 {
@@ -47,28 +58,21 @@ nsDoTestsForAutoCompleteWithComposition.
     }
 
     var test = this._tests[this._testingIndex];
     if (this._controller.input.completeDefaultIndex != test.completeDefaultIndex) {
       this._controller.input.completeDefaultIndex = test.completeDefaultIndex;
     }
     test.execute(this._window);
 
-    var timeout = this._controller.input.timeout + 10;
-    this._waitResult(timeout);
-  },
-
-  _waitResult: function (aTimes)
-  {
-    var obj = this;
-    if (aTimes-- > 0) {
-      setTimeout(function () { obj._waitResult(aTimes); }, 0);
-    } else {
-      setTimeout(function () { obj._checkResult(); }, 0);
-    }
+    waitForCondition(() => {
+      return this._controller.searchStatus >=
+             Components.interfaces.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH;
+    },
+    this._checkResult.bind(this));
   },
 
   _checkResult: function ()
   {
     var test = this._tests[this._testingIndex];
     this._is(this._getTargetValue(), test.value,
              this._description + ", " + test.description + ": value");
     this._is(this._controller.searchString, test.searchString,
@@ -265,17 +269,17 @@ nsDoTestsForAutoCompleteWithComposition.
               [
                 { "length": 0, "attr": 0 }
               ]
             },
             "caret": { "start": 0, "length": 0 }
           }, aWindow);
       }, popup: false, value: "Mo", searchString: "Mozi"
     },
-    { description: "canceled compositionend should seach the result with the latest value",
+    { description: "canceled compositionend should search the result with the latest value",
       completeDefaultIndex: false,
       execute: function (aWindow) {
         synthesizeComposition({ type: "compositioncommitasis",
           key: { key: "KEY_Escape", code: "Escape" } }, aWindow);
       }, popup: true, value: "Mo", searchString: "Mo"
     },
     //If all characters are removed, the popup should be closed.
     { description: "the value becomes empty by backspace, the popup should be closed",