summaryrefslogtreecommitdiffstats
path: root/toolkit/components/search/tests
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/search/tests')
-rw-r--r--toolkit/components/search/tests/xpcshell/.eslintrc.js7
-rw-r--r--toolkit/components/search/tests/xpcshell/data/chrome.manifest3
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-addon.xml8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-app.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-fr.xml12
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-override.xml8
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-pref.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml6
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml11
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml5
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml10
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine-update.xml10
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine.xml25
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engine2.xml9
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineImages.xml22
-rw-r--r--toolkit/components/search/tests/xpcshell/data/engineMaker.sjs54
-rw-r--r--toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.icobin0 -> 901 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/install.rdf23
-rw-r--r--toolkit/components/search/tests/xpcshell/data/invalid-engine.xml1
-rw-r--r--toolkit/components/search/tests/xpcshell/data/langpack-metadata.json5
-rw-r--r--toolkit/components/search/tests/xpcshell/data/list.json7
-rw-r--r--toolkit/components/search/tests/xpcshell/data/metadata.json30
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search.json86
-rw-r--r--toolkit/components/search/tests/xpcshell/data/search.sqlitebin0 -> 65536 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs78
-rw-r--r--toolkit/components/search/tests/xpcshell/data/searchTest.jarbin0 -> 1249 bytes
-rw-r--r--toolkit/components/search/tests/xpcshell/head_search.js544
-rw-r--r--toolkit/components/search/tests/xpcshell/test_645970.js22
-rw-r--r--toolkit/components/search/tests/xpcshell/test_SearchStaticData.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js34
-rw-r--r--toolkit/components/search/tests/xpcshell/test_addEngine_callback.js95
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async.js34
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_addon.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_distribution.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_migration.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_async_profile_engine.js42
-rw-r--r--toolkit/components/search/tests/xpcshell/test_bug930456.js11
-rw-r--r--toolkit/components/search/tests/xpcshell/test_bug930456_child.js3
-rw-r--r--toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js31
-rw-r--r--toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js23
-rw-r--r--toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js25
-rw-r--r--toolkit/components/search/tests/xpcshell/test_defaultEngine.js51
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engineUpdate.js50
-rw-r--r--toolkit/components/search/tests/xpcshell/test_engine_set_alias.js80
-rw-r--r--toolkit/components/search/tests/xpcshell/test_geodefaults.js253
-rw-r--r--toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js135
-rw-r--r--toolkit/components/search/tests/xpcshell/test_hidden.js93
-rw-r--r--toolkit/components/search/tests/xpcshell/test_identifiers.js56
-rw-r--r--toolkit/components/search/tests/xpcshell/test_init_async_multiple.js55
-rw-r--r--toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js68
-rw-r--r--toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_json_cache.js227
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location.js66
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_error.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_funnelcake.js17
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_malformed_json.js57
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js24
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js30
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_partner.js16
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_sync.js101
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_timeout.js78
-rw-r--r--toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js85
-rw-r--r--toolkit/components/search/tests/xpcshell/test_migration_langpack.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/test_multipleIcons.js61
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nocache.js60
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nodb.js37
-rw-r--r--toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js57
-rw-r--r--toolkit/components/search/tests/xpcshell/test_notifications.js72
-rw-r--r--toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js148
-rw-r--r--toolkit/components/search/tests/xpcshell/test_pref.js36
-rw-r--r--toolkit/components/search/tests/xpcshell/test_purpose.js70
-rw-r--r--toolkit/components/search/tests/xpcshell/test_rel_searchform.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js74
-rw-r--r--toolkit/components/search/tests/xpcshell/test_resultDomain.js33
-rw-r--r--toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js67
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchReset.js137
-rw-r--r--toolkit/components/search/tests/xpcshell/test_searchSuggest.js572
-rw-r--r--toolkit/components/search/tests/xpcshell/test_selectedEngine.js165
-rw-r--r--toolkit/components/search/tests/xpcshell/test_svg_icon.js52
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync.js27
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_addon.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js52
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_distribution.js26
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_fallback.js42
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_migration.js29
-rw-r--r--toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js35
-rw-r--r--toolkit/components/search/tests/xpcshell/test_update_telemetry.js36
-rw-r--r--toolkit/components/search/tests/xpcshell/xpcshell.ini102
94 files changed, 5257 insertions, 0 deletions
diff --git a/toolkit/components/search/tests/xpcshell/.eslintrc.js b/toolkit/components/search/tests/xpcshell/.eslintrc.js
new file mode 100644
index 000000000..d35787cd2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/.eslintrc.js
@@ -0,0 +1,7 @@
+"use strict";
+
+module.exports = {
+ "extends": [
+ "../../../../../testing/xpcshell/xpcshell.eslintrc.js"
+ ]
+};
diff --git a/toolkit/components/search/tests/xpcshell/data/chrome.manifest b/toolkit/components/search/tests/xpcshell/data/chrome.manifest
new file mode 100644
index 000000000..ec412e050
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/chrome.manifest
@@ -0,0 +1,3 @@
+locale testsearchplugin ar jar:jar:searchTest.jar!/chrome/searchTest.jar!/
+content testsearchplugin ./
+
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-addon.xml b/toolkit/components/search/tests/xpcshell/data/engine-addon.xml
new file mode 100644
index 000000000..24e53d0c1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-addon.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>addon</ShortName>
+<Description>addon</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-app.xml b/toolkit/components/search/tests/xpcshell/data/engine-app.xml
new file mode 100644
index 000000000..fe1b3a67c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-app.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>TestEngineApp</ShortName>
+<Description>A test search engine installed in the application directory</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://localhost/" resultdomain="localhost">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml
new file mode 100644
index 000000000..856732c6d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-chromeicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-chromeicon</ShortName>
+<Image width="16" height="16">chrome://branding/content/icon16.png</Image>
+<Image width="32" height="32">chrome://branding/content/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-fr.xml b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
new file mode 100644
index 000000000..fad3e7574
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-fr.xml
@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine (fr)</ShortName>
+<Description>A test search engine (based on Google search for a different locale)</Description>
+<InputEncoding>ISO-8859-1</InputEncoding>
+<Url type="text/html" method="GET" template="http://www.google.fr/search" resultdomain="google.fr">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="iso-8859-1"/>
+ <Param name="oe" value="iso-8859-1"/>
+</Url>
+<SearchForm>http://www.google.fr/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-override.xml b/toolkit/components/search/tests/xpcshell/data/engine-override.xml
new file mode 100644
index 000000000..473be82fd
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-override.xml
@@ -0,0 +1,8 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>bug645970</ShortName>
+<Description>override</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-pref.xml b/toolkit/components/search/tests/xpcshell/data/engine-pref.xml
new file mode 100644
index 000000000..0555caf3e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-pref.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-pref</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="code" condition="pref" pref="code"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml
new file mode 100644
index 000000000..8b6eb7cab
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-post.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform-post.xml</ShortName>
+<Url type="text/html" method="POST" template="http://engine-rel-searchform-post.xml/POST" rel="searchform"/>
+<SearchForm>http://engine-rel-searchform-post.xml/?search</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml
new file mode 100644
index 000000000..18026210f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform-purpose.xml
@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform-purpose</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com" rel="searchform">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml
new file mode 100644
index 000000000..bcd164877
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-rel-searchform.xml
@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-rel-searchform.xml</ShortName>
+<Url type="text/html" method="GET" template="http://engine-rel-searchform.xml/?search" rel="searchform"/>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml
new file mode 100644
index 000000000..6fb2a778d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-resourceicon.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-resourceicon</ShortName>
+<Image width="16" height="16">resource://search-plugins/icon16.png</Image>
+<Image width="32" height="32">resource://search-plugins/icon32.png</Image>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml
new file mode 100644
index 000000000..57ecd32d7
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-system-purpose.xml
@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>engine-system-purpose</ShortName>
+<Url type="text/html" method="GET" template="http://www.google.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="sb"/>
+ <MozParam name="channel" condition="purpose" purpose="system" value="sys"/>
+</Url>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine-update.xml b/toolkit/components/search/tests/xpcshell/data/engine-update.xml
new file mode 100644
index 000000000..b8ef7224d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine-update.xml
@@ -0,0 +1,10 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>update</ShortName>
+<Description>update</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Url type="text/html" method="GET" template="http://searchtest.local">
+ <Param name="search" value="{searchTerms}"/>
+</Url>
+<UpdateUrl>http://searchtest.local/opensearch.xml</UpdateUrl>
+<IconUpdateUrl>http://searchtest.local/favicon.ico</IconUpdateUrl>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine.xml b/toolkit/components/search/tests/xpcshell/data/engine.xml
new file mode 100644
index 000000000..e7af1d9e9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine.xml
@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>Test search engine</ShortName>
+<Description>A test search engine (based on Google search)</Description>
+<InputEncoding>UTF-8</InputEncoding>
+<Image width="16" height="16">data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA</Image>
+<Url type="application/x-suggestions+json" method="GET" template="http://suggestqueries.google.com/complete/search?output=firefox&amp;client=firefox&amp;hl={moz:locale}&amp;q={searchTerms}"/>
+<Url type="text/html" method="GET" template="http://www.google.com/search" resultdomain="google.com">
+ <Param name="q" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <Param name="oe" value="utf-8"/>
+ <Param name="aq" value="t"/>
+ <!-- Dynamic parameters -->
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+</Url>
+<Url type="application/x-moz-default-purpose" method="GET" template="http://www.google.com/search" resultdomain="purpose.google.com">
+ <Param name="q" value="{searchTerms}"/>
+ <!-- MozParam uses searchbar if purpose is not specified -->
+ <MozParam name="channel" condition="purpose" purpose="searchbar" value="ffsb"/>
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="rcs"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="fflb"/>
+</Url>
+<SearchForm>http://www.google.com/</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engine2.xml b/toolkit/components/search/tests/xpcshell/data/engine2.xml
new file mode 100644
index 000000000..9957bfdf4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engine2.xml
@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/">
+ <ShortName>A second test engine</ShortName>
+ <Description>A second test search engine (based on DuckDuckGo)</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <LongName>A second test search engine (based on DuckDuckGo)</LongName>
+ <Image width="16" height="16">data:image/vnd.microsoft.icon;base64,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</Image>
+ <Url type="text/html" method="get" template="https://duckduckgo.com/?q={searchTerms}"/>
+</OpenSearchDescription>
diff --git a/toolkit/components/search/tests/xpcshell/data/engineImages.xml b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
new file mode 100644
index 000000000..65b550b31
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineImages.xml
@@ -0,0 +1,22 @@
+<!-- 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/. -->
+
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>IconsTest</ShortName>
+ <Description>IconsTest. Search by Test.</Description>
+ <InputEncoding>UTF-8</InputEncoding>
+ <Image width="16" height="16">data:image/x-icon;base64,ico16</Image>
+ <Image width="32" height="32">data:image/x-icon;base64,ico32</Image>
+ <Image width="74" height="74">data:image/png;base64,ico74</Image>
+ <Url type="application/x-suggestions+json" template="http://api.bing.com/osjson.aspx">
+ <Param name="query" value="{searchTerms}"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <Url type="text/html" method="GET" template="http://www.bing.com/search">
+ <Param name="q" value="{searchTerms}"/>
+ <MozParam name="pc" condition="pref" pref="ms-pc"/>
+ <Param name="form" value="MOZW"/>
+ </Url>
+ <SearchForm>http://www.bing.com/search</SearchForm>
+</SearchPlugin>
diff --git a/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
new file mode 100644
index 000000000..4c432e7ee
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/engineMaker.sjs
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+/**
+ * Dynamically create a search engine offering search suggestions via searchSuggestions.sjs.
+ *
+ * The engine is constructed by passing a JSON object with engine datails as the query string.
+ */
+
+function handleRequest(request, response) {
+ let engineData = JSON.parse(unescape(request.queryString).replace("+", " "));
+
+ if (!engineData.baseURL) {
+ response.setStatusLine(request.httpVersion, 500, "baseURL required");
+ return;
+ }
+
+ engineData.name = engineData.name || "Generated test engine";
+ engineData.description = engineData.description || "Generated test engine description";
+ engineData.method = engineData.method || "GET";
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+ createOpenSearchEngine(response, engineData);
+}
+
+/**
+ * Create an OpenSearch engine for the given base URL.
+ */
+function createOpenSearchEngine(response, engineData) {
+ let params = "", queryString = "";
+ if (engineData.method == "POST") {
+ params = "<Param name='q' value='{searchTerms}'/>";
+ } else {
+ queryString = "?q={searchTerms}";
+ }
+
+ let result = "<?xml version='1.0' encoding='utf-8'?>\
+<OpenSearchDescription xmlns='http://a9.com/-/spec/opensearch/1.1/'>\
+ <ShortName>" + engineData.name + "</ShortName>\
+ <Description>" + engineData.description + "</Description>\
+ <InputEncoding>UTF-8</InputEncoding>\
+ <LongName>" + engineData.name + "</LongName>\
+ <Url type='application/x-suggestions+json' method='" + engineData.method + "'\
+ template='" + engineData.baseURL + "searchSuggestions.sjs" + queryString + "'>\
+ " + params + "\
+ </Url>\
+ <Url type='text/html' method='" + engineData.method + "'\
+ template='" + engineData.baseURL + queryString + "'/>\
+</OpenSearchDescription>\
+";
+ response.write(result);
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico b/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico
new file mode 100644
index 000000000..442ab4dc8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/ico-size-16x16-png.ico
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/install.rdf b/toolkit/components/search/tests/xpcshell/data/install.rdf
new file mode 100644
index 000000000..df361ade4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/install.rdf
@@ -0,0 +1,23 @@
+<?xml version="1.0"?>
+
+<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
+ xmlns:em="http://www.mozilla.org/2004/em-rdf#">
+
+ <Description about="urn:mozilla:install-manifest">
+ <em:id>search-engine@tests.mozilla.org</em:id>
+ <em:unpack>true</em:unpack>
+ <em:version>1.0</em:version>
+
+ <em:targetApplication>
+ <Description>
+ <em:id>toolkit@mozilla.org</em:id>
+ <em:minVersion>0</em:minVersion>
+ <em:maxVersion>*</em:maxVersion>
+ </Description>
+ </em:targetApplication>
+
+ <!-- Front End MetaData -->
+ <em:name>Search Engine</em:name>
+
+ </Description>
+</RDF>
diff --git a/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml b/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml
new file mode 100644
index 000000000..e8efce672
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/invalid-engine.xml
@@ -0,0 +1 @@
+# An invalid xml engine file.
diff --git a/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json b/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json
new file mode 100644
index 000000000..e1ff95bc0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/langpack-metadata.json
@@ -0,0 +1,5 @@
+{
+ "[app]/bug645970.xml": {
+ "alias": "lp"
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/list.json b/toolkit/components/search/tests/xpcshell/data/list.json
new file mode 100644
index 000000000..68163bb88
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/list.json
@@ -0,0 +1,7 @@
+{
+ "default": {
+ "visibleDefaultEngines": [
+ "engine", "engine-pref", "engine-rel-searchform-purpose", "engine-system-purpose", "engine-chromeicon", "engine-resourceicon"
+ ]
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/metadata.json b/toolkit/components/search/tests/xpcshell/data/metadata.json
new file mode 100644
index 000000000..77b003d4e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/metadata.json
@@ -0,0 +1,30 @@
+{
+ "[global]": {
+ "searchdefaultexpir": 1471013469846
+ },
+ "[profile]\/engine.xml": {
+ "order": 1,
+ "alias": "foo"
+ },
+ "[app]\/google.xml": {
+ "order": 2
+ },
+ "[app]\/yahoo.xml": {
+ "order": 3
+ },
+ "[app]\/bing.xml": {
+ "order": 4
+ },
+ "[app]\/amazondotcom.xml": {
+ "order": 5
+ },
+ "[app]\/ddg.xml": {
+ "order": 6
+ },
+ "[app]\/twitter.xml": {
+ "order": 7
+ },
+ "[app]\/wikipedia.xml": {
+ "order": 8
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search.json b/toolkit/components/search/tests/xpcshell/data/search.json
new file mode 100644
index 000000000..f4f907778
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search.json
@@ -0,0 +1,86 @@
+{
+ "version": 1,
+ "buildID": "20121106",
+ "locale": "en-US",
+ "metaData": {},
+ "engines": [
+ {
+ "_name": "Test search engine",
+ "_shortName": "test-search-engine",
+ "description": "A test search engine (based on Google search)",
+ "extensionID": "test-addon-id@mozilla.org",
+ "__searchForm": "http://www.google.com/",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ "_metaData": {},
+ "_urls": [
+ {
+ "template": "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox&hl={moz:locale}&q={searchTerms}",
+ "rels": [
+ ],
+ "type": "application/x-suggestions+json",
+ "params": [
+ ]
+ },
+ {
+ "template": "http://www.google.com/search",
+ "resultDomain": "google.com",
+ "rels": [
+ ],
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "ie",
+ "value": "utf-8"
+ },
+ {
+ "name": "oe",
+ "value": "utf-8"
+ },
+ {
+ "name": "aq",
+ "value": "t"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ },
+ {
+ "template": "http://www.google.com/search",
+ "resultDomain": "purpose.google.com",
+ "rels": [
+ ],
+ "type": "application/x-moz-default-purpose",
+ "params": [
+ {
+ "name": "q",
+ "value": "{searchTerms}"
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword"
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu"
+ }
+ ]
+ }
+ ],
+ "queryCharset": "UTF-8",
+ "_readOnly": false
+ }
+ ]
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/search.sqlite b/toolkit/components/search/tests/xpcshell/data/search.sqlite
new file mode 100644
index 000000000..983bb831a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/search.sqlite
Binary files differ
diff --git a/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
new file mode 100644
index 000000000..abd94428e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/searchSuggestions.sjs
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { classes: Cc, interfaces: Ci, utils: Cu, results: Cr } = Components;
+
+Cu.import("resource://gre/modules/Timer.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+/**
+ * Provide search suggestions in the OpenSearch JSON format.
+ */
+
+function handleRequest(request, response) {
+ // Get the query parameters from the query string.
+ let query = parseQueryString(request.queryString);
+
+ function writeSuggestions(query, completions = []) {
+ let result = [query, completions];
+ response.write(JSON.stringify(result));
+ return result;
+ }
+
+ response.setStatusLine(request.httpVersion, 200, "OK");
+
+ let q = request.method == "GET" ? query.q : undefined;
+ if (q == "no remote" || q == "no results") {
+ writeSuggestions(q);
+ } else if (q == "Query Mismatch") {
+ writeSuggestions("This is an incorrect query string", ["some result"]);
+ } else if (q == "Query Case Mismatch") {
+ writeSuggestions(q.toUpperCase(), [q]);
+ } else if (q == "") {
+ writeSuggestions("", ["The server should never be sent an empty query"]);
+ } else if (q && q.startsWith("mo")) {
+ writeSuggestions(q, ["Mozilla", "modern", "mom"]);
+ } else if (q && q.startsWith("I ❤️")) {
+ writeSuggestions(q, ["I ❤️ Mozilla"]);
+ } else if (q && q.startsWith("letter ")) {
+ let letters = [];
+ for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
+ letters.push("letter " + String.fromCharCode(charCode));
+ }
+ writeSuggestions(q, letters);
+ } else if (q && q.startsWith("HTTP ")) {
+ response.setStatusLine(request.httpVersion, q.replace("HTTP ", ""), q);
+ writeSuggestions(q, [q]);
+ } else if (q && q.startsWith("delay")) {
+ // Delay the response by 200 milliseconds (less than the timeout but hopefully enough to abort
+ // before completion).
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), 200);
+ } else if (q && q.startsWith("slow ")) {
+ // Delay the response by 10 seconds so the client timeout is reached.
+ response.processAsync();
+ writeSuggestions(q, [q]);
+ setTimeout(() => response.finish(), 10000);
+ } else if (request.method == "POST") {
+ // This includes headers, not just the body
+ let requestText = NetUtil.readInputStreamToString(request.bodyInputStream,
+ request.bodyInputStream.available());
+ // Only use the last line which contains the encoded params
+ let requestLines = requestText.split("\n");
+ let postParams = parseQueryString(requestLines[requestLines.length - 1]);
+ writeSuggestions(postParams.q, ["Mozilla", "modern", "mom"]);
+ } else {
+ response.setStatusLine(request.httpVersion, 404, "Not Found");
+ }
+}
+
+function parseQueryString(queryString) {
+ let query = {};
+ queryString.split('&').forEach(function (val) {
+ let [name, value] = val.split('=');
+ query[name] = unescape(value).replace(/[+]/g, " ");
+ });
+ return query;
+}
diff --git a/toolkit/components/search/tests/xpcshell/data/searchTest.jar b/toolkit/components/search/tests/xpcshell/data/searchTest.jar
new file mode 100644
index 000000000..8bfbe6f21
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/data/searchTest.jar
Binary files differ
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);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_645970.js b/toolkit/components/search/tests/xpcshell/test_645970.js
new file mode 100644
index 000000000..3204e03d9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_645970.js
@@ -0,0 +1,22 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Test nsSearchService with nested jar: uris, without async initialization
+ */
+function run_test() {
+ updateAppInfo();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // The search service needs to be started after the jarURIs pref has been
+ // set in order to initiate it correctly
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+ Services.obs.notifyObservers(null, "quit-application", null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
new file mode 100644
index 000000000..4e50ed2a9
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_SearchStaticData.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the SearchStaticData module.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/SearchStaticData.jsm", this);
+
+function run_test() {
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.com")
+ .indexOf("www.google.fr") != -1);
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.fr")
+ .indexOf("www.google.com") != -1);
+ do_check_true(SearchStaticData.getAlternateDomains("www.google.com")
+ .every(d => d.startsWith("www.google.")));
+ do_check_true(SearchStaticData.getAlternateDomains("google.com").length == 0);
+
+ // Test that methods from SearchStaticData module can be overwritten,
+ // needed for hotfixing.
+ let backup = SearchStaticData.getAlternateDomains;
+ SearchStaticData.getAlternateDomains = () => ["www.bing.fr"];
+ do_check_matches(SearchStaticData.getAlternateDomains("www.bing.com"), ["www.bing.fr"]);
+ SearchStaticData.getAlternateDomains = backup;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js b/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
new file mode 100644
index 000000000..14411eaaa
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngineWithDetails.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+const kSearchEngineID = "addEngineWithDetails_test_engine";
+const kSearchEngineURL = "http://example.com/?search={searchTerms}";
+const kSearchTerm = "foo";
+
+add_task(function* test_addEngineWithDetails() {
+ do_check_false(Services.search.isInitialized);
+
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setBoolPref("reset.enabled", true);
+
+ yield asyncInit();
+
+ Services.search.addEngineWithDetails(kSearchEngineID, "", "", "", "get",
+ kSearchEngineURL);
+
+ // An engine added with addEngineWithDetails should have a load path, even
+ // though we can't point to a specific file.
+ let engine = Services.search.getEngineByName(kSearchEngineID);
+ do_check_eq(engine.wrappedJSObject._loadPath, "[other]addEngineWithDetails");
+
+ // Set the engine as default; this should set a loadPath verification hash,
+ // which should ensure we don't show the search reset prompt.
+ Services.search.currentEngine = engine;
+
+ let expectedURL = kSearchEngineURL.replace("{searchTerms}", kSearchTerm);
+ let submission =
+ Services.search.currentEngine.getSubmission(kSearchTerm, null, "searchbar");
+ do_check_eq(submission.uri.spec, expectedURL);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js b/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
new file mode 100644
index 000000000..07eaf38bb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_addEngine_callback.js
@@ -0,0 +1,95 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests covering nsIBrowserSearchService::addEngine's optional callback.
+ */
+
+Components.utils.import("resource://testing-common/MockRegistrar.jsm");
+
+"use strict";
+
+// Only need to stub the methods actually called by nsSearchService
+var promptService = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPromptService]),
+ confirmEx: function() {}
+};
+var prompt = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrompt]),
+ alert: function() {}
+};
+// 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
+MockRegistrar.register("@mozilla.org/embedcomp/prompt-service;1", promptService);
+MockRegistrar.register("@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);
+ do_check_eq(engine.wrappedJSObject._loadPath,
+ "[http]localhost/test-search-engine.xml");
+ run_next_test();
+ },
+ onError: function (errorCode) {
+ do_throw("search callback returned error: " + errorCode);
+ }
+ }
+ Services.search.addEngine(gDataUrl + "engine.xml", null,
+ 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(gDataUrl + "engine.xml", null,
+ 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", null,
+ null, false, searchCallback);
+});
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async.js b/toolkit/components/search/tests/xpcshell/test_async.js
new file mode 100644
index 000000000..58b530464
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async.js
@@ -0,0 +1,34 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // Check the hidden engine is not loaded.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_addon.js b/toolkit/components/search/tests/xpcshell/test_async_addon.js
new file mode 100644
index 000000000..af488f301
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the add-on engine is loaded in addition to our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 2);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("addon");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "addon");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js b/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
new file mode 100644
index 000000000..5c48c108a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_addon_no_override.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine("engine-override");
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the add-on engine isn't overriding our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_distribution.js b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
new file mode 100644
index 000000000..4f3af0419
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_distribution.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installDistributionEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test that the engine from the distribution overrides our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // check the engine we have is actually the one from the distribution
+ do_check_eq(engine.description, "override");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_async_migration.js b/toolkit/components/search/tests/xpcshell/test_async_migration.js
new file mode 100644
index 000000000..4d0335c45
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_migration.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that legacy metadata from search-metadata.json is correctly
+ * transferred to the new metadata storage. */
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ do_get_file("data/metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ run_next_test();
+}
+
+add_task(function* test_async_metadata_migration() {
+ yield asyncInit();
+ yield promiseAfterCache();
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["engine"].order, 1);
+ do_check_eq(metadata["engine"].alias, "foo");
+
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata["searchDefaultExpir"], 1471013469846);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
new file mode 100644
index 000000000..cbcdbdcb0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_async_profile_engine.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Copy an engine in [profile]/searchplugin/ and ensure it's not
+ // overriding the same file from a jar.
+ // The description in the file we are copying is 'profile'.
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_bug930456.js b/toolkit/components/search/tests/xpcshell/test_bug930456.js
new file mode 100644
index 000000000..1dbb06c59
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_bug930456.js
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test()
+{
+ if (isChild) {
+ do_check_false("@mozilla.org/browser/search-service;1" in Cc);
+ } else {
+ do_check_true("@mozilla.org/browser/search-service;1" in Cc);
+ }
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_bug930456_child.js b/toolkit/components/search/tests/xpcshell/test_bug930456_child.js
new file mode 100644
index 000000000..8540a37f4
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_bug930456_child.js
@@ -0,0 +1,3 @@
+function run_test() {
+ run_test_in_child("test_bug930456.js");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js
new file mode 100644
index 000000000..7d3b1698a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon1.js
@@ -0,0 +1,31 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that resource URLs can be used in default engines */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines or the resource URL won't be used
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_defaultresourceicon() {
+ yield asyncInit();
+
+ let engine1 = Services.search.getEngineByName("engine-resourceicon");
+ do_check_eq(engine1.iconURI.spec, "resource://search-plugins/icon16.png");
+ do_check_eq(engine1.getIconURLBySize(32, 32), "resource://search-plugins/icon32.png");
+ let engine2 = Services.search.getEngineByName("engine-chromeicon");
+ do_check_eq(engine2.iconURI.spec, "chrome://branding/content/icon16.png");
+ do_check_eq(engine2.getIconURLBySize(32, 32), "chrome://branding/content/icon32.png");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js
new file mode 100644
index 000000000..52aff1168
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_chromeresource_icon2.js
@@ -0,0 +1,23 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that an installed engine can't use a resource URL for an icon */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_installedresourceicon() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "engine-resourceicon", xmlFileName: "engine-resourceicon.xml" },
+ { name: "engine-chromeicon", xmlFileName: "engine-chromeicon.xml" },
+ ]);
+ do_check_null(engine1.iconURI);
+ do_check_null(engine2.iconURI);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js b/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js
new file mode 100644
index 000000000..d4c699d97
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_currentEngine_fallback.js
@@ -0,0 +1,25 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_check_true(Services.search.getVisibleEngines().length > 1);
+ do_check_true(Services.search.isInitialized);
+
+ // Remove the current engine...
+ let currentEngine = Services.search.currentEngine;
+ Services.search.removeEngine(currentEngine);
+
+ // ... and verify a new current engine has been set.
+ do_check_neq(Services.search.currentEngine.name, currentEngine.name);
+ do_check_true(currentEngine.hidden);
+
+ // Remove all the other engines.
+ Services.search.getVisibleEngines().forEach(Services.search.removeEngine);
+ do_check_eq(Services.search.getVisibleEngines().length, 0);
+
+ // Verify the original default engine is used as a fallback and no
+ // longer hidden.
+ do_check_eq(Services.search.currentEngine.name, currentEngine.name);
+ do_check_false(currentEngine.hidden);
+ do_check_eq(Services.search.getVisibleEngines().length, 1);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_defaultEngine.js b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
new file mode 100644
index 000000000..13d9922de
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_defaultEngine.js
@@ -0,0 +1,51 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that currentEngine and defaultEngine properties can be set and yield the
+ * proper events and behavior (search results)
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_defaultEngine() {
+ let search = Services.search;
+
+ let originalDefault = search.defaultEngine;
+
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ ]);
+
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine1);
+ search.defaultEngine = engine2
+ do_check_eq(search.defaultEngine, engine2);
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine1);
+
+ // Test that hiding the currently-default engine affects the defaultEngine getter
+ // We fallback first to the original default...
+ engine1.hidden = true;
+ do_check_eq(search.defaultEngine, originalDefault);
+
+ // ... and then to the first visible engine in the list, so move our second
+ // engine to that position.
+ search.moveEngine(engine2, 0);
+ originalDefault.hidden = true;
+ do_check_eq(search.defaultEngine, engine2);
+
+ // Test that setting defaultEngine to an already-hidden engine works, but
+ // doesn't change the return value of the getter
+ search.defaultEngine = engine1;
+ do_check_eq(search.defaultEngine, engine2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engineUpdate.js b/toolkit/components/search/tests/xpcshell/test_engineUpdate.js
new file mode 100644
index 000000000..adff41ffb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engineUpdate.js
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that user-set metadata isn't lost on engine update */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_engineUpdate() {
+ const KEYWORD = "keyword";
+ const FILENAME = "engine.xml"
+ const TOPIC = "browser-search-engine-modified";
+ const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
+
+ yield asyncInit();
+
+ let [engine] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: FILENAME },
+ ]);
+
+ engine.alias = KEYWORD;
+ Services.search.moveEngine(engine, 0);
+ // can't have an accurate updateURL in the file since we can't know the test
+ // server origin, so manually set it
+ engine.wrappedJSObject._updateURL = gDataUrl + FILENAME;
+
+ yield new Promise(resolve => {
+ Services.obs.addObserver(function obs(subject, topic, data) {
+ if (data == "engine-loaded") {
+ let loadedEngine = subject.QueryInterface(Ci.nsISearchEngine);
+ let rawEngine = loadedEngine.wrappedJSObject;
+ equal(loadedEngine.alias, KEYWORD, "Keyword not cleared by update");
+ equal(rawEngine.getAttr("order"), 1, "Order not cleared by update");
+ Services.obs.removeObserver(obs, TOPIC, false);
+ resolve();
+ }
+ }, TOPIC, false);
+
+ // set last update to 8 days ago, since the default interval is 7, then
+ // trigger an update
+ engine.wrappedJSObject.setAttr("updateexpir", Date.now() - (ONE_DAY_IN_MS * 8));
+ Services.search.QueryInterface(Components.interfaces.nsITimerCallback).notify(null);
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
new file mode 100644
index 000000000..b3c51caa5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_engine_set_alias.js
@@ -0,0 +1,80 @@
+"use strict";
+
+function run_test() {
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_engine_set_alias() {
+ yield asyncInit();
+ do_print("Set engine alias");
+ let [engine1] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", "b", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine1.alias, "b");
+ engine1.alias = "a";
+ Assert.equal(engine1.alias, "a");
+ Services.search.removeEngine(engine1);
+});
+
+add_task(function* test_engine_set_alias_with_left_space() {
+ do_print("Set engine alias with left space");
+ let [engine2] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " a", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine2.alias, "a");
+ engine2.alias = " c";
+ Assert.equal(engine2.alias, "c");
+ Services.search.removeEngine(engine2);
+});
+
+add_task(function* test_engine_set_alias_with_right_space() {
+ do_print("Set engine alias with right space");
+ let [engine3] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", "c ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine3.alias, "c");
+ engine3.alias = "o ";
+ Assert.equal(engine3.alias, "o");
+ Services.search.removeEngine(engine3);
+});
+
+add_task(function* test_engine_set_alias_with_right_left_space() {
+ do_print("Set engine alias with left and right space");
+ let [engine4] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " o ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine4.alias, "o");
+ engine4.alias = " n ";
+ Assert.equal(engine4.alias, "n");
+ Services.search.removeEngine(engine4);
+});
+
+add_task(function* test_engine_set_alias_with_space() {
+ do_print("Set engine alias with space");
+ let [engine5] = yield addTestEngines([
+ {
+ name: "bacon",
+ details: ["", " ", "Search Bacon", "GET", "http://www.bacon.test/find"]
+ }
+ ]);
+ Assert.equal(engine5.alias, null);
+ engine5.alias = "b";
+ Assert.equal(engine5.alias, "b");
+ engine5.alias = " ";
+ Assert.equal(engine5.alias, null);
+ Services.search.removeEngine(engine5);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_geodefaults.js b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
new file mode 100644
index 000000000..2367bbbc2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_geodefaults.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var requests = [];
+var gServerCohort = "";
+
+const kUrlPref = "geoSpecificDefaults.url";
+
+const kDayInSeconds = 86400;
+const kYearInSeconds = kDayInSeconds * 365;
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ let srv = new HttpServer();
+
+ srv.registerPathHandler("/lookup_defaults", (metadata, response) => {
+ response.setStatusLine("1.1", 200, "OK");
+ let data = {interval: kYearInSeconds,
+ settings: {searchDefault: "Test search engine"}};
+ if (gServerCohort)
+ data.cohort = gServerCohort;
+ response.write(JSON.stringify(data));
+ requests.push(metadata);
+ });
+
+ srv.registerPathHandler("/lookup_fail", (metadata, response) => {
+ response.setStatusLine("1.1", 404, "Not Found");
+ requests.push(metadata);
+ });
+
+ srv.registerPathHandler("/lookup_unavailable", (metadata, response) => {
+ response.setStatusLine("1.1", 503, "Service Unavailable");
+ response.setHeader("Retry-After", kDayInSeconds.toString());
+ requests.push(metadata);
+ });
+
+ srv.start(-1);
+ do_register_cleanup(() => srv.stop(() => {}));
+
+ let url = "http://localhost:" + srv.identity.primaryPort + "/lookup_defaults?";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+ // Set a bogus user value so that running the test ensures we ignore it.
+ Services.prefs.setCharPref(BROWSER_SEARCH_PREF + kUrlPref, "about:blank");
+ Services.prefs.setCharPref("browser.search.geoip.url",
+ 'data:application/json,{"country_code": "FR"}');
+
+ run_next_test();
+}
+
+function checkNoRequest() {
+ do_check_eq(requests.length, 0);
+}
+
+function checkRequest(cohort = "") {
+ do_check_eq(requests.length, 1);
+ let req = requests.pop();
+ do_check_eq(req._method, "GET");
+ do_check_eq(req._queryString, cohort ? "/" + cohort : "");
+}
+
+add_task(function* no_request_if_prefed_off() {
+ // Disable geoSpecificDefaults and check no HTTP request is made.
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", false);
+ yield asyncInit();
+ checkNoRequest();
+ yield promiseAfterCache();
+
+ // The default engine should be set based on the prefs.
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+
+ // Ensure nothing related to geoSpecificDefaults has been written in the metadata.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "undefined");
+ do_check_eq(typeof metadata.searchDefault, "undefined");
+ do_check_eq(typeof metadata.searchDefaultHash, "undefined");
+
+ Services.prefs.setBoolPref("browser.search.geoSpecificDefaults", true);
+});
+
+add_task(function* should_get_geo_defaults_only_once() {
+ // (Re)initializing the search service should trigger a request,
+ // and set the default engine based on it.
+ // Due to the previous initialization, we expect the countryCode to already be set.
+ do_check_true(Services.prefs.prefHasUserValue("browser.search.countryCode"));
+ do_check_eq(Services.prefs.getCharPref("browser.search.countryCode"), "FR");
+ yield asyncReInit();
+ checkRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Verify the metadata was written correctly.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir > Date.now());
+ do_check_eq(typeof metadata.searchDefault, "string");
+ do_check_eq(metadata.searchDefault, "Test search engine");
+ do_check_eq(typeof metadata.searchDefaultHash, "string");
+ do_check_eq(metadata.searchDefaultHash.length, 44);
+
+ // The next restart shouldn't trigger a request.
+ yield asyncReInit();
+ checkNoRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+});
+
+add_task(function* should_request_when_countryCode_not_set() {
+ Services.prefs.clearUserPref("browser.search.countryCode");
+ yield asyncReInit();
+ checkRequest();
+ yield promiseAfterCache();
+});
+
+add_task(function* should_recheck_if_interval_expired() {
+ yield forceExpiration();
+
+ let date = Date.now();
+ yield asyncReInit();
+ checkRequest();
+ yield promiseAfterCache();
+
+ // Check that the expiration timestamp has been updated.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir >= date + kYearInSeconds * 1000);
+ do_check_true(metadata.searchDefaultExpir < date + (kYearInSeconds + 3600) * 1000);
+});
+
+add_task(function* should_recheck_when_broken_hash() {
+ // This test verifies both that we ignore saved geo-defaults if the
+ // hash is invalid, and that we keep the local preferences-based
+ // default for all of the session in case a synchronous
+ // initialization was triggered before our HTTP request completed.
+
+ let metadata = yield promiseGlobalMetadata();
+
+ // Break the hash.
+ let hash = metadata.searchDefaultHash;
+ metadata.searchDefaultHash = "broken";
+ yield promiseSaveGlobalMetadata(metadata);
+
+ let commitPromise = promiseAfterCache();
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Synchronously check the current default engine, to force a sync init.
+ // The hash is wrong, so we should fallback to the default engine from prefs.
+ do_check_false(Services.search.isInitialized)
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+ do_check_true(Services.search.isInitialized)
+
+ yield reInitPromise;
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the hash is back to its previous value.
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultHash, "string");
+ if (metadata.searchDefaultHash == "broken") {
+ // If the server takes more than 1000ms to return the result,
+ // the commitPromise was resolved by a first save of the cache
+ // that saved the engines, but not the request's results.
+ do_print("waiting for the cache to be saved a second time");
+ yield promiseAfterCache();
+ metadata = yield promiseGlobalMetadata();
+ }
+ do_check_eq(metadata.searchDefaultHash, hash);
+
+ // The current default engine shouldn't change during a session.
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName(false));
+
+ // After another restart, the current engine should be back to the geo default,
+ // without doing yet another request.
+ yield asyncReInit();
+ checkNoRequest();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+});
+
+add_task(function* should_remember_cohort_id() {
+ // Check that initially the cohort pref doesn't exist.
+ const cohortPref = "browser.search.cohort";
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
+
+ // Make the server send a cohort id.
+ let cohort = gServerCohort = "xpcshell";
+
+ // Trigger a new request.
+ yield forceExpiration();
+ let commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the cohort was saved.
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_STRING);
+ do_check_eq(Services.prefs.getCharPref(cohortPref), cohort);
+
+ // Make the server stop sending the cohort.
+ gServerCohort = "";
+
+ // Check that the next request sends the previous cohort id, and
+ // will remove it from the prefs due to the server no longer sending it.
+ yield forceExpiration();
+ commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest(cohort);
+ yield commitPromise;
+ do_check_eq(Services.prefs.getPrefType(cohortPref), Services.prefs.PREF_INVALID);
+});
+
+add_task(function* should_retry_after_failure() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let originalUrl = defaultBranch.getCharPref(kUrlPref);
+ defaultBranch.setCharPref(kUrlPref, originalUrl.replace("defaults", "fail"));
+
+ // Trigger a new request.
+ yield forceExpiration();
+ yield asyncReInit();
+ checkRequest();
+
+ // After another restart, a new request should be triggered automatically without
+ // the test having to call forceExpiration again.
+ yield asyncReInit();
+ checkRequest();
+});
+
+add_task(function* should_honor_retry_after_header() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let originalUrl = defaultBranch.getCharPref(kUrlPref);
+ defaultBranch.setCharPref(kUrlPref, originalUrl.replace("fail", "unavailable"));
+
+ // Trigger a new request.
+ yield forceExpiration();
+ let date = Date.now();
+ let commitPromise = promiseAfterCache();
+ yield asyncReInit();
+ checkRequest();
+ yield commitPromise;
+
+ // Check that the expiration timestamp has been updated.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(typeof metadata.searchDefaultExpir, "number");
+ do_check_true(metadata.searchDefaultExpir >= date + kDayInSeconds * 1000);
+ do_check_true(metadata.searchDefaultExpir < date + (kDayInSeconds + 3600) * 1000);
+
+ // After another restart, a new request should not be triggered.
+ yield asyncReInit();
+ checkNoRequest();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js b/toolkit/components/search/tests/xpcshell/test_hasEngineWithURL.js
new file mode 100644
index 000000000..e48b1673c
--- /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.");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_hidden.js b/toolkit/components/search/tests/xpcshell/test_hidden.js
new file mode 100644
index 000000000..b784f3624
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_hidden.js
@@ -0,0 +1,93 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const kUrlPref = "geoSpecificDefaults.url";
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Geo specific defaults won't be fetched if there's no country code.
+ Services.prefs.setCharPref("browser.search.geoip.url",
+ 'data:application/json,{"country_code": "US"}');
+
+ // Make 'hidden' the only visible engine.
+ let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\"]}}";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* async_init() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine has been hidden.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_eq(engine, null);
+
+ // The hidden engine is visible.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_neq(engine, null);
+
+ // The next test does a sync init, which won't do the geoSpecificDefaults XHR,
+ // so it depends on the metadata having been written to disk.
+ yield commitPromise;
+});
+
+add_task(function* sync_init() {
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+ do_check_false(Services.search.isInitialized);
+
+ // Synchronously check the current default engine, to force a sync init.
+ do_check_eq(Services.search.currentEngine.name, "hidden");
+ do_check_true(Services.search.isInitialized);
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine has been hidden.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_eq(engine, null);
+
+ // The hidden engine is visible.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_neq(engine, null);
+
+ yield reInitPromise;
+});
+
+add_task(function* invalid_engine() {
+ // Trigger a new request.
+ yield forceExpiration();
+
+ // Set the visibleDefaultEngines list to something that contains a non-existent engine.
+ // This should cause the search service to ignore the list altogether and fallback to
+ // local defaults.
+ let url = "data:application/json,{\"interval\": 31536000, \"settings\": {\"searchDefault\": \"hidden\", \"visibleDefaultEngines\": [\"hidden\", \"bogus\"]}}";
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF).setCharPref(kUrlPref, url);
+
+ yield asyncReInit();
+
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // The default test jar engine is visible.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // The hidden engine is... hidden.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_identifiers.js b/toolkit/components/search/tests/xpcshell/test_identifiers.js
new file mode 100644
index 000000000..0d5ca5b90
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_identifiers.js
@@ -0,0 +1,56 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that a search engine's identifier can be extracted from the filename.
+ */
+
+"use strict";
+
+const SEARCH_APP_DIR = 1;
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ updateAppInfo();
+
+ run_next_test();
+}
+
+add_test(function test_identifier() {
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the test engine to the test profile.
+ let engineTemplateFile = do_get_file("data/engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ Services.search.init(function initComplete(aResult) {
+ do_print("init'd search service");
+ do_check_true(Components.isSuccessCode(aResult));
+
+ let profileEngine = Services.search.getEngineByName("Test search engine");
+ let jarEngine = Services.search.getEngineByName("bug645970");
+
+ do_check_true(profileEngine instanceof Ci.nsISearchEngine);
+ do_check_true(jarEngine instanceof Ci.nsISearchEngine);
+
+ // An engine loaded from the profile directory won't have an identifier,
+ // because it's not built-in.
+ do_check_eq(profileEngine.identifier, null);
+
+ // An engine loaded from a JAR will have an identifier corresponding to
+ // the filename inside the JAR. (In this case it's the same as the name.)
+ do_check_eq(jarEngine.identifier, "bug645970");
+
+ removeMetadata();
+ removeCacheFile();
+ run_next_test();
+ });
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js b/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js
new file mode 100644
index 000000000..3aa3353ce
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_init_async_multiple.js
@@ -0,0 +1,55 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Test nsSearchService with with the following initialization scenario:
+ * - launch asynchronous initialization several times;
+ * - all asynchronous initializations must complete.
+ *
+ * Test case comes from test_645970.js
+ */
+function run_test() {
+ do_print("Setting up test");
+
+ do_test_pending();
+ updateAppInfo();
+
+ do_print("Test starting");
+ let numberOfInitializers = 4;
+ let pending = [];
+ let numberPending = numberOfInitializers;
+
+ // Start asynchronous initializations
+ for (let i = 0; i < numberOfInitializers; ++i) {
+ let me = i;
+ pending[me] = true;
+ Services.search.init(function search_initialized_0(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ init_complete(me);
+ });
+ }
+
+ // Wait until all initializers have completed
+ let init_complete = function init_complete(i) {
+ do_check_true(pending[i]);
+ pending[i] = false;
+ numberPending--;
+ do_check_true(numberPending >= 0);
+ do_check_true(Services.search.isInitialized);
+ if (numberPending == 0) {
+ // Just check that we can access a list of engines.
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+
+ // Wait a little before quitting: if some initializer is
+ // triggered twice, we want to catch that error.
+ do_timeout(1000, function() {
+ do_test_finished();
+ });
+ }
+ };
+}
+
diff --git a/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js b/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js
new file mode 100644
index 000000000..ed4ecdcd8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_init_async_multiple_then_sync.js
@@ -0,0 +1,68 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* 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/. */
+
+/**
+ * Test nsSearchService with with the following initialization scenario:
+ * - launch asynchronous initialization several times;
+ * - force fallback to synchronous initialization.
+ * - all asynchronous initializations must complete;
+ * - no asynchronous initialization must complete more than once.
+ *
+ * Test case comes from test_645970.js
+ */
+function run_test() {
+ do_print("Setting up test");
+ do_test_pending();
+ updateAppInfo();
+
+ do_print("Test starting");
+
+ let numberOfInitializers = 4;
+ let pending = [];
+ let numberPending = numberOfInitializers;
+
+ // Start asynchronous initializations
+ for (let i = 0; i < numberOfInitializers; ++i) {
+ let me = i;
+ pending[me] = true;
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ init_complete(me);
+ });
+ }
+
+ // Ensure that all asynchronous initializers eventually complete
+ let init_complete = function init_complete(i) {
+ do_print("init complete " + i);
+ do_check_true(pending[i]);
+ pending[i] = false;
+ numberPending--;
+ do_check_true(numberPending >= 0);
+ do_check_true(Services.search.isInitialized);
+ if (numberPending != 0) {
+ do_print("Still waiting for the following initializations: " + JSON.stringify(pending));
+ return;
+ }
+ do_print("All initializations have completed");
+ // Just check that we can access a list of engines.
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+
+ do_print("Waiting a second before quitting");
+ // Wait a little before quitting: if some initializer is
+ // triggered twice, we want to catch that error.
+ do_timeout(1000, function() {
+ do_print("Test is complete");
+ do_test_finished();
+ });
+ };
+
+ // ... but don't wait for asynchronous initializations to complete
+ let engines = Services.search.getEngines();
+ do_check_neq(engines, null);
+ do_print("Synchronous part of the test complete");
+}
+
diff --git a/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js b/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js
new file mode 100644
index 000000000..c6455735a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_invalid_engine_from_dir.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that invalid engine files with xml extensions will not break
+ * initialization. See Bug 940446.
+ */
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_check_false(Services.search.isInitialized);
+
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the invalid engine to the test profile.
+ let engineTemplateFile = do_get_file("data/invalid-engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ Services.search.init(function search_initialized(aStatus) {
+ // The invalid engine should have been skipped and should not
+ // have caused an exception.
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ removeMetadata();
+ removeCacheFile();
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_json_cache.js b/toolkit/components/search/tests/xpcshell/test_json_cache.js
new file mode 100644
index 000000000..c804b0bca
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_json_cache.js
@@ -0,0 +1,227 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test initializing from the search cache.
+ */
+
+"use strict";
+
+/**
+ * Gets a directory from the directory service.
+ * @param aKey
+ * The directory service key indicating the directory to get.
+ */
+var _dirSvc = null;
+function getDir(aKey, aIFace) {
+ if (!aKey) {
+ FAIL("getDir requires a directory key!");
+ }
+
+ if (!_dirSvc) {
+ _dirSvc = Cc["@mozilla.org/file/directory_service;1"].
+ getService(Ci.nsIProperties);
+ }
+ return _dirSvc.get(aKey, aIFace || Ci.nsIFile);
+}
+
+function makeURI(uri) {
+ return Services.io.newURI(uri, null, null);
+}
+
+var cacheTemplate, appPluginsPath, profPlugins;
+
+/**
+ * Test reading from search.json.mozlz4
+ */
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ updateAppInfo();
+
+ let cacheTemplateFile = do_get_file("data/search.json");
+ cacheTemplate = readJSONFile(cacheTemplateFile);
+ cacheTemplate.buildID = getAppInfo().platformBuildID;
+
+ let engineFile = gProfD.clone();
+ engineFile.append("searchplugins");
+ engineFile.append("test-search-engine.xml");
+ engineFile.parent.create(Ci.nsIFile.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+
+ // Copy the test engine to the test profile.
+ let engineTemplateFile = do_get_file("data/engine.xml");
+ engineTemplateFile.copyTo(engineFile.parent, "test-search-engine.xml");
+
+ // The list of visibleDefaultEngines needs to match or the cache will be ignored.
+ let chan = NetUtil.newChannel({
+ uri: "resource://search-plugins/list.json",
+ loadUsingSystemPrincipal: true
+ });
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].
+ createInstance(Ci.nsIScriptableInputStream);
+ sis.init(chan.open2());
+ let list = sis.read(sis.available());
+ let searchSettings = JSON.parse(list);
+
+ cacheTemplate.visibleDefaultEngines = searchSettings["default"]["visibleDefaultEngines"];
+
+ run_next_test();
+}
+
+add_test(function prepare_test_data() {
+ OS.File.writeAtomic(OS.Path.join(OS.Constants.Path.profileDir, CACHE_FILENAME),
+ new TextEncoder().encode(JSON.stringify(cacheTemplate)),
+ {compression: "lz4"})
+ .then(run_next_test);
+});
+
+/**
+ * Start the search service and confirm the engine properties match the expected values.
+ */
+add_test(function test_cached_engine_properties() {
+ do_print("init search service");
+
+ Services.search.init(function initComplete(aResult) {
+ do_print("init'd search service");
+ do_check_true(Components.isSuccessCode(aResult));
+
+ let engines = Services.search.getEngines({});
+ let engine = engines[0];
+
+ do_check_true(engine instanceof Ci.nsISearchEngine);
+ isSubObjectOf(EXPECTED_ENGINE.engine, engine);
+
+ let engineFromSS = Services.search.getEngineByName(EXPECTED_ENGINE.engine.name);
+ do_check_true(!!engineFromSS);
+ isSubObjectOf(EXPECTED_ENGINE.engine, engineFromSS);
+
+ removeMetadata();
+ removeCacheFile();
+ run_next_test();
+ });
+});
+
+/**
+ * Test that the JSON cache written in the profile is correct.
+ */
+add_test(function test_cache_write() {
+ do_print("test cache writing");
+
+ let cache = gProfD.clone();
+ cache.append(CACHE_FILENAME);
+ do_check_false(cache.exists());
+
+ do_print("Next step is forcing flush");
+ do_timeout(0, function forceFlush() {
+ do_print("Forcing flush");
+ // Force flush
+ // Note: the timeout is needed, to avoid some reentrency
+ // issues in nsSearchService.
+
+ let cacheWriteObserver = {
+ observe: function cacheWriteObserver_observe(aEngine, aTopic, aVerb) {
+ if (aTopic != "browser-search-service" || aVerb != "write-cache-to-disk-complete") {
+ return;
+ }
+ Services.obs.removeObserver(cacheWriteObserver, "browser-search-service");
+ do_print("Cache write complete");
+ do_check_true(cache.exists());
+ // Check that the search.json.mozlz4 cache matches the template
+
+ promiseCacheData().then(cacheWritten => {
+ do_print("Check search.json.mozlz4");
+ isSubObjectOf(cacheTemplate, cacheWritten);
+
+ run_next_test();
+ });
+ }
+ };
+ Services.obs.addObserver(cacheWriteObserver, "browser-search-service", false);
+
+ Services.search.QueryInterface(Ci.nsIObserver).observe(null, "browser-search-engine-modified", "engine-removed");
+ });
+});
+
+var EXPECTED_ENGINE = {
+ engine: {
+ name: "Test search engine",
+ alias: null,
+ description: "A test search engine (based on Google search)",
+ searchForm: "http://www.google.com/",
+ wrappedJSObject: {
+ _extensionID: "test-addon-id@mozilla.org",
+ "_iconURL": "data:image/png;base64,AAABAAEAEBAAAAEAGABoAwAAFgAAACgAAAAQAAAAIAAAAAEAGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAADs9Pt8xetPtu9FsfFNtu%2BTzvb2%2B%2Fne4dFJeBw0egA%2FfAJAfAA8ewBBegAAAAD%2B%2FPtft98Mp%2BwWsfAVsvEbs%2FQeqvF8xO7%2F%2F%2F63yqkxdgM7gwE%2FggM%2BfQA%2BegBDeQDe7PIbotgQufcMufEPtfIPsvAbs%2FQvq%2Bfz%2Bf%2F%2B%2B%2FZKhR05hgBBhQI8hgBAgAI9ewD0%2B%2Fg3pswAtO8Cxf4Kw%2FsJvvYAqupKsNv%2B%2Fv7%2F%2FP5VkSU0iQA7jQA9hgBDgQU%2BfQH%2F%2Ff%2FQ6fM4sM4KsN8AteMCruIqqdbZ7PH8%2Fv%2Fg6Nc%2Fhg05kAA8jAM9iQI%2BhQA%2BgQDQu6b97uv%2F%2F%2F7V8Pqw3eiWz97q8%2Ff%2F%2F%2F%2F7%2FPptpkkqjQE4kwA7kAA5iwI8iAA8hQCOSSKdXjiyflbAkG7u2s%2F%2B%2F%2F39%2F%2F7r8utrqEYtjQE8lgA7kwA7kwA9jwA9igA9hACiWSekVRyeSgiYSBHx6N%2F%2B%2Fv7k7OFRmiYtlAA5lwI7lwI4lAA7kgI9jwE9iwI4iQCoVhWcTxCmb0K%2BooT8%2Fv%2F7%2F%2F%2FJ2r8fdwI1mwA3mQA3mgA8lAE8lAE4jwA9iwE%2BhwGfXifWvqz%2B%2Ff%2F58u%2Fev6Dt4tr%2B%2F%2F2ZuIUsggA7mgM6mAM3lgA5lgA6kQE%2FkwBChwHt4dv%2F%2F%2F728ei1bCi7VAC5XQ7kz7n%2F%2F%2F6bsZkgcB03lQA9lgM7kwA2iQktZToPK4r9%2F%2F%2F9%2F%2F%2FSqYK5UwDKZAS9WALIkFn%2B%2F%2F3%2F%2BP8oKccGGcIRJrERILYFEMwAAuEAAdX%2F%2Ff7%2F%2FP%2B%2BfDvGXQLIZgLEWgLOjlf7%2F%2F%2F%2F%2F%2F9QU90EAPQAAf8DAP0AAfMAAOUDAtr%2F%2F%2F%2F7%2B%2Fu2bCTIYwDPZgDBWQDSr4P%2F%2Fv%2F%2F%2FP5GRuABAPkAA%2FwBAfkDAPAAAesAAN%2F%2F%2B%2Fz%2F%2F%2F64g1C5VwDMYwK8Yg7y5tz8%2Fv%2FV1PYKDOcAAP0DAf4AAf0AAfYEAOwAAuAAAAD%2F%2FPvi28ymXyChTATRrIb8%2F%2F3v8fk6P8MAAdUCAvoAAP0CAP0AAfYAAO4AAACAAQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAACAAQAA",
+ _urls : [
+ {
+ type: "application/x-suggestions+json",
+ method: "GET",
+ template: "http://suggestqueries.google.com/complete/search?output=firefox&client=firefox" +
+ "&hl={moz:locale}&q={searchTerms}",
+ params: "",
+ },
+ {
+ type: "text/html",
+ method: "GET",
+ template: "http://www.google.com/search",
+ resultDomain: "google.com",
+ params: [
+ {
+ "name": "q",
+ "value": "{searchTerms}",
+ "purpose": undefined,
+ },
+ {
+ "name": "ie",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "oe",
+ "value": "utf-8",
+ "purpose": undefined,
+ },
+ {
+ "name": "aq",
+ "value": "t",
+ "purpose": undefined,
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword",
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu",
+ },
+ ],
+ },
+ {
+ type: "application/x-moz-default-purpose",
+ method: "GET",
+ template: "http://www.google.com/search",
+ resultDomain: "purpose.google.com",
+ params: [
+ {
+ "name": "q",
+ "value": "{searchTerms}",
+ "purpose": undefined,
+ },
+ {
+ "name": "channel",
+ "value": "fflb",
+ "purpose": "keyword",
+ },
+ {
+ "name": "channel",
+ "value": "rcs",
+ "purpose": "contextmenu",
+ },
+ ],
+ },
+ ],
+ },
+ },
+};
diff --git a/toolkit/components/search/tests/xpcshell/test_location.js b/toolkit/components/search/tests/xpcshell/test_location.js
new file mode 100644
index 000000000..93e6139f6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location.js
@@ -0,0 +1,66 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ installTestEngine();
+
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU", "got the correct country code.");
+ equal(Services.prefs.getCharPref("browser.search.region"), "AU", "region pref also set to the countryCode.")
+ // No isUS pref is ever written
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "no isUS pref")
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+
+ }
+
+ // simple checks for our platform-specific telemetry. We can't influence
+ // what they return (as we can't influence the countryCode the platform
+ // thinks we are in), but we can check the values are correct given reality.
+ let probeUSMismatched, probeNonUSMismatched;
+ switch (Services.appinfo.OS) {
+ case "Darwin":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_OSX";
+ break;
+ case "WINNT":
+ probeUSMismatched = "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ probeNonUSMismatched = "SEARCH_SERVICE_NONUS_COUNTRY_MISMATCHED_PLATFORM_WIN";
+ break;
+ default:
+ break;
+ }
+
+ if (probeUSMismatched && probeNonUSMismatched) {
+ let countryCode = Services.sysinfo.get("countryCode");
+ print("Platform says the country-code is", countryCode);
+ let expectedResult;
+ let hid;
+ // We know geoip said AU - if the platform thinks US then we expect
+ // probeUSMismatched with true (ie, a mismatch)
+ if (countryCode == "US") {
+ hid = probeUSMismatched;
+ expectedResult = [0, 1, 0]; // boolean probe so 3 buckets, expect 1 result for |1|.
+ } else {
+ // We are expecting probeNonUSMismatched with false if the platform
+ // says AU (not a mismatch) and true otherwise.
+ hid = probeNonUSMismatched;
+ expectedResult = countryCode == "AU" ? [1, 0, 0] : [0, 1, 0];
+ }
+
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, expectedResult);
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_error.js b/toolkit/components/search/tests/xpcshell/test_location_error.js
new file mode 100644
index 000000000..049189351
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_error.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ installTestEngine();
+
+ // We use an invalid port that parses but won't open
+ let url = "http://localhost:0";
+
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ Services.search.init(() => {
+ try {
+ Services.prefs.getCharPref("browser.search.countryCode");
+ ok(false, "not expecting countryCode to be set");
+ } catch (ex) {}
+ // should have an error recorded.
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.ERROR);
+ // but false values for timeout and forced-sync-init.
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js b/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js
new file mode 100644
index 000000000..970ba5521
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_funnelcake.js
@@ -0,0 +1,17 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ // funnelcake builds start with "mozilla"
+ Services.prefs.setCharPref("distribution.id", 'mozilla38');
+ setUpGeoDefaults();
+
+ Services.search.init(() => {
+ equal(Services.search.defaultEngine.name, "A second test engine");
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js b/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
new file mode 100644
index 000000000..b1f30ad7c
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_malformed_json.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// A console listener so we can listen for a log message from nsSearchService.
+function promiseTimezoneMessage() {
+ return new Promise(resolve => {
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+ observe : function (msg) {
+ if (msg.message.startsWith("getIsUS() fell back to a timezone check with the result=")) {
+ Services.console.unregisterListener(listener);
+ resolve(msg);
+ }
+ }
+ };
+ Services.console.registerListener(listener);
+ });
+}
+
+function run_test() {
+ installTestEngine();
+
+ // setup a console listener for the timezone fallback message.
+ let promiseTzMessage = promiseTimezoneMessage();
+
+ // Here we have malformed JSON
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code"');
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never be an isUS pref");
+ // fetch the engines - this should force the timezone check, but still
+ // doesn't persist any prefs.
+ Services.search.getEngines();
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never be an isUS pref");
+ // should have recorded SUCCESS_WITHOUT_DATA
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS_WITHOUT_DATA);
+ // and false values for timeout and forced-sync-init.
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+
+ // Check we saw the timezone fallback message.
+ promiseTzMessage.then(msg => {
+ print("Timezone message:", msg.message);
+ ok(msg.message.endsWith(isUSTimezone().toString()), "fell back to timezone and it matches our timezone");
+ do_test_finished();
+ run_next_test();
+ });
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js
new file mode 100644
index 000000000..9e1019761
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_countrycode_isUS.js
@@ -0,0 +1,24 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" when both isUS and countryCode are
+// set.
+function run_test() {
+ installTestEngine();
+
+ // Set the prefs we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ Services.prefs.setCharPref("browser.search.countryCode", "US");
+ // And the geoip request that will return AU - it shouldn't be used.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ // "region" and countryCode should still reflect US.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "US", "got the correct country code.");
+ // should be no geoip evidence.
+ checkCountryResultTelemetry(null);
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js
new file mode 100644
index 000000000..b294b038b
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_isUS.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" from the isUS pref being true but when
+// no country-code exists.
+function run_test() {
+ installTestEngine();
+
+ // Set the pref we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", true);
+ // And the geoip request that will return *not* US.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+ Services.search.init(() => {
+ // "region" should be set to US, but countryCode to AU.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU", "got the correct country code.");
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js
new file mode 100644
index 000000000..9c0b810d3
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_migrate_no_countrycode_notUS.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Here we are testing the "migration" from the isUS pref being false but when
+// no country-code exists.
+function run_test() {
+ installTestEngine();
+
+ // Set the pref we care about.
+ Services.prefs.setBoolPref("browser.search.isUS", false);
+ // And the geoip request that will return US.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ Services.search.init(() => {
+ // "region" and countryCode should reflect US.
+ equal(Services.prefs.getCharPref("browser.search.region"), "US", "got the correct region.");
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "US", "got the correct country code.");
+ // check we have "success" recorded in telemetry
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+ // a false value for each of SEARCH_SERVICE_COUNTRY_TIMEOUT and SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT
+ for (let hid of ["SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT"]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [1, 0, 0]); // boolean probe so 3 buckets, expect 1 result for |0|.
+ }
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_partner.js b/toolkit/components/search/tests/xpcshell/test_location_partner.js
new file mode 100644
index 000000000..9151add9a
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_partner.js
@@ -0,0 +1,16 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "US"}');
+ Services.prefs.setCharPref("distribution.id", 'partner-1');
+ setUpGeoDefaults();
+
+ Services.search.init(() => {
+ equal(Services.search.defaultEngine.name, "Test search engine");
+
+ do_test_finished();
+ run_next_test();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_sync.js b/toolkit/components/search/tests/xpcshell/test_location_sync.js
new file mode 100644
index 000000000..524a440fb
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_sync.js
@@ -0,0 +1,101 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function getCountryCodePref() {
+ try {
+ return Services.prefs.getCharPref("browser.search.countryCode");
+ } catch (_) {
+ return undefined;
+ }
+}
+
+function getIsUSPref() {
+ try {
+ return Services.prefs.getBoolPref("browser.search.isUS");
+ } catch (_) {
+ return undefined;
+ }
+}
+
+// A console listener so we can listen for a log message from nsSearchService.
+function promiseTimezoneMessage() {
+ return new Promise(resolve => {
+ let listener = {
+ QueryInterface: XPCOMUtils.generateQI([Ci.nsIConsoleListener]),
+ observe : function (msg) {
+ if (msg.message.startsWith("getIsUS() fell back to a timezone check with the result=")) {
+ Services.console.unregisterListener(listener);
+ resolve(msg);
+ }
+ }
+ };
+ Services.console.registerListener(listener);
+ });
+}
+
+function run_test() {
+ installTestEngine();
+
+ run_next_test();
+}
+
+// Force a sync init and ensure the right thing happens (ie, that no xhr
+// request is made and we fall back to the timezone-only trick)
+add_task(function* test_simple() {
+ deepEqual(getCountryCodePref(), undefined, "no countryCode pref");
+ deepEqual(getIsUSPref(), undefined, "no isUS pref");
+
+ // Still set a geoip pref so we can (indirectly) check it wasn't used.
+ Services.prefs.setCharPref("browser.search.geoip.url", 'data:application/json,{"country_code": "AU"}');
+
+ ok(!Services.search.isInitialized);
+
+ // setup a console listener for the timezone fallback message.
+ let promiseTzMessage = promiseTimezoneMessage();
+
+ // fetching the engines forces a sync init, and should have caused us to
+ // check the timezone.
+ Services.search.getEngines();
+ ok(Services.search.isInitialized);
+
+ // a little wait to check we didn't do the xhr thang.
+ yield new Promise(resolve => {
+ do_timeout(500, resolve);
+ });
+
+ let msg = yield promiseTzMessage;
+ print("Timezone message:", msg.message);
+ ok(msg.message.endsWith(isUSTimezone().toString()), "fell back to timezone and it matches our timezone");
+
+ deepEqual(getCountryCodePref(), undefined, "didn't do the geoip xhr");
+ // and no telemetry evidence of geoip.
+ for (let hid of [
+ "SEARCH_SERVICE_COUNTRY_FETCH_RESULT",
+ "SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS",
+ "SEARCH_SERVICE_COUNTRY_TIMEOUT",
+ "SEARCH_SERVICE_US_COUNTRY_MISMATCHED_TIMEZONE",
+ "SEARCH_SERVICE_US_TIMEZONE_MISMATCHED_COUNTRY",
+ "SEARCH_SERVICE_COUNTRY_FETCH_CAUSED_SYNC_INIT",
+ ]) {
+ let histogram = Services.telemetry.getHistogramById(hid);
+ let snapshot = histogram.snapshot();
+ equal(snapshot.sum, 0, hid);
+ switch (snapshot.histogram_type) {
+ case Ci.nsITelemetry.HISTOGRAM_FLAG:
+ // flags are a special case in that they are initialized with a default
+ // of one |0|.
+ deepEqual(snapshot.counts, [1, 0, 0], hid);
+ break;
+ case Ci.nsITelemetry.HISTOGRAM_BOOLEAN:
+ // booleans aren't initialized at all, so should have all zeros.
+ deepEqual(snapshot.counts, [0, 0, 0], hid);
+ break;
+ case Ci.nsITelemetry.HISTOGRAM_EXPONENTIAL:
+ case Ci.nsITelemetry.HISTOGRAM_LINEAR:
+ equal(snapshot.counts.reduce((a, b) => a+b), 0, hid);
+ break;
+ default:
+ ok(false, "unknown histogram type " + snapshot.histogram_type + " for " + hid);
+ }
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout.js b/toolkit/components/search/tests/xpcshell/test_location_timeout.js
new file mode 100644
index 000000000..c1d5270e5
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout.js
@@ -0,0 +1,78 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the "normal" timer-based timeout for the location search.
+
+function startServer(continuePromise) {
+ let srv = new HttpServer();
+ function lookupCountry(metadata, response) {
+ response.processAsync();
+ // wait for our continuePromise to resolve before writing a valid
+ // response.
+ // This will be resolved after the timeout period, so we can check
+ // the behaviour in that case.
+ continuePromise.then(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write('{"country_code" : "AU"}');
+ response.finish();
+ });
+ }
+ srv.registerPathHandler("/lookup_country", lookupCountry);
+ srv.start(-1);
+ return srv;
+}
+
+function getProbeSum(probe, sum) {
+ let histogram = Services.telemetry.getHistogramById(probe);
+ return histogram.snapshot().sum;
+}
+
+function run_test() {
+ installTestEngine();
+
+ let resolveContinuePromise;
+ let continuePromise = new Promise(resolve => {
+ resolveContinuePromise = resolve;
+ });
+
+ let server = startServer(continuePromise);
+ let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 50);
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ // should be no result recorded at all.
+ checkCountryResultTelemetry(null);
+
+ // should have set the flag indicating we saw a timeout.
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [0, 1, 0]);
+ // should not yet have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+ // test server is still blocked on our promise.
+ equal(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS"), 0);
+
+ waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+ // now we *should* have a report of how long the response took even though
+ // it timed out.
+ // The telemetry "sum" will be the actual time in ms - just check it's non-zero.
+ ok(getProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS") != 0);
+ // should have reported the fetch ended up being successful
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.SUCCESS);
+
+ // and should have the result of the response that finally came in, and
+ // everything dependent should also be updated.
+ equal(Services.prefs.getCharPref("browser.search.countryCode"), "AU");
+ equal(Services.prefs.getCharPref("browser.search.region"), "AU");
+ ok(!Services.prefs.prefHasUserValue("browser.search.isUS"), "should never have an isUS pref");
+
+ do_test_finished();
+ server.stop(run_next_test);
+ });
+ // now tell the server to send its response. That will end up causing the
+ // search service to notify of that the response was received.
+ resolveContinuePromise();
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
new file mode 100644
index 000000000..4054cf0c2
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_location_timeout_xhr.js
@@ -0,0 +1,85 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// This is testing the long, last-resort XHR-based timeout for the location
+// search.
+
+function startServer(continuePromise) {
+ let srv = new HttpServer();
+ function lookupCountry(metadata, response) {
+ response.processAsync();
+ // wait for our continuePromise to resolve before writing a valid
+ // response.
+ // This will be resolved after the timeout period, so we can check
+ // the behaviour in that case.
+ continuePromise.then(() => {
+ response.setStatusLine("1.1", 200, "OK");
+ response.write('{"country_code" : "AU"}');
+ response.finish();
+ });
+ }
+ srv.registerPathHandler("/lookup_country", lookupCountry);
+ srv.start(-1);
+ return srv;
+}
+
+function verifyProbeSum(probe, sum) {
+ let histogram = Services.telemetry.getHistogramById(probe);
+ let snapshot = histogram.snapshot();
+ equal(snapshot.sum, sum, probe);
+}
+
+function run_test() {
+ installTestEngine();
+
+ let resolveContinuePromise;
+ let continuePromise = new Promise(resolve => {
+ resolveContinuePromise = resolve;
+ });
+
+ let server = startServer(continuePromise);
+ let url = "http://localhost:" + server.identity.primaryPort + "/lookup_country";
+ Services.prefs.setCharPref("browser.search.geoip.url", url);
+ // The timeout for the timer.
+ Services.prefs.setIntPref("browser.search.geoip.timeout", 10);
+ let promiseXHRStarted = waitForSearchNotification("geoip-lookup-xhr-starting");
+ Services.search.init(() => {
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+ // should be no result recorded at all.
+ checkCountryResultTelemetry(null);
+
+ // should have set the flag indicating we saw a timeout.
+ let histogram = Services.telemetry.getHistogramById("SEARCH_SERVICE_COUNTRY_TIMEOUT");
+ let snapshot = histogram.snapshot();
+ deepEqual(snapshot.counts, [0, 1, 0]);
+
+ // should not have SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS recorded as our
+ // test server is still blocked on our promise.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+
+ promiseXHRStarted.then(xhr => {
+ // Set the timeout on the xhr object to an extremely low value, so it
+ // should timeout immediately.
+ xhr.timeout = 10;
+ // wait for the xhr timeout to fire.
+ waitForSearchNotification("geoip-lookup-xhr-complete").then(() => {
+ // should have the XHR timeout recorded.
+ checkCountryResultTelemetry(TELEMETRY_RESULT_ENUM.XHRTIMEOUT);
+ // still should not have a report of how long the response took as we
+ // only record that on success responses.
+ verifyProbeSum("SEARCH_SERVICE_COUNTRY_FETCH_TIME_MS", 0);
+ // and we still don't know the country code or region.
+ ok(!Services.prefs.prefHasUserValue("browser.search.countryCode"), "should be no countryCode pref");
+ ok(!Services.prefs.prefHasUserValue("browser.search.region"), "should be no region pref");
+
+ // unblock the server even though nothing is listening.
+ resolveContinuePromise();
+
+ do_test_finished();
+ server.stop(run_next_test);
+ });
+ });
+ });
+ do_test_pending();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_migration_langpack.js b/toolkit/components/search/tests/xpcshell/test_migration_langpack.js
new file mode 100644
index 000000000..8cb2014bd
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_migration_langpack.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Unless we unset the XPCSHELL_TEST_PROFILE_DIR environment variable,
+ // engine._isDefault will be true for engines from the resource:// scheme,
+ // bypassing the codepath we want to test.
+ let env = Cc["@mozilla.org/process/environment;1"]
+ .getService(Ci.nsIEnvironment);
+ env.set("XPCSHELL_TEST_PROFILE_DIR", "");
+
+ do_get_file("data/langpack-metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* async_init() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+ do_check_eq(engine.wrappedJSObject._id, "[app]/bug645970.xml");
+
+ yield commitPromise;
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["bug645970"].alias, "lp");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_multipleIcons.js b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
new file mode 100644
index 000000000..314515f6d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_multipleIcons.js
@@ -0,0 +1,61 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getIcons() and getIconURLBySize() on engine with multiple icons.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_multipleIcons() {
+ let [engine] = yield addTestEngines([
+ { name: "IconsTest", xmlFileName: "engineImages.xml" },
+ ]);
+
+ do_print("The default should be the 16x16 icon");
+ do_check_true(engine.iconURI.spec.includes("ico16"));
+
+ do_check_true(engine.getIconURLBySize(16, 16).includes("ico16"));
+ do_check_true(engine.getIconURLBySize(32, 32).includes("ico32"));
+ do_check_true(engine.getIconURLBySize(74, 74).includes("ico74"));
+
+ do_print("Invalid dimensions should return null.");
+ do_check_null(engine.getIconURLBySize(50, 50));
+
+ let allIcons = engine.getIcons();
+
+ do_print("Check that allIcons contains expected icon sizes");
+ do_check_eq(allIcons.length, 3);
+ let expectedWidths = [16, 32, 74];
+ do_check_true(allIcons.every((item) => {
+ let width = item.width;
+ do_check_neq(expectedWidths.indexOf(width), -1);
+ do_check_eq(width, item.height);
+
+ let icon = item.url.split(",").pop();
+ do_check_eq(icon, "ico" + width);
+
+ return true;
+ }));
+});
+
+add_task(function* test_icon_not_in_file() {
+ let engineUrl = gDataUrl + "engine-fr.xml";
+ let engine = yield new Promise((resolve, reject) => {
+ Services.search.addEngine(engineUrl, null, "data:image/x-icon;base64,ico16",
+ false, {onSuccess: resolve, onError: reject});
+ });
+
+ // Even though the icon wasn't specified inside the XML file, it should be
+ // available both in the iconURI attribute and with getIconURLBySize.
+ do_check_true(engine.iconURI.spec.includes("ico16"));
+ do_check_true(engine.getIconURLBySize(16, 16).includes("ico16"));
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_nocache.js b/toolkit/components/search/tests/xpcshell/test_nocache.js
new file mode 100644
index 000000000..42776aef0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nocache.js
@@ -0,0 +1,60 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nocache: Start search engine
+ * - without search.json.mozlz4
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - search.json.mozlz4 is created.
+ */
+
+function run_test()
+{
+ removeCacheFile();
+ updateAppInfo();
+ do_load_manifest("data/chrome.manifest");
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_nocache() {
+ let search = Services.search;
+
+ let afterCachePromise = promiseAfterCache();
+
+ yield new Promise((resolve, reject) => search.init(rv => {
+ Components.isSuccessCode(rv) ? resolve() : reject();
+ }));
+
+ // Check that the cache is created at startup
+ yield afterCachePromise;
+
+ // Check that search.json has been created.
+ let cacheFile = gProfD.clone();
+ cacheFile.append(CACHE_FILENAME);
+ do_check_true(cacheFile.exists());
+
+ // Add engine and wait for cache update
+ yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ ]);
+
+ do_print("Engine has been added, let's wait for the cache to be built");
+ yield promiseAfterCache();
+
+ do_print("Searching test engine in cache");
+ let cache = yield promiseCacheData();
+ let found = false;
+ for (let engine of cache.engines) {
+ if (engine._shortName == "test-search-engine") {
+ found = true;
+ break;
+ }
+ }
+ do_check_true(found);
+
+ removeCacheFile();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_nodb.js b/toolkit/components/search/tests/xpcshell/test_nodb.js
new file mode 100644
index 000000000..66a003a5d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nodb.js
@@ -0,0 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_nodb: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - no search-metadata.json is created.
+ */
+
+
+function run_test()
+{
+ removeMetadata();
+ updateAppInfo();
+
+ let search = Services.search;
+
+ do_test_pending();
+ search.init(function ss_initialized(rv) {
+ do_check_true(Components.isSuccessCode(rv));
+ do_timeout(500, function() {
+ // Check that search-metadata.json has not been
+ // created. Note that we cannot do much better
+ // than a timeout for checking a non-event.
+ let metadata = gProfD.clone();
+ metadata.append("search-metadata.json");
+ do_check_true(!metadata.exists());
+ removeMetadata();
+
+ do_test_finished();
+ });
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
new file mode 100644
index 000000000..e6789e964
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_nodb_pluschanges.js
@@ -0,0 +1,57 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+
+/*
+ * test_nodb: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that :
+ * - nothing explodes;
+ * - if we change the order, search.json.mozlz4 is updated;
+ * - this search.json.mozlz4 can be parsed;
+ * - the order stored in search.json.mozlz4 is consistent.
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+function run_test() {
+ updateAppInfo();
+ do_load_manifest("data/chrome.manifest");
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_nodb_pluschanges() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml"},
+ ]);
+ yield promiseAfterCache();
+
+ let search = Services.search;
+
+ search.moveEngine(engine1, 0);
+ search.moveEngine(engine2, 1);
+
+ // This is needed to avoid some reentrency issues in nsSearchService.
+ do_print("Next step is forcing flush");
+ yield new Promise(resolve => do_execute_soon(resolve));
+
+ do_print("Forcing flush");
+ let promiseCommit = promiseAfterCache();
+ search.QueryInterface(Ci.nsIObserver)
+ .observe(null, "quit-application", "");
+ yield promiseCommit;
+ do_print("Commit complete");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["test-search-engine"].order, 1);
+ do_check_eq(metadata["a-second-test-engine"].order, 2);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_notifications.js b/toolkit/components/search/tests/xpcshell/test_notifications.js
new file mode 100644
index 000000000..3eecbf8b1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_notifications.js
@@ -0,0 +1,72 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var gTestLog = [];
+
+/**
+ * The order of notifications expected for this test is:
+ * - engine-changed (while we're installing the engine, we modify it, which notifies - bug 606886)
+ * - engine-added (engine was added to the store by the search service)
+ * -> our search observer is called, which sets:
+ * - .defaultEngine, triggering engine-default
+ * - .currentEngine, triggering engine-current (after bug 493051 - for now the search service sets this after engine-added)
+ * ...and then schedules a removal
+ * - engine-loaded (the search service's observer is garanteed to fire first, which is what causes engine-added to fire)
+ * - engine-removed (due to the removal schedule above)
+ */
+var expectedLog = [
+ "engine-changed", // XXX bug 606886
+ "engine-added",
+ "engine-default",
+ "engine-current",
+ "engine-loaded",
+ "engine-removed"
+];
+
+function search_observer(subject, topic, data) {
+ let engine = subject.QueryInterface(Ci.nsISearchEngine);
+ gTestLog.push(data + " for " + engine.name);
+
+ 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;
+ 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"))
+ do_print("gTestLog:\n" + gTestLog.join("\n"))
+ for (let i = 0; i < expectedLog.length; i++) {
+ do_check_eq(gTestLog[i], expectedLog[i]);
+ }
+ do_check_eq(gTestLog.length, expectedLog.length);
+ do_test_finished();
+ break;
+ }
+}
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ do_register_cleanup(function cleanup() {
+ Services.obs.removeObserver(search_observer, "browser-search-engine-modified");
+ });
+
+ do_test_pending();
+
+ Services.obs.addObserver(search_observer, "browser-search-engine-modified", false);
+
+ Services.search.addEngine(gDataUrl + "engine.xml", null, null, false);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
new file mode 100644
index 000000000..d6e21fc61
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_parseSubmissionURL.js
@@ -0,0 +1,148 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getAlternateDomains API.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_parseSubmissionURL() {
+ // Hide the default engines to prevent them from being used in the search.
+ for (let engine of Services.search.getEngines()) {
+ Services.search.removeEngine(engine);
+ }
+
+ let [engine1, engine2, engine3, engine4] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "Test search engine (fr)", xmlFileName: "engine-fr.xml" },
+ { name: "bacon_addParam", details: ["", "bacon_addParam", "Search Bacon",
+ "GET", "http://www.bacon.test/find"] },
+ { name: "idn_addParam", details: ["", "idn_addParam", "Search IDN",
+ "GET", "http://www.xn--bcher-kva.ch/search"] },
+ // The following engines cannot identify the search parameter.
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ { name: "bacon", details: ["", "bacon", "Search Bacon", "GET",
+ "http://www.bacon.moz/search?q={searchTerms}"] },
+ ]);
+
+ engine3.addParam("q", "{searchTerms}", null);
+ engine4.addParam("q", "{searchTerms}", null);
+
+ // Test the first engine, whose URLs use UTF-8 encoding.
+ let url = "http://www.google.com/search?foo=bar&q=caff%C3%A8";
+ let result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+ do_check_eq(result.termsLength, "caff%C3%A8".length);
+
+ // The second engine uses a locale-specific domain that is an alternate domain
+ // of the first one, but the second engine should get priority when matching.
+ // The URL used with this engine uses ISO-8859-1 encoding instead.
+ url = "http://www.google.fr/search?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine2);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%E8"));
+ do_check_eq(result.termsLength, "caff%E8".length);
+
+ // Test a domain that is an alternate domain of those defined. In this case,
+ // the first matching engine from the ordered list should be returned.
+ url = "http://www.google.co.uk/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+ do_check_eq(result.termsLength, "caff%C3%A8".length);
+
+ // We support parsing URLs from a dynamically added engine. Those engines use
+ // windows-1252 encoding by default.
+ url = "http://www.bacon.test/find?q=caff%E8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine3);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%E8"));
+ do_check_eq(result.termsLength, "caff%E8".length);
+
+ // Test URLs with unescaped unicode characters.
+ url = "http://www.google.com/search?q=foo+b\u00E4r";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "foo b\u00E4r");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+b\u00E4r"));
+ do_check_eq(result.termsLength, "foo+b\u00E4r".length);
+
+ // Test search engines with unescaped IDNs.
+ url = "http://www.b\u00FCcher.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine4);
+ do_check_eq(result.terms, "foo bar");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+bar"));
+ do_check_eq(result.termsLength, "foo+bar".length);
+
+ // Test search engines with escaped IDNs.
+ url = "http://www.xn--bcher-kva.ch/search?q=foo+bar";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine4);
+ do_check_eq(result.terms, "foo bar");
+ do_check_true(url.slice(result.termsOffset).startsWith("foo+bar"));
+ do_check_eq(result.termsLength, "foo+bar".length);
+
+ // Parsing of parameters from an engine template URL is not supported.
+ do_check_eq(Services.search.parseSubmissionURL(
+ "http://www.bacon.moz/search?q=").engine, null);
+ do_check_eq(Services.search.parseSubmissionURL(
+ "https://duckduckgo.com?q=test").engine, null);
+ do_check_eq(Services.search.parseSubmissionURL(
+ "https://duckduckgo.com/?q=test").engine, null);
+
+ // HTTP and HTTPS schemes are interchangeable.
+ url = "https://www.google.com/search?q=caff%C3%A8";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "caff\u00E8");
+ do_check_true(url.slice(result.termsOffset).startsWith("caff%C3%A8"));
+
+ // Decoding search terms with multiple spaces should work.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search?q=+with++spaces+");
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, " with spaces ");
+
+ // An empty query parameter should work the same.
+ url = "http://www.google.com/search?q=";
+ result = Services.search.parseSubmissionURL(url);
+ do_check_eq(result.engine, engine1);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, url.length);
+
+ // There should be no match when the path is different.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search/?q=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+
+ // There should be no match when the argument is different.
+ result = Services.search.parseSubmissionURL(
+ "http://www.google.com/search?q2=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+
+ // There should be no match for URIs that are not HTTP or HTTPS.
+ result = Services.search.parseSubmissionURL(
+ "file://localhost/search?q=test");
+ do_check_eq(result.engine, null);
+ do_check_eq(result.terms, "");
+ do_check_eq(result.termsOffset, -1);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_pref.js b/toolkit/components/search/tests/xpcshell/test_pref.js
new file mode 100644
index 000000000..f51ab4ee8
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_pref.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that MozParam condition="pref" values used in search URLs are from the
+ * default branch, and that their special characters are URL encoded. */
+
+"use strict";
+
+function run_test() {
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams will be ignored.
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_pref() {
+ let defaultBranch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ defaultBranch.setCharPref("param.code", "good&id=unique");
+ Services.prefs.setCharPref(BROWSER_SEARCH_PREF + "param.code", "bad");
+
+ yield asyncInit();
+
+ let engine = Services.search.getEngineByName("engine-pref");
+ let base = "http://www.google.com/search?q=foo&code=";
+ do_check_eq(engine.getSubmission("foo").uri.spec,
+ base + "good%26id%3Dunique");
+
+ do_test_finished();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_purpose.js b/toolkit/components/search/tests/xpcshell/test_purpose.js
new file mode 100644
index 000000000..46465e0a3
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_purpose.js
@@ -0,0 +1,70 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test that a search purpose can be specified and that query parameters for
+ * that purpose are included in the search URL.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+
+ // The test engines used in this test need to be recognized as 'default'
+ // engines, or their MozParams used to set the purpose will be ignored.
+ let url = "resource://test/data/";
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI(url, null, null));
+
+ run_next_test();
+}
+
+add_task(function* test_purpose() {
+ let engine = Services.search.getEngineByName("Test search engine");
+
+ function check_submission(aExpected, aSearchTerm, aType, aPurpose) {
+ do_check_eq(engine.getSubmission(aSearchTerm, aType, aPurpose).uri.spec,
+ base + aExpected);
+ }
+
+ let base = "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
+ check_submission("", "foo");
+ check_submission("", "foo", null);
+ check_submission("", "foo", "text/html");
+ check_submission("&channel=rcs", "foo", null, "contextmenu");
+ check_submission("&channel=rcs", "foo", "text/html", "contextmenu");
+ check_submission("&channel=fflb", "foo", null, "keyword");
+ check_submission("&channel=fflb", "foo", "text/html", "keyword");
+ check_submission("", "foo", "text/html", "invalid");
+
+ // Tests for a param that varies with a purpose but has a default value.
+ base = "http://www.google.com/search?q=foo";
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", null);
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "");
+ check_submission("&channel=rcs", "foo", "application/x-moz-default-purpose", "contextmenu");
+ check_submission("&channel=fflb", "foo", "application/x-moz-default-purpose", "keyword");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "searchbar");
+ check_submission("&channel=ffsb", "foo", "application/x-moz-default-purpose", "invalid");
+
+ // Tests for a purpose on the search form (ie. empty query).
+ engine = Services.search.getEngineByName("engine-rel-searchform-purpose");
+ base = "http://www.google.com/search?q=";
+ check_submission("&channel=sb", "", null, "searchbar");
+ check_submission("&channel=sb", "", "text/html", "searchbar");
+
+ // verify that the 'system' purpose falls back to the 'searchbar' purpose.
+ base = "http://www.google.com/search?q=foo";
+ check_submission("&channel=sb", "foo", "text/html", "system");
+ check_submission("&channel=sb", "foo", "text/html", "searchbar");
+ // Use an engine that actually defines the 'system' purpose...
+ engine = Services.search.getEngineByName("engine-system-purpose");
+ // ... and check that the system purpose is used correctly.
+ check_submission("&channel=sys", "foo", "text/html", "system");
+
+ do_test_finished();
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_rel_searchform.js b/toolkit/components/search/tests/xpcshell/test_rel_searchform.js
new file mode 100644
index 000000000..79f217e0d
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_rel_searchform.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests that <Url rel="searchform"/> is properly recognized as a searchForm.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_rel_searchform() {
+ let engineNames = [
+ "engine-rel-searchform.xml",
+ "engine-rel-searchform-post.xml",
+ ];
+
+ // The final searchForm of the engine should be a URL whose domain is the
+ // <ShortName> in the engine's XML and that has a ?search parameter. The
+ // point of the ?search parameter is to avoid accidentally matching the value
+ // returned as a last resort by Engine's searchForm getter, which is simply
+ // the prePath of the engine's first HTML <Url>.
+ let items = engineNames.map(e => ({ name: e, xmlFileName: e }));
+ for (let engine of yield addTestEngines(items)) {
+ do_check_eq(engine.searchForm, "http://" + engine.name + "/?search");
+ }
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
new file mode 100644
index 000000000..3a985db9e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_remove_profile_engine.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ do_test_pending();
+
+ // Copy an engine to [profile]/searchplugin/
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ let file = dir.clone();
+ file.append("bug645970.xml");
+ do_check_true(file.exists());
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test the engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // remove the engine and verify the file has been removed too.
+ Services.search.removeEngine(engine);
+ do_check_false(file.exists());
+
+ do_test_finished();
+ });
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
new file mode 100644
index 000000000..299121c4f
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_require_engines_in_cache.js
@@ -0,0 +1,74 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ do_check_false(Services.search.isInitialized);
+
+ run_next_test();
+}
+
+add_task(function* ignore_cache_files_without_engines() {
+ let commitPromise = promiseAfterCache()
+ yield asyncInit();
+
+ let engineCount = Services.search.getEngines().length;
+ do_check_eq(engineCount, 1);
+
+ // Wait for the file to be saved to disk, so that we can mess with it.
+ yield commitPromise;
+
+ // Remove all engines from the cache file.
+ let cache = yield promiseCacheData();
+ cache.engines = [];
+ yield promiseSaveCacheData(cache);
+
+ // Check that after an async re-initialization, we still have the same engine count.
+ commitPromise = promiseAfterCache()
+ yield asyncReInit();
+ do_check_eq(engineCount, Services.search.getEngines().length);
+ yield commitPromise;
+
+ // Check that after a sync re-initialization, we still have the same engine count.
+ yield promiseSaveCacheData(cache);
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+ do_check_false(Services.search.isInitialized);
+ // Synchronously check the engine count; will force a sync init.
+ do_check_eq(engineCount, Services.search.getEngines().length);
+ do_check_true(Services.search.isInitialized);
+ yield reInitPromise;
+});
+
+add_task(function* skip_writing_cache_without_engines() {
+ let unInitPromise = waitForSearchNotification("uninit-complete");
+ let reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Configure so that no engines will be found.
+ do_check_true(removeCacheFile());
+ let resProt = Services.io.getProtocolHandler("resource")
+ .QueryInterface(Ci.nsIResProtocolHandler);
+ resProt.setSubstitution("search-plugins",
+ Services.io.newURI("about:blank", null, null));
+
+ // Let the async-reInit happen.
+ yield reInitPromise;
+ do_check_eq(0, Services.search.getEngines().length);
+
+ // Trigger yet another re-init, to flush of any pending cache writing task.
+ unInitPromise = waitForSearchNotification("uninit-complete");
+ reInitPromise = asyncReInit();
+ yield unInitPromise;
+
+ // Now check that a cache file doesn't exist.
+ do_check_false(removeCacheFile());
+
+ yield reInitPromise;
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_resultDomain.js b/toolkit/components/search/tests/xpcshell/test_resultDomain.js
new file mode 100644
index 000000000..d7458a923
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_resultDomain.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests getResultDomain API.
+ */
+
+"use strict";
+
+function run_test() {
+ removeMetadata();
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_resultDomain() {
+ let [engine1, engine2, engine3] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml" },
+ { name: "bacon", details: ["", "bacon", "Search Bacon", "GET",
+ "http://www.bacon.moz/?search={searchTerms}"] },
+ ]);
+
+ do_check_eq(engine1.getResultDomain(), "google.com");
+ do_check_eq(engine1.getResultDomain("text/html"), "google.com");
+ do_check_eq(engine1.getResultDomain("application/x-moz-default-purpose"),
+ "purpose.google.com");
+ do_check_eq(engine1.getResultDomain("fake-response-type"), "");
+ do_check_eq(engine2.getResultDomain(), "duckduckgo.com");
+ do_check_eq(engine3.getResultDomain(), "bacon.moz");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
new file mode 100644
index 000000000..c509c5f77
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_save_sorted_engines.js
@@ -0,0 +1,67 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * test_save_sorted_engines: Start search engine
+ * - without search-metadata.json
+ * - without search.sqlite
+ *
+ * Ensure that search-metadata.json is correct after:
+ * - moving an engine
+ * - removing an engine
+ * - adding a new engine
+ *
+ * Notes:
+ * - we install the search engines of test "test_downloadAndAddEngines.js"
+ * to ensure that this test is independent from locale, commercial agreements
+ * and configuration of Firefox.
+ */
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer();
+
+ run_next_test();
+}
+
+add_task(function* test_save_sorted_engines() {
+ let [engine1, engine2] = yield addTestEngines([
+ { name: "Test search engine", xmlFileName: "engine.xml" },
+ { name: "A second test engine", xmlFileName: "engine2.xml"},
+ ]);
+ yield promiseAfterCache();
+
+ let search = Services.search;
+
+ // Test moving the engines
+ search.moveEngine(engine1, 0);
+ search.moveEngine(engine2, 1);
+
+ // Changes should be commited immediately
+ yield promiseAfterCache();
+ do_print("Commit complete after moveEngine");
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["test-search-engine"].order, 1);
+ do_check_eq(metadata["a-second-test-engine"].order, 2);
+
+ // Test removing an engine
+ search.removeEngine(engine1);
+ yield promiseAfterCache();
+ do_print("Commit complete after removeEngine");
+
+ // Check that the order of the remaining engine was updated correctly
+ metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["a-second-test-engine"].order, 1);
+
+ // Test adding a new engine
+ search.addEngineWithDetails("foo", "", "foo", "", "GET",
+ "http://searchget/?search={searchTerms}");
+ yield promiseAfterCache();
+ do_print("Commit complete after addEngineWithDetails");
+
+ metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["foo"].alias, "foo");
+ do_check_true(metadata["foo"].order > 0);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchReset.js b/toolkit/components/search/tests/xpcshell/test_searchReset.js
new file mode 100644
index 000000000..316069c95
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchReset.js
@@ -0,0 +1,137 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+const kTestEngineShortName = "engine";
+const kWhiteListPrefName = "reset.whitelist";
+
+function run_test() {
+ // Copy an engine to [profile]/searchplugin/
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine.xml").copyTo(dir, kTestEngineShortName + ".xml");
+
+ let file = dir.clone();
+ file.append(kTestEngineShortName + ".xml");
+ do_check_true(file.exists());
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF)
+ .setBoolPref("reset.enabled", true);
+
+ run_next_test();
+}
+
+function* removeLoadPathHash() {
+ // Remove the loadPathHash and re-initialize the search service.
+ let cache = yield promiseCacheData();
+ for (let engine of cache.engines) {
+ if (engine._shortName == kTestEngineShortName) {
+ delete engine._metaData["loadPathHash"];
+ break;
+ }
+ }
+ yield promiseSaveCacheData(cache);
+ yield asyncReInit();
+}
+
+add_task(function* test_no_prompt_when_valid_loadPathHash() {
+ yield asyncInit();
+
+ // test the engine is loaded ok.
+ let engine = Services.search.getEngineByName(kTestEngineName);
+ do_check_neq(engine, null);
+
+ yield promiseAfterCache();
+
+ // The test engine has been found in the profile directory and imported,
+ // so it shouldn't have a loadPathHash.
+ let metadata = yield promiseEngineMetadata();
+ do_check_true(kTestEngineShortName in metadata);
+ do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
+
+ // After making it the currentEngine with the search service API,
+ // the test engine should have a valid loadPathHash.
+ Services.search.currentEngine = engine;
+ yield promiseAfterCache();
+ metadata = yield promiseEngineMetadata();
+ do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
+ let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
+ do_check_eq(typeof loadPathHash, "string");
+ do_check_eq(loadPathHash.length, 44);
+
+ // A search should not cause the search reset prompt.
+ let submission =
+ Services.search.currentEngine.getSubmission("foo", null, "searchbar");
+ do_check_eq(submission.uri.spec,
+ "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
+});
+
+add_task(function* test_promptURLs() {
+ yield removeLoadPathHash();
+
+ // The default should still be the test engine.
+ let currentEngine = Services.search.currentEngine;
+ do_check_eq(currentEngine.name, kTestEngineName);
+ // but the submission url should be about:searchreset
+ let url = (data, purpose) =>
+ currentEngine.getSubmission(data, null, purpose).uri.spec;
+ do_check_eq(url("foo", "searchbar"),
+ "about:searchreset?data=foo&purpose=searchbar");
+ do_check_eq(url("foo"), "about:searchreset?data=foo");
+ do_check_eq(url("", "searchbar"), "about:searchreset?purpose=searchbar");
+ do_check_eq(url(""), "about:searchreset");
+ do_check_eq(url("", ""), "about:searchreset");
+
+ // Calling the currentEngine setter for the same engine should
+ // prevent further prompts.
+ Services.search.currentEngine = Services.search.currentEngine;
+ do_check_eq(url("foo", "searchbar"),
+ "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t");
+
+ // And the loadPathHash should be back.
+ yield promiseAfterCache();
+ let metadata = yield promiseEngineMetadata();
+ do_check_true("loadPathHash" in metadata[kTestEngineShortName]);
+ let loadPathHash = metadata[kTestEngineShortName].loadPathHash;
+ do_check_eq(typeof loadPathHash, "string");
+ do_check_eq(loadPathHash.length, 44);
+});
+
+add_task(function* test_whitelist() {
+ yield removeLoadPathHash();
+
+ // The default should still be the test engine.
+ let currentEngine = Services.search.currentEngine;
+ do_check_eq(currentEngine.name, kTestEngineName);
+ let expectPrompt = shouldPrompt => {
+ let expectedURL =
+ shouldPrompt ? "about:searchreset?data=foo&purpose=searchbar"
+ : "http://www.google.com/search?q=foo&ie=utf-8&oe=utf-8&aq=t";
+ let url = currentEngine.getSubmission("foo", null, "searchbar").uri.spec;
+ do_check_eq(url, expectedURL);
+ };
+ expectPrompt(true);
+
+ // Unless we whitelist our test engine.
+ let branch = Services.prefs.getDefaultBranch(BROWSER_SEARCH_PREF);
+ let initialWhiteList = branch.getCharPref(kWhiteListPrefName);
+ branch.setCharPref(kWhiteListPrefName, "example.com,test.tld");
+ expectPrompt(true);
+ branch.setCharPref(kWhiteListPrefName, "www.google.com");
+ expectPrompt(false);
+ branch.setCharPref(kWhiteListPrefName, "example.com,www.google.com,test.tld");
+ expectPrompt(false);
+
+ // The loadPathHash should not be back after the prompt was skipped due to the
+ // whitelist.
+ yield asyncReInit();
+ let metadata = yield promiseEngineMetadata();
+ do_check_false("loadPathHash" in metadata[kTestEngineShortName]);
+
+ branch.setCharPref(kWhiteListPrefName, initialWhiteList);
+ expectPrompt(true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_searchSuggest.js b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
new file mode 100644
index 000000000..9de2967fc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_searchSuggest.js
@@ -0,0 +1,572 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/**
+ * Testing search suggestions from SearchSuggestionController.jsm.
+ */
+
+"use strict";
+
+Cu.import("resource://gre/modules/FormHistory.jsm");
+Cu.import("resource://gre/modules/SearchSuggestionController.jsm");
+Cu.import("resource://gre/modules/Timer.jsm");
+
+// We must make sure the FormHistoryStartup component is
+// initialized in order for it to respond to FormHistory
+// requests from nsFormAutoComplete.js.
+var formHistoryStartup = Cc["@mozilla.org/satchel/form-history-startup;1"].
+ getService(Ci.nsIObserver);
+formHistoryStartup.observe(null, "profile-after-change", null);
+
+var httpServer = new HttpServer();
+var getEngine, postEngine, unresolvableEngine;
+
+function run_test() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+
+ removeMetadata();
+ updateAppInfo();
+
+ let server = useHttpServer();
+ server.registerContentType("sjs", "sjs");
+
+ do_register_cleanup(() => Task.spawn(function* cleanup() {
+ // Remove added form history entries
+ yield updateSearchHistory("remove", null);
+ FormHistory.shutdown();
+ Services.prefs.clearUserPref("browser.search.suggest.enabled");
+ }));
+
+ run_next_test();
+}
+
+add_task(function* add_test_engines() {
+ let getEngineData = {
+ baseURL: gDataUrl,
+ name: "GET suggestion engine",
+ method: "GET",
+ };
+
+ let postEngineData = {
+ baseURL: gDataUrl,
+ name: "POST suggestion engine",
+ method: "POST",
+ };
+
+ let unresolvableEngineData = {
+ baseURL: "http://example.invalid/",
+ name: "Offline suggestion engine",
+ method: "GET",
+ };
+
+ [getEngine, postEngine, unresolvableEngine] = yield addTestEngines([
+ {
+ name: getEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(getEngineData),
+ },
+ {
+ name: postEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(postEngineData),
+ },
+ {
+ name: unresolvableEngineData.name,
+ xmlFileName: "engineMaker.sjs?" + JSON.stringify(unresolvableEngineData),
+ },
+ ]);
+});
+
+
+// Begin tests
+
+add_task(function* simple_no_result_callback() {
+ let deferred = Promise.defer();
+ let controller = new SearchSuggestionController((result) => {
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+ deferred.resolve();
+ });
+
+ controller.fetch("no remote", false, getEngine);
+ yield deferred.promise;
+});
+
+add_task(function* simple_no_result_callback_and_promise() {
+ // Make sure both the callback and promise get results
+ let deferred = Promise.defer();
+ let controller = new SearchSuggestionController((result) => {
+ do_check_eq(result.term, "no results");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+ deferred.resolve();
+ });
+
+ let result = yield controller.fetch("no results", false, getEngine);
+ do_check_eq(result.term, "no results");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+
+ yield deferred.promise;
+});
+
+add_task(function* simple_no_result_promise() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("no remote", false, getEngine);
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* simple_remote_no_local_result() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 3);
+ do_check_eq(result.remote[0], "Mozilla");
+ do_check_eq(result.remote[1], "modern");
+ do_check_eq(result.remote[2], "mom");
+});
+
+add_task(function* remote_term_case_mismatch() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Query Case Mismatch", false, getEngine);
+ do_check_eq(result.term, "Query Case Mismatch");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "Query Case Mismatch");
+});
+
+add_task(function* simple_local_no_remote_result() {
+ yield updateSearchHistory("bump", "no remote entries");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("no remote", false, getEngine);
+ do_check_eq(result.term, "no remote");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "no remote entries");
+ do_check_eq(result.remote.length, 0);
+
+ yield updateSearchHistory("remove", "no remote entries");
+});
+
+add_task(function* simple_non_ascii() {
+ yield updateSearchHistory("bump", "I ❤️ XUL");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("I ❤️", false, getEngine);
+ do_check_eq(result.term, "I ❤️");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "I ❤️ XUL");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "I ❤️ Mozilla");
+});
+
+add_task(function* both_local_remote_result_dedupe() {
+ yield updateSearchHistory("bump", "Mozilla");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Mozilla");
+ do_check_eq(result.remote.length, 2);
+ do_check_eq(result.remote[0], "modern");
+ do_check_eq(result.remote[1], "mom");
+});
+
+add_task(function* POST_both_local_remote_result_dedupe() {
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, postEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Mozilla");
+ do_check_eq(result.remote.length, 2);
+ do_check_eq(result.remote[0], "modern");
+ do_check_eq(result.remote[1], "mom");
+});
+
+add_task(function* both_local_remote_result_dedupe2() {
+ yield updateSearchHistory("bump", "mom");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 2);
+ do_check_eq(result.local[0], "mom");
+ do_check_eq(result.local[1], "Mozilla");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "modern");
+});
+
+add_task(function* both_local_remote_result_dedupe3() {
+ // All of the server entries also exist locally
+ yield updateSearchHistory("bump", "modern");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("mo", false, getEngine);
+ do_check_eq(result.term, "mo");
+ do_check_eq(result.local.length, 3);
+ do_check_eq(result.local[0], "modern");
+ do_check_eq(result.local[1], "mom");
+ do_check_eq(result.local[2], "Mozilla");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* fetch_twice_in_a_row() {
+ // Two entries since the first will match the first fetch but not the second.
+ yield updateSearchHistory("bump", "delay local");
+ yield updateSearchHistory("bump", "delayed local");
+
+ let controller = new SearchSuggestionController();
+ let resultPromise1 = controller.fetch("delay", false, getEngine);
+
+ // A second fetch while the server is still waiting to return results leads to an abort.
+ let resultPromise2 = controller.fetch("delayed ", false, getEngine);
+ yield resultPromise1.then((results) => do_check_null(results));
+
+ let result = yield resultPromise2;
+ do_check_eq(result.term, "delayed ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "delayed local");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "delayed ");
+});
+
+add_task(function* fetch_twice_subset_reuse_formHistoryResult() {
+ // This tests if we mess up re-using the cached form history result.
+ // Two entries since the first will match the first fetch but not the second.
+ yield updateSearchHistory("bump", "delay local");
+ yield updateSearchHistory("bump", "delayed local");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("delay", false, getEngine);
+ do_check_eq(result.term, "delay");
+ do_check_eq(result.local.length, 2);
+ do_check_eq(result.local[0], "delay local");
+ do_check_eq(result.local[1], "delayed local");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "delay");
+
+ // Remove the entry from the DB but it should remain in the cached formHistoryResult.
+ yield updateSearchHistory("remove", "delayed local");
+
+ let result2 = yield controller.fetch("delayed ", false, getEngine);
+ do_check_eq(result2.term, "delayed ");
+ do_check_eq(result2.local.length, 1);
+ do_check_eq(result2.local[0], "delayed local");
+ do_check_eq(result2.remote.length, 1);
+ do_check_eq(result2.remote[0], "delayed ");
+});
+
+add_task(function* both_identical_with_more_than_max_results() {
+ // Add letters A through Z to form history which will match the server
+ for (let charCode = "A".charCodeAt(); charCode <= "Z".charCodeAt(); charCode++) {
+ yield updateSearchHistory("bump", "letter " + String.fromCharCode(charCode));
+ }
+
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 7;
+ controller.maxRemoteResults = 10;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 7);
+ for (let i = 0; i < controller.maxLocalResults; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.local.length + result.remote.length, 10);
+ for (let i = 0; i < result.remote.length; i++) {
+ do_check_eq(result.remote[i],
+ "letter " + String.fromCharCode("A".charCodeAt() + controller.maxLocalResults + i));
+ }
+});
+
+add_task(function* noremote_maxLocal() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2; // (should be ignored because no remote results)
+ controller.maxRemoteResults = 0;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < result.local.length; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* someremote_maxLocal() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 2;
+ controller.maxRemoteResults = 4;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 2);
+ for (let i = 0; i < result.local.length; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 2);
+ // "A" and "B" will have been de-duped, start at C for remote results
+ for (let i = 0; i < result.remote.length; i++) {
+ do_check_eq(result.remote[i], "letter " + String.fromCharCode("C".charCodeAt() + i));
+ }
+});
+
+add_task(function* one_of_each() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "letter A");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter B");
+});
+
+add_task(function* local_result_returned_remote_result_disabled() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* local_result_returned_remote_result_disabled_after_creation_of_controller() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 1;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* one_of_each_disabled_before_creation_enabled_after_creation_of_controller() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", false);
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 2;
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "letter A");
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter B");
+});
+
+add_task(function* reset_suggestions_pref() {
+ Services.prefs.setBoolPref("browser.search.suggest.enabled", true);
+});
+
+add_task(function* one_local_zero_remote() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 1;
+ controller.maxRemoteResults = 0;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 26);
+ for (let i = 0; i < 26; i++) {
+ do_check_eq(result.local[i], "letter " + String.fromCharCode("A".charCodeAt() + i));
+ }
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* zero_local_one_remote() {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 1;
+ let result = yield controller.fetch("letter ", false, getEngine);
+ do_check_eq(result.term, "letter ");
+ do_check_eq(result.local.length, 0);
+ do_check_eq(result.remote.length, 1);
+ do_check_eq(result.remote[0], "letter A");
+});
+
+add_task(function* stop_search() {
+ let controller = new SearchSuggestionController((result) => {
+ do_throw("The callback shouldn't be called after stop()");
+ });
+ let resultPromise = controller.fetch("mo", false, getEngine);
+ controller.stop();
+ yield resultPromise.then((result) => {
+ do_check_null(result);
+ });
+});
+
+add_task(function* empty_searchTerm() {
+ // Empty searches don't go to the server but still get form history.
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("", false, getEngine);
+ do_check_eq(result.term, "");
+ do_check_true(result.local.length > 0);
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* slow_timeout() {
+ let d = Promise.defer();
+ function check_result(result) {
+ do_check_eq(result.term, "slow ");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "slow local result");
+ do_check_eq(result.remote.length, 0);
+ }
+ yield updateSearchHistory("bump", "slow local result");
+
+ let controller = new SearchSuggestionController();
+ setTimeout(function check_timeout() {
+ // The HTTP response takes 10 seconds so check that we already have results after 2 seconds.
+ check_result(result);
+ d.resolve();
+ }, 2000);
+ let result = yield controller.fetch("slow ", false, getEngine);
+ check_result(result);
+ yield d.promise;
+});
+
+add_task(function* slow_stop() {
+ let d = Promise.defer();
+ let controller = new SearchSuggestionController();
+ let resultPromise = controller.fetch("slow ", false, getEngine);
+ setTimeout(function check_timeout() {
+ // The HTTP response takes 10 seconds but we timeout in less than a second so just use 0.
+ controller.stop();
+ d.resolve();
+ }, 0);
+ yield resultPromise.then((result) => {
+ do_check_null(result);
+ });
+
+ yield d.promise;
+});
+
+
+// Error handling
+
+add_task(function* remote_term_mismatch() {
+ yield updateSearchHistory("bump", "Query Mismatch Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Query Mismatch", false, getEngine);
+ do_check_eq(result.term, "Query Mismatch");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Query Mismatch Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* http_404() {
+ yield updateSearchHistory("bump", "HTTP 404 Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("HTTP 404", false, getEngine);
+ do_check_eq(result.term, "HTTP 404");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "HTTP 404 Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* http_500() {
+ yield updateSearchHistory("bump", "HTTP 500 Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("HTTP 500", false, getEngine);
+ do_check_eq(result.term, "HTTP 500");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "HTTP 500 Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+add_task(function* unresolvable_server() {
+ yield updateSearchHistory("bump", "Unresolvable Server Entry");
+
+ let controller = new SearchSuggestionController();
+ let result = yield controller.fetch("Unresolvable Server", false, unresolvableEngine);
+ do_check_eq(result.term, "Unresolvable Server");
+ do_check_eq(result.local.length, 1);
+ do_check_eq(result.local[0], "Unresolvable Server Entry");
+ do_check_eq(result.remote.length, 0);
+});
+
+
+// Exception handling
+
+add_task(function* missing_pb() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No privacy");
+ }, /priva/i);
+});
+
+add_task(function* missing_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("No engine", false);
+ }, /engine/i);
+});
+
+add_task(function* invalid_engine() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.fetch("invalid engine", false, {});
+ }, /engine/i);
+});
+
+add_task(function* no_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = 0;
+ controller.maxRemoteResults = 0;
+ controller.fetch("No results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(function* minus_one_results_requested() {
+ Assert.throws(() => {
+ let controller = new SearchSuggestionController();
+ controller.maxLocalResults = -1;
+ controller.fetch("-1 results requested", false, getEngine);
+ }, /result/i);
+});
+
+add_task(function* test_userContextId() {
+ let controller = new SearchSuggestionController();
+ controller._fetchRemote = function(searchTerm, engine, privateMode, userContextId) {
+ Assert.equal(userContextId, 1);
+ return Promise.defer();
+ };
+
+ controller.fetch("test", false, getEngine, 1);
+});
+
+// Helpers
+
+function updateSearchHistory(operation, value) {
+ let deferred = Promise.defer();
+ FormHistory.update({
+ op: operation,
+ fieldname: "searchbar-history",
+ value: value,
+ },
+ {
+ handleError: function (error) {
+ do_throw("Error occurred updating form history: " + error);
+ deferred.reject(error);
+ },
+ handleCompletion: function (reason) {
+ if (!reason)
+ deferred.resolve();
+ }
+ });
+ return deferred.promise;
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_selectedEngine.js b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
new file mode 100644
index 000000000..a1c0f363e
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_selectedEngine.js
@@ -0,0 +1,165 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://gre/modules/osfile.jsm");
+
+const kSelectedEnginePref = "browser.search.selectedEngine";
+
+// Check that the default engine matches the defaultenginename pref
+add_task(function* test_defaultEngine() {
+ yield asyncInit();
+
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// Giving prefs a user value shouldn't change the selected engine.
+add_task(function* test_selectedEngine() {
+ let defaultEngineName = getDefaultEngineName();
+ // Test the selectedEngine pref.
+ Services.prefs.setCharPref(kSelectedEnginePref, kTestEngineName);
+
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultEngineName);
+
+ Services.prefs.clearUserPref(kSelectedEnginePref);
+
+ // Test the defaultenginename pref.
+ Services.prefs.setCharPref(kDefaultenginenamePref, kTestEngineName);
+
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultEngineName);
+
+ Services.prefs.clearUserPref(kDefaultenginenamePref);
+});
+
+// Setting the search engine should be persisted across restarts.
+add_task(function* test_persistAcrossRestarts() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Check that the a hash was saved.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.hash.length, 44);
+
+ // Re-init and check the engine is still the same.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+
+ // Cleanup (set the engine back to default).
+ Services.search.resetToOriginalDefaultEngine();
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// An engine set without a valid hash should be ignored.
+add_task(function* test_ignoreInvalidHash() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Then mess with the file (make the hash invalid).
+ let metadata = yield promiseGlobalMetadata();
+ metadata.hash = "invalid";
+ yield promiseSaveGlobalMetadata(metadata);
+
+ // Re-init the search service, and check that the json file is ignored.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, getDefaultEngineName());
+});
+
+// Resetting the engine to the default should remove the saved value.
+add_task(function* test_settingToDefault() {
+ // Set the engine through the API.
+ Services.search.currentEngine = Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ // Check that the current engine was saved.
+ let metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.current, kTestEngineName);
+
+ // Then set the engine back to the default through the API.
+ Services.search.currentEngine =
+ Services.search.getEngineByName(getDefaultEngineName());
+ yield promiseAfterCache();
+
+ // Check that the current engine is no longer saved in the JSON file.
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata.current, "");
+});
+
+add_task(function* test_resetToOriginalDefaultEngine() {
+ let defaultName = getDefaultEngineName();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+
+ Services.search.currentEngine =
+ Services.search.getEngineByName(kTestEngineName);
+ do_check_eq(Services.search.currentEngine.name, kTestEngineName);
+ yield promiseAfterCache();
+
+ Services.search.resetToOriginalDefaultEngine();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+ yield promiseAfterCache();
+});
+
+add_task(function* test_fallback_kept_after_restart() {
+ // Set current engine to a default engine that isn't the original default.
+ let builtInEngines = Services.search.getDefaultEngines();
+ let defaultName = getDefaultEngineName();
+ let nonDefaultBuiltInEngine;
+ for (let engine of builtInEngines) {
+ if (engine.name != defaultName) {
+ nonDefaultBuiltInEngine = engine;
+ break;
+ }
+ }
+ Services.search.currentEngine = nonDefaultBuiltInEngine;
+ do_check_eq(Services.search.currentEngine.name, nonDefaultBuiltInEngine.name);
+ yield promiseAfterCache();
+
+ // Remove that engine...
+ Services.search.removeEngine(nonDefaultBuiltInEngine);
+ // The engine being a default (built-in) one, it should be hidden
+ // rather than actually removed.
+ do_check_true(nonDefaultBuiltInEngine.hidden);
+
+ // Using the currentEngine getter should force a fallback to the
+ // original default engine.
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+
+ // Restoring the default engines should unhide our built-in test
+ // engine, but not change the value of currentEngine.
+ Services.search.restoreDefaultEngines();
+ do_check_false(nonDefaultBuiltInEngine.hidden);
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+ yield promiseAfterCache();
+
+ // After a restart, the currentEngine value should still be unchanged.
+ yield asyncReInit();
+ do_check_eq(Services.search.currentEngine.name, defaultName);
+});
+
+
+function run_test() {
+ 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();
+ });
+
+ run_next_test();
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_svg_icon.js b/toolkit/components/search/tests/xpcshell/test_svg_icon.js
new file mode 100644
index 000000000..5fd4781a1
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_svg_icon.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var url;
+var requestHandled;
+
+const icon =
+ '<?xml version="1.0" encoding="UTF-8" standalone="no"?>' +
+ '<svg xmlns="http://www.w3.org/2000/svg" ' +
+ 'width="16" height="16" viewBox="0 0 16 16">' +
+ '<rect x="4" y="4" width="8px" height="8px" style="fill: blue"/>' +
+ '</svg>';
+
+function run_test() {
+ updateAppInfo();
+ useHttpServer(); // Unused, but required to call addTestEngines.
+
+ requestHandled = new Promise(resolve => {
+ let srv = new HttpServer();
+ srv.registerPathHandler("/icon.svg", (metadata, response) => {
+ response.setStatusLine("1.0", 200, "OK");
+ response.setHeader("Content-Type", "image/svg+xml", false);
+
+ response.write(icon);
+ resolve();
+ });
+ srv.start(-1);
+ do_register_cleanup(() => srv.stop(() => {}));
+
+ url = "http://localhost:" + srv.identity.primaryPort + "/icon.svg";
+ });
+
+ run_next_test();
+}
+
+add_task(function* test_svg_icon() {
+ yield asyncInit();
+
+ let [engine] = yield addTestEngines([
+ { name: "SVGIcon", details: [url, "", "SVG icon", "GET",
+ "http://icon.svg/search?q={searchTerms}"] },
+ ]);
+
+ yield requestHandled;
+ yield promiseAfterCache();
+
+ ok(engine.iconURI, "the engine has an icon");
+ ok(engine.iconURI.spec.startsWith("data:image/svg+xml"),
+ "the icon is saved as an SVG data url");
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sync.js b/toolkit/components/search/tests/xpcshell/test_sync.js
new file mode 100644
index 000000000..8f4eb22ee
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync.js
@@ -0,0 +1,27 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // Check the hidden engine is not loaded.
+ engine = Services.search.getEngineByName("hidden");
+ do_check_eq(engine, null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_addon.js b/toolkit/components/search/tests/xpcshell/test_sync_addon.js
new file mode 100644
index 000000000..7af957506
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test the add-on engine is loaded in addition to our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 2);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("addon");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "addon");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js b/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
new file mode 100644
index 000000000..3f4494905
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_addon_no_override.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installAddonEngine("engine-override");
+
+ do_check_false(Services.search.isInitialized);
+
+ // test the add-on engine isn't overriding our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js b/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js
new file mode 100644
index 000000000..1b41a71bf
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_delay_fallback.js
@@ -0,0 +1,52 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+ let fallback = false;
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(fallback);
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_test_finished();
+ });
+
+ // Execute test for the sync fallback while the async code is being executed.
+ Services.obs.addObserver(function searchServiceObserver(aResult, aTopic, aVerb) {
+ if (aVerb == "find-jar-engines") {
+ Services.obs.removeObserver(searchServiceObserver, aTopic);
+ fallback = true;
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_true(Services.search.isInitialized);
+ }
+ }, "browser-search-service", false);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_distribution.js b/toolkit/components/search/tests/xpcshell/test_sync_distribution.js
new file mode 100644
index 000000000..63a8b66f0
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_distribution.js
@@ -0,0 +1,26 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+ installDistributionEngine();
+
+ do_check_false(Services.search.isInitialized);
+
+ // test that the engine from the distribution overrides our jar engine
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ // check the engine we have is actually the one from the distribution
+ do_check_eq(engine.description, "override");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_fallback.js b/toolkit/components/search/tests/xpcshell/test_sync_fallback.js
new file mode 100644
index 000000000..dad73fabc
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_fallback.js
@@ -0,0 +1,42 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_test_pending();
+
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ do_check_false(Services.search.isInitialized);
+
+ Services.search.init(function search_initialized(aStatus) {
+ do_check_true(Components.isSuccessCode(aStatus));
+ do_check_true(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_test_finished();
+ });
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_migration.js b/toolkit/components/search/tests/xpcshell/test_sync_migration.js
new file mode 100644
index 000000000..53e945dfd
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_migration.js
@@ -0,0 +1,29 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* Test that legacy metadata from search-metadata.json is correctly
+ * transferred to the new metadata storage. */
+
+function run_test() {
+ updateAppInfo();
+ installTestEngine();
+
+ do_get_file("data/metadata.json").copyTo(gProfD, "search-metadata.json");
+
+ run_next_test();
+}
+
+add_task(function* test_sync_metadata_migration() {
+ do_check_false(Services.search.isInitialized);
+ Services.search.getEngines();
+ do_check_true(Services.search.isInitialized);
+ yield promiseAfterCache();
+
+ // Check that the entries are placed as specified correctly
+ let metadata = yield promiseEngineMetadata();
+ do_check_eq(metadata["engine"].order, 1);
+ do_check_eq(metadata["engine"].alias, "foo");
+
+ metadata = yield promiseGlobalMetadata();
+ do_check_eq(metadata["searchDefaultExpir"], 1471013469846);
+});
diff --git a/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js b/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
new file mode 100644
index 000000000..f8d38e571
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_sync_profile_engine.js
@@ -0,0 +1,35 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const NS_APP_USER_SEARCH_DIR = "UsrSrchPlugns";
+
+function run_test() {
+ removeMetadata();
+ removeCacheFile();
+
+ do_load_manifest("data/chrome.manifest");
+
+ configureToLoadJarEngines();
+
+ // Copy an engine in [profile]/searchplugin/ and ensure it's not
+ // overriding the same file from a jar.
+ // The description in the file we are copying is 'profile'.
+ let dir = Services.dirsvc.get(NS_APP_USER_SEARCH_DIR, Ci.nsIFile);
+ if (!dir.exists())
+ dir.create(dir.DIRECTORY_TYPE, FileUtils.PERMS_DIRECTORY);
+ do_get_file("data/engine-override.xml").copyTo(dir, "bug645970.xml");
+
+ do_check_false(Services.search.isInitialized);
+
+ // test engines from dir are not loaded.
+ let engines = Services.search.getEngines();
+ do_check_eq(engines.length, 1);
+
+ do_check_true(Services.search.isInitialized);
+
+ // test jar engine is loaded ok.
+ let engine = Services.search.getEngineByName("bug645970");
+ do_check_neq(engine, null);
+
+ do_check_eq(engine.description, "bug645970");
+}
diff --git a/toolkit/components/search/tests/xpcshell/test_update_telemetry.js b/toolkit/components/search/tests/xpcshell/test_update_telemetry.js
new file mode 100644
index 000000000..f73e765c6
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/test_update_telemetry.js
@@ -0,0 +1,36 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function run_test() {
+ do_check_false(Services.search.isInitialized);
+
+ useHttpServer();
+ run_next_test();
+}
+
+function checkTelemetry(histogramName, expected) {
+ let histogram = Services.telemetry.getHistogramById(histogramName);
+ let snapshot = histogram.snapshot();
+ let expectedCounts = [0, 0, 0];
+ expectedCounts[expected ? 1 : 0] = 1;
+ Assert.deepEqual(snapshot.counts, expectedCounts,
+ "histogram has expected content");
+ histogram.clear();
+}
+
+add_task(function* ignore_cache_files_without_engines() {
+ yield asyncInit();
+
+ checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", false);
+ checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", false);
+
+ // Add an engine with update urls and re-init, as we record the presence of
+ // engine update urls only while initializing the search service.
+ yield addTestEngines([
+ { name: "update", xmlFileName: "engine-update.xml" },
+ ]);
+ yield asyncReInit();
+
+ checkTelemetry("SEARCH_SERVICE_HAS_UPDATES", true);
+ checkTelemetry("SEARCH_SERVICE_HAS_ICON_UPDATES", true);
+});
diff --git a/toolkit/components/search/tests/xpcshell/xpcshell.ini b/toolkit/components/search/tests/xpcshell/xpcshell.ini
new file mode 100644
index 000000000..1fb5a3423
--- /dev/null
+++ b/toolkit/components/search/tests/xpcshell/xpcshell.ini
@@ -0,0 +1,102 @@
+[DEFAULT]
+head = head_search.js
+tail =
+firefox-appdir = browser
+skip-if = toolkit == 'android'
+support-files =
+ data/chrome.manifest
+ data/engine.xml
+ data/engine2.xml
+ data/engine-addon.xml
+ data/engine-override.xml
+ data/engine-app.xml
+ data/engine-fr.xml
+ data/engineMaker.sjs
+ data/engine-pref.xml
+ data/engine-rel-searchform.xml
+ data/engine-rel-searchform-post.xml
+ data/engine-rel-searchform-purpose.xml
+ data/engine-system-purpose.xml
+ data/engine-update.xml
+ data/engineImages.xml
+ data/engine-chromeicon.xml
+ data/engine-resourceicon.xml
+ data/ico-size-16x16-png.ico
+ data/invalid-engine.xml
+ data/install.rdf
+ data/list.json
+ data/langpack-metadata.json
+ data/metadata.json
+ data/search.json
+ data/search.sqlite
+ 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]
+[test_location_migrate_countrycode_isUS.js]
+[test_location_migrate_no_countrycode_isUS.js]
+[test_location_migrate_no_countrycode_notUS.js]
+[test_location_partner.js]
+[test_location_funnelcake.js]
+[test_location_sync.js]
+[test_location_timeout.js]
+[test_location_timeout_xhr.js]
+[test_nodb.js]
+[test_nodb_pluschanges.js]
+[test_save_sorted_engines.js]
+[test_pref.js]
+[test_purpose.js]
+[test_defaultEngine.js]
+[test_notifications.js]
+[test_parseSubmissionURL.js]
+[test_SearchStaticData.js]
+[test_addEngine_callback.js]
+[test_migration_langpack.js]
+[test_multipleIcons.js]
+[test_resultDomain.js]
+[test_searchSuggest.js]
+[test_async.js]
+[test_async_addon.js]
+tags = addons
+[test_async_addon_no_override.js]
+tags = addons
+[test_async_distribution.js]
+[test_async_migration.js]
+[test_async_profile_engine.js]
+[test_sync.js]
+[test_sync_addon.js]
+tags = addons
+[test_sync_addon_no_override.js]
+tags = addons
+[test_sync_distribution.js]
+[test_sync_fallback.js]
+[test_sync_delay_fallback.js]
+[test_sync_migration.js]
+[test_sync_profile_engine.js]
+[test_rel_searchform.js]
+[test_remove_profile_engine.js]
+[test_selectedEngine.js]
+[test_geodefaults.js]
+[test_hidden.js]
+[test_currentEngine_fallback.js]
+[test_require_engines_in_cache.js]
+[test_update_telemetry.js]
+[test_svg_icon.js]
+[test_searchReset.js]
+[test_addEngineWithDetails.js]
+[test_chromeresource_icon1.js]
+[test_chromeresource_icon2.js]
+[test_engineUpdate.js]