Merge f-t and m-i to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sun, 15 May 2016 20:37:48 -0700
changeset 336480 d0be57e84807ce0853b2406de7ff6abb195ac898
parent 336477 898d5f48ff51c7ee571cbda456661f42ce48cc8b (current diff)
parent 336479 091bc7c572dbbfcb751c8e0d9c37b36ff0a83a6a (diff)
child 336482 668a9155cc8b0dad7e84ea83019ba2f2ce20a4c1
child 336497 90d3d6d61504290f0d4ba35fddbe5106d8173ea0
push id6249
push userjlund@mozilla.com
push dateMon, 01 Aug 2016 13:59:36 +0000
treeherdermozilla-beta@bad9d4f5bf7e [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmerge
milestone49.0a1
first release with
nightly linux32
d0be57e84807 / 49.0a1 / 20160516030211 / files
nightly linux64
d0be57e84807 / 49.0a1 / 20160516030211 / files
nightly mac
d0be57e84807 / 49.0a1 / 20160516030211 / files
nightly win32
d0be57e84807 / 49.0a1 / 20160516030211 / files
nightly win64
d0be57e84807 / 49.0a1 / 20160516030211 / files
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
releases
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge f-t and m-i to m-c, a=merge
--- a/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/ActionBarTextSelection.java
@@ -218,16 +218,21 @@ class ActionBarTextSelection extends Lay
 
                         selectionID = message.getString("selectionID");
                         mCurrentItems = null;
                         if (mActionModeTimerTask != null) {
                             mActionModeTimerTask.cancel();
                         }
 
                     } else if (event.equals("TextSelection:ActionbarStatus")) {
+                        // Ensure async updates from SearchService for example are valid.
+                        if (selectionID != message.optString("selectionID")) {
+                            return;
+                        }
+
                         // Update the actionBar actions as provided by Gecko.
                         showActionMode(message.getJSONArray("actions"));
 
                     } else if (event.equals("TextSelection:ActionbarUninit")) {
                         // Uninit the actionbar. Schedule a cancellable close
                         // action to avoid UI jank. (During SelectionAll for ex).
                         mCurrentItems = null;
                         mActionModeTimerTask = new ActionModeTimerTask();
--- a/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
+++ b/mobile/android/base/java/org/mozilla/gecko/text/FloatingToolbarTextSelection.java
@@ -116,16 +116,21 @@ public class FloatingToolbarTextSelectio
 
     private void handleOnMainThread(final String event, final JSONObject message) {
         if ("TextSelection:ActionbarInit".equals(event)) {
             Telemetry.sendUIEvent(TelemetryContract.Event.SHOW,
                 TelemetryContract.Method.CONTENT, "text_selection");
 
             selectionID = message.optString("selectionID");
         } else if ("TextSelection:ActionbarStatus".equals(event)) {
+            // Ensure async updates from SearchService for example are valid.
+            if (selectionID != message.optString("selectionID")) {
+                return;
+            }
+
             updateRect(message);
 
             if (!isRectVisible()) {
                 finishActionMode();
             } else {
                 startActionMode(TextAction.fromEventMessage(message));
             }
         } else if ("TextSelection:ActionbarUninit".equals(event)) {
--- a/mobile/android/chrome/content/ActionBarHandler.js
+++ b/mobile/android/chrome/content/ActionBarHandler.js
@@ -17,16 +17,18 @@ var ActionBarHandler = {
   // Error codes returned from _init().
   START_TOUCH_ERROR: {
     NO_CONTENT_WINDOW: "No valid content Window found.",
     NONE: "",
   },
 
   _nextSelectionID: 1, // Next available.
   _selectionID: null, // Unique Selection ID, assigned each time we _init().
+
+  _boundingClientRect: null, // Current selections boundingClientRect.
   _actionBarActions: null, // Most-recent set of actions sent to ActionBar.
 
   /**
    * Receive and act on AccessibleCarets caret state-change
    * (mozcaretstatechanged) events.
    */
   caretStateChangedHandler: function(e) {
     // Close an open ActionBar, if carets no longer logically visible.
@@ -62,20 +64,23 @@ var ActionBarHandler = {
     }
 
     // Else, update an open ActionBar.
     if (this._selectionID) {
       if (!this._selectionHasChanged()) {
         // Still the same active selection.
         if (e.reason == 'visibilitychange' || e.reason == 'presscaret' ||
             e.reason == 'scroll' ) {
+          // Visibility changes don't affect boundingClientRect.
           this._updateVisibility();
         } else {
+          // Selection changes update boundingClientRect.
+          this._boundingClientRect = e.boundingClientRect;
           let forceUpdate = e.reason == 'updateposition' || e.reason == 'releasecaret';
-          this._sendActionBarActions(forceUpdate, e.boundingClientRect);
+          this._sendActionBarActions(forceUpdate);
         }
       } else {
         // We've started a new selection entirely.
         this._uninit(false);
         this._init(e.boundingClientRect);
       }
     }
   },
@@ -130,23 +135,24 @@ var ActionBarHandler = {
     let [element, win] = this._getSelectionTargets();
     if (!win) {
       return this.START_TOUCH_ERROR.NO_CONTENT_WINDOW;
     }
 
     // Hold the ActionBar ID provided by Gecko.
     this._selectionID = this._nextSelectionID++;
     [this._targetElement, this._contentWindow] = [element, win];
+    this._boundingClientRect = boundingClientRect;
 
     // Open the ActionBar, send it's actions list.
     Messaging.sendRequest({
       type: "TextSelection:ActionbarInit",
       selectionID: this._selectionID,
     });
-    this._sendActionBarActions(true, boundingClientRect);
+    this._sendActionBarActions(true);
 
     return this.START_TOUCH_ERROR.NONE;
   },
 
   /**
    * Called when content is scrolled and handles are hidden.
    */
   _updateVisibility: function() {
@@ -204,16 +210,17 @@ var ActionBarHandler = {
     Messaging.sendRequest({
       type: "TextSelection:ActionbarUninit",
     });
 
     // Clear the selection ID to complete the uninit(), but leave our reference
     // to selectionTargets (_targetElement, _contentWindow) in case we need
     // a final clearSelection().
     this._selectionID = null;
+    this._boundingClientRect = null;
 
     // Clear selection required if triggered by self, or TextSelection icon
     // actions. If called by Gecko CaretStateChangedEvent,
     // visibility state is already correct.
     if (clearSelection) {
       this._clearSelection();
     }
   },
@@ -243,34 +250,36 @@ var ActionBarHandler = {
    * Called to determine current ActionBar actions and send to TextSelection
    * handler. By default we only send if current action state differs from
    * the previous.
    * @param By default we only send an ActionBarStatus update message if
    *        there is a change from the previous state. sendAlways can be
    *        set by init() for example, where we want to always send the
    *        current state.
    */
-  _sendActionBarActions: function(sendAlways, boundingClientRect) {
+  _sendActionBarActions: function(sendAlways) {
     let actions = this._getActionBarActions();
+
     let actionCountUnchanged = this._actionBarActions &&
       actions.length === this._actionBarActions.length;
     let actionsMatch = actionCountUnchanged &&
       this._actionBarActions.every((e,i) => {
         return e.id === actions[i].id;
       });
 
     if (sendAlways || !actionsMatch) {
       Messaging.sendRequest({
         type: "TextSelection:ActionbarStatus",
+        selectionID: this._selectionID,
         actions: actions,
-        x: boundingClientRect.x,
-        y: boundingClientRect.y,
-        width: boundingClientRect.width,
-        height: boundingClientRect.height
-      });;
+        x: this._boundingClientRect.x,
+        y: this._boundingClientRect.y,
+        width: this._boundingClientRect.width,
+        height: this._boundingClientRect.height
+      });
     }
 
     this._actionBarActions = actions;
   },
 
   /**
    * Determine and return current ActionBar state.
    */
@@ -530,25 +539,42 @@ var ActionBarHandler = {
         matches: function(element, win) {
           if(!(element instanceof HTMLInputElement)) {
             return false;
           }
           let form = element.form;
           if (!form || element.type == "password") {
             return false;
           }
+
           let method = form.method.toUpperCase();
-          return (method == "GET" || method == "") ||
-                 (form.enctype != "text/plain") && (form.enctype != "multipart/form-data");
+          let canAddEngine = (method == "GET") ||
+            (method == "POST" && (form.enctype != "text/plain" && form.enctype != "multipart/form-data"));
+          if (!canAddEngine) {
+            return false;
+          }
+
+          // If SearchEngine query finds it, then we don't want action to add displayed.
+          if (SearchEngines.visibleEngineExists(element)) {
+            return false;
+          }
+
+          return true;
         },
       },
 
       action: function(element, win) {
         UITelemetry.addEvent("action.1", "actionbar", null, "add_search_engine");
-        SearchEngines.addEngine(element);
+
+        // Engines are added asynch. If required, update SelectionUI on callback.
+        SearchEngines.addEngine(element, (result) => {
+          if (result) {
+            ActionBarHandler._sendActionBarActions(true);
+          }
+        });
       },
     },
 
     SHARE: {
       id: "share_action",
       label: Strings.browser.GetStringFromName("contextmenu.share"),
       icon: "drawable://ic_menu_share",
       order: 0,
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -6691,100 +6691,164 @@ var SearchEngines = {
           errorMessage = "alertSearchEngineErrorToast";
         }
 
         Snackbars.show(Strings.browser.formatStringFromName(errorMessage, [engine.title], 1), Snackbars.LENGTH_LONG);
       }
     });
   },
 
-  addEngine: function addEngine(aElement) {
+  /**
+   * Build and return an array of sorted form data / Query Parameters
+   * for an element in a submission form.
+   *
+   * @param element
+   *        A valid submission element of a form.
+   */
+  _getSortedFormData: function(element) {
+    let formData = [];
+
+    for (let formElement of element.form.elements) {
+      if (!formElement.type) {
+        continue;
+      }
+
+      // Make this text field a generic search parameter.
+      if (element == formElement) {
+        formData.push({ name: formElement.name, value: "{searchTerms}" });
+        continue;
+      }
+
+      // Add other form elements as parameters.
+      switch (formElement.type.toLowerCase()) {
+        case "checkbox":
+        case "radio":
+          if (!formElement.checked) {
+            break;
+          }
+        case "text":
+        case "hidden":
+        case "textarea":
+          formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+          break;
+
+        case "select-one": {
+          for (let option of formElement.options) {
+            if (option.selected) {
+              formData.push({ name: escape(formElement.name), value: escape(formElement.value) });
+              break;
+            }
+          }
+        }
+      }
+    };
+
+    // Return valid, pre-sorted queryParams. 
+    return formData.filter(a => a.name && a.value).sort((a, b) => {
+      // nsIBrowserSearchService.hasEngineWithURL() ensures sort, but this helps.
+      if (a.name > b.name) {
+        return 1;
+      }
+      if (b.name > a.name) {
+        return -1;
+      }
+
+      if (a.value > b.value) {
+        return 1;
+      }
+      if (b.value > a.value) {
+        return -1;
+      }
+
+      return 0;
+    });
+  },
+
+  /**
+   * Check if any search engines already handle an EngineURL of type
+   * URLTYPE_SEARCH_HTML, matching this request-method, formURL, and queryParams.
+   */
+  visibleEngineExists: function(element) {
+    let formData = this._getSortedFormData(element);
+
+    let form = element.form;
+    let method = form.method.toUpperCase();
+
+    let charset = element.ownerDocument.characterSet;
+    let docURI = Services.io.newURI(element.ownerDocument.URL, charset, null);
+    let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
+
+    return Services.search.hasEngineWithURL(method, formURL, formData);
+  },
+
+  /**
+   * Adds a new search engine to the BrowserSearchService, based on its provided element. Prompts for an engine
+   * name, and appends a simple version-number in case of collision with an existing name.
+   *
+   * @return callback to handle success value. Currently used for ActionBarHandler.js and UI updates.
+   */
+  addEngine: function addEngine(aElement, resultCallback) {
     let form = aElement.form;
     let charset = aElement.ownerDocument.characterSet;
     let docURI = Services.io.newURI(aElement.ownerDocument.URL, charset, null);
     let formURL = Services.io.newURI(form.getAttribute("action"), charset, docURI).spec;
     let method = form.method.toUpperCase();
-    let formData = [];
-
-    for (let i = 0; i < form.elements.length; ++i) {
-      let el = form.elements[i];
-      if (!el.type)
-        continue;
-
-      // make this text field a generic search parameter
-      if (aElement == el) {
-        formData.push({ name: el.name, value: "{searchTerms}" });
-        continue;
-      }
-
-      let type = el.type.toLowerCase();
-      let escapedName = escape(el.name);
-      let escapedValue = escape(el.value);
-
-      // add other form elements as parameters
-      switch (el.type) {
-        case "checkbox":
-        case "radio":
-          if (!el.checked) break;
-        case "text":
-        case "hidden":
-        case "textarea":
-          formData.push({ name: escapedName, value: escapedValue });
-          break;
-        case "select-one":
-          for (let option of el.options) {
-            if (option.selected) {
-              formData.push({ name: escapedName, value: escapedValue });
-              break;
-            }
-          }
-      }
-    }
+    let formData = this._getSortedFormData(aElement);
 
     // prompt user for name of search engine
     let promptTitle = Strings.browser.GetStringFromName("contextmenu.addSearchEngine3");
     let title = { value: (aElement.ownerDocument.title || docURI.host) };
-    if (!Services.prompt.prompt(null, promptTitle, null, title, null, {}))
+    if (!Services.prompt.prompt(null, promptTitle, null, title, null, {})) {
+      if (resultCallback) {
+        resultCallback(false);
+      };
       return;
+    }
 
     // fetch the favicon for this page
     let dbFile = FileUtils.getFile("ProfD", ["browser.db"]);
     let mDBConn = Services.storage.openDatabase(dbFile);
     let stmts = [];
     stmts[0] = mDBConn.createStatement("SELECT favicon FROM history_with_favicons WHERE url = ?");
     stmts[0].bindByIndex(0, docURI.spec);
     let favicon = null;
+
     Services.search.init(function addEngine_cb(rv) {
       if (!Components.isSuccessCode(rv)) {
         Cu.reportError("Could not initialize search service, bailing out.");
+        if (resultCallback) {
+          resultCallback(false);
+        };
         return;
       }
+
       mDBConn.executeAsync(stmts, stmts.length, {
         handleResult: function (results) {
           let bytes = results.getNextRow().getResultByName("favicon");
           if (bytes && bytes.length) {
             favicon = "data:image/x-icon;base64," + btoa(String.fromCharCode.apply(null, bytes));
           }
         },
         handleCompletion: function (reason) {
           // if there's already an engine with this name, add a number to
           // make the name unique (e.g., "Google" becomes "Google 2")
           let name = title.value;
           for (let i = 2; Services.search.getEngineByName(name); i++)
             name = title.value + " " + i;
 
           Services.search.addEngineWithDetails(name, favicon, null, null, method, formURL);
           Snackbars.show(Strings.browser.formatStringFromName("alertSearchEngineAddedToast", [name], 1), Snackbars.LENGTH_LONG);
+
           let engine = Services.search.getEngineByName(name);
           engine.wrappedJSObject._queryCharset = charset;
-          for (let i = 0; i < formData.length; ++i) {
-            let param = formData[i];
-            if (param.name && param.value)
-              engine.addParam(param.name, param.value, null);
-          }
+          formData.forEach(param => { engine.addParam(param.name, param.value, null); });
+
+          if (resultCallback) {
+            return resultCallback(true);
+          };
         }
       });
     });
   }
 };
 
 var ActivityObserver = {
   init: function ao_init() {
--- a/netwerk/base/nsIBrowserSearchService.idl
+++ b/netwerk/base/nsIBrowserSearchService.idl
@@ -264,16 +264,33 @@ interface nsIBrowserSearchService : nsIS
   readonly attribute bool isInitialized;
 
   /**
    * Resets the default engine to its original value.
    */
   void resetToOriginalDefaultEngine();
 
   /**
+   * Checks if an EngineURL of type URLTYPE_SEARCH_HTML exists for
+   * any engine, with a matching method, template URL, and query params.
+   *
+   * @param method
+   *        The HTTP request method used when submitting a search query.
+   *        Must be a case insensitive value of either "get" or "post".
+   *
+   * @param url
+   *        The URL to which search queries should be sent.
+   *        Must not be null.
+   *
+   * @param formData
+   *        The un-sorted form data used as query params.
+   */
+  boolean hasEngineWithURL(in AString method, in AString url, in jsval formData);
+
+  /**
    * Adds a new search engine from the file at the supplied URI, optionally
    * asking the user for confirmation first.  If a confirmation dialog is
    * shown, it will offer the option to begin using the newly added engine
    * right away.
    *
    * @param engineURL
    *        The URL to the search engine's description file.
    *
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -4352,16 +4352,64 @@ SearchService.prototype = {
       };
 
       processDomain(urlParsingInfo.mainDomain, false);
       SearchStaticData.getAlternateDomains(urlParsingInfo.mainDomain)
                       .forEach(d => processDomain(d, true));
     }
   },
 
+  /**
+   * Checks to see if any engine has an EngineURL of type URLTYPE_SEARCH_HTML
+   * for this request-method, template URL, and query params.
+   */
+  hasEngineWithURL: function(method, template, formData) {
+    this._ensureInitialized();
+
+    // Quick helper method to ensure formData filtered/sorted for compares.
+    let getSortedFormData = data => {
+      return data.filter(a => a.name && a.value).sort((a, b) => {
+        return (a.name > b.name) ? 1 : (b.name > a.name) ? -1 :
+               (a.value > b.value) ? 1 : (b.value > a.value) ? -1 : 0;
+      });
+    };
+
+    // Sanitize method, ensure formData is pre-sorted.
+    let methodUpper = method.toUpperCase();
+    let sortedFormData = getSortedFormData(formData);
+    let sortedFormLength = sortedFormData.length;
+
+    return this._getSortedEngines(false).some(engine => {
+      return engine._urls.some(url => {
+        // Not an engineURL match if type, method, url, #params don't match.
+        if (url.type != URLTYPE_SEARCH_HTML ||
+            url.method != methodUpper ||
+            url.template != template ||
+            url.params.length != sortedFormLength) {
+          return false;
+        };
+
+        // Ensure engineURL formData is pre-sorted. Then, we're
+        // not an engineURL match if any queryParam doesn't compare.
+        let sortedParams = getSortedFormData(url.params);
+        for (let i = 0; i < sortedFormLength; i++) {
+          let formData = sortedFormData[i];
+          let param = sortedParams[i];
+          if (param.name != formData.name ||
+              param.value != formData.value ||
+              param.purpose != formData.purpose) {
+            return false;
+          };
+        };
+        // Else we're a match.
+        return true;
+      });
+    });
+  },
+
   parseSubmissionURL: function SRCH_SVC_parseSubmissionURL(aURL) {
     this._ensureInitialized();
     LOG("parseSubmissionURL: Parsing \"" + aURL + "\".");
 
     if (!this._parseSubmissionMap) {
       this._buildParseSubmissionMap();
     }
 
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js
@@ -0,0 +1,135 @@
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Tests the hasEngineWithURL() method of the nsIBrowserSearchService.
+ */
+function run_test() {
+  do_print("Setting up test");
+
+  updateAppInfo();
+  useHttpServer();
+
+  do_print("Test starting");
+  run_next_test();
+}
+
+
+// Return a discreet, cloned copy of an (engine) object.
+function getEngineClone(engine) {
+  return JSON.parse(JSON.stringify(engine));
+}
+
+// Check whether and engine does or doesn't exist.
+function checkEngineState(exists, engine) {
+  do_check_eq(exists, Services.search.hasEngineWithURL(engine.method,
+                                                       engine.formURL,
+                                                       engine.queryParams));
+}
+
+// Add a search engine for testing.
+function addEngineWithParams(engine) {
+  Services.search.addEngineWithDetails(engine.name, null, null, null,
+                                       engine.method, engine.formURL);
+
+  let addedEngine = Services.search.getEngineByName(engine.name);
+  for (let param of engine.queryParams) {
+    addedEngine.addParam(param.name, param.value, null);
+  }
+}
+
+// Main test.
+add_task(function* test_hasEngineWithURL() {
+  // Avoid deprecated synchronous initialization.
+  yield asyncInit();
+
+  // Setup various Engine definitions for method tests.
+  let UNSORTED_ENGINE = {
+    name: "mySearch Engine",
+    method: "GET",
+    formURL: "https://totallyNotRealSearchEngine.com/",
+    queryParams: [
+      { name: "DDs", value: "38s" },
+      { name: "DCs", value: "39s" },
+      { name: "DDs", value: "39s" },
+      { name: "DDs", value: "38s" },
+      { name: "DDs", value: "37s" },
+      { name: "DDs", value: "38s" },
+      { name: "DEs", value: "38s" },
+      { name: "DCs", value: "38s" },
+      { name: "DEs", value: "37s" },
+    ],
+  };
+
+  // Same as UNSORTED_ENGINE, but sorted.
+  let SORTED_ENGINE = {
+    name: "mySearch Engine",
+    method: "GET",
+    formURL: "https://totallyNotRealSearchEngine.com/",
+    queryParams: [
+      { name: "DCs", value: "38s" },
+      { name: "DCs", value: "39s" },
+      { name: "DDs", value: "37s" },
+      { name: "DDs", value: "38s" },
+      { name: "DDs", value: "38s" },
+      { name: "DDs", value: "38s" },
+      { name: "DDs", value: "39s" },
+      { name: "DEs", value: "37s" },
+      { name: "DEs", value: "38s" },
+    ],
+  };
+
+  // Unique variations of the SORTED_ENGINE.
+  let SORTED_ENGINE_METHOD_CHANGE = getEngineClone(SORTED_ENGINE);
+  SORTED_ENGINE_METHOD_CHANGE.method = "PoST";
+
+  let SORTED_ENGINE_FORMURL_CHANGE = getEngineClone(SORTED_ENGINE);
+  SORTED_ENGINE_FORMURL_CHANGE.formURL = "http://www.ahighrpowr.com/"
+
+  let SORTED_ENGINE_QUERYPARM_CHANGE = getEngineClone(SORTED_ENGINE);
+  SORTED_ENGINE_QUERYPARM_CHANGE.queryParams = [];
+
+  let SORTED_ENGINE_NAME_CHANGE = getEngineClone(SORTED_ENGINE);
+  SORTED_ENGINE_NAME_CHANGE.name += " 2";
+
+
+  // First ensure neither the unsorted engine, nor the same engine
+  // with a pre-sorted list of query parms matches.
+  checkEngineState(false, UNSORTED_ENGINE);
+  do_print("The unsorted version of the test engine does not exist.");
+  checkEngineState(false, SORTED_ENGINE);
+  do_print("The sorted version of the test engine does not exist.");
+
+  // Ensure variations of the engine definition do not match.
+  checkEngineState(false, SORTED_ENGINE_METHOD_CHANGE);
+  checkEngineState(false, SORTED_ENGINE_FORMURL_CHANGE);
+  checkEngineState(false, SORTED_ENGINE_QUERYPARM_CHANGE);
+  do_print("There are no modified versions of the sorted test engine.");
+
+  // Note that this method doesn't check name variations.
+  checkEngineState(false, SORTED_ENGINE_NAME_CHANGE);
+  do_print("There is no NAME modified version of the sorted test engine.");
+
+
+  // Add the unsorted engine and it's queryParams.
+  addEngineWithParams(UNSORTED_ENGINE);
+  do_print("The unsorted engine has been added.");
+
+
+  // Then, ensure we find a match for the unsorted engine, and for the
+  // same engine with a pre-sorted list of query parms.
+  checkEngineState(true, UNSORTED_ENGINE);
+  do_print("The unsorted version of the test engine now exists.");
+  checkEngineState(true, SORTED_ENGINE);
+  do_print("The sorted version of the same test engine also now exists.");
+
+  // Ensure variations of the engine definition still do not match.
+  checkEngineState(false, SORTED_ENGINE_METHOD_CHANGE);
+  checkEngineState(false, SORTED_ENGINE_FORMURL_CHANGE);
+  checkEngineState(false, SORTED_ENGINE_QUERYPARM_CHANGE);
+  do_print("There are still no modified versions of the sorted test engine.");
+
+  // Note that this method still doesn't check name variations.
+  checkEngineState(true, SORTED_ENGINE_NAME_CHANGE);
+  do_print("There IS now a NAME modified version of the sorted test engine.");
+});
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -31,16 +31,17 @@ support-files =
   data/searchSuggestions.sjs
   data/searchTest.jar
 
 [test_nocache.js]
 [test_645970.js]
 [test_bug930456.js]
 [test_bug930456_child.js]
 [test_engine_set_alias.js]
+[test_hasEngineWithURL.js]
 [test_identifiers.js]
 [test_invalid_engine_from_dir.js]
 [test_init_async_multiple.js]
 [test_init_async_multiple_then_sync.js]
 [test_json_cache.js]
 [test_location.js]
 [test_location_error.js]
 [test_location_malformed_json.js]