Bug 493051: avoid having addEngine select the engine by default, by adding an optional callback to let callers observe the successful addition of the engine, r=MattN
authorGavin Sharp <gavin@gavinsharp.com>
Tue, 18 Jun 2013 09:39:02 -0400
changeset 146968 e9b946da20cb5c817b9b9649b671eb0739ea5be7
parent 146967 9b0ceec4270eff98b84e8ebe922ba5665eb39da5
child 146969 d4518f89e75a3731a8676308f3c73a55bc1abdde
push id2697
push userbbajaj@mozilla.com
push dateMon, 05 Aug 2013 18:49:53 +0000
treeherdermozilla-beta@dfec938c7b63 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersMattN
bugs493051
milestone24.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 493051: avoid having addEngine select the engine by default, by adding an optional callback to let callers observe the successful addition of the engine, r=MattN
browser/components/search/content/search.xml
browser/components/search/test/browser_426329.js
browser/components/search/test/browser_contextmenu.js
browser/components/search/test/browser_private_search_perwindowpb.js
netwerk/base/public/nsIBrowserSearchService.idl
toolkit/components/search/nsSearchService.js
toolkit/components/search/tests/xpcshell/head_search.js
toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
toolkit/components/search/tests/xpcshell/test_notifications.js
toolkit/components/search/tests/xpcshell/xpcshell.ini
--- a/browser/components/search/content/search.xml
+++ b/browser/components/search/content/search.xml
@@ -483,27 +483,32 @@
           openUILinkIn(submission.uri.spec, aWhere, null, submission.postData);
         ]]></body>
       </method>
     </implementation>
 
     <handlers>
       <handler event="command"><![CDATA[
         const target = event.originalTarget;
-        if (target.classList.contains("addengine-item")) {
+        if (target.engine) {
+          this.currentEngine = target.engine;
+        } else if (target.classList.contains("addengine-item")) {
           var searchService =
             Components.classes["@mozilla.org/browser/search-service;1"]
                       .getService(Components.interfaces.nsIBrowserSearchService);
           // We only detect OpenSearch files
           var type = Components.interfaces.nsISearchEngine.DATA_XML;
+          // Select the installed engine if the installation succeeds
+          var installCallback = {
+            onSuccess: engine => this.currentEngine = engine
+          }
           searchService.addEngine(target.getAttribute("uri"), type,
-                                  target.getAttribute("src"), false);
+                                  target.getAttribute("src"), false,
+                                  installCallback);
         }
-        else if (target.engine)
-          this.currentEngine = target.engine;
         else
           return;
 
         this.focus();
         this.select();
       ]]></handler>
 
       <handler event="popupshowing" action="this.rebuildPopupDynamic();"/>
--- a/browser/components/search/test/browser_426329.js
+++ b/browser/components/search/test/browser_426329.js
@@ -25,18 +25,17 @@ function test() {
 
   let testIterator;
 
   function observer(aSub, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         var engine = ss.getEngineByName("Bug 426329");
         ok(engine, "Engine was added.");
-        //XXX Bug 493051
-        //ss.currentEngine = engine;
+        ss.currentEngine = engine;
         break;
       case "engine-current":
         ok(ss.currentEngine.name == "Bug 426329", "currentEngine set");
         testReturn();
         break;
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         finish();
--- a/browser/components/search/test/browser_contextmenu.js
+++ b/browser/components/search/test/browser_contextmenu.js
@@ -11,18 +11,17 @@ function test() {
   const ENGINE_NAME = "Foo";
   var contextMenu;
 
   function observer(aSub, aTopic, aData) {
     switch (aData) {
       case "engine-added":
         var engine = ss.getEngineByName(ENGINE_NAME);
         ok(engine, "Engine was added.");
-        //XXX Bug 493051
-        //ss.currentEngine = engine;
+        ss.currentEngine = engine;
         break;
       case "engine-current":
         is(ss.currentEngine.name, ENGINE_NAME, "currentEngine set");
         startTest();
         break;
       case "engine-removed":
         Services.obs.removeObserver(observer, "browser-search-engine-modified");
         finish();
--- a/browser/components/search/test/browser_private_search_perwindowpb.js
+++ b/browser/components/search/test/browser_private_search_perwindowpb.js
@@ -37,30 +37,28 @@ function test() {
     onPageLoad(aWin, aCallback);
 
     searchBar.value = aIsPrivate ? "private test" : "public test";
     searchBar.focus();
     EventUtils.synthesizeKey("VK_RETURN", {}, aWin);
   }
 
   function addEngine(aCallback) {
-    function observer(aSub, aTopic, aData) {
-      switch (aData) {
-        case "engine-current":
-          ok(Services.search.currentEngine.name == "Bug 426329",
-             "currentEngine set");
-          aCallback();
-          break;
+    let installCallback = {
+      onSuccess: function (engine) {
+        Services.search.currentEngine = engine;
+        aCallback();
+      },
+      onError: function (errorCode) {
+        ok(false, "failed to install engine: " + errorCode);
       }
-    }
-
-    Services.obs.addObserver(observer, "browser-search-engine-modified", false);
-    Services.search.addEngine(
-      engineURL + "426329.xml", Ci.nsISearchEngine.DATA_XML,
-      "data:image/x-icon,%00", false);
+    };
+    Services.search.addEngine(engineURL + "426329.xml",
+                              Ci.nsISearchEngine.DATA_XML,
+                              "data:image/x-icon,%00", false, installCallback);
   }
 
   function testOnWindow(aIsPrivate, aCallback) {
     let win = OpenBrowserWindow({ private: aIsPrivate });
     waitForFocus(function() {
       windowsToClose.push(win);
       executeSoon(function() aCallback(win));
     }, win);
--- a/netwerk/base/public/nsIBrowserSearchService.idl
+++ b/netwerk/base/public/nsIBrowserSearchService.idl
@@ -135,16 +135,40 @@ interface nsISearchEngine : nsISupports
   /**
    * An optional unique identifier for this search engine within the context of
    * the distribution, as provided by the distributing entity.
    */
   readonly attribute AString identifier;
 
 };
 
+[scriptable, uuid(9fc39136-f08b-46d3-b232-96f4b7b0e235)]
+interface nsISearchInstallCallback : nsISupports
+{
+  const unsigned long ERROR_UNKNOWN_FAILURE = 0x1;
+  const unsigned long ERROR_DUPLICATE_ENGINE = 0x2;
+
+  /**
+   * Called to indicate that the engine addition process succeeded.
+   *
+   * @param engine
+   *        The nsISearchEngine object that was added (will not be null).
+   */
+  void onSuccess(in nsISearchEngine engine);
+
+  /**
+   * Called to indicate that the engine addition process failed.
+   *
+   * @param errorCode
+   *        One of the ERROR_* values described above indicating the cause of
+   *        the failure.
+   */
+  void onError(in unsigned long errorCode);
+};
+
 /**
  * Callback for asynchronous initialization of nsIBrowserSearchService
  */
 [scriptable, function, uuid(02256156-16e4-47f1-9979-76ff98ceb590)]
 interface nsIBrowserSearchInitObserver : nsISupports
 {
   /**
    * Called once initialization of the browser search service is complete.
@@ -181,18 +205,17 @@ interface nsIBrowserSearchService : nsIS
    * initialization has not been triggered yet.
    */
   readonly attribute bool isInitialized;
 
   /**
    * 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; if no confirmation dialog is shown, the new engine will be
-   * used right away automatically.
+   * right away.
    *
    * @param engineURL
    *        The URL to the search engine's description file.
    *
    * @param dataType
    *        An integer representing the plugin file format. Must be one
    *        of the supported search engine data types defined above.
    *
@@ -202,21 +225,26 @@ interface nsIBrowserSearchService : nsIS
    *        engine description file.
    *
    * @param confirm
    *        A boolean value indicating whether the user should be asked for
    *        confirmation before this engine is added to the list.  If this
    *        value is false, the engine will be added to the list upon successful
    *        load, but it will not be selected as the current engine.
    *
+   * @param callback
+   *        A nsISearchInstallCallback that will be notified when the
+   *        addition is complete, or if the addition fails. It will not be
+   *        called if addEngine throws an exception.
+   *
    * @throws NS_ERROR_FAILURE if the type is invalid, or if the description
    *         file cannot be successfully loaded.
    */
   void addEngine(in AString engineURL, in long dataType, in AString iconURL,
-                 in boolean confirm);
+                 in boolean confirm, [optional] in nsISearchInstallCallback callback);
 
   /**
    * Adds a new search engine, without asking the user for confirmation and
    * without starting to use it right away.
    *
    * @param name
    *        The search engine's name. Must be unique. Must not be null.
    *
--- a/toolkit/components/search/nsSearchService.js
+++ b/toolkit/components/search/nsSearchService.js
@@ -1,15 +1,16 @@
 # 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 Ci = Components.interfaces;
 const Cc = Components.classes;
 const Cr = Components.results;
+const Cu = Components.utils;
 
 Components.utils.import("resource://gre/modules/XPCOMUtils.jsm");
 Components.utils.import("resource://gre/modules/Services.jsm");
 Components.utils.import("resource://gre/modules/commonjs/sdk/core/promise.js");
 
 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
   "resource://gre/modules/DeferredTask.jsm");
 XPCOMUtils.defineLazyModuleGetter(this, "OS",
@@ -1145,17 +1146,20 @@ Engine.prototype = {
   set _uri(aValue) {
     this.__uri = aValue;
   },
   // Whether to obtain user confirmation before adding the engine. This is only
   // used when the engine is first added to the list.
   _confirm: false,
   // Whether to set this as the current engine as soon as it is loaded.  This
   // is only used when the engine is first added to the list.
-  _useNow: true,
+  _useNow: false,
+  // A function to be invoked when this engine object's addition completes (or
+  // fails). Only used for installation via addEngine.
+  _installCallback: null,
   // Where the engine was loaded from. Can be one of: SEARCH_APP_DIR,
   // SEARCH_PROFILE_DIR, SEARCH_IN_EXTENSION.
   __installLocation: null,
   // The number of days between update checks for new versions
   _updateInterval: null,
   // The url to check at for a new update
   _updateURL: null,
   // The url to check for a new icon
@@ -1299,41 +1303,50 @@ Engine.prototype = {
 
   /**
    * Handle the successful download of an engine. Initializes the engine and
    * triggers parsing of the data. The engine is then flushed to disk. Notifies
    * the search service once initialization is complete.
    */
   _onLoad: function SRCH_ENG_onLoad(aBytes, aEngine) {
     /**
-     * Handle an error during the load of an engine by prompting the user to
-     * notify him that the load failed.
+     * Handle an error during the load of an engine by notifying the engine's
+     * error callback, if any.
      */
-    function onError(aErrorString, aTitleString) {
+    function onError(errorCode = Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE) {
+      // Notify the callback of the failure
+      if (aEngine._installCallback) {
+        aEngine._installCallback(errorCode);
+      }
+    }
+
+    function promptError(strings = {}, error = undefined) {
+      onError(error);
+
       if (aEngine._engineToUpdate) {
         // We're in an update, so just fail quietly
         LOG("updating " + aEngine._engineToUpdate.name + " failed");
         return;
       }
       var brandBundle = Services.strings.createBundle(BRAND_BUNDLE);
       var brandName = brandBundle.GetStringFromName("brandShortName");
 
       var searchBundle = Services.strings.createBundle(SEARCH_BUNDLE);
-      var msgStringName = aErrorString || "error_loading_engine_msg2";
-      var titleStringName = aTitleString || "error_loading_engine_title";
+      var msgStringName = strings.error || "error_loading_engine_msg2";
+      var titleStringName = strings.title || "error_loading_engine_title";
       var title = searchBundle.GetStringFromName(titleStringName);
       var text = searchBundle.formatStringFromName(msgStringName,
                                                    [brandName, aEngine._location],
                                                    2);
 
       Services.ww.getNewPrompter(null).alert(title, text);
     }
 
     if (!aBytes) {
-      onError();
+      promptError();
       return;
     }
 
     var engineToUpdate = null;
     if (aEngine._engineToUpdate) {
       engineToUpdate = aEngine._engineToUpdate.wrappedJSObject;
 
       // Make this new engine use the old engine's file.
@@ -1346,53 +1359,55 @@ Engine.prototype = {
                      createInstance(Ci.nsIDOMParser);
         var doc = parser.parseFromBuffer(aBytes, aBytes.length, "text/xml");
         aEngine._data = doc.documentElement;
         break;
       case SEARCH_DATA_TEXT:
         aEngine._data = aBytes;
         break;
       default:
-        onError();
+        promptError();
         LOG("_onLoad: Bogus engine _dataType: \"" + this._dataType + "\"");
         return;
     }
 
     try {
       // Initialize the engine from the obtained data
       aEngine._initFromData();
     } catch (ex) {
       LOG("_onLoad: Failed to init engine!\n" + ex);
       // Report an error to the user
-      onError();
+      promptError();
       return;
     }
 
     // Check to see if this is a duplicate engine. If we're confirming the
     // engine load, then we display a "this is a duplicate engine" prompt,
     // otherwise we fail silently.
     if (!engineToUpdate) {
       if (Services.search.getEngineByName(aEngine.name)) {
-        if (aEngine._confirm)
-          onError("error_duplicate_engine_msg", "error_invalid_engine_title");
-
+        promptError({ error: "error_duplicate_engine_msg",
+                      title: "error_invalid_engine_title"
+                    }, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
         LOG("_onLoad: duplicate engine found, bailing");
         return;
       }
     }
 
     // If requested, confirm the addition now that we have the title.
     // This property is only ever true for engines added via
     // nsIBrowserSearchService::addEngine.
     if (aEngine._confirm) {
       var confirmation = aEngine._confirmAddEngine();
       LOG("_onLoad: confirm is " + confirmation.confirmed +
           "; useNow is " + confirmation.useNow);
-      if (!confirmation.confirmed)
+      if (!confirmation.confirmed) {
+        onError();
         return;
+      }
       aEngine._useNow = confirmation.useNow;
     }
 
     // If we don't yet have a file, get one now. The only case where we would
     // already have a file is if this is an update and _file was set above.
     if (!aEngine._file)
       aEngine._file = getSanitizedFile(aEngine.name);
 
@@ -1409,44 +1424,47 @@ Engine.prototype = {
         let newUpdateURL = aEngine._updateURL;
         let oldSelfURL = engineToUpdate._getURLOfType(URLTYPE_OPENSEARCH);
         if (oldSelfURL && oldSelfURL._hasRelation("self")) {
           oldUpdateURL = oldSelfURL.template;
           let newSelfURL = aEngine._getURLOfType(URLTYPE_OPENSEARCH);
           if (!newSelfURL || !newSelfURL._hasRelation("self")) {
             LOG("_onLoad: updateURL missing in updated engine for " +
                 aEngine.name + " aborted");
+            onError();
             return;
           }
           newUpdateURL = newSelfURL.template;
         }
 
         if (oldUpdateURL != newUpdateURL) {
           LOG("_onLoad: updateURLs do not match! Update of " + aEngine.name + " aborted");
+          onError();
           return;
         }
       }
 
       // Set the new engine's icon, if it doesn't yet have one.
       if (!aEngine._iconURI && engineToUpdate._iconURI)
         aEngine._iconURI = engineToUpdate._iconURI;
-
-      // Clear the "use now" flag since we don't want to be changing the
-      // current engine for an update.
-      aEngine._useNow = false;
     }
 
     // Write the engine to file. For readOnly engines, they'll be stored in the
     // cache following the notification below.
     if (!aEngine._readOnly)
       aEngine._serializeToFile();
 
     // Notify the search service of the successful load. It will deal with
     // updates by checking aEngine._engineToUpdate.
     notifyAction(aEngine, SEARCH_ENGINE_LOADED);
+
+    // Notify the callback if needed
+    if (aEngine._installCallback) {
+      aEngine._installCallback();
+    }
   },
 
   /**
    * Sets the .iconURI property of the engine.
    *
    *  @param aIconURL
    *         A URI string pointing to the engine's icon. Must have a http[s],
    *         ftp, or data scheme. Icons with HTTP[S] or FTP schemes will be
@@ -3336,24 +3354,41 @@ SearchService.prototype = {
     var engine = new Engine(getSanitizedFile(aName), SEARCH_DATA_XML, false);
     engine._initFromMetadata(aName, aIconURL, aAlias, aDescription,
                              aMethod, aTemplate);
     this._addEngineToStore(engine);
     this._batchCacheInvalidation();
   },
 
   addEngine: function SRCH_SVC_addEngine(aEngineURL, aDataType, aIconURL,
-                                         aConfirm) {
+                                         aConfirm, aCallback) {
     LOG("addEngine: Adding \"" + aEngineURL + "\".");
     this._ensureInitialized();
     try {
       var uri = makeURI(aEngineURL);
       var engine = new Engine(uri, aDataType, false);
+      if (aCallback) {
+        engine._installCallback = function (errorCode) {
+          try {
+            if (errorCode == null)
+              aCallback.onSuccess(engine);
+            else
+              aCallback.onError(errorCode);
+          } catch (ex) {
+            Cu.reportError("Error invoking addEngine install callback: " + ex);
+          }
+          // Clear the reference to the callback now that it's been invoked.
+          engine._installCallback = null;
+        };
+      }
       engine._initFromURI();
     } catch (ex) {
+      // Drop the reference to the callback, if set
+      if (engine)
+        engine._installCallback = null;
       FAIL("addEngine: Error adding engine:\n" + ex, Cr.NS_ERROR_FAILURE);
     }
     engine._setIcon(aIconURL, false);
     engine._confirm = aConfirm;
   },
 
   removeEngine: function SRCH_SVC_removeEngine(aEngine) {
     this._ensureInitialized();
--- a/toolkit/components/search/tests/xpcshell/head_search.js
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -92,17 +92,17 @@ function afterCache(callback)
       callback(result);
     } else {
       dump("TOPIC: " + topic+ "\n");
     }
   }
   Services.obs.addObserver(obs, "browser-search-service", false);
 }
 
-function  parseJsonFromStream(aInputStream) {
+function parseJsonFromStream(aInputStream) {
   const json = Cc["@mozilla.org/dom/json;1"].createInstance(Components.interfaces.nsIJSON);
   const data = json.decodeFromStream(aInputStream, aInputStream.available());
   return data;
 }
 
 /**
  * Read a JSON file and return the JS object
  */
new file mode 100644
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
@@ -0,0 +1,123 @@
+/* Any copyright is dedicated to the Public Domain.
+ *    http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests covering nsIBrowserSearchService::addEngine's optional callback.
+ */
+
+"use strict";
+
+const Ci = Components.interfaces;
+
+Components.utils.import("resource://testing-common/httpd.js");
+
+// Override the prompt service and nsIPrompt, since the search service currently
+// prompts in response to certain installation failures we test here
+// XXX this should disappear once bug 863474 is fixed
+function replaceService(contractID, component) {
+  let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+  let cid = registrar.contractIDToCID(contractID);
+
+  let oldFactory = Components.manager.getClassObject(Components.classes[contractID],
+                                                     Ci.nsIFactory);
+  registrar.unregisterFactory(cid, oldFactory);
+
+  let factory = {
+    createInstance: function(aOuter, aIid) {
+      if (aOuter != null)
+        throw Components.results.NS_ERROR_NO_AGGREGATION;
+      return component.QueryInterface(aIid);
+    }
+  };
+
+  registrar.registerFactory(cid, "", contractID, factory);
+}
+// Only need to stub the methods actually called by nsSearchService
+let promptService = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+  confirmEx: function() {}
+};
+let prompt = {
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
+  alert: function() {}
+};
+replaceService("@mozilla.org/embedcomp/prompt-service;1", promptService);
+replaceService("@mozilla.org/prompter;1", prompt);
+
+
+// First test inits the search service
+add_test(function init_search_service() {
+  Services.search.init(function (status) {
+    if (!Components.isSuccessCode(status))
+      do_throw("Failed to initialize search service");
+
+    run_next_test();
+  });
+});
+
+// Simple test of the search callback
+add_test(function simple_callback_test() {
+  let searchCallback = {
+    onSuccess: function (engine) {
+      do_check_true(!!engine);
+      do_check_neq(engine.name, Services.search.defaultEngine.name);
+      run_next_test();
+    },
+    onError: function (errorCode) {
+      do_throw("search callback returned error: " + errorCode);
+    }
+  }
+  Services.search.addEngine("http://localhost:4444/data/engine.xml",
+                            Ci.nsISearchEngine.DATA_XML,
+                            null, false, searchCallback);
+});
+
+// Test of the search callback on duplicate engine failures
+add_test(function duplicate_failure_test() {
+  let searchCallback = {
+    onSuccess: function (engine) {
+      do_throw("this addition should not have succeeded");
+    },
+    onError: function (errorCode) {
+      do_check_true(!!errorCode);
+      do_check_eq(errorCode, Ci.nsISearchInstallCallback.ERROR_DUPLICATE_ENGINE);
+      run_next_test();
+    }
+  }
+  // Re-add the same engine added in the previous test
+  Services.search.addEngine("http://localhost:4444/data/engine.xml",
+                            Ci.nsISearchEngine.DATA_XML,
+                            null, false, searchCallback);
+});
+
+// Test of the search callback on failure to load the engine failures
+add_test(function load_failure_test() {
+  let searchCallback = {
+    onSuccess: function (engine) {
+      do_throw("this addition should not have succeeded");
+    },
+    onError: function (errorCode) {
+      do_check_true(!!errorCode);
+      do_check_eq(errorCode, Ci.nsISearchInstallCallback.ERROR_UNKNOWN_FAILURE);
+      run_next_test();
+    }
+  }
+  // Try adding an engine that doesn't exist
+  Services.search.addEngine("http://invalid/data/engine.xml",
+                            Ci.nsISearchEngine.DATA_XML,
+                            null, false, searchCallback);
+});
+
+function run_test() {
+  updateAppInfo();
+
+  let httpServer = new HttpServer();
+  httpServer.start(4444);
+  httpServer.registerDirectory("/", do_get_cwd());
+
+  do_register_cleanup(function cleanup() {
+    httpServer.stop(function() {});
+  });
+
+  run_next_test();
+}
--- a/toolkit/components/search/tests/xpcshell/test_notifications.js
+++ b/toolkit/components/search/tests/xpcshell/test_notifications.js
@@ -35,18 +35,17 @@ function search_observer(subject, topic,
 
   do_print("Observer: " + data + " for " + engine.name);
 
   switch (data) {
     case "engine-added":
       let retrievedEngine = Services.search.getEngineByName("Test search engine");
       do_check_eq(engine, retrievedEngine);
       Services.search.defaultEngine = engine;
-      // XXX bug 493051
-      // Services.search.currentEngine = engine;
+      Services.search.currentEngine = engine;
       do_execute_soon(function () {
         Services.search.removeEngine(engine);
       });
       break;
     case "engine-removed":
       let engineNameOutput = " for Test search engine";
       expectedLog = expectedLog.map(logLine => logLine + engineNameOutput);
       do_print("expectedLog:\n" + expectedLog.join("\n"))
--- a/toolkit/components/search/tests/xpcshell/xpcshell.ini
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -14,9 +14,9 @@ skip-if = debug && os == "linux"
 [test_migratedb.js]
 [test_nodb.js]
 [test_nodb_pluschanges.js]
 [test_save_sorted_engines.js]
 [test_purpose.js]
 [test_defaultEngine.js]
 [test_prefSync.js]
 [test_notifications.js]
-
+[test_addEngine_callback.js]