summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests/xpcshell/head_search.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/tests/xpcshell/head_search.js')
-rw-r--r--toolkit/components/search/tests/xpcshell/head_search.js544
1 files changed, 544 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/xpcshell/head_search.js b/toolkit/components/search/tests/xpcshell/head_search.js
new file mode 100644
index 000000000..2f40d84f8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/head_search.js
@@ -0,0 +1,544 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+
+var { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/osfile.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://gre/modules/Promise.jsm");
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/Task.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://testing-common/AppInfo.jsm");
+Cu.import("resource://testing-common/httpd.js");
+
+const BROWSER_SEARCH_PREF = "browser.search.";
+const NS_APP_SEARCH_DIR = "SrchPlugns";
+
+const MODE_RDONLY = FileUtils.MODE_RDONLY;
+const MODE_WRONLY = FileUtils.MODE_WRONLY;
+const MODE_CREATE = FileUtils.MODE_CREATE;
+const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE;
+
+const CACHE_FILENAME = "search.json.mozlz4";
+
+// nsSearchService.js uses Services.appinfo.name to build a salt for a hash.
+var XULRuntime = Components.classesByID["{95d89e3e-a169-41a3-8e56-719978e15b12}"]
+ .getService(Ci.nsIXULRuntime);
+
+var isChild = XULRuntime.processType == XULRuntime.PROCESS_TYPE_CONTENT;
+
+updateAppInfo({
+ name: "XPCShell",
+ ID: "xpcshell@test.mozilla.org",
+ version: "5",
+ platformVersion: "1.9",
+ // mirror OS from the base impl as some of the "location" tests rely on it
+ OS: XULRuntime.OS,
+ // mirror processType from the base implementation
+ extraProps: {
+ processType: XULRuntime.processType,
+ },
+});
+
+var gProfD;
+if (!isChild) {
+ // Need to create and register a profile folder.
+ gProfD = do_get_profile();
+}
+
+function dumpn(text)
+{
+ dump("search test: " + text + "\n");
+}
+
+/**
+ * Configure preferences to load engines from
+ * chrome://testsearchplugin/locale/searchplugins/
+ */
+function configureToLoadJarEngines()
+{
+ let url = "chrome://testsearchplugin/locale/searchplugins/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ // Ensure a test engine exists in the app dir anyway.
+ let dir = Services.dirsvc.get(NS_APP_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-app.xml").copyTo(dir, "app.xml");
+}
+
+/**
+ * Fake the installation of an add-on in the profile, by creating the
+ * directory and registering it with the directory service.
+ */
+function installAddonEngine(name = "engine-addon")
+{
+ const XRE_EXTENSIONS_DIR_LIST = "XREExtDL";
+ const profD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+ let dir = profD.clone();
+ dir.append("extensions");
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ dir.append("search-engine@tests.mozilla.org");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/install.rdf").copyTo(dir, "install.rdf");
+ let addonDir = dir.clone();
+ dir.append("searchplugins");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/" + name + ".xml").copyTo(dir, "bug645970.xml");
+
+ Services.dirsvc.registerProvider({
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIDirectoryServiceProvider,
+ Ci.nsIDirectoryServiceProvider2]),
+
+ getFile: function (prop, persistant) {
+ throw Cr.NS_ERROR_FAILURE;
+ },
+
+ getFiles: function (prop) {
+ let result = [];
+
+ switch (prop) {
+ case XRE_EXTENSIONS_DIR_LIST:
+ result.push(addonDir);
+ break;
+ default:
+ throw Cr.NS_ERROR_FAILURE;
+ }
+
+ return {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsISimpleEnumerator]),
+ hasMoreElements: () => result.length > 0,
+ getNext: () => result.shift()
+ };
+ }
+ });
+}
+
+/**
+ * Copy the engine-distribution.xml engine to a fake distribution
+ * created in the profile, and registered with the directory service.
+ */
+function installDistributionEngine()
+{
+ const XRE_APP_DISTRIBUTION_DIR = "XREAppDist";
+
+ const profD = do_get_profile().QueryInterface(Ci.nsILocalFile);
+
+ let dir = profD.clone();
+ dir.append("distribution");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ let distDir = dir.clone();
+
+ dir.append("searchplugins");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ dir.append("common");
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ Services.dirsvc.registerProvider({
+ getFile: function(aProp, aPersistent) {
+ aPersistent.value = true;
+ if (aProp == XRE_APP_DISTRIBUTION_DIR)
+ return distDir.clone();
+ return null;
+ }
+ });
+}
+
+/**
+ * Clean the profile of any metadata files left from a previous run.
+ */
+function removeMetadata()
+{
+ let file = gProfD.clone();
+ file.append("search-metadata.json");
+ if (file.exists()) {
+ file.remove(false);
+ }
+
+ file = gProfD.clone();
+ file.append("search.sqlite");
+ if (file.exists()) {
+ file.remove(false);
+ }
+}
+
+function promiseCacheData() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let path = OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME);
+ let bytes = yield OS.File.read(path, {compression: "lz4"});
+ resolve(JSON.parse(new TextDecoder().decode(bytes)));
+ }));
+}
+
+function promiseSaveCacheData(data) {
+ return OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME),
+ new TextEncoder().encode(JSON.stringify(data)),
+ {compression: "lz4"});
+}
+
+function promiseEngineMetadata() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let cache = yield promiseCacheData();
+ let data = {};
+ for (let engine of cache.engines) {
+ data[engine._shortName] = engine._metaData;
+ }
+ resolve(data);
+ }));
+}
+
+function promiseGlobalMetadata() {
+ return new Promise(resolve => Task.spawn(function* () {
+ let cache = yield promiseCacheData();
+ resolve(cache.metaData);
+ }));
+}
+
+function promiseSaveGlobalMetadata(globalData) {
+ return new Promise(resolve => Task.spawn(function* () {
+ let data = yield promiseCacheData();
+ data.metaData = globalData;
+ yield promiseSaveCacheData(data);
+ resolve();
+ }));
+}
+
+var forceExpiration = Task.async(function* () {
+ let metadata = yield promiseGlobalMetadata();
+
+ // Make the current geodefaults expire 1s ago.
+ metadata.searchDefaultExpir = Date.now() - 1000;
+ yield promiseSaveGlobalMetadata(metadata);
+});
+
+/**
+ * Clean the profile of any cache file left from a previous run.
+ * Returns a boolean indicating if the cache file existed.
+ */
+function removeCacheFile()
+{
+ let file = gProfD.clone();
+ file.append(CACHE_FILENAME);
+ if (file.exists()) {
+ file.remove(false);
+ return true;
+ }
+ return false;
+}
+
+/**
+ * isUSTimezone taken from nsSearchService.js
+ */
+function isUSTimezone() {
+ // Timezone assumptions! We assume that if the system clock's timezone is
+ // between Newfoundland and Hawaii, that the user is in North America.
+
+ // This includes all of South America as well, but we have relatively few
+ // en-US users there, so that's OK.
+
+ // 150 minutes = 2.5 hours (UTC-2.5), which is
+ // Newfoundland Daylight Time (http://www.timeanddate.com/time/zones/ndt)
+
+ // 600 minutes = 10 hours (UTC-10), which is
+ // Hawaii-Aleutian Standard Time (http://www.timeanddate.com/time/zones/hast)
+
+ let UTCOffset = (new Date()).getTimezoneOffset();
+ return UTCOffset >= 150 && UTCOffset <= 600;
+}
+
+const kDefaultenginenamePref = "browser.search.defaultenginename";
+const kTestEngineName = "Test search engine";
+const kLocalePref = "general.useragent.locale";
+
+function getDefaultEngineName(isUS) {
+ const nsIPLS = Ci.nsIPrefLocalizedString;
+ // Copy the logic from nsSearchService
+ let pref = kDefaultenginenamePref;
+ if (isUS === undefined)
+ isUS = Services.prefs.getCharPref(kLocalePref) == "en-US" && isUSTimezone();
+ if (isUS) {
+ pref += ".US";
+ }
+ return Services.prefs.getComplexValue(pref, nsIPLS).data;
+}
+
+/**
+ * Waits for the cache file to be saved.
+ * @return {Promise} Resolved when the cache file is saved.
+ */
+function promiseAfterCache() {
+ return waitForSearchNotification("write-cache-to-disk-complete");
+}
+
+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
+ */
+function readJSONFile(aFile) {
+ let stream = Cc["@mozilla.org/network/file-input-stream;1"].
+ createInstance(Ci.nsIFileInputStream);
+ try {
+ stream.init(aFile, MODE_RDONLY, FileUtils.PERMS_FILE, 0);
+ return parseJsonFromStream(stream, stream.available());
+ } catch (ex) {
+ dumpn("readJSONFile: Error reading JSON file: " + ex);
+ } finally {
+ stream.close();
+ }
+ return false;
+}
+
+/**
+ * Recursively compare two objects and check that every property of expectedObj has the same value
+ * on actualObj.
+ */
+function isSubObjectOf(expectedObj, actualObj) {
+ for (let prop in expectedObj) {
+ if (expectedObj[prop] instanceof Object) {
+ do_check_eq(expectedObj[prop].length, actualObj[prop].length);
+ isSubObjectOf(expectedObj[prop], actualObj[prop]);
+ } else {
+ if (expectedObj[prop] != actualObj[prop])
+ do_print("comparing property " + prop);
+ do_check_eq(expectedObj[prop], actualObj[prop]);
+ }
+ }
+}
+
+// Can't set prefs if we're running in a child process, but the search service
+// doesn't run in child processes anyways.
+if (!isChild) {
+ // Expand the amount of information available in error logs
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ // The geo-specific search tests assume certain prefs are already setup, which
+ // might not be true when run in comm-central etc. So create them here.
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 3000);
+ // But still disable geoip lookups - tests that need it will re-configure this.
+ Services.prefs.setCharPref("browser.search.geoip.url", "");
+ // Also disable region defaults - tests using it will also re-configure it.
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref("geoSpecificDefaults.url", "");
+}
+
+/**
+ * After useHttpServer() is called, this string contains the URL of the "data"
+ * directory, including the final slash.
+ */
+var gDataUrl;
+
+/**
+ * Initializes the HTTP server and ensures that it is terminated when tests end.
+ *
+ * @return The HttpServer object in case further customization is needed.
+ */
+function useHttpServer() {
+ let httpServer = new HttpServer();
+ httpServer.start(-1);
+ httpServer.registerDirectory("/", do_get_cwd());
+ gDataUrl = "http://localhost:" + httpServer.identity.primaryPort + "/data/";
+ do_register_cleanup(() => httpServer.stop(() => {}));
+ return httpServer;
+}
+
+/**
+ * Adds test engines and returns a promise resolved when they are installed.
+ *
+ * The engines are added in the given order.
+ *
+ * @param aItems
+ * Array of objects with the following properties:
+ * {
+ * name: Engine name, used to wait for it to be loaded.
+ * xmlFileName: Name of the XML file in the "data" folder.
+ * details: Array containing the parameters of addEngineWithDetails,
+ * except for the engine name. Alternative to xmlFileName.
+ * }
+ */
+var addTestEngines = Task.async(function* (aItems) {
+ if (!gDataUrl) {
+ do_throw("useHttpServer must be called before addTestEngines.");
+ }
+
+ let engines = [];
+
+ for (let item of aItems) {
+ do_print("Adding engine: " + item.name);
+ yield new Promise((resolve, reject) => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ try {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ do_print("Observed " + data + " for " + engine.name);
+ if (data != "engine-added" || engine.name != item.name) {
+ return;
+ }
+
+ Services.obs.removeObserver(obs, "browser-search-engine-modified");
+ engines.push(engine);
+ resolve();
+ } catch (ex) {
+ reject(ex);
+ }
+ }, "browser-search-engine-modified", false);
+
+ if (item.xmlFileName) {
+ Services.search.addEngine(gDataUrl + item.xmlFileName,
+ null, null, false);
+ } else {
+ Services.search.addEngineWithDetails(item.name, ...item.details);
+ }
+ });
+ }
+
+ return engines;
+});
+
+/**
+ * Installs a test engine into the test profile.
+ */
+function installTestEngine() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine.xml");
+ let engineDir = engineDummyFile.parent;
+ engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+ do_register_cleanup(function() {
+ removeMetadata();
+ removeCacheFile();
+ });
+}
+
+/**
+ * Set a localized preference on the default branch
+ * @param aPrefName
+ * The name of the pref to set.
+ */
+function setLocalizedDefaultPref(aPrefName, aValue) {
+ let value = "data:text/plain," + BROWSER_SEARCH_PREF + aPrefName + "=" + aValue;
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setCharPref(aPrefName, value);
+}
+
+
+/**
+ * Installs two test engines, sets them as default for US vs. general.
+ */
+function setUpGeoDefaults() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine.xml");
+ let engineDir = engineDummyFile.parent;
+ engineDir.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ do_get_file("data/engine.xml").copyTo(engineDir, "engine.xml");
+
+ engineDummyFile = gProfD.clone();
+ engineDummyFile.append("searchplugins");
+ engineDummyFile.append("test-search-engine2.xml");
+
+ do_get_file("data/engine2.xml").copyTo(engineDir, "engine2.xml");
+
+ setLocalizedDefaultPref("defaultenginename", "Test search engine");
+ setLocalizedDefaultPref("defaultenginename.US", "A second test engine");
+
+ do_register_cleanup(function() {
+ removeMetadata();
+ removeCacheFile();
+ });
+}
+
+/**
+ * Returns a promise that is resolved when an observer notification from the
+ * search service fires with the specified data.
+ *
+ * @param aExpectedData
+ * The value the observer notification sends that causes us to resolve
+ * the promise.
+ */
+function waitForSearchNotification(aExpectedData) {
+ return new Promise(resolve => {
+ const SEARCH_SERVICE_TOPIC = "browser-search-service";
+ Services.obs.addObserver(function observer(aSubject, aTopic, aData) {
+ if (aData != aExpectedData)
+ return;
+
+ Services.obs.removeObserver(observer, SEARCH_SERVICE_TOPIC);
+ resolve(aSubject);
+ }, SEARCH_SERVICE_TOPIC, false);
+ });
+}
+
+function asyncInit() {
+ return new Promise(resolve => {
+ Services.search.init(function() {
+ do_check_true(Services.search.isInitialized);
+ resolve();
+ });
+ });
+}
+
+function asyncReInit() {
+ let promise = waitForSearchNotification("reinit-complete");
+
+ Services.search.QueryInterface(Ci.nsIObserver)
+ .observe(null, "nsPref:changed", kLocalePref);
+
+ return promise;
+}
+
+// This "enum" from nsSearchService.js
+const TELEMETRY_RESULT_ENUM = {
+ SUCCESS: 0,
+ SUCCESS_WITHOUT_DATA: 1,
+ XHRTIMEOUT: 2,
+ ERROR: 3,
+};
+
+/**
+ * Checks the value of the SEARCH_SERVICE_COUNTRY_FETCH_RESULT probe.
+ *
+ * @param aExpectedValue
+ * If a value from TELEMETRY_RESULT_ENUM, we expect to see this value
+ * recorded exactly once in the probe. If |null|, we expect to see
+ * nothing recorded in the probe at all.
+ */
+function checkCountryResultTelemetry(aExpectedValue) {
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_FETCH_RESULT");
+ let snapshot = histogram.snapshot();
+ // The probe is declared with 8 values, but we get 9 back from .counts
+ let expectedCounts = [0, 0, 0, 0, 0, 0, 0, 0, 0];
+ if (aExpectedValue != null) {
+ expectedCounts[aExpectedValue] = 1;
+ }
+ deepEqual(snapshot.counts, expectedCounts);
+}