summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test')
-rw-r--r--browser/components/search/test/browser/426329.xml11
-rw-r--r--browser/components/search/test/browser/483086-1.xml10
-rw-r--r--browser/components/search/test/browser/483086-2.xml10
-rw-r--r--browser/components/search/test/browser/browser.ini79
-rw-r--r--browser/components/search/test/browser/browser_426329.js333
-rw-r--r--browser/components/search/test/browser/browser_483086.js56
-rw-r--r--browser/components/search/test/browser/browser_addEngine.js121
-rw-r--r--browser/components/search/test/browser/browser_contentContextMenu.js224
-rw-r--r--browser/components/search/test/browser/browser_contentContextMenu.xhtml22
-rw-r--r--browser/components/search/test/browser/browser_contentSearchUI.js1084
-rw-r--r--browser/components/search/test/browser/browser_contentSearchUI_default.js116
-rw-r--r--browser/components/search/test/browser/browser_contextSearchTabPosition.js82
-rw-r--r--browser/components/search/test/browser/browser_contextmenu.js236
-rw-r--r--browser/components/search/test/browser/browser_google_behavior.js207
-rw-r--r--browser/components/search/test/browser/browser_healthreport.js110
-rw-r--r--browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js123
-rw-r--r--browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js70
-rw-r--r--browser/components/search/test/browser/browser_oneOffContextMenu.js83
-rw-r--r--browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js235
-rw-r--r--browser/components/search/test/browser/browser_private_search_perwindowpb.js81
-rw-r--r--browser/components/search/test/browser/browser_searchEngine_behaviors.js205
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_aboutHome.js122
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_content.js198
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_searchbar.js467
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_sources.js336
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_sources_ads.js355
-rw-r--r--browser/components/search/test/browser/browser_search_telemetry_sources_navigation.js238
-rw-r--r--browser/components/search/test/browser/browser_searchbar_addEngine.js68
-rw-r--r--browser/components/search/test/browser/browser_searchbar_context.js199
-rw-r--r--browser/components/search/test/browser/browser_searchbar_default.js183
-rw-r--r--browser/components/search/test/browser/browser_searchbar_focus_timing.js87
-rw-r--r--browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js647
-rw-r--r--browser/components/search/test/browser/browser_searchbar_openpopup.js792
-rw-r--r--browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js447
-rw-r--r--browser/components/search/test/browser/browser_searchbar_widths.js33
-rw-r--r--browser/components/search/test/browser/browser_tooManyEnginesOffered.js104
-rw-r--r--browser/components/search/test/browser/contentSearchUI.html22
-rw-r--r--browser/components/search/test/browser/contentSearchUI.js13
-rw-r--r--browser/components/search/test/browser/google_codes/browser.ini6
-rw-r--r--browser/components/search/test/browser/head.js233
-rw-r--r--browser/components/search/test/browser/mozsearch.sjs11
-rw-r--r--browser/components/search/test/browser/opensearch.html10
-rw-r--r--browser/components/search/test/browser/searchSuggestionEngine.sjs50
-rw-r--r--browser/components/search/test/browser/searchTelemetry.html10
-rw-r--r--browser/components/search/test/browser/searchTelemetryAd.html9
-rw-r--r--browser/components/search/test/browser/telemetrySearchSuggestions.sjs9
-rw-r--r--browser/components/search/test/browser/telemetrySearchSuggestions.xml6
-rw-r--r--browser/components/search/test/browser/test.html8
-rw-r--r--browser/components/search/test/browser/testEngine.xml12
-rw-r--r--browser/components/search/test/browser/testEngine_diacritics.xml12
-rw-r--r--browser/components/search/test/browser/testEngine_dupe.xml12
-rw-r--r--browser/components/search/test/browser/testEngine_mozsearch.xml14
-rw-r--r--browser/components/search/test/browser/test_search.html1
-rw-r--r--browser/components/search/test/browser/tooManyEnginesOffered.html13
-rw-r--r--browser/components/search/test/marionette/manifest.ini4
-rw-r--r--browser/components/search/test/marionette/test_engines_on_restart.py41
-rw-r--r--browser/components/search/test/unit/test_urlTelemetry.js253
-rw-r--r--browser/components/search/test/unit/xpcshell.ini4
58 files changed, 8527 insertions, 0 deletions
diff --git a/browser/components/search/test/browser/426329.xml b/browser/components/search/test/browser/426329.xml
new file mode 100644
index 0000000000..b565ed7288
--- /dev/null
+++ b/browser/components/search/test/browser/426329.xml
@@ -0,0 +1,11 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Bug 426329</ShortName>
+ <Description>426329 Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/test.html">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/test.html</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/483086-1.xml b/browser/components/search/test/browser/483086-1.xml
new file mode 100644
index 0000000000..765cd13d4f
--- /dev/null
+++ b/browser/components/search/test/browser/483086-1.xml
@@ -0,0 +1,10 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>483086a</ShortName>
+ <Description>Bug 483086 Test 1</Description>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>foo://example.com</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/483086-2.xml b/browser/components/search/test/browser/483086-2.xml
new file mode 100644
index 0000000000..ce952ac2e7
--- /dev/null
+++ b/browser/components/search/test/browser/483086-2.xml
@@ -0,0 +1,10 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>483086b</ShortName>
+ <Description>Bug 483086 Test 2</Description>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://example.com</moz:SearchForm>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/browser.ini b/browser/components/search/test/browser/browser.ini
new file mode 100644
index 0000000000..3f04570b45
--- /dev/null
+++ b/browser/components/search/test/browser/browser.ini
@@ -0,0 +1,79 @@
+[DEFAULT]
+prefs =
+ browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
+support-files =
+ mozsearch.sjs
+ test_search.html
+ 426329.xml
+ 483086-1.xml
+ 483086-2.xml
+ head.js
+ opensearch.html
+ test.html
+ testEngine.xml
+ testEngine_diacritics.xml
+ testEngine_dupe.xml
+ testEngine_mozsearch.xml
+ tooManyEnginesOffered.html
+
+[browser_426329.js]
+[browser_483086.js]
+[browser_addEngine.js]
+[browser_contentContextMenu.js]
+support-files = browser_contentContextMenu.xhtml
+[browser_contentSearchUI.js]
+support-files =
+ contentSearchUI.html
+ contentSearchUI.js
+ searchSuggestionEngine.sjs
+[browser_contentSearchUI_default.js]
+[browser_contextmenu.js]
+skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to 1531590
+[browser_contextSearchTabPosition.js]
+skip-if = verify && debug && os == 'win' # Bug 1412519
+[browser_google_behavior.js]
+skip-if = (os == 'linux') && ccov # Bug 1511273
+[browser_healthreport.js]
+skip-if = (verify && debug && (os == 'win' || os == 'linux'))
+[browser_hiddenOneOffs_cleanup.js]
+[browser_hiddenOneOffs_diacritics.js]
+[browser_oneOffContextMenu.js]
+[browser_oneOffContextMenu_setDefault.js]
+skip-if = (os == "win" && processor == "aarch64") # disabled on aarch64 due to Bug 1584886
+[browser_private_search_perwindowpb.js]
+[browser_search_telemetry_aboutHome.js]
+tags = search-telemetry
+[browser_search_telemetry_content.js]
+tags = search-telemetry
+[browser_search_telemetry_searchbar.js]
+tags = search-telemetry
+support-files =
+ telemetrySearchSuggestions.sjs
+ telemetrySearchSuggestions.xml
+[browser_search_telemetry_sources_ads.js]
+skip-if = !debug && (os == 'linux') # Bug 1515466
+tags = search-telemetry
+support-files =
+ searchTelemetry.html
+ searchTelemetryAd.html
+[browser_search_telemetry_sources_navigation.js]
+tags = search-telemetry
+support-files =
+ searchTelemetry.html
+ searchTelemetryAd.html
+[browser_search_telemetry_sources.js]
+tags = search-telemetry
+support-files =
+ searchTelemetry.html
+ searchTelemetryAd.html
+[browser_searchbar_addEngine.js]
+[browser_searchbar_context.js]
+[browser_searchbar_default.js]
+[browser_searchbar_focus_timing.js]
+[browser_searchbar_openpopup.js]
+skip-if = os == "linux" # Linux has different focus behaviours.
+[browser_searchbar_keyboard_navigation.js]
+[browser_searchbar_smallpanel_keyboard_navigation.js]
+[browser_searchbar_widths.js]
+[browser_searchEngine_behaviors.js]
+[browser_tooManyEnginesOffered.js]
diff --git a/browser/components/search/test/browser/browser_426329.js b/browser/components/search/test/browser/browser_426329.js
new file mode 100644
index 0000000000..cbd6096122
--- /dev/null
+++ b/browser/components/search/test/browser/browser_426329.js
@@ -0,0 +1,333 @@
+/* eslint-disable mozilla/no-arbitrary-setTimeout */
+XPCOMUtils.defineLazyModuleGetters(this, {
+ FormHistoryTestUtils: "resource://testing-common/FormHistoryTestUtils.jsm",
+});
+
+function expectedURL(aSearchTerms) {
+ const ENGINE_HTML_BASE =
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/test.html";
+ let searchArg = Services.textToSubURI.ConvertAndEscape("utf-8", aSearchTerms);
+ return ENGINE_HTML_BASE + "?test=" + searchArg;
+}
+
+function simulateClick(aEvent, aTarget) {
+ let event = document.createEvent("MouseEvent");
+ let ctrlKeyArg = aEvent.ctrlKey || false;
+ let altKeyArg = aEvent.altKey || false;
+ let shiftKeyArg = aEvent.shiftKey || false;
+ let metaKeyArg = aEvent.metaKey || false;
+ let buttonArg = aEvent.button || 0;
+ event.initMouseEvent(
+ "click",
+ true,
+ true,
+ window,
+ 0,
+ 0,
+ 0,
+ 0,
+ 0,
+ ctrlKeyArg,
+ altKeyArg,
+ shiftKeyArg,
+ metaKeyArg,
+ buttonArg,
+ null
+ );
+ aTarget.dispatchEvent(event);
+}
+
+// modified from toolkit/components/satchel/test/test_form_autocomplete.html
+function checkMenuEntries(expectedValues) {
+ let actualValues = getMenuEntries();
+ is(
+ actualValues.length,
+ expectedValues.length,
+ "Checking length of expected menu"
+ );
+ for (let i = 0; i < expectedValues.length; i++) {
+ is(actualValues[i], expectedValues[i], "Checking menu entry #" + i);
+ }
+}
+
+function getMenuEntries() {
+ // Could perhaps pull values directly from the controller, but it seems
+ // more reliable to test the values that are actually in the richlistbox?
+ return Array.from(searchBar.textbox.popup.richlistbox.itemChildren, item =>
+ item.getAttribute("ac-value")
+ );
+}
+
+var searchBar;
+var searchButton;
+var searchEntries = ["test"];
+function promiseSetEngine() {
+ return new Promise(resolve => {
+ let ss = Services.search;
+
+ function observer(aSub, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ let engine = ss.getEngineByName("Bug 426329");
+ ok(engine, "Engine was added.");
+ ss.defaultEngine = engine;
+ break;
+ case "engine-default":
+ ok(ss.defaultEngine.name == "Bug 426329", "defaultEngine set");
+ searchBar = BrowserSearch.searchBar;
+ searchButton = searchBar.querySelector(".search-go-button");
+ ok(searchButton, "got search-go-button");
+
+ Services.obs.removeObserver(
+ observer,
+ "browser-search-engine-modified"
+ );
+ resolve();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ ss.addOpenSearchEngine(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/426329.xml",
+ "data:image/x-icon,%00"
+ );
+ });
+}
+
+function promiseRemoveEngine() {
+ return new Promise(resolve => {
+ let ss = Services.search;
+
+ function observer(aSub, aTopic, aData) {
+ if (aData == "engine-removed") {
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ resolve();
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ let engine = ss.getEngineByName("Bug 426329");
+ ss.removeEngine(engine);
+ });
+}
+
+var preSelectedBrowser;
+var preTabNo;
+async function prepareTest() {
+ await Services.search.init();
+
+ preSelectedBrowser = gBrowser.selectedBrowser;
+ preTabNo = gBrowser.tabs.length;
+ searchBar = BrowserSearch.searchBar;
+
+ await SimpleTest.promiseFocus();
+
+ if (document.activeElement == searchBar) {
+ return;
+ }
+
+ let focusPromise = BrowserTestUtils.waitForEvent(searchBar.textbox, "focus");
+ gURLBar.focus();
+ searchBar.focus();
+ await focusPromise;
+}
+
+add_task(async function testSetup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function testSetupEngine() {
+ await promiseSetEngine();
+ searchBar.value = "test";
+});
+
+add_task(async function testReturn() {
+ await prepareTest();
+ EventUtils.synthesizeKey("KEY_Enter");
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+
+ is(gBrowser.tabs.length, preTabNo, "Return key did not open new tab");
+ is(
+ gBrowser.currentURI.spec,
+ expectedURL(searchBar.value),
+ "testReturn opened correct search page"
+ );
+});
+
+add_task(async function testAltReturn() {
+ await prepareTest();
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ EventUtils.synthesizeKey("KEY_Enter", { altKey: true });
+ });
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Alt+Return key added new tab");
+ is(
+ gBrowser.currentURI.spec,
+ expectedURL(searchBar.value),
+ "testAltReturn opened correct search page"
+ );
+});
+
+add_task(async function testAltGrReturn() {
+ await prepareTest();
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ EventUtils.synthesizeKey("KEY_Enter", { altGraphKey: true });
+ });
+
+ is(gBrowser.tabs.length, preTabNo + 1, "AltGr+Return key added new tab");
+ is(
+ gBrowser.currentURI.spec,
+ expectedURL(searchBar.value),
+ "testAltGrReturn opened correct search page"
+ );
+});
+
+// Shift key has no effect for now, so skip it
+add_task(async function testShiftAltReturn() {
+ /*
+ yield* prepareTest();
+
+ let url = expectedURL(searchBar.value);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ EventUtils.synthesizeKey("VK_RETURN", { shiftKey: true, altKey: true });
+ yield newTabPromise;
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Shift+Alt+Return key added new tab");
+ is(gBrowser.currentURI.spec, url, "testShiftAltReturn opened correct search page");
+ */
+});
+
+add_task(async function testLeftClick() {
+ await prepareTest();
+ simulateClick({ button: 0 }, searchButton);
+ await BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ is(gBrowser.tabs.length, preTabNo, "LeftClick did not open new tab");
+ is(
+ gBrowser.currentURI.spec,
+ expectedURL(searchBar.value),
+ "testLeftClick opened correct search page"
+ );
+});
+
+add_task(async function testMiddleClick() {
+ await prepareTest();
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, () => {
+ simulateClick({ button: 1 }, searchButton);
+ });
+ is(gBrowser.tabs.length, preTabNo + 1, "MiddleClick added new tab");
+ is(
+ gBrowser.currentURI.spec,
+ expectedURL(searchBar.value),
+ "testMiddleClick opened correct search page"
+ );
+});
+
+add_task(async function testShiftMiddleClick() {
+ await prepareTest();
+
+ let url = expectedURL(searchBar.value);
+
+ let newTabPromise = BrowserTestUtils.waitForNewTab(gBrowser, url);
+ simulateClick({ button: 1, shiftKey: true }, searchButton);
+ let newTab = await newTabPromise;
+
+ is(gBrowser.tabs.length, preTabNo + 1, "Shift+MiddleClick added new tab");
+ is(
+ newTab.linkedBrowser.currentURI.spec,
+ url,
+ "testShiftMiddleClick opened correct search page"
+ );
+});
+
+add_task(async function testRightClick() {
+ preTabNo = gBrowser.tabs.length;
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank", {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ });
+ await new Promise(resolve => {
+ setTimeout(function() {
+ is(gBrowser.tabs.length, preTabNo, "RightClick did not open new tab");
+ is(gBrowser.currentURI.spec, "about:blank", "RightClick did nothing");
+ resolve();
+ }, 5000);
+ simulateClick({ button: 2 }, searchButton);
+ });
+ // The click in the searchbox focuses it, which opens the suggestion
+ // panel. Clean up after ourselves.
+ searchBar.textbox.popup.hidePopup();
+});
+
+add_task(async function testSearchHistory() {
+ let textbox = searchBar._textbox;
+ for (let i = 0; i < searchEntries.length; i++) {
+ let count = await FormHistoryTestUtils.count(
+ textbox.getAttribute("autocompletesearchparam"),
+ { value: searchEntries[i], source: "Bug 426329" }
+ );
+ ok(count > 0, "form history entry '" + searchEntries[i] + "' should exist");
+ }
+});
+
+add_task(async function testAutocomplete() {
+ let popup = searchBar.textbox.popup;
+ let popupShownPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ searchBar.textbox.showHistoryPopup();
+ await popupShownPromise;
+ checkMenuEntries(searchEntries);
+ searchBar.textbox.closePopup();
+});
+
+add_task(async function testClearHistory() {
+ // Open the textbox context menu to trigger controller attachment.
+ let textbox = searchBar.textbox;
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ window,
+ "popupshown",
+ false,
+ event => event.target.classList.contains("textbox-contextmenu")
+ );
+ EventUtils.synthesizeMouseAtCenter(textbox, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await popupShownPromise;
+ // Close the context menu.
+ EventUtils.synthesizeKey("KEY_Escape");
+
+ let menuitem = searchBar._menupopup.querySelector(".searchbar-clear-history");
+ ok(!menuitem.disabled, "Clear history menuitem enabled");
+
+ let historyCleared = promiseObserver("satchel-storage-changed");
+ menuitem.click();
+ await historyCleared;
+ let count = await FormHistoryTestUtils.count(
+ textbox.getAttribute("autocompletesearchparam")
+ );
+ ok(count == 0, "History cleared");
+});
+
+add_task(async function asyncCleanup() {
+ searchBar.value = "";
+ while (gBrowser.tabs.length != 1) {
+ gBrowser.removeTab(gBrowser.tabs[0], { animate: false });
+ }
+ BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:blank", {
+ triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
+ });
+ await promiseRemoveEngine();
+});
+
+function promiseObserver(topic) {
+ return new Promise(resolve => {
+ let obs = (aSubject, aTopic, aData) => {
+ Services.obs.removeObserver(obs, aTopic);
+ resolve(aSubject);
+ };
+ Services.obs.addObserver(obs, topic);
+ });
+}
diff --git a/browser/components/search/test/browser/browser_483086.js b/browser/components/search/test/browser/browser_483086.js
new file mode 100644
index 0000000000..9a487c403b
--- /dev/null
+++ b/browser/components/search/test/browser/browser_483086.js
@@ -0,0 +1,56 @@
+/* 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/. */
+var gSS = Services.search;
+
+function test() {
+ waitForExplicitFinish();
+
+ function observer(aSubject, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ let engine = gSS.getEngineByName("483086a");
+ ok(engine, "Test engine 1 installed");
+ isnot(
+ engine.searchForm,
+ "foo://example.com",
+ "Invalid SearchForm URL dropped"
+ );
+ gSS.removeEngine(engine);
+ break;
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ test2();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ gSS.addOpenSearchEngine(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/483086-1.xml",
+ "data:image/x-icon;%00"
+ );
+}
+
+function test2() {
+ function observer(aSubject, aTopic, aData) {
+ switch (aData) {
+ case "engine-added":
+ let engine = gSS.getEngineByName("483086b");
+ ok(engine, "Test engine 2 installed");
+ is(engine.searchForm, "http://example.com", "SearchForm is correct");
+ gSS.removeEngine(engine);
+ break;
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ finish();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ gSS.addOpenSearchEngine(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/483086-2.xml",
+ "data:image/x-icon;%00"
+ );
+}
diff --git a/browser/components/search/test/browser/browser_addEngine.js b/browser/components/search/test/browser/browser_addEngine.js
new file mode 100644
index 0000000000..78d1ad956e
--- /dev/null
+++ b/browser/components/search/test/browser/browser_addEngine.js
@@ -0,0 +1,121 @@
+/* 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/. */
+var gSS = Services.search;
+
+function observer(aSubject, aTopic, aData) {
+ if (!gCurrentTest) {
+ info("Observer called with no test active");
+ return;
+ }
+
+ let engine = aSubject.QueryInterface(Ci.nsISearchEngine);
+ info("Observer: " + aData + " for " + engine.name);
+ let method;
+ switch (aData) {
+ case "engine-added":
+ if (gCurrentTest.added) {
+ method = "added";
+ }
+ break;
+ case "engine-default":
+ if (gCurrentTest.current) {
+ method = "current";
+ }
+ break;
+ case "engine-removed":
+ if (gCurrentTest.removed) {
+ method = "removed";
+ }
+ break;
+ }
+
+ if (method) {
+ gCurrentTest[method](engine);
+ }
+}
+
+function checkEngine(checkObj, engineObj) {
+ info("Checking engine");
+ for (let prop in checkObj) {
+ is(checkObj[prop], engineObj[prop], prop + " is correct");
+ }
+}
+
+var gTests = [
+ {
+ name: "opensearch install",
+ engine: {
+ name: "Foo",
+ alias: null,
+ description: "Foo Search",
+ searchForm:
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/",
+ },
+ run() {
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+
+ gSS.addOpenSearchEngine(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml",
+ "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC"
+ );
+ },
+ async added(engine) {
+ ok(engine, "engine was added.");
+
+ checkEngine(this.engine, engine);
+
+ let engineFromSS = gSS.getEngineByName(this.engine.name);
+ is(engine, engineFromSS, "engine is obtainable via getEngineByName");
+
+ let aEngine = await gSS.getEngineByAlias("fooalias");
+ ok(!aEngine, "Alias was not parsed from engine description");
+
+ gSS.defaultEngine = engine;
+ },
+ current(engine) {
+ let defaultEngine = gSS.defaultEngine;
+ is(engine, defaultEngine, "engine is current");
+ is(
+ engine.name,
+ this.engine.name,
+ "current engine was changed successfully"
+ );
+
+ gSS.removeEngine(engine);
+ },
+ removed(engine) {
+ // Remove the observer before calling the defaultEngine getter,
+ // as that getter will set the defaultEngine to the original default
+ // which will trigger a notification causing the test to loop over all
+ // engines.
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+
+ let defaultEngine = gSS.defaultEngine;
+ ok(defaultEngine, "An engine is present.");
+ isnot(
+ defaultEngine.name,
+ this.engine.name,
+ "Current engine reset after removal"
+ );
+
+ nextTest();
+ },
+ },
+];
+
+var gCurrentTest = null;
+function nextTest() {
+ if (gTests.length) {
+ gCurrentTest = gTests.shift();
+ info("Running " + gCurrentTest.name);
+ gCurrentTest.run();
+ } else {
+ executeSoon(finish);
+ }
+}
+
+function test() {
+ waitForExplicitFinish();
+ gSS.init().then(nextTest);
+}
diff --git a/browser/components/search/test/browser/browser_contentContextMenu.js b/browser/components/search/test/browser/browser_contentContextMenu.js
new file mode 100644
index 0000000000..38c60e22de
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contentContextMenu.js
@@ -0,0 +1,224 @@
+/* Make sure context menu includes option to search hyperlink text on search
+ * engine.
+ */
+
+add_task(async function() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ],
+ });
+
+ const url =
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/browser_contentContextMenu.xhtml";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ const ellipsis = "\u2026";
+
+ let contentAreaContextMenu = document.getElementById(
+ "contentAreaContextMenu"
+ );
+
+ const originalPrivateDefault = await Services.search.getDefaultPrivate();
+ let otherPrivateDefault;
+ for (let engine of await Services.search.getVisibleEngines()) {
+ if (engine.name != originalPrivateDefault.name) {
+ otherPrivateDefault = engine;
+ break;
+ }
+ }
+
+ // Tests if the "Search <engine> for '<some terms>'" context menu item is
+ // shown for the given query string of an element. Tests to make sure label
+ // includes the proper search terms.
+ //
+ // Each test:
+ //
+ // id: The id of the element to test.
+ // isSelected: Flag to enable selecting (text highlight) the contents of the
+ // element.
+ // shouldBeShown: The display state of the menu item.
+ // expectedLabelContents: The menu item label should contain a portion of
+ // this string. Will only be tested if shouldBeShown
+ // is true.
+ // shouldPrivateBeShown: The display state of the Private Window menu item.
+ // expectedPrivateLabelContents: The menu item label for the Private Window
+ // should contain a portion of this string.
+ // Will only be tested if shouldPrivateBeShown
+ // is true.
+ let tests = [
+ {
+ id: "link",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a link!",
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "link",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a link!",
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "longLink",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a really lo" + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "longLink",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm a really lo" + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "plainText",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "Right clicking " + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "plainText",
+ isSelected: false,
+ shouldBeShown: false,
+ shouldPrivateBeShown: false,
+ },
+ {
+ id: "mixedContent",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "I'm some text, " + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "mixedContent",
+ isSelected: false,
+ shouldBeShown: false,
+ shouldPrivateBeShown: false,
+ },
+ {
+ id: "partialLink",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "link selection",
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search in",
+ },
+ {
+ id: "partialLink",
+ isSelected: false,
+ shouldBeShown: true,
+ expectedLabelContents: "A partial link " + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search with " + otherPrivateDefault.name,
+ changePrivateDefaultEngine: true,
+ },
+ {
+ id: "surrogatePair",
+ isSelected: true,
+ shouldBeShown: true,
+ expectedLabelContents: "This character\uD83D\uDD25" + ellipsis,
+ shouldPrivateBeShown: true,
+ expectedPrivateLabelContents: "Search with " + otherPrivateDefault.name,
+ changePrivateDefaultEngine: true,
+ },
+ ];
+
+ for (let test of tests) {
+ if (test.changePrivateDefaultEngine) {
+ await Services.search.setDefaultPrivate(otherPrivateDefault);
+ }
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [{ selectElement: test.isSelected ? test.id : null }],
+ async function(arg) {
+ let selection = content.getSelection();
+ selection.removeAllRanges();
+
+ if (arg.selectElement) {
+ selection.selectAllChildren(
+ content.document.getElementById(arg.selectElement)
+ );
+ }
+ }
+ );
+
+ let popupShownPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popupshown"
+ );
+ await BrowserTestUtils.synthesizeMouseAtCenter(
+ "#" + test.id,
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupShownPromise;
+
+ let menuItem = document.getElementById("context-searchselect");
+ is(
+ menuItem.hidden,
+ !test.shouldBeShown,
+ "search context menu item is shown for '#" +
+ test.id +
+ "' and selected is '" +
+ test.isSelected +
+ "'"
+ );
+
+ if (test.shouldBeShown) {
+ ok(
+ menuItem.label.includes(test.expectedLabelContents),
+ "Menu item text '" +
+ menuItem.label +
+ "' contains the correct search terms '" +
+ test.expectedLabelContents +
+ "'"
+ );
+ }
+
+ menuItem = document.getElementById("context-searchselect-private");
+ is(
+ menuItem.hidden,
+ !test.shouldPrivateBeShown,
+ "private search context menu item is shown for '#" + test.id + "' "
+ );
+
+ if (test.shouldPrivateBeShown) {
+ ok(
+ menuItem.label.includes(test.expectedPrivateLabelContents),
+ "Menu item text '" +
+ menuItem.label +
+ "' contains the correct search terms '" +
+ test.expectedPrivateLabelContents +
+ "'"
+ );
+ }
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contentAreaContextMenu,
+ "popuphidden"
+ );
+ contentAreaContextMenu.hidePopup();
+ await popupHiddenPromise;
+
+ if (test.changePrivateDefaultEngine) {
+ await Services.search.setDefaultPrivate(originalPrivateDefault);
+ }
+ }
+
+ // Cleanup.
+ gBrowser.removeCurrentTab();
+});
diff --git a/browser/components/search/test/browser/browser_contentContextMenu.xhtml b/browser/components/search/test/browser/browser_contentContextMenu.xhtml
new file mode 100644
index 0000000000..16e32eb8ac
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contentContextMenu.xhtml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<html xmlns="http://www.w3.org/1999/xhtml">
+ <body>
+ <a href="http://mozilla.org" id="link">I'm a link!</a>
+ <br/>
+ <a href="http://mozilla.org" id="longLink">I'm a really long link and I should be truncated.</a>
+ <br/>
+ <span id="plainText">
+ Right clicking me when I'm selected should show the menu item.
+ </span>
+ <br/>
+ <span id="mixedContent">
+ I'm some text, and <a href="http://mozilla.org">I'm a link!</a>
+ </span>
+ <br/>
+ <a href="http://mozilla.org">A partial <span id="partialLink">link selection</span></a>
+ <br/>
+ <span id="surrogatePair">
+ This character🔥 shouldn't be truncated.
+ </span>
+ </body>
+</html>
diff --git a/browser/components/search/test/browser/browser_contentSearchUI.js b/browser/components/search/test/browser/browser_contentSearchUI.js
new file mode 100644
index 0000000000..c84f67394f
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contentSearchUI.js
@@ -0,0 +1,1084 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_PAGE_BASENAME = "contentSearchUI.html";
+const TEST_ENGINE1_NAME = "searchSuggestionEngine1";
+const TEST_ENGINE2_NAME = "searchSuggestionEngine2";
+
+const TEST_MSG = "ContentSearchUIControllerTest";
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ContentSearch: "resource:///actors/ContentSearchParent.jsm",
+ FormHistoryTestUtils: "resource://testing-common/FormHistoryTestUtils.jsm",
+ SearchSuggestionController:
+ "resource://gre/modules/SearchSuggestionController.jsm",
+});
+
+const pageURL = getRootDirectory(gTestPath) + TEST_PAGE_BASENAME;
+BrowserTestUtils.registerAboutPage(
+ registerCleanupFunction,
+ "test-about-content-search-ui",
+ pageURL,
+ Ci.nsIAboutModule.URI_SAFE_FOR_UNTRUSTED_CONTENT |
+ Ci.nsIAboutModule.URI_MUST_LOAD_IN_CHILD |
+ Ci.nsIAboutModule.ALLOW_SCRIPT |
+ Ci.nsIAboutModule.URI_CAN_LOAD_IN_PRIVILEGEDABOUT_PROCESS
+);
+
+requestLongerTimeout(2);
+
+function waitForSuggestions() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () =>
+ ContentTaskUtils.waitForCondition(
+ () =>
+ Cu.waiveXrays(content).gController.input.getAttribute(
+ "aria-expanded"
+ ) == "true",
+ "Waiting for suggestions",
+ 200 // Increased interval to support long textruns.
+ )
+ );
+}
+
+async function waitForSearch() {
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "ContentSearchClient",
+ true,
+ event => {
+ if (event.detail.type == "Search") {
+ event.target._eventDetail = event.detail.data;
+ return true;
+ }
+ return false;
+ },
+ true
+ );
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let eventDetail = content._eventDetail;
+ delete content._eventDetail;
+ return eventDetail;
+ });
+}
+
+async function waitForSearchSettings() {
+ await BrowserTestUtils.waitForContentEvent(
+ gBrowser.selectedBrowser,
+ "ContentSearchClient",
+ true,
+ event => {
+ if (event.detail.type == "ManageEngines") {
+ event.target._eventDetail = event.detail.data;
+ return true;
+ }
+ return false;
+ },
+ true
+ );
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let eventDetail = content._eventDetail;
+ delete content._eventDetail;
+ return eventDetail;
+ });
+}
+
+function getCurrentState() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let controller = Cu.waiveXrays(content).gController;
+ let state = {
+ selectedIndex: controller.selectedIndex,
+ selectedButtonIndex: controller.selectedButtonIndex,
+ numSuggestions: controller._table.hidden ? 0 : controller.numSuggestions,
+ suggestionAtIndex: [],
+ isFormHistorySuggestionAtIndex: [],
+
+ tableHidden: controller._table.hidden,
+
+ inputValue: controller.input.value,
+ ariaExpanded: controller.input.getAttribute("aria-expanded"),
+ };
+
+ if (state.numSuggestions) {
+ for (let i = 0; i < controller.numSuggestions; i++) {
+ state.suggestionAtIndex.push(controller.suggestionAtIndex(i));
+ state.isFormHistorySuggestionAtIndex.push(
+ controller.isFormHistorySuggestionAtIndex(i)
+ );
+ }
+ }
+
+ return state;
+ });
+}
+
+async function msg(type, data = null) {
+ switch (type) {
+ case "reset":
+ // Reset both the input and suggestions by select all + delete. If there was
+ // no text entered, this won't have any effect, so also escape to ensure the
+ // suggestions table is closed.
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Cu.waiveXrays(content).gController.input.focus();
+ EventUtils.synthesizeKey("a", { accelKey: true }, content);
+ EventUtils.synthesizeKey("KEY_Delete", {}, content);
+ EventUtils.synthesizeKey("KEY_Escape", {}, content);
+ });
+ break;
+
+ case "key": {
+ let keyName = typeof data == "string" ? data : data.key;
+ await BrowserTestUtils.synthesizeKey(
+ keyName,
+ data.modifiers || {},
+ gBrowser.selectedBrowser
+ );
+ if (data?.waitForSuggestions) {
+ await waitForSuggestions();
+ }
+ break;
+ }
+ case "text": {
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [data.value],
+ text => {
+ Cu.waiveXrays(content).gController.input.value = text.substring(
+ 0,
+ text.length - 1
+ );
+ EventUtils.synthesizeKey(
+ text.substring(text.length - 1),
+ {},
+ content
+ );
+ }
+ );
+ if (data?.waitForSuggestions) {
+ await waitForSuggestions();
+ }
+ break;
+ }
+ case "startComposition":
+ await BrowserTestUtils.synthesizeComposition(
+ "compositionstart",
+ gBrowser.selectedBrowser
+ );
+ break;
+ case "changeComposition": {
+ await BrowserTestUtils.synthesizeCompositionChange(
+ {
+ composition: {
+ string: data.data,
+ clauses: [
+ {
+ length: data.length,
+ attr: Ci.nsITextInputProcessor.ATTR_RAW_CLAUSE,
+ },
+ ],
+ },
+ caret: { start: data.length, length: 0 },
+ },
+ gBrowser.selectedBrowser
+ );
+ if (data?.waitForSuggestions) {
+ await waitForSuggestions();
+ }
+ break;
+ }
+ case "commitComposition":
+ await BrowserTestUtils.synthesizeComposition(
+ "compositioncommitasis",
+ gBrowser.selectedBrowser
+ );
+ break;
+ case "mousemove":
+ case "click": {
+ let event;
+ let index;
+ if (type == "mousemove") {
+ event = {
+ type: "mousemove",
+ clickcount: 0,
+ };
+ index = data;
+ } else {
+ event = data.modifiers || null;
+ index = data.eltIdx;
+ }
+
+ await SpecialPowers.spawn(
+ gBrowser.selectedBrowser,
+ [type, event, index],
+ (eventType, eventArgs, itemIndex) => {
+ let controller = Cu.waiveXrays(content).gController;
+ return new Promise(resolve => {
+ let row;
+ if (itemIndex == -1) {
+ row = controller._table.firstChild;
+ } else {
+ let allElts = [
+ ...controller._suggestionsList.children,
+ ...controller._oneOffButtons,
+ content.document.getElementById("contentSearchSettingsButton"),
+ ];
+ row = allElts[itemIndex];
+ }
+ row.addEventListener(eventType, () => resolve(), { once: true });
+ EventUtils.synthesizeMouseAtCenter(row, eventArgs, content);
+ });
+ }
+ );
+ break;
+ }
+ }
+
+ return getCurrentState();
+}
+
+/**
+ * Focusses the in-content search bar.
+ *
+ * @returns {Promise}
+ * A promise that is resolved once the focus is complete.
+ */
+function focusContentSearchBar() {
+ return SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Cu.waiveXrays(content).input.focus();
+ });
+}
+
+let extension1;
+let extension2;
+let currentEngineName;
+
+add_task(async function setup() {
+ let originalOnMessageSearch = ContentSearch._onMessageSearch;
+ let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
+
+ ContentSearch._onMessageSearch = () => {};
+ ContentSearch._onMessageManageEngines = () => {};
+
+ currentEngineName = (await Services.search.getDefault()).name;
+ let currentEngines = await Services.search.getVisibleEngines();
+
+ extension1 = await SearchTestUtils.installSearchExtension({
+ id: TEST_ENGINE1_NAME,
+ name: TEST_ENGINE1_NAME,
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+ extension2 = await SearchTestUtils.installSearchExtension({
+ id: TEST_ENGINE2_NAME,
+ name: TEST_ENGINE2_NAME,
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+
+ let engine1 = Services.search.getEngineByName(TEST_ENGINE1_NAME);
+
+ await Services.search.setDefault(engine1);
+ for (let engine of currentEngines) {
+ await Services.search.removeEngine(engine);
+ }
+
+ registerCleanupFunction(async () => {
+ ContentSearch._onMessageSearch = originalOnMessageSearch;
+ ContentSearch._onMessageManageEngines = originalOnMessageManageEngines;
+ });
+
+ await promiseTab();
+});
+
+add_task(async function emptyInput() {
+ await focusContentSearchBar();
+
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_BACK_SPACE");
+ checkState(state, "", [], -1);
+
+ await msg("reset");
+});
+
+add_task(async function blur() {
+ await focusContentSearchBar();
+
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Cu.waiveXrays(content).gController.input.blur();
+ });
+ state = await getCurrentState();
+ checkState(state, "x", [], -1);
+
+ await msg("reset");
+});
+
+add_task(async function upDownKeys() {
+ await focusContentSearchBar();
+
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ // Cycle down the suggestions starting from no selection.
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ // Cycle up starting from no selection.
+ state = await msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = await msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = await msg("key", "VK_UP");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = await msg("key", "VK_UP");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = await msg("key", "VK_UP");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await msg("reset");
+});
+
+add_task(async function rightLeftKeys() {
+ await focusContentSearchBar();
+
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_LEFT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_LEFT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_RIGHT");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_RIGHT");
+ checkState(state, "x", [], -1);
+
+ state = await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ // This should make the xfoo suggestion sticky. To make sure it sticks,
+ // trigger suggestions again and cycle through them by pressing Down until
+ // nothing is selected again.
+ state = await msg("key", "VK_RIGHT");
+ checkState(state, "xfoo", [], -1);
+
+ state = await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoofoo", ["xfoofoo", "xfoobar"], 0);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoobar", ["xfoofoo", "xfoobar"], 1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 2);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], 3);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoofoo", "xfoobar"], -1);
+
+ await msg("reset");
+});
+
+add_task(async function tabKey() {
+ await focusContentSearchBar();
+ await msg("key", { key: "x", waitForSuggestions: true });
+
+ let state = await msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+
+ state = await msg("key", { key: "VK_TAB", modifiers: { shiftKey: true } });
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = await msg("key", { key: "VK_TAB", modifiers: { shiftKey: true } });
+ checkState(state, "x", [], -1);
+
+ await focusContentSearchBar();
+
+ await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+ for (let i = 0; i < 3; ++i) {
+ state = await msg("key", "VK_TAB");
+ }
+ checkState(state, "x", [], -1);
+
+ await focusContentSearchBar();
+
+ await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, 0);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, 1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+
+ state = await msg("key", "VK_UP");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xbar", [], -1);
+
+ await msg("reset");
+});
+
+add_task(async function cycleSuggestions() {
+ await focusContentSearchBar();
+ await msg("key", { key: "x", waitForSuggestions: true });
+
+ let cycle = async function(aSelectedButtonIndex) {
+ let modifiers = {
+ shiftKey: true,
+ accelKey: true,
+ };
+
+ let state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xfoo", ["xfoo", "xbar"], 0, aSelectedButtonIndex);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "x", ["xfoo", "xbar"], -1, aSelectedButtonIndex);
+ };
+
+ await cycle();
+
+ // Repeat with a one-off selected.
+ let state = await msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 2);
+ await cycle(0);
+
+ // Repeat with the settings button selected.
+ state = await msg("key", "VK_TAB");
+ checkState(state, "x", ["xfoo", "xbar"], 3);
+ await cycle(1);
+
+ await msg("reset");
+});
+
+add_task(async function cycleOneOffs() {
+ await focusContentSearchBar();
+ await msg("key", { key: "x", waitForSuggestions: true });
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ let btn = Cu.waiveXrays(content).gController._oneOffButtons[
+ Cu.waiveXrays(content).gController._oneOffButtons.length - 1
+ ];
+ let newBtn = btn.cloneNode(true);
+ btn.parentNode.appendChild(newBtn);
+ Cu.waiveXrays(content).gController._oneOffButtons.push(newBtn);
+ });
+
+ let state = await msg("key", "VK_DOWN");
+ state = await msg("key", "VK_DOWN");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ let modifiers = {
+ altKey: true,
+ };
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1);
+
+ // If the settings button is selected, pressing alt+up/down should select the
+ // last/first one-off respectively (and deselect the settings button).
+ await msg("key", "VK_TAB");
+ await msg("key", "VK_TAB");
+ state = await msg("key", "VK_TAB"); // Settings button selected.
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+ state = await msg("key", { key: "VK_UP", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 1);
+
+ state = await msg("key", "VK_TAB");
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 2);
+
+ state = await msg("key", { key: "VK_DOWN", modifiers });
+ checkState(state, "xbar", ["xfoo", "xbar"], 1, 0);
+
+ await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ Cu.waiveXrays(content)
+ .gController._oneOffButtons.pop()
+ .remove();
+ });
+ await msg("reset");
+});
+
+add_task(async function mouse() {
+ await focusContentSearchBar();
+
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("mousemove", 0);
+ checkState(state, "x", ["xfoo", "xbar"], 0);
+
+ state = await msg("mousemove", 1);
+ checkState(state, "x", ["xfoo", "xbar"], 1);
+
+ state = await msg("mousemove", 2);
+ checkState(state, "x", ["xfoo", "xbar"], 2, 0);
+
+ state = await msg("mousemove", 3);
+ checkState(state, "x", ["xfoo", "xbar"], 3, 1);
+
+ state = await msg("mousemove", -1);
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await msg("reset");
+ await focusContentSearchBar();
+
+ state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ state = await msg("mousemove", 0);
+ checkState(state, "x", ["xfoo", "xbar"], 0);
+
+ state = await msg("mousemove", 2);
+ checkState(state, "x", ["xfoo", "xbar"], 2, 0);
+
+ state = await msg("mousemove", -1);
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await msg("reset");
+});
+
+add_task(async function formHistory() {
+ await focusContentSearchBar();
+
+ // Type an X and add it to form history.
+ let state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+ // Wait for Satchel to say it's been added to form history.
+ let observePromise = new Promise(resolve => {
+ Services.obs.addObserver(function onAdd(subj, topic, data) {
+ if (data == "formhistory-add") {
+ Services.obs.removeObserver(onAdd, "satchel-storage-changed");
+ executeSoon(resolve);
+ }
+ }, "satchel-storage-changed");
+ });
+
+ await FormHistoryTestUtils.clear("searchbar-history");
+ let entry = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return Cu.waiveXrays(content).gController.addInputValueToFormHistory();
+ });
+ await observePromise;
+ Assert.greater(
+ await FormHistoryTestUtils.count("searchbar-history", {
+ source: entry.source,
+ }),
+ 0
+ );
+
+ // Reset the input.
+ state = await msg("reset");
+ checkState(state, "", [], -1);
+
+ // Type an X again. The form history entry should appear.
+ state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(
+ state,
+ "x",
+ [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
+ -1
+ );
+
+ // Select the form history entry and delete it.
+ state = await msg("key", "VK_DOWN");
+ checkState(
+ state,
+ "x",
+ [{ str: "x", type: "formHistory" }, "xfoo", "xbar"],
+ 0
+ );
+
+ // Wait for Satchel.
+ observePromise = new Promise(resolve => {
+ Services.obs.addObserver(function onRemove(subj, topic, data) {
+ if (data == "formhistory-remove") {
+ Services.obs.removeObserver(onRemove, "satchel-storage-changed");
+ executeSoon(resolve);
+ }
+ }, "satchel-storage-changed");
+ });
+ state = await msg("key", "VK_DELETE");
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await observePromise;
+
+ // Reset the input.
+ state = await msg("reset");
+ checkState(state, "", [], -1);
+
+ // Type an X again. The form history entry should still be gone.
+ state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await msg("reset");
+});
+
+add_task(async function formHistory_limit() {
+ info("Check long strings are not added to form history");
+ await focusContentSearchBar();
+ const gLongString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1
+ )
+ .fill("x")
+ .join("");
+ // Type and confirm a very long string.
+ let state = await msg("text", {
+ value: gLongString,
+ waitForSuggestions: true,
+ });
+ checkState(
+ state,
+ gLongString,
+ [`${gLongString}foo`, `${gLongString}bar`],
+ -1
+ );
+
+ await FormHistoryTestUtils.clear("searchbar-history");
+ let entry = await SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return Cu.waiveXrays(content).gController.addInputValueToFormHistory();
+ });
+ // There's nothing we can wait for, since addition should not be happening.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 500));
+ Assert.equal(
+ await FormHistoryTestUtils.count("searchbar-history", {
+ source: entry.source,
+ }),
+ 0
+ );
+
+ await msg("reset");
+});
+
+add_task(async function cycleEngines() {
+ await focusContentSearchBar();
+ await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+
+ let promiseEngineChange = function(newEngineName) {
+ return new Promise(resolve => {
+ Services.obs.addObserver(function resolver(subj, topic, data) {
+ if (data != "engine-default") {
+ return;
+ }
+ subj.QueryInterface(Ci.nsISearchEngine);
+ SimpleTest.is(subj.name, newEngineName, "Engine cycled correctly");
+ Services.obs.removeObserver(resolver, "browser-search-engine-modified");
+ resolve();
+ }, "browser-search-engine-modified");
+ });
+ };
+
+ let p = promiseEngineChange(TEST_ENGINE2_NAME);
+ await msg("key", { key: "VK_DOWN", modifiers: { accelKey: true } });
+ await p;
+
+ p = promiseEngineChange(TEST_ENGINE1_NAME);
+ await msg("key", { key: "VK_UP", modifiers: { accelKey: true } });
+ await p;
+
+ await msg("reset");
+});
+
+add_task(async function search() {
+ await focusContentSearchBar();
+
+ let modifiers = {};
+ ["altKey", "ctrlKey", "metaKey", "shiftKey"].forEach(
+ k => (modifiers[k] = true)
+ );
+
+ // Test typing a query and pressing enter.
+ let p = waitForSearch();
+ await msg("key", { key: "x", waitForSuggestions: true });
+ await msg("key", { key: "VK_RETURN", modifiers });
+ let mesg = await p;
+ let eventData = {
+ engineName: TEST_ENGINE1_NAME,
+ searchString: "x",
+ healthReportKey: "test",
+ searchPurpose: "test",
+ originalEvent: modifiers,
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test typing a query, then selecting a suggestion and pressing enter.
+ p = waitForSearch();
+ await msg("key", { key: "x", waitForSuggestions: true });
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_DOWN");
+ await msg("key", { key: "VK_RETURN", modifiers });
+ mesg = await p;
+ eventData.searchString = "xfoo";
+ eventData.engineName = TEST_ENGINE1_NAME;
+ eventData.selection = {
+ index: 1,
+ kind: "key",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test typing a query, then selecting a one-off button and pressing enter.
+ p = waitForSearch();
+ await msg("key", { key: "x", waitForSuggestions: true });
+ await msg("key", "VK_UP");
+ await msg("key", "VK_UP");
+ await msg("key", { key: "VK_RETURN", modifiers });
+ mesg = await p;
+ delete eventData.selection;
+ eventData.searchString = "x";
+ eventData.engineName = TEST_ENGINE2_NAME;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test typing a query and clicking the search engine header.
+ p = waitForSearch();
+ modifiers.button = 0;
+ await msg("key", { key: "x", waitForSuggestions: true });
+ await msg("mousemove", -1);
+ await msg("click", { eltIdx: -1, modifiers });
+ mesg = await p;
+ eventData.originalEvent = modifiers;
+ eventData.engineName = TEST_ENGINE1_NAME;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test typing a query and then clicking a suggestion.
+ await msg("key", { key: "x", waitForSuggestions: true });
+ p = waitForSearch();
+ await msg("mousemove", 1);
+ await msg("click", { eltIdx: 1, modifiers });
+ mesg = await p;
+ eventData.searchString = "xfoo";
+ eventData.selection = {
+ index: 1,
+ kind: "mouse",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test typing a query and then clicking a one-off button.
+ await msg("key", { key: "x", waitForSuggestions: true });
+ p = waitForSearch();
+ await msg("mousemove", 3);
+ await msg("click", { eltIdx: 3, modifiers });
+ mesg = await p;
+ eventData.searchString = "x";
+ eventData.engineName = TEST_ENGINE2_NAME;
+ delete eventData.selection;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test selecting a suggestion, then clicking a one-off without deselecting the
+ // suggestion, using the keyboard.
+ delete modifiers.button;
+ await msg("key", { key: "x", waitForSuggestions: true });
+ p = waitForSearch();
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_TAB");
+ await msg("key", { key: "VK_RETURN", modifiers });
+ mesg = await p;
+ eventData.searchString = "xfoo";
+ eventData.selection = {
+ index: 1,
+ kind: "key",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Test searching when using IME composition.
+ let state = await msg("startComposition", { data: "" });
+ checkState(state, "", [], -1);
+ state = await msg("changeComposition", {
+ data: "x",
+ waitForSuggestions: true,
+ });
+ checkState(
+ state,
+ "x",
+ [
+ { str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" },
+ "xbar",
+ ],
+ -1
+ );
+ await msg("commitComposition");
+ delete modifiers.button;
+ p = waitForSearch();
+ await msg("key", { key: "VK_RETURN", modifiers });
+ mesg = await p;
+ eventData.searchString = "x";
+ eventData.originalEvent = modifiers;
+ eventData.engineName = TEST_ENGINE1_NAME;
+ delete eventData.selection;
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ state = await msg("startComposition", { data: "" });
+ checkState(state, "", [], -1);
+ state = await msg("changeComposition", {
+ data: "x",
+ waitForSuggestions: true,
+ });
+ checkState(
+ state,
+ "x",
+ [
+ { str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" },
+ "xbar",
+ ],
+ -1
+ );
+
+ // Mouse over the first suggestion.
+ state = await msg("mousemove", 0);
+ checkState(
+ state,
+ "x",
+ [
+ { str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" },
+ "xbar",
+ ],
+ 0
+ );
+
+ // Mouse over the second suggestion.
+ state = await msg("mousemove", 1);
+ checkState(
+ state,
+ "x",
+ [
+ { str: "x", type: "formHistory" },
+ { str: "xfoo", type: "formHistory" },
+ "xbar",
+ ],
+ 1
+ );
+
+ modifiers.button = 0;
+ p = waitForSearch();
+ await msg("click", { eltIdx: 1, modifiers });
+ mesg = await p;
+ eventData.searchString = "xfoo";
+ eventData.originalEvent = modifiers;
+ eventData.selection = {
+ index: 1,
+ kind: "mouse",
+ };
+ SimpleTest.isDeeply(eventData, mesg, "Search event data");
+
+ await promiseTab();
+ await focusContentSearchBar();
+
+ // Remove form history entries.
+ // Wait for Satchel.
+ let observePromise = new Promise(resolve => {
+ let historyCount = 2;
+ Services.obs.addObserver(function onRemove(subj, topic, data) {
+ if (data == "formhistory-remove") {
+ if (--historyCount) {
+ return;
+ }
+ Services.obs.removeObserver(onRemove, "satchel-storage-changed");
+ executeSoon(resolve);
+ }
+ }, "satchel-storage-changed");
+ });
+
+ await msg("key", { key: "x", waitForSuggestions: true });
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_DELETE");
+ await msg("key", "VK_DOWN");
+ await msg("key", "VK_DELETE");
+ await observePromise;
+
+ await msg("reset");
+ state = await msg("key", { key: "x", waitForSuggestions: true });
+ checkState(state, "x", ["xfoo", "xbar"], -1);
+
+ await promiseTab();
+ await focusContentSearchBar();
+ await msg("reset");
+});
+
+add_task(async function settings() {
+ await focusContentSearchBar();
+ await msg("key", { key: "VK_DOWN", waitForSuggestions: true });
+ await msg("key", "VK_UP");
+ let p = waitForSearchSettings();
+ await msg("key", "VK_RETURN");
+ await p;
+
+ await msg("reset");
+});
+
+add_task(async function cleanup() {
+ // Extensions must be unloaded before registerCleanupFunction runs, so
+ // unload them here.
+ Services.search.restoreDefaultEngines();
+ await Services.search.setDefault(
+ Services.search.getEngineByName(currentEngineName)
+ );
+ await extension1.unload();
+ await extension2.unload();
+});
+
+function checkState(
+ actualState,
+ expectedInputVal,
+ expectedSuggestions,
+ expectedSelectedIdx,
+ expectedSelectedButtonIdx
+) {
+ expectedSuggestions = expectedSuggestions.map(sugg => {
+ return typeof sugg == "object"
+ ? sugg
+ : {
+ str: sugg,
+ type: "remote",
+ };
+ });
+
+ if (expectedSelectedIdx == -1 && expectedSelectedButtonIdx != undefined) {
+ expectedSelectedIdx =
+ expectedSuggestions.length + expectedSelectedButtonIdx;
+ }
+
+ let expectedState = {
+ selectedIndex: expectedSelectedIdx,
+ numSuggestions: expectedSuggestions.length,
+ suggestionAtIndex: expectedSuggestions.map(s => s.str),
+ isFormHistorySuggestionAtIndex: expectedSuggestions.map(
+ s => s.type == "formHistory"
+ ),
+
+ tableHidden: !expectedSuggestions.length,
+
+ inputValue: expectedInputVal,
+ ariaExpanded: !expectedSuggestions.length ? "false" : "true",
+ };
+ if (expectedSelectedButtonIdx != undefined) {
+ expectedState.selectedButtonIndex = expectedSelectedButtonIdx;
+ } else if (expectedSelectedIdx < expectedSuggestions.length) {
+ expectedState.selectedButtonIndex = -1;
+ } else {
+ expectedState.selectedButtonIndex =
+ expectedSelectedIdx - expectedSuggestions.length;
+ }
+
+ SimpleTest.isDeeply(actualState, expectedState, "State");
+}
+
+var gMsgMan;
+
+async function promiseTab() {
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+ registerCleanupFunction(() => BrowserTestUtils.removeTab(tab));
+
+ let loadedPromise = BrowserTestUtils.firstBrowserLoaded(window);
+ openTrustedLinkIn("about:test-about-content-search-ui", "current");
+ await loadedPromise;
+}
diff --git a/browser/components/search/test/browser/browser_contentSearchUI_default.js b/browser/components/search/test/browser/browser_contentSearchUI_default.js
new file mode 100644
index 0000000000..3c31186b17
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contentSearchUI_default.js
@@ -0,0 +1,116 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const TEST_ENGINE_NAME = "searchSuggestionEngine";
+const HANDOFF_PREF =
+ "browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar";
+
+let extension;
+let defaultEngine;
+let addedEngine;
+
+add_task(async function setup() {
+ defaultEngine = await Services.search.getDefault();
+
+ extension = await SearchTestUtils.installSearchExtension({
+ id: TEST_ENGINE_NAME,
+ name: TEST_ENGINE_NAME,
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+
+ addedEngine = await Services.search.getEngineByName(TEST_ENGINE_NAME);
+
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(defaultEngine);
+ });
+});
+
+// async function runAboutNewTabTest(expectedIcon)
+
+async function ensureIcon(tab, expectedIcon) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [expectedIcon], async function(
+ icon
+ ) {
+ await ContentTaskUtils.waitForCondition(() => !content.document.hidden);
+
+ let computedStyle = content.window.getComputedStyle(content.document.body);
+ await ContentTaskUtils.waitForCondition(
+ () => computedStyle.getPropertyValue("--newtab-search-icon") != "null",
+ "Search Icon not set."
+ );
+
+ Assert.equal(
+ computedStyle.getPropertyValue("--newtab-search-icon"),
+ `url(${icon})`,
+ "Should have the expected icon"
+ );
+ });
+}
+
+async function runNewTabTest() {
+ let tab = await BrowserTestUtils.openNewForegroundTab({
+ url: "about:newtab",
+ gBrowser,
+ waitForLoad: false,
+ });
+
+ let engineIcon = defaultEngine.getIconURLBySize(16, 16);
+
+ await ensureIcon(tab, engineIcon);
+
+ await Services.search.setDefault(addedEngine);
+
+ // We only show the engine's own icon for app provided engines, otherwise show
+ // a default. xref https://bugzilla.mozilla.org/show_bug.cgi?id=1449338#c19
+ await ensureIcon(tab, "chrome://browser/skin/search-glass.svg");
+
+ await Services.search.setDefault(defaultEngine);
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_content_search_icon() {
+ SpecialPowers.pushPrefEnv({
+ set: [[HANDOFF_PREF, true]],
+ });
+
+ await runNewTabTest();
+});
+
+add_task(async function test_content_search_icon_no_handoff() {
+ SpecialPowers.pushPrefEnv({
+ set: [[HANDOFF_PREF, false]],
+ });
+
+ await runNewTabTest();
+});
+
+add_task(async function test_content_search_icon_in_private_window() {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ waitForTabURL: "about:privatebrowsing",
+ });
+ let tab = win.gBrowser.selectedTab;
+
+ let engineIcon = defaultEngine.getIconURLBySize(16, 16);
+
+ await ensureIcon(tab, engineIcon);
+
+ await Services.search.setDefault(addedEngine);
+
+ // We only show the engine's own icon for app provided engines, otherwise show
+ // a default. xref https://bugzilla.mozilla.org/show_bug.cgi?id=1449338#c19
+ await ensureIcon(tab, "chrome://browser/skin/search-glass.svg");
+
+ await Services.search.setDefault(defaultEngine);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function cleanup() {
+ // Extensions must be unloaded before registerCleanupFunction runs, so
+ // unload them here.
+ await extension.unload();
+});
diff --git a/browser/components/search/test/browser/browser_contextSearchTabPosition.js b/browser/components/search/test/browser/browser_contextSearchTabPosition.js
new file mode 100644
index 0000000000..fbaac3c255
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contextSearchTabPosition.js
@@ -0,0 +1,82 @@
+/* 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/. */
+
+add_task(async function test() {
+ let engine = await promiseNewEngine("testEngine.xml");
+ let histogramKey = "other-" + engine.name + ".contextmenu";
+ let numSearchesBefore = 0;
+
+ try {
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ let tabs = [];
+ let tabsLoadedDeferred = new Deferred();
+
+ function tabAdded(event) {
+ let tab = event.target;
+ tabs.push(tab);
+
+ // We wait for the blank tab and the two context searches tabs to open.
+ if (tabs.length == 3) {
+ tabsLoadedDeferred.resolve();
+ }
+ }
+
+ let container = gBrowser.tabContainer;
+ container.addEventListener("TabOpen", tabAdded);
+
+ BrowserTestUtils.addTab(gBrowser, "about:blank");
+ BrowserSearch.loadSearchFromContext(
+ "mozilla",
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ BrowserSearch.loadSearchFromContext(
+ "firefox",
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+
+ // Wait for all the tabs to open.
+ await tabsLoadedDeferred.promise;
+
+ is(tabs[0], gBrowser.tabs[3], "blank tab has been pushed to the end");
+ is(
+ tabs[1],
+ gBrowser.tabs[1],
+ "first search tab opens next to the current tab"
+ );
+ is(
+ tabs[2],
+ gBrowser.tabs[2],
+ "second search tab opens next to the first search tab"
+ );
+
+ container.removeEventListener("TabOpen", tabAdded);
+ tabs.forEach(gBrowser.removeTab, gBrowser);
+
+ // Make sure that the context searches are correctly recorded in telemetry.
+ // Telemetry is not updated synchronously here, we must wait for it.
+ await TestUtils.waitForCondition(() => {
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ return histogramKey in hs && hs[histogramKey].sum == numSearchesBefore + 2;
+ }, "The histogram must contain the correct search count");
+});
+
+function Deferred() {
+ this.promise = new Promise((resolve, reject) => {
+ this.resolve = resolve;
+ this.reject = reject;
+ });
+}
diff --git a/browser/components/search/test/browser/browser_contextmenu.js b/browser/components/search/test/browser/browser_contextmenu.js
new file mode 100644
index 0000000000..382e9e710d
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contextmenu.js
@@ -0,0 +1,236 @@
+/* Any copyright is dedicated to the Public Domain.
+ * * http://creativecommons.org/publicdomain/zero/1.0/ */
+/*
+ * Test searching for the selected text using the context menu
+ */
+
+const { AddonTestUtils } = ChromeUtils.import(
+ "resource://testing-common/AddonTestUtils.jsm"
+);
+
+AddonTestUtils.initMochitest(this);
+
+const ENGINE_NAME = "mozSearch";
+const PRIVATE_ENGINE_NAME = "mozPrivateSearch";
+const ENGINE_DATA = new Map([
+ [
+ ENGINE_NAME,
+ "https://example.com/browser/browser/components/search/test/browser/mozsearch.sjs",
+ ],
+ [PRIVATE_ENGINE_NAME, "https://example.com:443/browser/"],
+]);
+
+let engine;
+let privateEngine;
+let extensions = [];
+let oldDefaultEngine;
+let oldDefaultPrivateEngine;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We want select events to be fired.
+ ["dom.select_events.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ],
+ });
+
+ await Services.search.init();
+
+ for (let [name, search_url] of ENGINE_DATA) {
+ let extension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ chrome_settings_overrides: {
+ search_provider: {
+ name,
+ search_url,
+ params: [
+ {
+ name: "test",
+ value: "{searchTerms}",
+ },
+ ],
+ },
+ },
+ },
+ });
+
+ await extension.startup();
+ await AddonTestUtils.waitForSearchProviderStartup(extension);
+ extensions.push(extension);
+ }
+
+ engine = await Services.search.getEngineByName(ENGINE_NAME);
+ Assert.ok(engine, "Got a search engine");
+ oldDefaultEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engine);
+
+ privateEngine = await Services.search.getEngineByName(PRIVATE_ENGINE_NAME);
+ Assert.ok(privateEngine, "Got a search engine");
+ oldDefaultPrivateEngine = await Services.search.getDefaultPrivate();
+ await Services.search.setDefaultPrivate(privateEngine);
+});
+
+async function checkContextMenu(
+ win,
+ expectedName,
+ expectedBaseUrl,
+ expectedPrivateName
+) {
+ let contextMenu = win.document.getElementById("contentAreaContextMenu");
+ Assert.ok(contextMenu, "Got context menu XUL");
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ win.gBrowser,
+ "https://example.com/browser/browser/components/search/test/browser/test_search.html"
+ );
+
+ await SpecialPowers.spawn(tab.linkedBrowser, [""], async function() {
+ return new Promise(resolve => {
+ content.document.addEventListener(
+ "selectionchange",
+ function() {
+ resolve();
+ },
+ { once: true }
+ );
+ content.document.getSelection().selectAllChildren(content.document.body);
+ });
+ });
+
+ let eventDetails = { type: "contextmenu", button: 2 };
+
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ eventDetails,
+ win.gBrowser.selectedBrowser
+ );
+ await popupPromise;
+
+ info("checkContextMenu");
+ let searchItem = contextMenu.getElementsByAttribute(
+ "id",
+ "context-searchselect"
+ )[0];
+ Assert.ok(searchItem, "Got search context menu item");
+ Assert.equal(
+ searchItem.label,
+ "Search " + expectedName + " for \u201ctest%20search\u201d",
+ "Check context menu label"
+ );
+ Assert.equal(
+ searchItem.disabled,
+ false,
+ "Check that search context menu item is enabled"
+ );
+
+ let loaded = BrowserTestUtils.waitForNewTab(
+ win.gBrowser,
+ expectedBaseUrl + "?test=test%2520search",
+ true
+ );
+ searchItem.click();
+ let searchTab = await loaded;
+ let browser = win.gBrowser.selectedBrowser;
+ await SpecialPowers.spawn(browser, [], async function() {
+ Assert.ok(
+ !/error/.test(content.document.body.innerHTML),
+ "Ensure there were no errors loading the search page"
+ );
+ });
+
+ searchItem = contextMenu.getElementsByAttribute(
+ "id",
+ "context-searchselect-private"
+ )[0];
+ Assert.ok(searchItem, "Got search in private window context menu item");
+ if (PrivateBrowsingUtils.isWindowPrivate(win)) {
+ Assert.ok(searchItem.hidden, "Search in private window should be hidden");
+ } else {
+ let expectedLabel = expectedPrivateName
+ ? "Search with " + expectedPrivateName + " in a Private Window"
+ : "Search in a Private Window";
+ Assert.equal(searchItem.label, expectedLabel, "Check context menu label");
+ Assert.equal(
+ searchItem.disabled,
+ false,
+ "Check that search context menu item is enabled"
+ );
+ }
+
+ contextMenu.hidePopup();
+
+ BrowserTestUtils.removeTab(searchTab);
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_normalWindow() {
+ await checkContextMenu(
+ window,
+ ENGINE_NAME,
+ "https://example.com/browser/browser/components/search/test/browser/mozsearch.sjs",
+ PRIVATE_ENGINE_NAME
+ );
+});
+
+add_task(async function test_privateWindow() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await checkContextMenu(
+ win,
+ PRIVATE_ENGINE_NAME,
+ "https://example.com/browser/"
+ );
+});
+
+add_task(async function test_normalWindow_sameDefaults() {
+ // Set the private default engine to be the same as the current default engine
+ // in 'normal' mode.
+ await Services.search.setDefaultPrivate(await Services.search.getDefault());
+
+ await checkContextMenu(
+ window,
+ ENGINE_NAME,
+ "https://example.com/browser/browser/components/search/test/browser/mozsearch.sjs"
+ );
+});
+
+add_task(async function test_privateWindow_no_separate_engine() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ // We want select events to be fired.
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ registerCleanupFunction(async () => {
+ await BrowserTestUtils.closeWindow(win);
+ });
+
+ await checkContextMenu(
+ win,
+ ENGINE_NAME,
+ "https://example.com/browser/browser/components/search/test/browser/mozsearch.sjs"
+ );
+});
+
+// We can't do the unload within registerCleanupFunction as that's too late for
+// the test to be happy. Do it into a cleanup "test" here instead.
+add_task(async function cleanup() {
+ await Services.search.setDefault(oldDefaultEngine);
+ await Services.search.setDefaultPrivate(oldDefaultPrivateEngine);
+ await Services.search.removeEngine(engine);
+ await Services.search.removeEngine(privateEngine);
+
+ for (let extension of extensions) {
+ await extension.unload();
+ }
+});
diff --git a/browser/components/search/test/browser/browser_google_behavior.js b/browser/components/search/test/browser/browser_google_behavior.js
new file mode 100644
index 0000000000..485e65157c
--- /dev/null
+++ b/browser/components/search/test/browser/browser_google_behavior.js
@@ -0,0 +1,207 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test Google search plugin URLs
+ * TODO: This test is a near duplicate of browser_searchEngine_behaviors.js but
+ * specific to Google. This is required due to bug 1315953.
+ *
+ * Note: Although we have tests for codes in
+ * toolkit/components/tests/xpcshell/searchconfigs, we also need this test as an
+ * integration test to check the search service to selector integration is
+ * working correctly (especially the ESR codes).
+ */
+
+"use strict";
+
+let searchEngineDetails = [
+ {
+ alias: "g",
+ codes: {
+ context: "",
+ keyword: "",
+ newTab: "",
+ submission: "",
+ },
+ name: "Google",
+ },
+];
+
+let region = Services.prefs.getCharPref("browser.search.region");
+let code = "";
+switch (region) {
+ case "US":
+ if (AppConstants.MOZ_APP_VERSION_DISPLAY.endsWith("esr")) {
+ code = "firefox-b-1-e";
+ } else {
+ code = "firefox-b-1-d";
+ }
+ break;
+ case "DE":
+ if (AppConstants.MOZ_APP_VERSION_DISPLAY.endsWith("esr")) {
+ code = "firefox-b-e";
+ } else {
+ code = "firefox-b-d";
+ }
+ break;
+}
+
+if (code) {
+ let codes = searchEngineDetails[0].codes;
+ codes.context = code;
+ codes.newTab = code;
+ codes.submission = code;
+ codes.keyword = code;
+}
+
+function promiseContentSearchReady(browser) {
+ return SpecialPowers.spawn(browser, [], async function(args) {
+ return new Promise(resolve => {
+ if (content.wrappedJSObject.gContentSearchController) {
+ let searchController = content.wrappedJSObject.gContentSearchController;
+ if (searchController.defaultEngine) {
+ resolve();
+ }
+ }
+
+ content.addEventListener("ContentSearchService", function listener(
+ aEvent
+ ) {
+ if (aEvent.detail.type == "State") {
+ content.removeEventListener("ContentSearchService", listener);
+ resolve();
+ }
+ });
+ });
+ });
+}
+
+add_task(async function setup() {
+ await Services.search.init();
+});
+
+for (let engine of searchEngineDetails) {
+ add_task(async function() {
+ let previouslySelectedEngine = Services.search.defaultEngine;
+
+ registerCleanupFunction(function() {
+ Services.search.defaultEngine = previouslySelectedEngine;
+ });
+
+ await testSearchEngine(engine);
+ });
+}
+
+async function testSearchEngine(engineDetails) {
+ let engine = Services.search.getEngineByName(engineDetails.name);
+ Assert.ok(engine, `${engineDetails.name} is installed`);
+
+ Services.search.defaultEngine = engine;
+ engine.alias = engineDetails.alias;
+
+ // Test search URLs (including purposes).
+ let url = engine.getSubmission("foo").uri.spec;
+ let urlParams = new URLSearchParams(url.split("?")[1]);
+ Assert.equal(urlParams.get("q"), "foo", "Check search URL for 'foo'");
+
+ let engineTests = [
+ {
+ name: "context menu search",
+ code: engineDetails.codes.context,
+ run() {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch._loadSearch(
+ "foo",
+ false,
+ false,
+ "contextmenu",
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ },
+ },
+ {
+ name: "keyword search",
+ code: engineDetails.codes.keyword,
+ run() {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ {
+ name: "keyword search with alias",
+ code: engineDetails.codes.keyword,
+ run() {
+ gURLBar.value = `${engineDetails.alias} foo`;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ {
+ name: "search bar search",
+ code: engineDetails.codes.submission,
+ async preTest() {
+ await gCUITestUtils.addSearchBar();
+ },
+ run() {
+ let sb = BrowserSearch.searchBar;
+ sb.focus();
+ sb.value = "foo";
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ postTest() {
+ BrowserSearch.searchBar.value = "";
+ gCUITestUtils.removeSearchBar();
+ },
+ },
+ {
+ name: "new tab search",
+ code: engineDetails.codes.newTab,
+ async preTest(tab) {
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURI(browser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:newtab");
+
+ await promiseContentSearchReady(browser);
+ },
+ async run(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function(args) {
+ let input = content.document.querySelector("input[id*=search-]");
+ input.focus();
+ input.value = "foo";
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ ];
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ for (let test of engineTests) {
+ info(`Running: ${test.name}`);
+
+ if (test.preTest) {
+ await test.preTest(tab);
+ }
+
+ let promises = [
+ BrowserTestUtils.waitForDocLoadAndStopIt(
+ "https://www.google.com/search?client=" + test.code + "&q=foo",
+ tab
+ ),
+ BrowserTestUtils.browserStopped(tab.linkedBrowser, null, true),
+ ];
+
+ await test.run(tab);
+
+ await Promise.all(promises);
+
+ if (test.postTest) {
+ await test.postTest(tab);
+ }
+ }
+
+ engine.alias = undefined;
+ BrowserTestUtils.removeTab(tab);
+}
diff --git a/browser/components/search/test/browser/browser_healthreport.js b/browser/components/search/test/browser/browser_healthreport.js
new file mode 100644
index 0000000000..3d2b6ff078
--- /dev/null
+++ b/browser/components/search/test/browser/browser_healthreport.js
@@ -0,0 +1,110 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+var Preferences = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm",
+ {}
+).Preferences;
+
+function test() {
+ waitForExplicitFinish();
+ resetPreferences();
+
+ let calledTestTelemetry = false;
+ function testTelemetry() {
+ calledTestTelemetry = true;
+ // Find the right bucket for the "Foo" engine.
+ let engine = Services.search.getEngineByName("Foo");
+ let histogramKey = (engine.identifier || "other-Foo") + ".searchbar";
+ let numSearchesBefore = 0;
+ try {
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ if (histogramKey in hs) {
+ numSearchesBefore = hs[histogramKey].sum;
+ }
+ } catch (ex) {
+ // No searches performed yet, not a problem, |numSearchesBefore| is 0.
+ }
+
+ // Now perform a search and ensure the count is incremented.
+ let tab = BrowserTestUtils.addTab(gBrowser);
+ gBrowser.selectedTab = tab;
+ let searchBar = BrowserSearch.searchBar;
+
+ searchBar.value = "firefox health report";
+ searchBar.focus();
+
+ function afterSearch() {
+ searchBar.value = "";
+ gBrowser.removeTab(tab);
+
+ // Make sure that the context searches are correctly recorded.
+ let hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ Assert.ok(
+ histogramKey in hs,
+ "The histogram must contain the correct key"
+ );
+ Assert.equal(
+ hs[histogramKey].sum,
+ numSearchesBefore + 1,
+ "Performing a search increments the related SEARCH_COUNTS key by 1."
+ );
+
+ let fooEngine = Services.search.getEngineByName("Foo");
+ Services.search.removeEngine(fooEngine);
+ }
+
+ EventUtils.synthesizeKey("KEY_Enter");
+ executeSoon(() => executeSoon(afterSearch));
+ }
+
+ function observer(subject, topic, data) {
+ switch (data) {
+ case "engine-added":
+ let engine = Services.search.getEngineByName("Foo");
+ ok(engine, "Engine was added.");
+ Services.search.defaultEngine = engine;
+ break;
+
+ case "engine-default":
+ // We may be called again when resetting the engine at the end.
+ if (!calledTestTelemetry) {
+ is(
+ Services.search.defaultEngine.name,
+ "Foo",
+ "Current engine is Foo"
+ );
+ testTelemetry();
+ }
+ break;
+
+ case "engine-removed":
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ gCUITestUtils.removeSearchBar();
+ finish();
+ break;
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ gCUITestUtils.addSearchBar().then(function() {
+ Services.search.addOpenSearchEngine(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml",
+ "data:image/x-icon,%00"
+ );
+ });
+}
+
+function resetPreferences() {
+ Preferences.resetBranch("datareporting.policy.");
+ Preferences.set(
+ "datareporting.policy.dataSubmissionPolicyBypassNotification",
+ true
+ );
+}
diff --git a/browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js b/browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
new file mode 100644
index 0000000000..f23d913a1e
--- /dev/null
+++ b/browser/components/search/test/browser/browser_hiddenOneOffs_cleanup.js
@@ -0,0 +1,123 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+const testPref = "Foo,FooDupe";
+
+async function promiseNewEngine(basename) {
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(gTestPath) + basename;
+ let engine = await Services.search.addOpenSearchEngine(url, "");
+ info("Search engine added: " + basename);
+ return engine;
+}
+
+add_task(async function test_remove() {
+ await promiseNewEngine("testEngine_dupe.xml");
+ await promiseNewEngine("testEngine.xml");
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
+
+ info("Removing testEngine_dupe.xml");
+ await Services.search.removeEngine(
+ Services.search.getEngineByName("FooDupe")
+ );
+
+ let hiddenOneOffs = Services.prefs
+ .getCharPref("browser.search.hiddenOneOffs")
+ .split(",");
+
+ is(
+ hiddenOneOffs.length,
+ 1,
+ "hiddenOneOffs has the correct engine count post removal."
+ );
+ is(
+ hiddenOneOffs.some(x => x == "FooDupe"),
+ false,
+ "Removed Engine is not in hiddenOneOffs after removal"
+ );
+ is(
+ hiddenOneOffs.some(x => x == "Foo"),
+ true,
+ "Current hidden engine is not affected by removal."
+ );
+
+ info("Removing testEngine.xml");
+ await Services.search.removeEngine(Services.search.getEngineByName("Foo"));
+
+ is(
+ Services.prefs.getCharPref("browser.search.hiddenOneOffs"),
+ "",
+ "hiddenOneOffs is empty after removing all hidden engines."
+ );
+});
+
+add_task(async function test_add() {
+ await promiseNewEngine("testEngine.xml");
+ info("setting prefs to " + testPref);
+ Services.prefs.setCharPref("browser.search.hiddenOneOffs", testPref);
+ await promiseNewEngine("testEngine_dupe.xml");
+
+ let hiddenOneOffs = Services.prefs
+ .getCharPref("browser.search.hiddenOneOffs")
+ .split(",");
+
+ is(
+ hiddenOneOffs.length,
+ 1,
+ "hiddenOneOffs has the correct number of hidden engines present post add."
+ );
+ is(
+ hiddenOneOffs.some(x => x == "FooDupe"),
+ false,
+ "Added engine is not present in hidden list."
+ );
+ is(
+ hiddenOneOffs.some(x => x == "Foo"),
+ true,
+ "Adding an engine does not remove engines from hidden list."
+ );
+});
+
+add_task(async function test_diacritics() {
+ const diacritic_engine = "Foo \u2661";
+ let Preferences = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm",
+ {}
+ ).Preferences;
+
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+ await promiseNewEngine("testEngine_diacritics.xml");
+
+ let hiddenOneOffs = Preferences.get("browser.search.hiddenOneOffs").split(
+ ","
+ );
+ is(
+ hiddenOneOffs.some(x => x == diacritic_engine),
+ false,
+ "Observer cleans up added hidden engines that include a diacritic."
+ );
+
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+
+ info("Removing testEngine_diacritics.xml");
+ await Services.search.removeEngine(
+ Services.search.getEngineByName(diacritic_engine)
+ );
+
+ hiddenOneOffs = Preferences.get("browser.search.hiddenOneOffs").split(",");
+ is(
+ hiddenOneOffs.some(x => x == diacritic_engine),
+ false,
+ "Observer cleans up removed hidden engines that include a diacritic."
+ );
+});
+
+registerCleanupFunction(async () => {
+ info("Removing testEngine.xml");
+ await Services.search.removeEngine(Services.search.getEngineByName("Foo"));
+ info("Removing testEngine_dupe.xml");
+ await Services.search.removeEngine(
+ Services.search.getEngineByName("FooDupe")
+ );
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+});
diff --git a/browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js b/browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
new file mode 100644
index 0000000000..d8af87af42
--- /dev/null
+++ b/browser/components/search/test/browser/browser_hiddenOneOffs_diacritics.js
@@ -0,0 +1,70 @@
+/* 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/. */
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+
+const diacritic_engine = "Foo \u2661";
+
+var Preferences = ChromeUtils.import(
+ "resource://gre/modules/Preferences.jsm",
+ {}
+).Preferences;
+
+let searchIcon;
+
+add_task(async function init() {
+ let searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ searchIcon = searchbar.querySelector(".searchbar-search-button");
+
+ let defaultEngine = await Services.search.getDefault();
+ await promiseNewEngine("testEngine_diacritics.xml", { setAsCurrent: false });
+ registerCleanupFunction(async () => {
+ await Services.search.setDefault(defaultEngine);
+ Services.prefs.clearUserPref("browser.search.hiddenOneOffs");
+ });
+});
+
+add_task(async function test_hidden() {
+ Preferences.set("browser.search.hiddenOneOffs", diacritic_engine);
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await promise;
+
+ ok(
+ !getOneOffs().some(x => x.getAttribute("tooltiptext") == diacritic_engine),
+ "Search engines with diacritics are hidden when added to hiddenOneOffs preference."
+ );
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ info("Closing search panel");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+});
+
+add_task(async function test_shown() {
+ Preferences.set("browser.search.hiddenOneOffs", "");
+
+ let oneOffsContainer = searchPopup.searchOneOffsContainer;
+ let shownPromise = promiseEvent(searchPopup, "popupshown");
+ let builtPromise = promiseEvent(oneOffsContainer, "rebuild");
+ info("Opening search panel");
+
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await Promise.all([shownPromise, builtPromise]);
+
+ ok(
+ getOneOffs().some(x => x.getAttribute("tooltiptext") == diacritic_engine),
+ "Search engines with diacritics are shown when removed from hiddenOneOffs preference."
+ );
+
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+});
diff --git a/browser/components/search/test/browser/browser_oneOffContextMenu.js b/browser/components/search/test/browser/browser_oneOffContextMenu.js
new file mode 100644
index 0000000000..b6d564131a
--- /dev/null
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu.js
@@ -0,0 +1,83 @@
+"use strict";
+
+const TEST_ENGINE_NAME = "Foo";
+const TEST_ENGINE_BASENAME = "testEngine.xml";
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffInstance = searchPopup.oneOffButtons;
+const contextMenu = oneOffInstance.querySelector(
+ ".search-one-offs-context-menu"
+);
+const oneOffButtons = oneOffInstance.buttons;
+const searchInNewTabMenuItem = oneOffInstance.querySelector(
+ ".search-one-offs-context-open-in-new-tab"
+);
+
+let searchbar;
+let searchIcon;
+
+add_task(async function init() {
+ searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ searchIcon = searchbar.querySelector(".searchbar-search-button");
+
+ await promiseNewEngine(TEST_ENGINE_BASENAME, {
+ setAsCurrent: false,
+ });
+});
+
+add_task(async function telemetry() {
+ // Open the popup.
+ let shownPromise = promiseEvent(searchPopup, "popupshown");
+ let builtPromise = promiseEvent(oneOffInstance, "rebuild");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await Promise.all([shownPromise, builtPromise]);
+
+ // Get the one-off button for the test engine.
+ let oneOffButton;
+ for (let node of oneOffButtons.children) {
+ if (node.engine && node.engine.name == TEST_ENGINE_NAME) {
+ oneOffButton = node;
+ break;
+ }
+ }
+ Assert.notEqual(
+ oneOffButton,
+ undefined,
+ "One-off for test engine should exist"
+ );
+
+ // Open the context menu on the one-off.
+ let promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(oneOffButton, {
+ type: "contextmenu",
+ button: 2,
+ });
+ await promise;
+
+ // Click the Search in New Tab menu item.
+ promise = BrowserTestUtils.waitForNewTab(gBrowser);
+ EventUtils.synthesizeMouseAtCenter(searchInNewTabMenuItem, {});
+ let tab = await promise;
+
+ // By default the search will open in the background and the popup will stay open:
+ promise = promiseEvent(searchPopup, "popuphidden");
+ info("Closing search panel");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+
+ // Check the loaded tab.
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/",
+ "Expected search tab should have loaded"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+
+ // Move the cursor out of the panel area to avoid messing with other tests.
+ await EventUtils.synthesizeNativeMouseMove(searchbar);
+});
diff --git a/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js b/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
new file mode 100644
index 0000000000..0eaa8029f6
--- /dev/null
+++ b/browser/components/search/test/browser/browser_oneOffContextMenu_setDefault.js
@@ -0,0 +1,235 @@
+"use strict";
+
+const TEST_ENGINE_NAME = "Foo";
+const TEST_ENGINE_BASENAME = "testEngine.xml";
+const SEARCHBAR_BASE_ID = "searchbar-engine-one-off-item-";
+
+let originalEngine;
+let originalPrivateEngine;
+
+async function resetEngines() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.setDefaultPrivate(originalPrivateEngine);
+}
+
+registerCleanupFunction(resetEngines);
+
+add_task(async function init() {
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", true],
+ ["browser.search.widget.inNavBar", true],
+ ],
+ });
+ originalEngine = await Services.search.getDefault();
+ originalPrivateEngine = await Services.search.getDefaultPrivate();
+ registerCleanupFunction(async () => {
+ await resetEngines();
+ });
+
+ await promiseNewEngine(TEST_ENGINE_BASENAME, {
+ setAsCurrent: false,
+ });
+});
+
+async function testSearchBarChangeEngine(win, testPrivate, isPrivateWindow) {
+ info(
+ `Testing search bar with testPrivate: ${testPrivate} isPrivateWindow: ${isPrivateWindow}`
+ );
+
+ const searchPopup = win.document.getElementById("PopupSearchAutoComplete");
+ const searchOneOff = searchPopup.oneOffButtons;
+
+ // Ensure the engine is reset.
+ await resetEngines();
+
+ let oneOffButton = await openPopupAndGetEngineButton(
+ searchPopup,
+ searchOneOff,
+ SEARCHBAR_BASE_ID,
+ TEST_ENGINE_NAME
+ );
+
+ const setDefaultEngineMenuItem = searchOneOff.querySelector(
+ ".search-one-offs-context-set-default" + (testPrivate ? "-private" : "")
+ );
+
+ // Click the set default engine menu item.
+ let promise = promiseDefaultEngineChanged(testPrivate);
+ EventUtils.synthesizeMouseAtCenter(setDefaultEngineMenuItem, {}, win);
+
+ // This also checks the engine correctly changed.
+ await promise;
+
+ if (testPrivate == isPrivateWindow) {
+ let expectedName = originalEngine.name;
+ let expectedImage = originalEngine.iconURI.spec;
+ if (isPrivateWindow) {
+ expectedName = originalPrivateEngine.name;
+ expectedImage = originalPrivateEngine.iconURI.spec;
+ }
+
+ Assert.equal(
+ oneOffButton.id,
+ SEARCHBAR_BASE_ID + expectedName,
+ "Should now have the original engine's id for the button"
+ );
+ Assert.equal(
+ oneOffButton.getAttribute("tooltiptext"),
+ expectedName,
+ "Should now have the original engine's name for the tooltip"
+ );
+ Assert.equal(
+ oneOffButton.image,
+ expectedImage,
+ "Should now have the original engine's uri for the image"
+ );
+ }
+
+ await promiseClosePopup(searchPopup);
+}
+
+add_task(async function test_searchBarChangeEngine() {
+ await testSearchBarChangeEngine(window, false, false);
+ await testSearchBarChangeEngine(window, true, false);
+});
+
+add_task(async function test_searchBarChangeEngine_privateWindow() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({
+ private: true,
+ });
+ await testSearchBarChangeEngine(win, true, true);
+ await BrowserTestUtils.closeWindow(win);
+});
+
+/**
+ * Promises that an engine change has happened for the current engine, which
+ * has resulted in the test engine now being the current engine.
+ *
+ * @param {boolean} testPrivate
+ * Set to true if we're testing the private default engine.
+ * @returns {Promise} Resolved once the test engine is set as the current engine.
+ */
+function promiseDefaultEngineChanged(testPrivate) {
+ const expectedNotification = testPrivate
+ ? "engine-default-private"
+ : "engine-default";
+ return new Promise(resolve => {
+ function observer(aSub, aTopic, aData) {
+ if (aData == expectedNotification) {
+ Assert.equal(
+ Services.search[
+ testPrivate ? "defaultPrivateEngine" : "defaultEngine"
+ ].name,
+ TEST_ENGINE_NAME,
+ "defaultEngine set"
+ );
+ Services.obs.removeObserver(observer, "browser-search-engine-modified");
+ resolve();
+ }
+ }
+
+ Services.obs.addObserver(observer, "browser-search-engine-modified");
+ });
+}
+
+/**
+ * Opens the specified search popup and gets the test engine from the
+ * one-off buttons.
+ *
+ * @param {object} popup The expected popup.
+ * @param {object} oneOffInstance The expected one-off instance for the popup.
+ * @param {string} baseId The expected string for the id of the current
+ * engine button, without the engine name.
+ * @param {string} engineName The engine name for finding the one-off button.
+ * @returns {object} Returns an object that represents the one off button for the
+ * test engine.
+ */
+async function openPopupAndGetEngineButton(
+ popup,
+ oneOffInstance,
+ baseId,
+ engineName
+) {
+ const win = oneOffInstance.container.ownerGlobal;
+ // Open the popup.
+ win.gURLBar.blur();
+ let shownPromise = promiseEvent(popup, "popupshown");
+ let builtPromise = promiseEvent(oneOffInstance, "rebuild");
+ let searchbar = win.document.getElementById("searchbar");
+ let searchIcon = searchbar.querySelector(".searchbar-search-button");
+ // Use the search icon to avoid hitting the network.
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {}, win);
+ await Promise.all([shownPromise, builtPromise]);
+
+ const contextMenu = oneOffInstance.contextMenuPopup;
+ let oneOffButton = oneOffInstance.buttons;
+
+ // Get the one-off button for the test engine.
+ for (
+ oneOffButton = oneOffButton.firstChild;
+ oneOffButton;
+ oneOffButton = oneOffButton.nextSibling
+ ) {
+ if (
+ oneOffButton.nodeType == Node.ELEMENT_NODE &&
+ oneOffButton.engine &&
+ oneOffButton.engine.name == engineName
+ ) {
+ break;
+ }
+ }
+
+ Assert.notEqual(
+ oneOffButton,
+ undefined,
+ "One-off for test engine should exist"
+ );
+ Assert.equal(
+ oneOffButton.getAttribute("tooltiptext"),
+ engineName,
+ "One-off should have the tooltip set to the engine name"
+ );
+
+ Assert.equal(
+ oneOffButton.id,
+ baseId + engineName,
+ "Should have the correct id"
+ );
+
+ // Open the context menu on the one-off.
+ let promise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(
+ oneOffButton,
+ {
+ type: "contextmenu",
+ button: 2,
+ },
+ win
+ );
+ await promise;
+
+ return oneOffButton;
+}
+
+/**
+ * Closes the popup and moves the mouse away from it.
+ *
+ * @param {Button} popup The popup to close.
+ */
+async function promiseClosePopup(popup) {
+ // close the panel using the escape key.
+ let promise = promiseEvent(popup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape", {}, popup.ownerGlobal);
+ await promise;
+
+ // Move the cursor out of the panel area to avoid messing with other tests.
+ await EventUtils.synthesizeNativeMouseMove(
+ popup,
+ undefined,
+ undefined,
+ undefined,
+ popup.ownerGlobal
+ );
+}
diff --git a/browser/components/search/test/browser/browser_private_search_perwindowpb.js b/browser/components/search/test/browser/browser_private_search_perwindowpb.js
new file mode 100644
index 0000000000..2f3930e8d6
--- /dev/null
+++ b/browser/components/search/test/browser/browser_private_search_perwindowpb.js
@@ -0,0 +1,81 @@
+// This test performs a search in a public window, then a different
+// search in a private window, and then checks in the public window
+// whether there is an autocomplete entry for the private search.
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function() {
+ let windowsToClose = [];
+
+ function performSearch(aWin, aIsPrivate) {
+ let searchBar = aWin.BrowserSearch.searchBar;
+ ok(searchBar, "got search bar");
+
+ let loadPromise = BrowserTestUtils.browserLoaded(
+ aWin.gBrowser.selectedBrowser
+ );
+
+ searchBar.value = aIsPrivate ? "private test" : "public test";
+ searchBar.focus();
+ EventUtils.synthesizeKey("VK_RETURN", {}, aWin);
+
+ return loadPromise;
+ }
+
+ async function testOnWindow(aIsPrivate) {
+ let win = await BrowserTestUtils.openNewBrowserWindow({
+ private: aIsPrivate,
+ });
+ await SimpleTest.promiseFocus(win);
+ windowsToClose.push(win);
+ return win;
+ }
+
+ await promiseNewEngine("426329.xml", { iconURL: "data:image/x-icon,%00" });
+
+ let newWindow = await testOnWindow(false);
+ await performSearch(newWindow, false);
+
+ newWindow = await testOnWindow(true);
+ await performSearch(newWindow, true);
+
+ newWindow = await testOnWindow(false);
+
+ let searchBar = newWindow.BrowserSearch.searchBar;
+ searchBar.value = "p";
+ searchBar.focus();
+
+ let popup = searchBar.textbox.popup;
+ let popupPromise = BrowserTestUtils.waitForEvent(popup, "popupshown");
+ searchBar.textbox.showHistoryPopup();
+ await popupPromise;
+
+ let entries = getMenuEntries(searchBar);
+ for (let i = 0; i < entries.length; i++) {
+ isnot(
+ entries[i],
+ "private test",
+ "shouldn't see private autocomplete entries"
+ );
+ }
+
+ searchBar.textbox.toggleHistoryPopup();
+ searchBar.value = "";
+
+ windowsToClose.forEach(function(win) {
+ win.close();
+ });
+});
+
+function getMenuEntries(searchBar) {
+ // Could perhaps pull values directly from the controller, but it seems
+ // more reliable to test the values that are actually in the richlistbox?
+ return Array.from(searchBar.textbox.popup.richlistbox.itemChildren, item =>
+ item.getAttribute("ac-value")
+ );
+}
diff --git a/browser/components/search/test/browser/browser_searchEngine_behaviors.js b/browser/components/search/test/browser/browser_searchEngine_behaviors.js
new file mode 100644
index 0000000000..34000740ed
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchEngine_behaviors.js
@@ -0,0 +1,205 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Test search plugin URLs
+ */
+
+"use strict";
+
+const SEARCH_ENGINE_DETAILS = [
+ {
+ alias: "a",
+ baseURL:
+ "https://www.amazon.com/exec/obidos/external-search/?field-keywords=foo&ie=UTF-8&mode=blended&tag=mozilla-20&sourceid=Mozilla-search",
+ codes: {
+ context: "",
+ keyword: "",
+ newTab: "",
+ submission: "",
+ },
+ name: "Amazon.com",
+ },
+ {
+ alias: "b",
+ baseURL: "https://www.bing.com/search?{code}pc=MOZI&q=foo",
+ codes: {
+ context: "form=MOZCON&",
+ keyword: "form=MOZLBR&",
+ newTab: "form=MOZTSB&",
+ submission: "form=MOZSBR&",
+ },
+ name: "Bing",
+ },
+ {
+ alias: "d",
+ baseURL: "https://duckduckgo.com/?{code}q=foo",
+ codes: {
+ context: "t=ffcm&",
+ keyword: "t=ffab&",
+ newTab: "t=ffnt&",
+ submission: "t=ffsb&",
+ },
+ name: "DuckDuckGo",
+ },
+ {
+ alias: "e",
+ baseURL:
+ "https://www.ebay.com/sch/?toolid=20004&campid=5338192028&mkevt=1&mkcid=1&mkrid=711-53200-19255-0&kw=foo",
+ codes: {
+ context: "",
+ keyword: "",
+ newTab: "",
+ submission: "",
+ },
+ name: "eBay",
+ },
+ // {
+ // TODO: Google is tested in browser_google_behaviors.js - we can't test it here
+ // yet because of bug 1315953.
+ // alias: "g",
+ // baseURL: "https://www.google.com/search?q=foo&ie=utf-8&oe=utf-8",
+ // codes: {
+ // context: "",
+ // keyword: "",
+ // newTab: "",
+ // submission: "",
+ // },
+ // name: "Google",
+ // },
+];
+
+function promiseContentSearchReady(browser) {
+ return SpecialPowers.spawn(browser, [], async function(args) {
+ await ContentTaskUtils.waitForCondition(
+ () =>
+ content.wrappedJSObject.gContentSearchController &&
+ content.wrappedJSObject.gContentSearchController.defaultEngine
+ );
+ });
+}
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+for (let engine of SEARCH_ENGINE_DETAILS) {
+ add_task(async function() {
+ let previouslySelectedEngine = await Services.search.getDefault();
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(previouslySelectedEngine);
+ });
+
+ await testSearchEngine(engine);
+ });
+}
+
+async function testSearchEngine(engineDetails) {
+ let engine = Services.search.getEngineByName(engineDetails.name);
+ Assert.ok(engine, `${engineDetails.name} is installed`);
+
+ await Services.search.setDefault(engine);
+ engine.alias = engineDetails.alias;
+
+ let base = engineDetails.baseURL;
+
+ // Test search URLs (including purposes).
+ let url = engine.getSubmission("foo").uri.spec;
+ Assert.equal(
+ url,
+ base.replace("{code}", engineDetails.codes.submission),
+ "Check search URL for 'foo'"
+ );
+ let sb = BrowserSearch.searchBar;
+
+ let engineTests = [
+ {
+ name: "context menu search",
+ searchURL: base.replace("{code}", engineDetails.codes.context),
+ run() {
+ // Simulate a contextmenu search
+ // FIXME: This is a bit "low-level"...
+ BrowserSearch._loadSearch(
+ "foo",
+ false,
+ false,
+ "contextmenu",
+ Services.scriptSecurityManager.getSystemPrincipal()
+ );
+ },
+ },
+ {
+ name: "keyword search",
+ searchURL: base.replace("{code}", engineDetails.codes.keyword),
+ run() {
+ gURLBar.value = "? foo";
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ {
+ name: "keyword search with alias",
+ searchURL: base.replace("{code}", engineDetails.codes.keyword),
+ run() {
+ gURLBar.value = `${engineDetails.alias} foo`;
+ gURLBar.focus();
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ {
+ name: "search bar search",
+ searchURL: base.replace("{code}", engineDetails.codes.submission),
+ run() {
+ sb.focus();
+ sb.value = "foo";
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ {
+ name: "new tab search",
+ searchURL: base.replace("{code}", engineDetails.codes.newTab),
+ async preTest(tab) {
+ let browser = tab.linkedBrowser;
+ BrowserTestUtils.loadURI(browser, "about:newtab");
+ await BrowserTestUtils.browserLoaded(browser, false, "about:newtab");
+
+ await promiseContentSearchReady(browser);
+ },
+ async run(tab) {
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ let input = content.document.querySelector("input[id*=search-]");
+ input.focus();
+ input.value = "foo";
+ });
+ EventUtils.synthesizeKey("KEY_Enter");
+ },
+ },
+ ];
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ for (let test of engineTests) {
+ info(`Running: ${test.name}`);
+
+ if (test.preTest) {
+ await test.preTest(tab);
+ }
+
+ let promises = [
+ BrowserTestUtils.waitForDocLoadAndStopIt(test.searchURL, tab),
+ BrowserTestUtils.browserStopped(tab.linkedBrowser, null, true),
+ ];
+
+ await test.run(tab);
+
+ await Promise.all(promises);
+ }
+
+ engine.alias = undefined;
+ sb.value = "";
+ BrowserTestUtils.removeTab(tab);
+}
diff --git a/browser/components/search/test/browser/browser_search_telemetry_aboutHome.js b/browser/components/search/test/browser/browser_search_telemetry_aboutHome.js
new file mode 100644
index 0000000000..d8c5c2b4f1
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_aboutHome.js
@@ -0,0 +1,122 @@
+"use strict";
+
+const SCALAR_ABOUT_HOME = "browser.engagement.navigation.about_home";
+
+add_task(async function setup() {
+ // about:home uses IndexedDB. However, the test finishes too quickly and doesn't
+ // allow it enougth time to save. So it throws. This disables all the uncaught
+ // exception in this file and that's the reason why we split about:home tests
+ // out of the other UsageTelemetry files.
+ ignoreAllUncaughtExceptions();
+
+ // Create two new search engines. Mark one as the default engine, so
+ // the test don't crash. We need to engines for this test as the searchbar
+ // in content doesn't display the default search engine among the one-off engines.
+ await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "mozalias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ await Services.search.addEngineWithDetails("MozSearch2", {
+ alias: "mozalias2",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ // Make the first engine the default search engine.
+ let engineDefault = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engineDefault);
+
+ // Move the second engine at the beginning of the one-off list.
+ let engineOneOff = Services.search.getEngineByName("MozSearch2");
+ await Services.search.moveEngine(engineOneOff, 0);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engineDefault);
+ await Services.search.removeEngine(engineOneOff);
+ await PlacesUtils.history.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+});
+
+add_task(async function test_abouthome_activitystream_simpleQuery() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ info("Load about:home.");
+ BrowserTestUtils.loadURI(tab.linkedBrowser, "about:home");
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser, "about:home");
+
+ info("Wait for ContentSearchUI search provider to initialize.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ await ContentTaskUtils.waitForCondition(
+ () => content.wrappedJSObject.gContentSearchController.defaultEngine
+ );
+ });
+
+ info("Trigger a simple search, just test + enter.");
+ let p = BrowserTestUtils.browserStopped(
+ tab.linkedBrowser,
+ "http://example.com/?q=test+query"
+ );
+ await typeInSearchField(
+ tab.linkedBrowser,
+ "test query",
+ "newtab-search-text"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_ABOUT_HOME,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_ABOUT_HOME]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.abouthome",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "about_home",
+ value: "enter",
+ extra: { engine: "other-MozSearch" },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_search_telemetry_content.js b/browser/components/search/test/browser/browser_search_telemetry_content.js
new file mode 100644
index 0000000000..71cb09898b
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_content.js
@@ -0,0 +1,198 @@
+"use strict";
+
+const BASE_PROBE_NAME = "browser.engagement.navigation.";
+const SCALAR_CONTEXT_MENU = BASE_PROBE_NAME + "contextmenu";
+const SCALAR_ABOUT_NEWTAB = BASE_PROBE_NAME + "about_newtab";
+
+add_task(async function setup() {
+ // Create two new search engines. Mark one as the default engine, so
+ // the test don't crash. We need to engines for this test as the searchbar
+ // in content doesn't display the default search engine among the one-off engines.
+ await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "mozalias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ await Services.search.addEngineWithDetails("MozSearch2", {
+ alias: "mozalias2",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ // Make the first engine the default search engine.
+ let engineDefault = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engineDefault);
+
+ // Move the second engine at the beginning of the one-off list.
+ let engineOneOff = Services.search.getEngineByName("MozSearch2");
+ await Services.search.moveEngine(engineOneOff, 0);
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["dom.select_events.enabled", true], // We want select events to be fired.
+ ],
+ });
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engineDefault);
+ await Services.search.removeEngine(engineOneOff);
+ await PlacesUtils.history.clear();
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ });
+});
+
+add_task(async function test_context_menu() {
+ // Let's reset the Telemetry data.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ // Open a new tab with a page containing some text.
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "data:text/plain;charset=utf8,test%20search"
+ );
+
+ info("Select all the text in the page.");
+ await SpecialPowers.spawn(tab.linkedBrowser, [""], async function() {
+ return new Promise(resolve => {
+ content.document.addEventListener("selectionchange", () => resolve(), {
+ once: true,
+ });
+ content.document.getSelection().selectAllChildren(content.document.body);
+ });
+ });
+
+ info("Open the context menu.");
+ let contextMenu = document.getElementById("contentAreaContextMenu");
+ let popupPromise = BrowserTestUtils.waitForEvent(contextMenu, "popupshown");
+ BrowserTestUtils.synthesizeMouseAtCenter(
+ "body",
+ { type: "contextmenu", button: 2 },
+ gBrowser.selectedBrowser
+ );
+ await popupPromise;
+
+ info("Click on search.");
+ let searchItem = contextMenu.getElementsByAttribute(
+ "id",
+ "context-searchselect"
+ )[0];
+ searchItem.click();
+
+ info("Validate the search metrics.");
+
+ // Telemetry is not updated synchronously here, we must wait for it.
+ await TestUtils.waitForCondition(() => {
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ return Object.keys(scalars[SCALAR_CONTEXT_MENU] || {}).length == 1;
+ }, "This search must increment one entry in the scalar.");
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_CONTEXT_MENU,
+ "search",
+ 1
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.contextmenu",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "contextmenu",
+ value: null,
+ extra: { engine: "other-MozSearch" },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ contextMenu.hidePopup();
+ BrowserTestUtils.removeTab(gBrowser.selectedTab);
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_about_newtab() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:newtab",
+ false
+ );
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ await ContentTaskUtils.waitForCondition(() => !content.document.hidden);
+ });
+
+ info("Trigger a simple serch, just text + enter.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await typeInSearchField(
+ tab.linkedBrowser,
+ "test query",
+ "newtab-search-text"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_ABOUT_NEWTAB,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_ABOUT_NEWTAB]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.newtab",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "about_newtab",
+ value: "enter",
+ extra: { engine: "other-MozSearch" },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_search_telemetry_searchbar.js b/browser/components/search/test/browser/browser_search_telemetry_searchbar.js
new file mode 100644
index 0000000000..4e13a5446f
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_searchbar.js
@@ -0,0 +1,467 @@
+"use strict";
+
+const SCALAR_SEARCHBAR = "browser.engagement.navigation.searchbar";
+
+ChromeUtils.defineModuleGetter(
+ this,
+ "UrlbarTestUtils",
+ "resource://testing-common/UrlbarTestUtils.jsm"
+);
+
+function checkHistogramResults(resultIndexes, expected, histogram) {
+ for (let [i, val] of Object.entries(resultIndexes.values)) {
+ if (i == expected) {
+ Assert.equal(
+ val,
+ 1,
+ `expected counts should match for ${histogram} index ${i}`
+ );
+ } else {
+ Assert.equal(
+ !!val,
+ false,
+ `unexpected counts should be zero for ${histogram} index ${i}`
+ );
+ }
+ }
+}
+
+let searchInSearchbar = async function(inputText) {
+ let win = window;
+ await new Promise(r => waitForFocus(r, win));
+ let sb = BrowserSearch.searchBar;
+ // Write the search query in the searchbar.
+ sb.focus();
+ sb.value = inputText;
+ sb.textbox.controller.startSearch(inputText);
+ // Wait for the popup to show.
+ await BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown");
+ // And then for the search to complete.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ sb.textbox.controller.searchStatus >=
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+ "The search in the searchbar must complete."
+ );
+};
+
+/**
+ * Click one of the entries in the search suggestion popup.
+ *
+ * @param {string} entryName
+ * The name of the elemet to click on.
+ * @param {object} [clickOptions]
+ * The options to use for the click.
+ */
+function clickSearchbarSuggestion(entryName, clickOptions = {}) {
+ let richlistbox = BrowserSearch.searchBar.textbox.popup.richlistbox;
+ let richlistitem = Array.prototype.find.call(
+ richlistbox.children,
+ item => item.getAttribute("ac-value") == entryName
+ );
+
+ // Make sure the suggestion is visible and simulate the click.
+ richlistbox.ensureElementIsVisible(richlistitem);
+ EventUtils.synthesizeMouseAtCenter(richlistitem, clickOptions);
+}
+
+add_task(async function setup() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+
+ // Create two new search engines. Mark one as the default engine, so
+ // the test don't crash. We need to engines for this test as the searchbar
+ // doesn't display the default search engine among the one-off engines.
+ await Services.search.addEngineWithDetails("MozSearch", {
+ alias: "mozalias",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ await Services.search.addEngineWithDetails("MozSearch2", {
+ alias: "mozalias2",
+ method: "GET",
+ template: "http://example.com/?q={searchTerms}",
+ });
+
+ // Make the first engine the default search engine.
+ let engineDefault = Services.search.getEngineByName("MozSearch");
+ let originalEngine = await Services.search.getDefault();
+ await Services.search.setDefault(engineDefault);
+
+ // Move the second engine at the beginning of the one-off list.
+ let engineOneOff = Services.search.getEngineByName("MozSearch2");
+ await Services.search.moveEngine(engineOneOff, 0);
+
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+
+ // Enable event recording for the events tested here.
+ Services.telemetry.setEventRecordingEnabled("navigation", true);
+
+ // Make sure to restore the engine once we're done.
+ registerCleanupFunction(async function() {
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engineDefault);
+ await Services.search.removeEngine(engineOneOff);
+ Services.telemetry.setEventRecordingEnabled("navigation", false);
+ });
+});
+
+add_task(async function test_plainQuery() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Simulate entering a simple search.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInSearchbar("simple query");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHBAR,
+ "search_enter",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch.searchbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "searchbar",
+ value: "enter",
+ extra: { engine: "other-MozSearch" },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the first result, a one-off button, and the Return
+// (Enter) key.
+add_task(async function test_oneOff_enter() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Perform a one-off search using the first engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInSearchbar("query");
+
+ info("Pressing Alt+Down to highlight the first one off engine.");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHBAR,
+ "search_oneoff",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ "other-MozSearch2.searchbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "searchbar",
+ value: "oneoff",
+ extra: { engine: "other-MozSearch2" },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enter,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using the second result, a one-off button, and the Return
+// (Enter) key. This only tests the FX_SEARCHBAR_SELECTED_RESULT_METHOD
+// histogram since test_oneOff_enter covers everything else.
+add_task(async function test_oneOff_enterSelection() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ // Create an engine to generate search suggestions and add it as default
+ // for this test.
+ const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
+ let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+ let previousEngine = await Services.search.getDefault();
+ await Services.search.setDefault(suggestionEngine);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInSearchbar("query");
+
+ info(
+ "Select the second result, press Alt+Down to take us to the first one-off engine."
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await Services.search.setDefault(previousEngine);
+ await Services.search.removeEngine(suggestionEngine);
+ BrowserTestUtils.removeTab(tab);
+});
+
+// Performs a search using a click on a one-off button. This only tests the
+// FX_SEARCHBAR_SELECTED_RESULT_METHOD histogram since test_oneOff_enter covers
+// everything else.
+add_task(async function test_oneOff_click() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInSearchbar("query");
+ info("Click the first one-off button.");
+ BrowserSearch.searchBar.textbox.popup.oneOffButtons
+ .getSelectableButtons(false)[0]
+ .click();
+ await p;
+
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+async function checkSuggestionClick(clickOptions, waitForActionFn) {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ Services.telemetry.clearEvents();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+ let search_hist = TelemetryTestUtils.getAndClearKeyedHistogram(
+ "SEARCH_COUNTS"
+ );
+
+ // Create an engine to generate search suggestions and add it as default
+ // for this test.
+ const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
+ let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+ let previousEngine = await Services.search.getDefault();
+ await Services.search.setDefault(suggestionEngine);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Perform a one-off search using the first engine.");
+ let p = waitForActionFn(tab);
+ await searchInSearchbar("query");
+ info("Clicking the searchbar suggestion.");
+ clickSearchbarSuggestion("queryfoo", clickOptions);
+ await p;
+
+ // Check if the scalars contain the expected values.
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, false);
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ SCALAR_SEARCHBAR,
+ "search_suggestion",
+ 1
+ );
+ Assert.equal(
+ Object.keys(scalars[SCALAR_SEARCHBAR]).length,
+ 1,
+ "This search must only increment one entry in the scalar."
+ );
+
+ // Make sure SEARCH_COUNTS contains identical values.
+ let searchEngineId = "other-" + suggestionEngine.name;
+ TelemetryTestUtils.assertKeyedHistogramSum(
+ search_hist,
+ searchEngineId + ".searchbar",
+ 1
+ );
+
+ // Also check events.
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "searchbar",
+ value: "suggestion",
+ extra: { engine: searchEngineId },
+ },
+ ],
+ { category: "navigation", method: "search" }
+ );
+
+ // Check the histograms as well.
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.click,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await Services.search.setDefault(previousEngine);
+ await Services.search.removeEngine(suggestionEngine);
+ BrowserTestUtils.removeTab(tab);
+}
+
+// Clicks the first suggestion offered by the test search engine.
+add_task(async function test_suggestion_click() {
+ await checkSuggestionClick({}, tab => {
+ return BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ });
+});
+
+add_task(async function test_suggestion_middle_click() {
+ let openedTab;
+ await checkSuggestionClick({ button: 1 }, () => {
+ return BrowserTestUtils.waitForNewTab(gBrowser, "http://example.com/").then(
+ tab => (openedTab = tab)
+ );
+ });
+ BrowserTestUtils.removeTab(openedTab);
+});
+
+// Selects and presses the Return (Enter) key on the first suggestion offered by
+// the test search engine. This only tests the
+// FX_SEARCHBAR_SELECTED_RESULT_METHOD histogram since test_suggestion_click
+// covers everything else.
+add_task(async function test_suggestion_enterSelection() {
+ // Let's reset the counts.
+ Services.telemetry.clearScalars();
+ let resultMethodHist = TelemetryTestUtils.getAndClearHistogram(
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ // Create an engine to generate search suggestions and add it as default
+ // for this test.
+ const url = getRootDirectory(gTestPath) + "telemetrySearchSuggestions.xml";
+ let suggestionEngine = await Services.search.addOpenSearchEngine(url, "");
+ let previousEngine = await Services.search.getDefault();
+ await Services.search.setDefault(suggestionEngine);
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ info("Type a query. Suggestions should be generated by the test engine.");
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await searchInSearchbar("query");
+ info("Select the second result and press Return.");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ EventUtils.synthesizeKey("KEY_Enter");
+ await p;
+
+ let resultMethods = resultMethodHist.snapshot();
+ checkHistogramResults(
+ resultMethods,
+ UrlbarTestUtils.SELECTED_RESULT_METHODS.enterSelection,
+ "FX_SEARCHBAR_SELECTED_RESULT_METHOD"
+ );
+
+ await Services.search.setDefault(previousEngine);
+ await Services.search.removeEngine(suggestionEngine);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_search_telemetry_sources.js b/browser/components/search/test/browser/browser_search_telemetry_sources.js
new file mode 100644
index 0000000000..d7e6a08f12
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_sources.js
@@ -0,0 +1,336 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Main tests for SearchSERPTelemetry - general engine visiting and link clicking.
+ */
+
+"use strict";
+
+const { BrowserSearchTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserSearchTelemetry.jsm"
+);
+const { SearchSERPTelemetry } = ChromeUtils.import(
+ "resource:///modules/SearchSERPTelemetry.jsm"
+);
+const { UrlbarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/UrlbarTestUtils.jsm"
+);
+const { SearchTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+);
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp: /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ codePrefixes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ },
+];
+
+function getPageUrl(useAdPage = false) {
+ let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+ return `https://example.com/browser/browser/components/search/test/browser/${page}`;
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @returns {number} An index, or -1 if there are no search suggestions.
+ */
+async function getFirstSuggestionIndex() {
+ const matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+// sharedData messages are only passed to the child on idle. Therefore
+// we wait for a few idles to try and ensure the messages have been able
+// to be passed across and handled.
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+let extension;
+let currentEngineName;
+
+SearchTestUtils.init(this);
+UrlbarTestUtils.init(this);
+
+add_task(async function setup() {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ currentEngineName = (await Services.search.getDefault()).name;
+
+ extension = await SearchTestUtils.installSearchExtension({
+ search_url: getPageUrl(true),
+ search_url_get_params: "s={searchTerms}&abc=ff",
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+ let engine1 = Services.search.getEngineByName("Example");
+
+ await Services.search.setDefault(engine1);
+
+ await gCUITestUtils.addSearchBar();
+
+ registerCleanupFunction(async () => {
+ gCUITestUtils.removeSearchBar();
+ Services.prefs.clearUserPref("browser.search.log");
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearScalars();
+ });
+});
+
+async function track_ad_click(
+ expectedHistogramSource,
+ expectedScalarSource,
+ searchAdsFn,
+ cleanupFn
+) {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ let expectedScalarKeyOld = "example:sap";
+ let expectedScalarKey = "example:tagged";
+ let expectedHistogramKey = "example.in-content:sap:ff";
+ let expectedHistogramSAPSourceKey = `other-Example.${expectedHistogramSource}`;
+ let expectedWithAdsScalar = `browser.search.withads.${expectedScalarSource}`;
+ let expectedAdClicksScalar = `browser.search.adclicks.${expectedScalarSource}`;
+
+ let tab = await searchAdsFn();
+
+ await assertSearchSourcesTelemetry(
+ {
+ [expectedHistogramKey]: 1,
+ [expectedHistogramSAPSourceKey]: 1,
+ },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 1 },
+ [expectedWithAdsScalar]: { [expectedScalarKey]: 1 },
+ }
+ );
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("ad1").click();
+ });
+ await pageLoadPromise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ {
+ [expectedHistogramKey]: 1,
+ [expectedHistogramSAPSourceKey]: 1,
+ },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 1 },
+ [expectedWithAdsScalar]: { [expectedScalarKey]: 1 },
+ "browser.search.ad_clicks": { [expectedScalarKeyOld]: 1 },
+ [expectedAdClicksScalar]: { [expectedScalarKey]: 1 },
+ }
+ );
+
+ await cleanupFn();
+}
+
+add_task(async function test_source_urlbar() {
+ let tab;
+ await track_ad_click(
+ "urlbar",
+ "urlbar",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_searchbar() {
+ let tab;
+ await track_ad_click(
+ "searchbar",
+ "searchbar",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let sb = BrowserSearch.searchBar;
+ // Write the search query in the searchbar.
+ sb.focus();
+ sb.value = "searchSuggestion";
+ sb.textbox.controller.startSearch("searchSuggestion");
+ // Wait for the popup to show.
+ await BrowserTestUtils.waitForEvent(sb.textbox.popup, "popupshown");
+ // And then for the search to complete.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ sb.textbox.controller.searchStatus >=
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+ "The search in the searchbar must complete."
+ );
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter");
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+async function checkAboutPage(
+ page,
+ expectedHistogramSource,
+ expectedScalarSource
+) {
+ let tab;
+ await track_ad_click(
+ expectedHistogramSource,
+ expectedScalarSource,
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ BrowserTestUtils.loadURI(tab.linkedBrowser, page);
+ await BrowserTestUtils.browserStopped(tab.linkedBrowser, page);
+
+ // Wait for the full load.
+ await SpecialPowers.spawn(tab.linkedBrowser, [], async function() {
+ await ContentTaskUtils.waitForCondition(
+ () => content.wrappedJSObject.gContentSearchController.defaultEngine
+ );
+ });
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ await typeInSearchField(
+ tab.linkedBrowser,
+ "test query",
+ "newtab-search-text"
+ );
+ await BrowserTestUtils.synthesizeKey("VK_RETURN", {}, tab.linkedBrowser);
+ await p;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+}
+
+add_task(async function test_source_about_home() {
+ await checkAboutPage("about:home", "abouthome", "about_home");
+});
+
+add_task(async function test_source_about_newtab() {
+ await checkAboutPage("about:newtab", "newtab", "about_newtab");
+});
+
+add_task(async function test_source_system() {
+ let tab;
+ await track_ad_click(
+ "system",
+ "system",
+ async () => {
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+
+ // This is not quite the same as calling from the commandline, but close
+ // enough for this test.
+ BrowserSearch.loadSearchFromCommandLine(
+ "searchSuggestion",
+ false,
+ Services.scriptSecurityManager.getSystemPrincipal(),
+ gBrowser.selectedBrowser.csp
+ );
+
+ await loadPromise;
+ return tab;
+ },
+ async () => {
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function test_source_webextension() {
+ /* global browser */
+ async function background(SEARCH_TERM) {
+ // Search with no tabId
+ browser.search.search({ query: "searchSuggestion", engine: "Example" });
+ }
+
+ let searchExtension = ExtensionTestUtils.loadExtension({
+ manifest: {
+ permissions: ["search", "tabs"],
+ },
+ background,
+ useAddonManager: "temporary",
+ });
+
+ let tab;
+ await track_ad_click(
+ "webextension",
+ "webextension",
+ async () => {
+ let tabPromise = BrowserTestUtils.waitForNewTab(gBrowser, null, true);
+
+ await searchExtension.startup();
+
+ return (tab = await tabPromise);
+ },
+ async () => {
+ await searchExtension.unload();
+ BrowserTestUtils.removeTab(tab);
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ await Services.search.setDefault(
+ Services.search.getEngineByName(currentEngineName)
+ );
+ // Extension must be unloaded before registerCleanupFunction is called.
+ await extension.unload();
+});
diff --git a/browser/components/search/test/browser/browser_search_telemetry_sources_ads.js b/browser/components/search/test/browser/browser_search_telemetry_sources_ads.js
new file mode 100644
index 0000000000..5881436b63
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_sources_ads.js
@@ -0,0 +1,355 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Main tests for SearchSERPTelemetry - general engine visiting and link clicking.
+ */
+
+"use strict";
+
+const { BrowserSearchTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserSearchTelemetry.jsm"
+);
+const { SearchSERPTelemetry } = ChromeUtils.import(
+ "resource:///modules/SearchSERPTelemetry.jsm"
+);
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp: /^http:\/\/mochi.test:.+\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ codePrefixes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ },
+];
+
+function getPageUrl(useExample = false, useAdPage = false) {
+ let server = useExample ? "example.com" : "mochi.test:8888";
+ let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+ return `http://${server}/browser/browser/components/search/test/browser/${page}`;
+}
+
+function getSERPUrl(page, organic = false) {
+ return `${page}?s=test${organic ? "" : "&abc=ff"}`;
+}
+
+function getSERPFollowOnUrl(page) {
+ return page + "?s=test&abc=ff&a=foo";
+}
+
+// sharedData messages are only passed to the child on idle. Therefore
+// we wait for a few idles to try and ensure the messages have been able
+// to be passed across and handled.
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+add_task(async function setup() {
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ registerCleanupFunction(async () => {
+ Services.prefs.clearUserPref("browser.search.log");
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearScalars();
+ });
+});
+
+add_task(async function test_simple_search_page_visit() {
+ searchCounts.clear();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: getSERPUrl(getPageUrl()),
+ },
+ async () => {
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 1 },
+ {}
+ );
+ }
+ );
+});
+
+add_task(async function test_simple_search_page_visit_telemetry() {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ /* URL must not be in the cache */
+ url: getSERPUrl(getPageUrl()) + `&random=${Math.random()}`,
+ },
+ async () => {
+ let scalars = {};
+ const key = "browser.search.data_transferred";
+
+ await TestUtils.waitForCondition(() => {
+ scalars =
+ Services.telemetry.getSnapshotForKeyedScalars("main", false).parent ||
+ {};
+ return key in scalars;
+ }, "should have the expected keyed scalars");
+
+ const scalar = scalars[key];
+ Assert.ok("example" in scalar, "correct telemetry category");
+ Assert.notEqual(scalars[key].example, 0, "bandwidth logged");
+ }
+ );
+});
+
+add_task(async function test_follow_on_visit() {
+ await BrowserTestUtils.withNewTab(
+ {
+ gBrowser,
+ url: getSERPFollowOnUrl(getPageUrl()),
+ },
+ async () => {
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 1,
+ "example.in-content:sap-follow-on:ff": 1,
+ },
+ {}
+ );
+ }
+ );
+});
+
+add_task(async function test_track_ad() {
+ searchCounts.clear();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ getSERPUrl(getPageUrl(false, true))
+ );
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 1 },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_track_ad_organic() {
+ Services.telemetry.clearScalars();
+ searchCounts.clear();
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ getSERPUrl(getPageUrl(false, true), true)
+ );
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:organic:none": 1 },
+ {
+ "browser.search.with_ads": { "example:organic": 1 },
+ "browser.search.withads.unknown": { "example:organic": 1 },
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_track_ad_new_window() {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+
+ let url = getSERPUrl(getPageUrl(false, true));
+ BrowserTestUtils.loadURI(win.gBrowser.selectedBrowser, url);
+ await BrowserTestUtils.browserLoaded(
+ win.gBrowser.selectedBrowser,
+ false,
+ url
+ );
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 1 },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ }
+ );
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_track_ad_pages_without_ads() {
+ // Note: the above tests have already checked a page with no ad-urls.
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ let tabs = [];
+
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ getSERPUrl(getPageUrl(false, false))
+ )
+ );
+ tabs.push(
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ getSERPUrl(getPageUrl(false, true))
+ )
+ );
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 2 },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ }
+ );
+
+ for (let tab of tabs) {
+ BrowserTestUtils.removeTab(tab);
+ }
+});
+
+async function track_ad_click(testOrganic) {
+ // Note: the above tests have already checked a page with no ad-urls.
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ let expectedScalarKeyOld = `example:${testOrganic ? "organic" : "sap"}`;
+ let expectedScalarKey = `example:${testOrganic ? "organic" : "tagged"}`;
+ let expectedHistogramKey = `example.in-content:${
+ testOrganic ? "organic:none" : "sap:ff"
+ }`;
+
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ getSERPUrl(getPageUrl(false, true), testOrganic)
+ );
+
+ await assertSearchSourcesTelemetry(
+ { [expectedHistogramKey]: 1 },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 1 },
+ "browser.search.withads.unknown": {
+ [expectedScalarKey.replace("sap", "tagged")]: 1,
+ },
+ }
+ );
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("ad1").click();
+ });
+ await pageLoadPromise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ { [expectedHistogramKey]: 1 },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 1 },
+ "browser.search.withads.unknown": { [expectedScalarKey]: 1 },
+ "browser.search.ad_clicks": { [expectedScalarKeyOld]: 1 },
+ "browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
+ }
+ );
+
+ // Now go back, and click again.
+ pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ gBrowser.goBack();
+ await pageLoadPromise;
+ await promiseWaitForAdLinkCheck();
+
+ // We've gone back, so we register an extra display & if it is with ads or not.
+ await assertSearchSourcesTelemetry(
+ { [expectedHistogramKey]: 2 },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 2 },
+ "browser.search.withads.unknown": { [expectedScalarKey]: 2 },
+ "browser.search.ad_clicks": { [expectedScalarKeyOld]: 1 },
+ "browser.search.adclicks.unknown": { [expectedScalarKey]: 1 },
+ }
+ );
+
+ pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("ad1").click();
+ });
+ await pageLoadPromise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ { [expectedHistogramKey]: 2 },
+ {
+ "browser.search.with_ads": { [expectedScalarKeyOld]: 2 },
+ "browser.search.withads.unknown": { [expectedScalarKey]: 2 },
+ "browser.search.ad_clicks": { [expectedScalarKeyOld]: 2 },
+ "browser.search.adclicks.unknown": { [expectedScalarKey]: 2 },
+ }
+ );
+
+ BrowserTestUtils.removeTab(tab);
+}
+
+add_task(async function test_track_ad_click() {
+ await track_ad_click(false);
+});
+
+add_task(async function test_track_ad_click_organic() {
+ await track_ad_click(true);
+});
+
+add_task(async function test_track_ad_click_with_location_change_other_tab() {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+ const url = getSERPUrl(getPageUrl(false, true));
+ let tab = await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 1 },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ }
+ );
+
+ const newTab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "https://example.com"
+ );
+
+ await BrowserTestUtils.switchTab(gBrowser, tab);
+
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("ad1").click();
+ });
+ await pageLoadPromise;
+
+ await assertSearchSourcesTelemetry(
+ { "example.in-content:sap:ff": 1 },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ "browser.search.ad_clicks": { "example:sap": 1 },
+ "browser.search.adclicks.unknown": { "example:tagged": 1 },
+ }
+ );
+
+ BrowserTestUtils.removeTab(newTab);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_search_telemetry_sources_navigation.js b/browser/components/search/test/browser/browser_search_telemetry_sources_navigation.js
new file mode 100644
index 0000000000..cc652e55d0
--- /dev/null
+++ b/browser/components/search/test/browser/browser_search_telemetry_sources_navigation.js
@@ -0,0 +1,238 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Main tests for SearchSERPTelemetry - general engine visiting and link clicking.
+ */
+
+"use strict";
+
+const { BrowserSearchTelemetry } = ChromeUtils.import(
+ "resource:///modules/BrowserSearchTelemetry.jsm"
+);
+const { SearchSERPTelemetry } = ChromeUtils.import(
+ "resource:///modules/SearchSERPTelemetry.jsm"
+);
+const { UrlbarTestUtils } = ChromeUtils.import(
+ "resource://testing-common/UrlbarTestUtils.jsm"
+);
+const { SearchTestUtils } = ChromeUtils.import(
+ "resource://testing-common/SearchTestUtils.jsm"
+);
+
+const TEST_PROVIDER_INFO = [
+ {
+ telemetryId: "example",
+ searchPageRegexp: /^https:\/\/example.com\/browser\/browser\/components\/search\/test\/browser\/searchTelemetry(?:Ad)?.html/,
+ queryParamName: "s",
+ codeParamName: "abc",
+ codePrefixes: ["ff"],
+ followOnParamNames: ["a"],
+ extraAdServersRegexps: [/^https:\/\/example\.com\/ad2?/],
+ },
+];
+
+function getPageUrl(useAdPage = false) {
+ let page = useAdPage ? "searchTelemetryAd.html" : "searchTelemetry.html";
+ return `https://example.com/browser/browser/components/search/test/browser/${page}`;
+}
+
+/**
+ * Returns the index of the first search suggestion in the urlbar results.
+ *
+ * @returns {number} An index, or -1 if there are no search suggestions.
+ */
+async function getFirstSuggestionIndex() {
+ const matchCount = UrlbarTestUtils.getResultCount(window);
+ for (let i = 0; i < matchCount; i++) {
+ let result = await UrlbarTestUtils.getDetailsOfResultAt(window, i);
+ if (
+ result.type == UrlbarUtils.RESULT_TYPE.SEARCH &&
+ result.searchParams.suggestion
+ ) {
+ return i;
+ }
+ }
+ return -1;
+}
+
+// sharedData messages are only passed to the child on idle. Therefore
+// we wait for a few idles to try and ensure the messages have been able
+// to be passed across and handled.
+async function waitForIdle() {
+ for (let i = 0; i < 10; i++) {
+ await new Promise(resolve => Services.tm.idleDispatchToMainThread(resolve));
+ }
+}
+
+let extension;
+let currentEngineName;
+
+SearchTestUtils.init(this);
+UrlbarTestUtils.init(this);
+
+let tab;
+
+add_task(async function setup() {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ SearchSERPTelemetry.overrideSearchTelemetryForTests(TEST_PROVIDER_INFO);
+ await waitForIdle();
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.urlbar.suggest.searches", true]],
+ });
+ // Enable local telemetry recording for the duration of the tests.
+ let oldCanRecord = Services.telemetry.canRecordExtended;
+ Services.telemetry.canRecordExtended = true;
+ Services.prefs.setBoolPref("browser.search.log", true);
+
+ currentEngineName = (await Services.search.getDefault()).name;
+
+ extension = await SearchTestUtils.installSearchExtension({
+ search_url: getPageUrl(true),
+ search_url_get_params: "s={searchTerms}&abc=ff",
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+ let engine1 = Services.search.getEngineByName("Example");
+
+ await Services.search.setDefault(engine1);
+
+ tab = await BrowserTestUtils.openNewForegroundTab(gBrowser);
+
+ registerCleanupFunction(async () => {
+ BrowserTestUtils.removeTab(tab);
+ Services.prefs.clearUserPref("browser.search.log");
+ SearchSERPTelemetry.overrideSearchTelemetryForTests();
+ Services.telemetry.canRecordExtended = oldCanRecord;
+ Services.telemetry.clearScalars();
+ });
+});
+
+// These tests are consecutive and intentionally build on the results of the
+// previous test.
+
+async function loadSearchPage() {
+ await UrlbarTestUtils.promiseAutocompleteResultPopup({
+ window,
+ value: "searchSuggestion",
+ });
+ let idx = await getFirstSuggestionIndex();
+ Assert.greaterOrEqual(idx, 0, "there should be a first suggestion");
+ let loadPromise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ while (idx--) {
+ EventUtils.sendKey("down");
+ }
+ EventUtils.sendKey("return");
+ await loadPromise;
+}
+
+add_task(async function test_search() {
+ // Load a page via the address bar.
+ await loadSearchPage();
+
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 1,
+ "other-Example.urlbar": 1,
+ },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.urlbar": { "example:tagged": 1 },
+ }
+ );
+});
+
+add_task(async function test_reload() {
+ let promise = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ tab.linkedBrowser.reload();
+ await promise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 2,
+ "other-Example.urlbar": 1,
+ },
+ {
+ "browser.search.with_ads": { "example:sap": 2 },
+ "browser.search.withads.urlbar": { "example:tagged": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ }
+ );
+});
+
+let searchUrl;
+
+add_task(async function test_fresh_search() {
+ searchCounts.clear();
+ Services.telemetry.clearScalars();
+
+ // Load a page via the address bar.
+ await loadSearchPage();
+
+ searchUrl = tab.linkedBrowser.url;
+
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 1,
+ "other-Example.urlbar": 1,
+ },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.urlbar": { "example:tagged": 1 },
+ }
+ );
+});
+
+add_task(async function test_click_ad() {
+ let pageLoadPromise = BrowserTestUtils.waitForLocationChange(gBrowser);
+ await SpecialPowers.spawn(tab.linkedBrowser, [], () => {
+ content.document.getElementById("ad1").click();
+ });
+ await pageLoadPromise;
+
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 1,
+ "other-Example.urlbar": 1,
+ },
+ {
+ "browser.search.with_ads": { "example:sap": 1 },
+ "browser.search.withads.urlbar": { "example:tagged": 1 },
+ "browser.search.ad_clicks": { "example:sap": 1 },
+ "browser.search.adclicks.urlbar": { "example:tagged": 1 },
+ }
+ );
+});
+
+add_task(async function test_go_back() {
+ let promise = BrowserTestUtils.waitForLocationChange(gBrowser, searchUrl);
+ tab.linkedBrowser.goBack();
+ await promise;
+ await promiseWaitForAdLinkCheck();
+
+ await assertSearchSourcesTelemetry(
+ {
+ "example.in-content:sap:ff": 2,
+ "other-Example.urlbar": 1,
+ },
+ {
+ "browser.search.with_ads": { "example:sap": 2 },
+ "browser.search.withads.urlbar": { "example:tagged": 1 },
+ "browser.search.withads.unknown": { "example:tagged": 1 },
+ "browser.search.ad_clicks": { "example:sap": 1 },
+ "browser.search.adclicks.urlbar": { "example:tagged": 1 },
+ }
+ );
+});
+
+add_task(async function cleanup() {
+ await Services.search.setDefault(
+ Services.search.getEngineByName(currentEngineName)
+ );
+ // Extension must be unloaded before registerCleanupFunction is called.
+ await extension.unload();
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_addEngine.js b/browser/components/search/test/browser/browser_searchbar_addEngine.js
new file mode 100644
index 0000000000..0bce17e7e1
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_addEngine.js
@@ -0,0 +1,68 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the Add Search Engine option in the search bar.
+ */
+
+"use strict";
+
+const { PromptTestUtils } = ChromeUtils.import(
+ "resource://testing-common/PromptTestUtils.jsm"
+);
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+let searchbar;
+
+add_task(async function setup() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["prompts.contentPromptSubDialog", false]],
+ });
+
+ searchbar = await gCUITestUtils.addSearchBar();
+
+ registerCleanupFunction(async function() {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function test_invalidEngine() {
+ let rootDir = getRootDirectory(gTestPath);
+ let tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ rootDir + "opensearch.html"
+ );
+ let promise = promiseEvent(searchPopup, "popupshown");
+ await EventUtils.synthesizeMouseAtCenter(
+ searchbar.querySelector(".searchbar-search-button"),
+ {}
+ );
+ await promise;
+
+ let addEngineList = searchPopup.querySelector(".search-add-engines");
+ let item = addEngineList.lastElementChild;
+
+ Assert.ok(
+ item.tooltipText.includes("engineInvalid"),
+ "Last item should be the invalid entry"
+ );
+
+ let promptPromise = PromptTestUtils.waitForPrompt(tab.linkedBrowser, {
+ modalType: Ci.nsIPromptService.MODAL_TYPE_CONTENT,
+ promptType: "alert",
+ });
+
+ await EventUtils.synthesizeMouseAtCenter(item, {});
+
+ let prompt = await promptPromise;
+
+ Assert.ok(
+ prompt.ui.infoBody.textContent.includes(
+ "http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine_404.xml"
+ ),
+ "Should have included the url in the prompt body"
+ );
+
+ await PromptTestUtils.handlePrompt(prompt);
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_context.js b/browser/components/search/test/browser/browser_searchbar_context.js
new file mode 100644
index 0000000000..da5c98509e
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_context.js
@@ -0,0 +1,199 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the context menu for the search bar.
+ */
+
+"use strict";
+
+let win;
+
+add_task(async function setup() {
+ await gCUITestUtils.addSearchBar();
+
+ win = await BrowserTestUtils.openNewBrowserWindow();
+
+ // Create an engine to use for the test.
+ await Services.search.addEngineWithDetails("MozSearch1", {
+ alias: "mozalias",
+ method: "GET",
+ template: "https://example.com/?q={searchTerms}",
+ });
+
+ let originalEngine = await Services.search.getDefault();
+ let engineDefault = Services.search.getEngineByName("MozSearch1");
+ await Services.search.setDefault(engineDefault);
+
+ registerCleanupFunction(async function() {
+ await Services.search.setDefault(originalEngine);
+ await Services.search.removeEngine(engineDefault);
+ await BrowserTestUtils.closeWindow(win);
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function test_emptybar() {
+ const searchbar = win.BrowserSearch.searchBar;
+ searchbar.focus();
+
+ let contextMenu = searchbar.querySelector(".textbox-contextmenu");
+ let contextMenuPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ await EventUtils.synthesizeMouseAtCenter(
+ searchbar,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await contextMenuPromise;
+
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_cut")[0].disabled,
+ "Should have disabled the cut menuitem"
+ );
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_copy")[0].disabled,
+ "Should have disabled the copy menuitem"
+ );
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_delete")[0].disabled,
+ "Should have disabled the delete menuitem"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+});
+
+add_task(async function test_text_in_bar() {
+ const searchbar = win.BrowserSearch.searchBar;
+ searchbar.focus();
+
+ searchbar.value = "Test";
+ searchbar._textbox.editor.selectAll();
+
+ let contextMenu = searchbar.querySelector(".textbox-contextmenu");
+ let contextMenuPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ await EventUtils.synthesizeMouseAtCenter(
+ searchbar,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await contextMenuPromise;
+
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_cut")[0].disabled,
+ "Should have enabled the cut menuitem"
+ );
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_copy")[0].disabled,
+ "Should have enabled the copy menuitem"
+ );
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_delete")[0].disabled,
+ "Should have enabled the delete menuitem"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+});
+
+add_task(async function test_unfocused_emptybar() {
+ const searchbar = win.BrowserSearch.searchBar;
+ // clear searchbar value from previous test
+ searchbar.value = "";
+
+ // force focus onto another component
+ win.gURLBar.focus();
+
+ let contextMenu = searchbar.querySelector(".textbox-contextmenu");
+ let contextMenuPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ searchbar.focus();
+ await EventUtils.synthesizeMouseAtCenter(
+ searchbar,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await contextMenuPromise;
+
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_cut")[0].disabled,
+ "Should have disabled the cut menuitem"
+ );
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_copy")[0].disabled,
+ "Should have disabled the copy menuitem"
+ );
+ Assert.ok(
+ contextMenu.getElementsByAttribute("cmd", "cmd_delete")[0].disabled,
+ "Should have disabled the delete menuitem"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+});
+
+add_task(async function test_text_in_unfocused_bar() {
+ const searchbar = win.BrowserSearch.searchBar;
+
+ searchbar.value = "Test";
+
+ // force focus onto another component
+ win.gURLBar.focus();
+
+ let contextMenu = searchbar.querySelector(".textbox-contextmenu");
+ let contextMenuPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popupshown"
+ );
+
+ searchbar.focus();
+ await EventUtils.synthesizeMouseAtCenter(
+ searchbar,
+ { type: "contextmenu", button: 2 },
+ win
+ );
+ await contextMenuPromise;
+
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_cut")[0].disabled,
+ "Should have enabled the cut menuitem"
+ );
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_copy")[0].disabled,
+ "Should have enabled the copy menuitem"
+ );
+ Assert.ok(
+ !contextMenu.getElementsByAttribute("cmd", "cmd_delete")[0].disabled,
+ "Should have enabled the delete menuitem"
+ );
+
+ let popupHiddenPromise = BrowserTestUtils.waitForEvent(
+ contextMenu,
+ "popuphidden"
+ );
+ contextMenu.hidePopup();
+ await popupHiddenPromise;
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_default.js b/browser/components/search/test/browser/browser_searchbar_default.js
new file mode 100644
index 0000000000..f58a4e33d5
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_default.js
@@ -0,0 +1,183 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/*
+ * Tests the correct default engines in the search bar.
+ */
+
+"use strict";
+
+const { SearchSuggestionController } = ChromeUtils.import(
+ "resource://gre/modules/SearchSuggestionController.jsm"
+);
+
+const templateNormal = "https://example.com/?q=";
+const templatePrivate = "https://example.com/?query=";
+
+async function searchInSearchbar(win, inputText) {
+ await new Promise(r => waitForFocus(r, win));
+ let searchbar = win.BrowserSearch.searchBar;
+ // Write the search query in the searchbar.
+ searchbar.focus();
+ searchbar.value = inputText;
+ searchbar.textbox.controller.startSearch(inputText);
+ // Wait for the popup to show.
+ await BrowserTestUtils.waitForEvent(searchbar.textbox.popup, "popupshown");
+ // And then for the search to complete.
+ await BrowserTestUtils.waitForCondition(
+ () =>
+ searchbar.textbox.controller.searchStatus >=
+ Ci.nsIAutoCompleteController.STATUS_COMPLETE_NO_MATCH,
+ "The search in the searchbar must complete."
+ );
+}
+
+add_task(async function setup() {
+ await gCUITestUtils.addSearchBar();
+
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", false]],
+ });
+
+ // Create two new search engines. Mark one as the default engine, so
+ // the test don't crash. We need to engines for this test as the searchbar
+ // doesn't display the default search engine among the one-off engines.
+ await Services.search.addEngineWithDetails("MozSearch1", {
+ alias: "mozalias",
+ method: "GET",
+ template: templateNormal + "{searchTerms}",
+ });
+
+ await Services.search.addEngineWithDetails("MozSearch2", {
+ alias: "mozalias2",
+ method: "GET",
+ template: templatePrivate + "{searchTerms}",
+ });
+
+ await SpecialPowers.pushPrefEnv({
+ set: [
+ ["browser.search.separatePrivateDefault.ui.enabled", true],
+ ["browser.search.separatePrivateDefault", false],
+ ],
+ });
+
+ let originalEngine = await Services.search.getDefault();
+ let originalPrivateEngine = await Services.search.getDefaultPrivate();
+
+ let engineDefault = Services.search.getEngineByName("MozSearch1");
+ await Services.search.setDefault(engineDefault);
+
+ let engineOneOff = Services.search.getEngineByName("MozSearch2");
+
+ registerCleanupFunction(async function() {
+ gCUITestUtils.removeSearchBar();
+ await Services.search.setDefault(originalEngine);
+ await Services.search.setDefaultPrivate(originalPrivateEngine);
+ await Services.search.removeEngine(engineDefault);
+ await Services.search.removeEngine(engineOneOff);
+ });
+});
+
+async function doSearch(
+ win,
+ tab,
+ engineName,
+ templateUrl,
+ inputText = "query"
+) {
+ await searchInSearchbar(win, inputText);
+
+ Assert.ok(
+ win.BrowserSearch.searchBar.textbox.popup.searchbarEngineName
+ .getAttribute("value")
+ .includes(engineName),
+ "Should have the correct engine name displayed in the bar"
+ );
+
+ let p = BrowserTestUtils.browserLoaded(tab.linkedBrowser);
+ EventUtils.synthesizeKey("KEY_Enter", {}, win);
+ await p;
+
+ Assert.equal(
+ tab.linkedBrowser.currentURI.spec,
+ templateUrl + inputText,
+ "Should have loaded the expected search page."
+ );
+}
+
+add_task(async function test_default_search() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+
+ await doSearch(window, tab, "MozSearch1", templateNormal);
+
+ BrowserTestUtils.removeTab(tab);
+});
+
+add_task(async function test_default_search_private_no_separate() {
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await doSearch(win, win.gBrowser.selectedTab, "MozSearch1", templateNormal);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_default_search_private_no_separate() {
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.separatePrivateDefault", true]],
+ });
+
+ await Services.search.setDefaultPrivate(
+ Services.search.getEngineByName("MozSearch2")
+ );
+
+ const win = await BrowserTestUtils.openNewBrowserWindow({ private: true });
+
+ await doSearch(win, win.gBrowser.selectedTab, "MozSearch2", templatePrivate);
+
+ await BrowserTestUtils.closeWindow(win);
+});
+
+add_task(async function test_form_history() {
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "about:blank"
+ );
+ await FormHistoryTestUtils.clear("searchbar-history");
+ const gShortString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH
+ )
+ .fill("a")
+ .join("");
+ let promiseAdd = TestUtils.topicObserved("satchel-storage-changed");
+ await doSearch(window, tab, "MozSearch1", templateNormal, gShortString);
+ await promiseAdd;
+ let entries = (await FormHistoryTestUtils.search("searchbar-history")).map(
+ entry => entry.value
+ );
+ Assert.deepEqual(
+ entries,
+ [gShortString],
+ "Should have stored search history"
+ );
+
+ await FormHistoryTestUtils.clear("searchbar-history");
+ const gLongString = new Array(
+ SearchSuggestionController.SEARCH_HISTORY_MAX_VALUE_LENGTH + 1
+ )
+ .fill("a")
+ .join("");
+ await doSearch(window, tab, "MozSearch1", templateNormal, gLongString);
+ // There's nothing we can wait for, since addition should not be happening.
+ /* eslint-disable mozilla/no-arbitrary-setTimeout */
+ await new Promise(resolve => setTimeout(resolve, 500));
+ entries = (await FormHistoryTestUtils.search("searchbar-history")).map(
+ entry => entry.value
+ );
+ Assert.deepEqual(entries, [], "Should not find form history");
+
+ await FormHistoryTestUtils.clear("searchbar-history");
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_focus_timing.js b/browser/components/search/test/browser/browser_searchbar_focus_timing.js
new file mode 100644
index 0000000000..2b8f40ee30
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_focus_timing.js
@@ -0,0 +1,87 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test the focus timing for the search bar.
+
+add_task(async function setup() {
+ await gCUITestUtils.addSearchBar();
+
+ const engine = await Services.search.addEngineWithDetails("dummy", {
+ method: "GET",
+ template: "https://example.com/?q={searchTerms}",
+ });
+
+ const defaultEngine = Services.search.defaultEngine;
+ Services.search.defaultEngine = engine;
+
+ registerCleanupFunction(async function() {
+ Services.search.defaultEngine = defaultEngine;
+ await Services.search.removeEngine(engine);
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function() {
+ info("Open a page");
+ const tab = await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ "http://example.com"
+ );
+
+ const onLoad = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ const onBeforeUnload = ContentTask.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("beforeunload", () => {
+ resolve();
+ });
+ });
+ });
+ const onResult = SpecialPowers.spawn(gBrowser.selectedBrowser, [], () => {
+ return new Promise(resolve => {
+ content.window.addEventListener("keyup", () => {
+ resolve("keyup");
+ });
+ content.window.addEventListener("unload", () => {
+ resolve("unload");
+ });
+ });
+ });
+
+ info("Focus on the search bar");
+ const searchBarTextBox = BrowserSearch.searchBar.textbox;
+ EventUtils.synthesizeMouseAtCenter(searchBarTextBox, {});
+ const ownerDocument = gBrowser.selectedBrowser.ownerDocument;
+ is(ownerDocument.activeElement, searchBarTextBox, "The search bar has focus");
+
+ info("Keydown a char and Enter");
+ EventUtils.synthesizeKey("x", { type: "keydown" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keydown" });
+
+ info("Wait for beforeUnload event in the content");
+ await onBeforeUnload;
+ is(
+ ownerDocument.activeElement,
+ searchBarTextBox,
+ "The search bar still has focus"
+ );
+
+ // Keyup both key as soon as beforeUnload event happens.
+ EventUtils.synthesizeKey("x", { type: "keyup" });
+ EventUtils.synthesizeKey("KEY_Enter", { type: "keyup" });
+
+ await TestUtils.waitForCondition(
+ () => ownerDocument.activeElement === gBrowser.selectedBrowser,
+ "Wait for focus to be moved to the browser"
+ );
+ info("The focus is moved to the browser");
+
+ // Check whether keyup event is not captured before unload event happens.
+ const result = await onResult;
+ is(result, "unload", "Keyup event is not captured");
+
+ // Cleanup.
+ await onLoad;
+ BrowserTestUtils.removeTab(tab);
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
new file mode 100644
index 0000000000..aad9d926f7
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js
@@ -0,0 +1,647 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
+
+const kValues = ["foo1", "foo2", "foo3"];
+const kUserValue = "foo";
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList = oneOffsContainer.querySelector(".search-add-engines");
+ for (
+ let item = addEngineList.firstElementChild;
+ item;
+ item = item.nextElementSibling
+ ) {
+ os.push(item);
+ }
+
+ return os;
+}
+
+let searchbar;
+let textbox;
+
+async function checkHeader(engine) {
+ // The header can be updated after getting the engine, so we may have to
+ // wait for it.
+ let header = searchPopup.searchbarEngineName;
+ if (!header.getAttribute("value").includes(engine.name)) {
+ await new Promise(resolve => {
+ let observer = new MutationObserver(() => {
+ observer.disconnect();
+ resolve();
+ });
+ observer.observe(searchPopup.searchbarEngineName, {
+ attributes: true,
+ attributeFilter: ["value"],
+ });
+ });
+ }
+ Assert.ok(
+ header.getAttribute("value").includes(engine.name),
+ "Should have the correct engine name displayed in the header"
+ );
+}
+
+add_task(async function init() {
+ searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ textbox = searchbar.textbox;
+
+ await promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ await new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update(
+ { op: "remove", fieldname: "searchbar-history" },
+ { handleCompletion: resolve, handleError: reject }
+ );
+ });
+
+ await new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let addOps = kValues.map(value => {
+ return { op: "add", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(addOps, {
+ handleCompletion: resolve,
+ handleError: reject,
+ });
+ });
+
+ textbox.value = kUserValue;
+});
+
+add_task(async function test_arrows() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ await promise;
+ is(
+ textbox.mController.searchString,
+ kUserValue,
+ "The search string should be 'foo'"
+ );
+
+ // Check the initial state of the panel before sending keyboard events.
+ is(searchPopup.matchCount, kValues.length, "There should be 3 suggestions");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // The tests will be less meaningful if the first, second, last, and
+ // before-last one-off buttons aren't different. We should always have more
+ // than 4 default engines, but it's safer to check this assumption.
+ let oneOffs = getOneOffs();
+ ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed");
+
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // The down arrow should first go through the suggestions.
+ for (let i = 0; i < kValues.length; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ searchPopup.selectedIndex,
+ i,
+ "the suggestion at index " + i + " should be selected"
+ );
+ is(
+ textbox.value,
+ kValues[i],
+ "the textfield value should be " + kValues[i]
+ );
+ await checkHeader(Services.search.defaultEngine);
+ }
+
+ // Pressing down again should remove suggestion selection and change the text
+ // field value back to what the user typed, and select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(
+ textbox.value,
+ kUserValue,
+ "the textfield value should be back to initial value"
+ );
+
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ let oneOffButton = oneOffs[i];
+ is(
+ textbox.selectedButton,
+ oneOffButton,
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ await checkHeader(oneOffButton.engine);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+ await checkHeader(Services.search.defaultEngine);
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // We should now be back to the initial situation.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ await checkHeader(Services.search.defaultEngine);
+
+ info("now test the up arrow key");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+ await checkHeader(Services.search.defaultEngine);
+
+ // cycle through the one-off items, the first one is already selected.
+ for (let i = oneOffs.length; i; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ let oneOffButton = oneOffs[i - 1];
+ is(
+ textbox.selectedButton,
+ oneOffButton,
+ "the one-off button #" + i + " should be selected"
+ );
+ await checkHeader(oneOffButton.engine);
+ }
+
+ // Another press on up should clear the one-off selection and select the
+ // last suggestion.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ for (let i = kValues.length - 1; i >= 0; --i) {
+ is(
+ searchPopup.selectedIndex,
+ i,
+ "the suggestion at index " + i + " should be selected"
+ );
+ is(
+ textbox.value,
+ kValues[i],
+ "the textfield value should be " + kValues[i]
+ );
+ await checkHeader(Services.search.defaultEngine);
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ }
+
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(
+ textbox.value,
+ kUserValue,
+ "the textfield value should be back to initial value"
+ );
+});
+
+add_task(async function test_typing_clears_button_selection() {
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "the search bar should be focused"
+ ); // from the previous test.
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // Type a character.
+ EventUtils.sendString("a");
+ ok(!textbox.selectedButton, "the settings item should be de-selected");
+
+ // Remove the character.
+ EventUtils.synthesizeKey("KEY_Backspace");
+});
+
+add_task(async function test_tab() {
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "the search bar should be focused"
+ ); // from the previous test.
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing tab should select the first one-off without selecting suggestions.
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // One more <tab> selects the settings button.
+ EventUtils.synthesizeKey("KEY_Tab");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // Pressing tab again should close the panel...
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(
+ Services.focus.focusedElement,
+ textbox.inputField,
+ "the search bar no longer be focused"
+ );
+});
+
+add_task(async function test_shift_tab() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ await promise;
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Press up once to select the last button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // Press up again to select the last one-off button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Pressing shift+tab should cycle through the one-off items.
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ if (i) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ }
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing shift+tab again should close the panel...
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(
+ Services.focus.focusedElement,
+ textbox.inputField,
+ "the search bar no longer be focused"
+ );
+});
+
+add_task(async function test_alt_down() {
+ // First refocus the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ await promise;
+
+ // close the panel using the escape key.
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+
+ // check that alt+down opens the panel...
+ promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ await promise;
+
+ // ... and does nothing else.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing alt+down should select the first one-off without selecting suggestions
+ // and cycle through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+});
+
+add_task(async function test_alt_up() {
+ // close the panel using the escape key.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+ ok(
+ !textbox.selectedButton,
+ "no one-off button should be selected after closing the panel"
+ );
+
+ // check that alt+up opens the panel...
+ promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ await promise;
+
+ // ... and does nothing else.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // Pressing alt+up should select the last one-off without selecting suggestions
+ // and cycle up through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the last one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[oneOffs.length - 1],
+ "the last one-off button should be selected"
+ );
+
+ // Cleanup for the next test.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(!textbox.selectedButton, "no one-off should be selected anymore");
+});
+
+add_task(async function test_accel_down() {
+ // Pressing accel+down should select the next visible search engine, without
+ // selecting suggestions.
+ let engines = await Services.search.getVisibleEngines();
+ let current = Services.search.defaultEngine;
+ let currIdx = -1;
+ for (let i = 0, l = engines.length; i < l; ++i) {
+ if (engines[i].name == current.name) {
+ currIdx = i;
+ break;
+ }
+ }
+ for (let i = 0, l = engines.length; i < l; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { accelKey: true });
+ await SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ let expected = engines[++currIdx % engines.length];
+ is(
+ Services.search.defaultEngine.name,
+ expected.name,
+ "Default engine should have changed"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+ Services.search.defaultEngine = current;
+});
+
+add_task(async function test_accel_up() {
+ // Pressing accel+down should select the previous visible search engine, without
+ // selecting suggestions.
+ let engines = await Services.search.getVisibleEngines();
+ let current = Services.search.defaultEngine;
+ let currIdx = -1;
+ for (let i = 0, l = engines.length; i < l; ++i) {
+ if (engines[i].name == current.name) {
+ currIdx = i;
+ break;
+ }
+ }
+ for (let i = 0, l = engines.length; i < l; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp", { accelKey: true });
+ await SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ let expected =
+ engines[--currIdx < 0 ? (currIdx = engines.length - 1) : currIdx];
+ is(
+ Services.search.defaultEngine.name,
+ expected.name,
+ "Default engine should have changed"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+ Services.search.defaultEngine = current;
+});
+
+add_task(async function test_tab_and_arrows() {
+ // Check the initial state is as expected.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, kUserValue, "the textfield value should be unmodified");
+
+ // After pressing down, the first sugggestion should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(searchPopup.selectedIndex, 0, "first suggestion should be selected");
+ is(textbox.value, kValues[0], "the textfield value should have changed");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // After pressing tab, the first one-off should be selected,
+ // and no suggestion should be selected.
+ let oneOffs = getOneOffs();
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing down, the second one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ textbox.selectedButton,
+ oneOffs[1],
+ "the second one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing right, the third one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(
+ textbox.selectedButton,
+ oneOffs[2],
+ "the third one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing left, the second one-off should be selected again.
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ textbox.selectedButton,
+ oneOffs[1],
+ "the second one-off button should be selected again"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing up, the first one-off should be selected again.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected again"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing up again, the last suggestion should be selected.
+ // the textfield value back to the user-typed value, and still the first one-off
+ // selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ searchPopup.selectedIndex,
+ kValues.length - 1,
+ "last suggestion should be selected"
+ );
+ is(
+ textbox.value,
+ kValues[kValues.length - 1],
+ "the textfield value should match the suggestion"
+ );
+ is(textbox.selectedButton, null, "no one-off button should be selected");
+
+ // Now pressing down should select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "there should be no selected suggestion");
+
+ // Finally close the panel.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+});
+
+add_task(async function test_open_search() {
+ let rootDir = getRootDirectory(gTestPath);
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ rootDir + "opensearch.html"
+ );
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ await promise;
+
+ let engines = getOpenSearchItems();
+ is(engines.length, 3, "the opensearch.html page exposes 3 engines");
+
+ // Check that there's initially no selection.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ // Pressing up once selects the setting button...
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // ...and then pressing up selects open search engines.
+ for (let i = engines.length; i; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ let selectedButton = textbox.selectedButton;
+ is(
+ selectedButton,
+ engines[i - 1],
+ "the engine #" + i + " should be selected"
+ );
+ ok(
+ selectedButton.classList.contains("addengine-item"),
+ "the button is themed as an engine item"
+ );
+ }
+
+ // Pressing up again should select the last one-off button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ textbox.selectedButton,
+ getOneOffs().pop(),
+ "the last one-off button should be selected"
+ );
+
+ info("now check that the down key navigates open search items as expected");
+ for (let i = 0; i < engines.length; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ textbox.selectedButton,
+ engines[i],
+ "the engine #" + (i + 1) + " should be selected"
+ );
+ }
+
+ // Pressing down on the last engine item selects the settings button.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function cleanup() {
+ info("removing search history values: " + kValues);
+ let removeOps = kValues.map(value => {
+ return { op: "remove", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(removeOps);
+
+ textbox.value = "";
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_openpopup.js b/browser/components/search/test/browser/browser_searchbar_openpopup.js
new file mode 100644
index 0000000000..542431160c
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_openpopup.js
@@ -0,0 +1,792 @@
+// Tests that the suggestion popup appears at the right times in response to
+// focus and user events (mouse, keyboard, drop).
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const kValues = ["long text", "long text 2", "long text 3"];
+
+function synthesizeNativeMouseClickAtCenterAsync(aElement) {
+ // Wait for the mouseup event to occur before continuing.
+ return new Promise((resolve, reject) => {
+ function eventOccurred(e) {
+ aElement.removeEventListener("mouseup", eventOccurred, true);
+ resolve();
+ }
+
+ aElement.addEventListener("mouseup", eventOccurred, true);
+
+ EventUtils.synthesizeNativeMouseClickAtCenter(aElement);
+ });
+}
+
+async function endCustomizing(aWindow = window) {
+ if (aWindow.document.documentElement.getAttribute("customizing") != "true") {
+ return true;
+ }
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ aWindow.gNavToolbox,
+ "aftercustomization"
+ );
+ aWindow.gCustomizeMode.exit();
+ return eventPromise;
+}
+
+async function startCustomizing(aWindow = window) {
+ if (aWindow.document.documentElement.getAttribute("customizing") == "true") {
+ return true;
+ }
+ let eventPromise = BrowserTestUtils.waitForEvent(
+ aWindow.gNavToolbox,
+ "customizationready"
+ );
+ aWindow.gCustomizeMode.enter();
+ return eventPromise;
+}
+
+let searchbar;
+let textbox;
+let searchIcon;
+let goButton;
+
+add_task(async function init() {
+ searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ textbox = searchbar.textbox;
+ searchIcon = searchbar.querySelector(".searchbar-search-button");
+ goButton = searchbar.querySelector(".search-go-button");
+
+ await promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ await new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update(
+ { op: "remove", fieldname: "searchbar-history" },
+ { handleCompletion: resolve, handleError: reject }
+ );
+ });
+
+ await new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let addOps = kValues.map(value => {
+ return { op: "add", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(addOps, {
+ handleCompletion: resolve,
+ handleError: reject,
+ });
+ });
+});
+
+// Adds a task that shouldn't show the search suggestions popup.
+function add_no_popup_task(task) {
+ add_task(async function() {
+ let sawPopup = false;
+ function listener() {
+ sawPopup = true;
+ }
+
+ info("Entering test " + task.name);
+ searchPopup.addEventListener("popupshowing", listener);
+ await task();
+ searchPopup.removeEventListener("popupshowing", listener);
+ ok(!sawPopup, "Shouldn't have seen the suggestions popup");
+ info("Leaving test " + task.name);
+ });
+}
+
+// Simulates the full set of events for a context click
+function context_click(target) {
+ for (let event of ["mousedown", "contextmenu", "mouseup"]) {
+ EventUtils.synthesizeMouseAtCenter(target, { type: event, button: 2 });
+ }
+}
+
+// Right clicking the icon should not open the popup.
+add_no_popup_task(async function open_icon_context() {
+ gURLBar.focus();
+ let toolbarPopup = document.getElementById("toolbar-context-menu");
+
+ let promise = promiseEvent(toolbarPopup, "popupshown");
+ context_click(searchIcon);
+ await promise;
+
+ promise = promiseEvent(toolbarPopup, "popuphidden");
+ toolbarPopup.hidePopup();
+ await promise;
+});
+
+// With no text in the search box left clicking the icon should open the popup.
+// Clicking the icon again should hide the popup and not show it again.
+add_task(async function open_empty() {
+ gURLBar.focus();
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Clicking icon");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await promise;
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should only show the settings"
+ );
+ is(textbox.mController.searchString, "", "Should be an empty search string");
+
+ // By giving the textbox some text any next attempt to open the search popup
+ // from the click handler will try to search for this text.
+ textbox.value = "foo";
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+
+ info("Hiding popup");
+ await synthesizeNativeMouseClickAtCenterAsync(searchIcon);
+ await promise;
+
+ is(
+ textbox.mController.searchString,
+ "",
+ "Should not have started to search for the new text"
+ );
+
+ // Cancel the search if it started.
+ if (textbox.mController.searchString != "") {
+ textbox.mController.stopSearch();
+ }
+
+ textbox.value = "";
+});
+
+// With no text in the search box left clicking it should not open the popup.
+add_no_popup_task(function click_doesnt_open_popup() {
+ gURLBar.focus();
+
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 0, "Should have selected all of the text");
+});
+
+// Left clicking in a non-empty search box when unfocused should focus it and open the popup.
+add_task(async function click_opens_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+
+ textbox.value = "";
+});
+
+add_task(async function open_empty_hiddenOneOffs() {
+ // Disable all the engines but the current one and check the oneoffs.
+ let defaultEngine = await Services.search.getDefault();
+ let engines = (await Services.search.getVisibleEngines()).filter(
+ e => e.name != defaultEngine.name
+ );
+ await SpecialPowers.pushPrefEnv({
+ set: [["browser.search.hiddenOneOffs", engines.map(e => e.name).join(",")]],
+ });
+
+ let oneOffButtons = searchPopup.searchOneOffsContainer.querySelector(
+ ".search-panel-one-offs"
+ );
+ textbox.value = "foo";
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+
+ Assert.ok(
+ oneOffButtons.getAttribute("hidden"),
+ "The one-offs buttons should have the hidden attribute."
+ );
+ Assert.equal(
+ getComputedStyle(oneOffButtons).display,
+ "none",
+ "The one-off buttons should be hidden."
+ );
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+
+ info("Hiding popup");
+ await synthesizeNativeMouseClickAtCenterAsync(searchIcon);
+ await promise;
+
+ await SpecialPowers.popPrefEnv();
+ textbox.value = "";
+});
+
+// Right clicking in a non-empty search box when unfocused should open the edit context menu.
+add_no_popup_task(async function right_click_doesnt_open_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ // Can't wait for an event on the actual menu since it is created
+ // lazily the first time it is displayed.
+ let promise = new Promise(resolve => {
+ let listener = event => {
+ if (searchbar._menupopup && event.target == searchbar._menupopup) {
+ resolve(searchbar._menupopup);
+ }
+ };
+ window.addEventListener("popupshown", listener);
+ });
+ context_click(textbox);
+ let contextPopup = await promise;
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(contextPopup, "popuphidden");
+ contextPopup.hidePopup();
+ await promise;
+
+ textbox.value = "";
+});
+
+// Moving focus away from the search box should close the popup
+add_task(async function focus_change_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let promise2 = promiseEvent(searchbar.textbox, "blur");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await promise;
+ await promise2;
+
+ textbox.value = "";
+});
+
+// Moving focus away from the search box should close the small popup
+add_task(async function focus_change_closes_small_popup() {
+ gURLBar.focus();
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ // For some reason sending the mouse event immediately doesn't open the popup.
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ await promise;
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the small popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let promise2 = promiseEvent(searchbar.textbox, "blur");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await promise;
+ await promise2;
+});
+
+// Pressing escape should close the popup.
+add_task(async function escape_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+
+ textbox.value = "";
+});
+
+// Pressing contextmenu should close the popup.
+add_task(async function contextmenu_closes_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ context_click(textbox);
+ await promise;
+
+ let contextPopup = searchbar._menupopup;
+ promise = promiseEvent(contextPopup, "popuphidden");
+ contextPopup.hidePopup();
+ await promise;
+
+ textbox.value = "";
+});
+
+// Tabbing to the search box should open the popup if it contains text.
+add_task(async function tab_opens_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+
+ textbox.value = "";
+});
+
+// Tabbing to the search box should not open the popup if it doesn't contain text.
+add_no_popup_task(function tab_doesnt_open_popup() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ EventUtils.synthesizeKey("KEY_Tab");
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ textbox.value = "";
+});
+
+// Switching back to the window when the search box has focus from mouse should not open the popup.
+add_task(async function refocus_window_doesnt_open_popup_mouse() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(searchbar, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let newWin = OpenBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, newWin));
+ await promise;
+
+ function listener() {
+ ok(false, "Should not have shown the popup.");
+ }
+ searchPopup.addEventListener("popupshowing", listener);
+
+ promise = promiseEvent(searchbar.textbox, "focus");
+ newWin.close();
+ await promise;
+
+ // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
+ await new Promise(resolve => executeSoon(resolve));
+ await new Promise(resolve => executeSoon(resolve));
+ await new Promise(resolve => executeSoon(resolve));
+
+ searchPopup.removeEventListener("popupshowing", listener);
+ textbox.value = "";
+});
+
+// Switching back to the window when the search box has focus from keyboard should not open the popup.
+add_task(async function refocus_window_doesnt_open_popup_keyboard() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ let newWin = OpenBrowserWindow();
+ await new Promise(resolve => waitForFocus(resolve, newWin));
+ await promise;
+
+ function listener() {
+ ok(false, "Should not have shown the popup.");
+ }
+ searchPopup.addEventListener("popupshowing", listener);
+
+ promise = promiseEvent(searchbar.textbox, "focus");
+ newWin.close();
+ await promise;
+
+ // Wait a few ticks to allow any focus handlers to show the popup if they are going to.
+ await new Promise(resolve => executeSoon(resolve));
+ await new Promise(resolve => executeSoon(resolve));
+ await new Promise(resolve => executeSoon(resolve));
+
+ searchPopup.removeEventListener("popupshowing", listener);
+ textbox.value = "";
+});
+
+// Clicking the search go button shouldn't open the popup
+add_no_popup_task(async function search_go_doesnt_open_popup() {
+ gBrowser.selectedTab = BrowserTestUtils.addTab(gBrowser);
+
+ gURLBar.focus();
+ textbox.value = "foo";
+ searchbar.updateGoButtonVisibility();
+
+ let promise = BrowserTestUtils.browserLoaded(gBrowser.selectedBrowser);
+ EventUtils.synthesizeMouseAtCenter(goButton, {});
+ await promise;
+
+ textbox.value = "";
+ gBrowser.removeCurrentTab();
+});
+
+// Clicks outside the search popup should close the popup but not consume the click.
+add_task(async function dont_consume_clicks() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ is(textbox.selectionStart, 0, "Should have selected all of the text");
+ is(textbox.selectionEnd, 3, "Should have selected all of the text");
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ await synthesizeNativeMouseClickAtCenterAsync(gURLBar.inputField);
+ await promise;
+
+ is(
+ Services.focus.focusedElement,
+ gURLBar.inputField,
+ "Should have focused the URL bar"
+ );
+
+ textbox.value = "";
+});
+
+// Dropping text to the searchbar should open the popup
+add_task(async function drop_opens_popup() {
+ // The previous task leaves focus in the URL bar. However, in that case drags
+ // can be interpreted as being selection drags by the drag manager, which
+ // breaks the drag synthesis from EventUtils.js below. To avoid this, focus
+ // the browser content instead.
+ let focusEventPromise = BrowserTestUtils.waitForEvent(
+ gBrowser.selectedBrowser,
+ "focus"
+ );
+ gBrowser.selectedBrowser.focus();
+ await focusEventPromise;
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ // Use a source for the drop that is outside of the search bar area, to avoid
+ // it receiving a mousedown and causing the popup to sometimes open.
+ let homeButton = document.getElementById("home-button");
+ EventUtils.synthesizeDrop(
+ homeButton,
+ textbox,
+ [[{ type: "text/plain", data: "foo" }]],
+ "move",
+ window
+ );
+ await promise;
+
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "Should have focused the search bar"
+ );
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+
+ textbox.value = "";
+});
+
+// Moving the caret using the cursor keys should not close the popup.
+add_task(async function dont_rollup_oncaretmove() {
+ gURLBar.focus();
+ textbox.value = "long text";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeMouseAtCenter(textbox, {});
+ await promise;
+
+ // Deselect the text
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(
+ textbox.selectionStart,
+ 9,
+ "Should have moved the caret (selectionStart after deselect right)"
+ );
+ is(
+ textbox.selectionEnd,
+ 9,
+ "Should have moved the caret (selectionEnd after deselect right)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ textbox.selectionStart,
+ 8,
+ "Should have moved the caret (selectionStart after left)"
+ );
+ is(
+ textbox.selectionEnd,
+ 8,
+ "Should have moved the caret (selectionEnd after left)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(
+ textbox.selectionStart,
+ 9,
+ "Should have moved the caret (selectionStart after right)"
+ );
+ is(
+ textbox.selectionEnd,
+ 9,
+ "Should have moved the caret (selectionEnd after right)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ // Ensure caret movement works while a suggestion is selected.
+ is(textbox.popup.selectedIndex, -1, "No selected item in list");
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(textbox.popup.selectedIndex, 0, "Selected item in list");
+ is(
+ textbox.selectionStart,
+ 9,
+ "Should have moved the caret to the end (selectionStart after selection)"
+ );
+ is(
+ textbox.selectionEnd,
+ 9,
+ "Should have moved the caret to the end (selectionEnd after selection)"
+ );
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ textbox.selectionStart,
+ 8,
+ "Should have moved the caret again (selectionStart after left)"
+ );
+ is(
+ textbox.selectionEnd,
+ 8,
+ "Should have moved the caret again (selectionEnd after left)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("KEY_ArrowLeft");
+ is(
+ textbox.selectionStart,
+ 7,
+ "Should have moved the caret (selectionStart after left)"
+ );
+ is(
+ textbox.selectionEnd,
+ 7,
+ "Should have moved the caret (selectionEnd after left)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ is(
+ textbox.selectionStart,
+ 8,
+ "Should have moved the caret (selectionStart after right)"
+ );
+ is(
+ textbox.selectionEnd,
+ 8,
+ "Should have moved the caret (selectionEnd after right)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+
+ if (!navigator.platform.includes("Mac")) {
+ EventUtils.synthesizeKey("KEY_Home");
+ is(
+ textbox.selectionStart,
+ 0,
+ "Should have moved the caret (selectionStart after home)"
+ );
+ is(
+ textbox.selectionEnd,
+ 0,
+ "Should have moved the caret (selectionEnd after home)"
+ );
+ is(searchPopup.state, "open", "Popup should still be open");
+ }
+
+ // Close the popup again
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+
+ textbox.value = "";
+});
+
+// Entering customization mode shouldn't open the popup.
+add_task(async function dont_open_in_customization() {
+ gURLBar.focus();
+ textbox.value = "foo";
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await promise;
+ isnot(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the full popup"
+ );
+
+ info("Entering customization mode");
+ let sawPopup = false;
+ function listener() {
+ sawPopup = true;
+ }
+ searchPopup.addEventListener("popupshowing", listener);
+ await gCUITestUtils.openMainMenu();
+ promise = promiseEvent(searchPopup, "popuphidden");
+ await startCustomizing();
+ await promise;
+
+ searchPopup.removeEventListener("popupshowing", listener);
+ ok(!sawPopup, "Shouldn't have seen the suggestions popup");
+
+ await endCustomizing();
+ textbox.value = "";
+});
+
+add_task(async function cleanup() {
+ info("removing search history values: " + kValues);
+ let removeOps = kValues.map(value => {
+ return { op: "remove", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(removeOps);
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
new file mode 100644
index 0000000000..3d3a2e0d33
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -0,0 +1,447 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
+
+const kValues = ["foo1", "foo2", "foo3"];
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList = oneOffsContainer.querySelector(".search-add-engines");
+ for (
+ let item = addEngineList.firstElementChild;
+ item;
+ item = item.nextElementSibling
+ ) {
+ os.push(item);
+ }
+
+ return os;
+}
+
+let searchbar;
+let textbox;
+let searchIcon;
+
+add_task(async function init() {
+ searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ textbox = searchbar.textbox;
+ searchIcon = searchbar.querySelector(".searchbar-search-button");
+
+ await promiseNewEngine("testEngine.xml");
+
+ // First cleanup the form history in case other tests left things there.
+ await new Promise((resolve, reject) => {
+ info("cleanup the search history");
+ searchbar.FormHistory.update(
+ { op: "remove", fieldname: "searchbar-history" },
+ { handleCompletion: resolve, handleError: reject }
+ );
+ });
+
+ await new Promise((resolve, reject) => {
+ info("adding search history values: " + kValues);
+ let addOps = kValues.map(value => {
+ return { op: "add", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(addOps, {
+ handleCompletion: resolve,
+ handleError: reject,
+ });
+ });
+});
+
+add_task(async function test_arrows() {
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await promise;
+ info(
+ "textbox.mController.searchString = " + textbox.mController.searchString
+ );
+ is(textbox.mController.searchString, "", "The search string should be empty");
+
+ // Check the initial state of the panel before sending keyboard events.
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the small popup"
+ );
+ // Having suggestions populated (but hidden) is important, because if there
+ // are none we can't ensure the keyboard events don't reach them.
+ is(searchPopup.matchCount, kValues.length, "There should be 3 suggestions");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // The tests will be less meaningful if the first, second, last, and
+ // before-last one-off buttons aren't different. We should always have more
+ // than 4 default engines, but it's safer to check this assumption.
+ let oneOffs = getOneOffs();
+ ok(oneOffs.length >= 4, "we have at least 4 one-off buttons displayed");
+
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing should select the first one-off.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ }
+
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+
+ // We should now be back to the initial situation.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ info("now test the up arrow key");
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // cycle through the one-off items, the first one is already selected.
+ for (let i = oneOffs.length; i; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ textbox.selectedButton,
+ oneOffs[i - 1],
+ "the one-off button #" + i + " should be selected"
+ );
+ }
+
+ // Another press on up should clear the one-off selection.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+});
+
+add_task(async function test_tab() {
+ is(
+ Services.focus.focusedElement,
+ textbox,
+ "the search bar should be focused"
+ ); // from the previous test.
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // Pressing tab should select the first one-off without selecting suggestions.
+ // now cycle through the one-off items, the first one is already selected.
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // One more <tab> selects the settings button.
+ EventUtils.synthesizeKey("KEY_Tab");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // Pressing tab again should close the panel...
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Tab");
+ await promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(
+ Services.focus.focusedElement,
+ textbox,
+ "the search bar no longer be focused"
+ );
+});
+
+add_task(async function test_shift_tab() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ await promise;
+
+ let oneOffs = getOneOffs();
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the small popup"
+ );
+
+ // Press up once to select the last button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // Press up again to select the last one-off button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+
+ // Pressing shift+tab should cycle through the one-off items.
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ if (i) {
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ }
+ }
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing shift+tab again should close the panel...
+ promise = promiseEvent(searchPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Tab", { shiftKey: true });
+ await promise;
+
+ // ... and move the focus out of the searchbox.
+ isnot(
+ Services.focus.focusedElement,
+ textbox,
+ "the search bar no longer be focused"
+ );
+});
+
+add_task(async function test_alt_down() {
+ // First reopen the panel.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ SimpleTest.executeSoon(() => {
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ });
+ await promise;
+
+ // and check it's in a correct initial state.
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the small popup"
+ );
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing alt+down should select the first one-off without selecting suggestions
+ // and cycle through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = 0; i < oneOffs.length; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the first one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowDown", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+
+ // Clear the selection with an alt+up keypress
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+});
+
+add_task(async function test_alt_up() {
+ // Check the initial state of the panel
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // Pressing alt+up should select the last one-off without selecting suggestions
+ // and cycle up through the one-off items.
+ let oneOffs = getOneOffs();
+ for (let i = oneOffs.length - 1; i >= 0; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[i],
+ "the one-off button #" + (i + 1) + " should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ }
+
+ // One more alt+down keypress and nothing should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+
+ // another one and the last one-off should be selected.
+ EventUtils.synthesizeKey("KEY_ArrowUp", { altKey: true });
+ is(
+ textbox.selectedButton,
+ oneOffs[oneOffs.length - 1],
+ "the last one-off button should be selected"
+ );
+
+ // Cleanup for the next test.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(!textbox.selectedButton, "no one-off should be selected anymore");
+});
+
+add_task(async function test_tab_and_arrows() {
+ // Check the initial state is as expected.
+ ok(!textbox.selectedButton, "no one-off button should be selected");
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ is(textbox.value, "", "the textfield value should be unmodified");
+
+ // After pressing down, the first one-off should be selected.
+ let oneOffs = getOneOffs();
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing tab, the second one-off should be selected.
+ EventUtils.synthesizeKey("KEY_Tab");
+ is(
+ textbox.selectedButton,
+ oneOffs[1],
+ "the second one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // After pressing up, the first one-off should be selected again.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ textbox.selectedButton,
+ oneOffs[0],
+ "the first one-off button should be selected"
+ );
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+
+ // Finally close the panel.
+ let promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+});
+
+add_task(async function test_open_search() {
+ let rootDir = getRootDirectory(gTestPath);
+ await BrowserTestUtils.openNewForegroundTab(
+ gBrowser,
+ rootDir + "opensearch.html"
+ );
+
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ EventUtils.synthesizeMouseAtCenter(searchIcon, {});
+ await promise;
+ is(
+ searchPopup.getAttribute("showonlysettings"),
+ "true",
+ "Should show the small popup"
+ );
+
+ let engines = getOpenSearchItems();
+ is(engines.length, 3, "the opensearch.html page exposes 3 engines");
+
+ // Check that there's initially no selection.
+ is(searchPopup.selectedIndex, -1, "no suggestion should be selected");
+ ok(!textbox.selectedButton, "no button should be selected");
+
+ // Pressing up once selects the setting button...
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ // ...and then pressing up selects open search engines.
+ for (let i = engines.length; i; --i) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ let selectedButton = textbox.selectedButton;
+ is(
+ selectedButton,
+ engines[i - 1],
+ "the engine #" + i + " should be selected"
+ );
+ ok(
+ selectedButton.classList.contains("addengine-item"),
+ "the button is themed as an engine item"
+ );
+ }
+
+ // Pressing up again should select the last one-off button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ is(
+ textbox.selectedButton,
+ getOneOffs().pop(),
+ "the last one-off button should be selected"
+ );
+
+ info("now check that the down key navigates open search items as expected");
+ for (let i = 0; i < engines.length; ++i) {
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ is(
+ textbox.selectedButton,
+ engines[i],
+ "the engine #" + (i + 1) + " should be selected"
+ );
+ }
+
+ // Pressing down on the last engine item selects the settings button.
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ ok(
+ textbox.selectedButton.classList.contains("search-setting-button"),
+ "the settings item should be selected"
+ );
+
+ promise = promiseEvent(searchPopup, "popuphidden");
+ searchPopup.hidePopup();
+ await promise;
+
+ gBrowser.removeCurrentTab();
+});
+
+add_task(async function cleanup() {
+ info("removing search history values: " + kValues);
+ let removeOps = kValues.map(value => {
+ return { op: "remove", fieldname: "searchbar-history", value };
+ });
+ searchbar.FormHistory.update(removeOps);
+});
diff --git a/browser/components/search/test/browser/browser_searchbar_widths.js b/browser/components/search/test/browser/browser_searchbar_widths.js
new file mode 100644
index 0000000000..fcb2d32f89
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_widths.js
@@ -0,0 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Check that when the searchbar has a specific width, opening a new window
+// honours that specific width.
+add_task(async function test_searchbar_width_persistence() {
+ await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(async function() {
+ gCUITestUtils.removeSearchBar();
+ });
+
+ // Really, we should use the splitter, but drag/drop is hard and fragile in
+ // tests, so let's just fake it real quick:
+ let container = BrowserSearch.searchBar.parentNode;
+ // There's no width attribute set initially, just grab the info from layout:
+ let oldWidth = container.getBoundingClientRect().width;
+ let newWidth = "" + Math.round(oldWidth * 2);
+ container.setAttribute("width", newWidth);
+
+ let win = await BrowserTestUtils.openNewBrowserWindow();
+ let otherBar = win.BrowserSearch.searchBar;
+ ok(otherBar, "Should have a search bar in the other window");
+ if (otherBar) {
+ is(
+ otherBar.parentNode.getAttribute("width"),
+ newWidth,
+ "Should have matching width"
+ );
+ }
+ await BrowserTestUtils.closeWindow(win);
+});
diff --git a/browser/components/search/test/browser/browser_tooManyEnginesOffered.js b/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
new file mode 100644
index 0000000000..7275b047e4
--- /dev/null
+++ b/browser/components/search/test/browser/browser_tooManyEnginesOffered.js
@@ -0,0 +1,104 @@
+"use strict";
+
+// This test makes sure that when a page offers many search engines, the search
+// popup shows a submenu that lists them instead of showing them in the popup
+// itself.
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+const oneOffsContainer = searchPopup.searchOneOffsContainer;
+
+add_task(async function test_setup() {
+ await gCUITestUtils.addSearchBar();
+
+ await Services.search.init();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+add_task(async function test() {
+ let searchbar = BrowserSearch.searchBar;
+
+ let rootDir = getRootDirectory(gTestPath);
+ let url = rootDir + "tooManyEnginesOffered.html";
+ await BrowserTestUtils.openNewForegroundTab(gBrowser, url);
+
+ // Open the search popup.
+ let promise = promiseEvent(searchPopup, "popupshown");
+ info("Opening search panel");
+ searchbar.focus();
+ EventUtils.synthesizeKey("KEY_ArrowDown");
+ await promise;
+
+ // Make sure it has only one add-engine menu button item.
+ let items = getOpenSearchItems();
+ Assert.equal(items.length, 1, "A single button");
+ let menuButton = items[0];
+ Assert.equal(menuButton.type, "menu", "A menu button");
+
+ // Mouse over the menu button to open it.
+ let buttonPopup = menuButton.menupopup;
+ promise = promiseEvent(buttonPopup, "popupshown");
+ EventUtils.synthesizeMouse(menuButton, 5, 5, { type: "mousemove" });
+ await promise;
+
+ Assert.ok(menuButton.open, "Submenu should be open");
+
+ // Check the engines inside the submenu.
+ Assert.equal(buttonPopup.children.length, 6, "Expected number of engines");
+ for (let i = 0; i < buttonPopup.children.length; i++) {
+ let item = buttonPopup.children[i];
+ Assert.equal(
+ item.getAttribute("title"),
+ "engine" + (i + 1),
+ "Expected engine title"
+ );
+ }
+
+ // Mouse out of the menu button to close it.
+ promise = promiseEvent(buttonPopup, "popuphidden");
+ EventUtils.synthesizeMouse(searchbar, 5, 5, { type: "mousemove" });
+ await promise;
+
+ Assert.ok(!menuButton.open, "Submenu should be closed");
+
+ // Key up until the menu button is selected.
+ for (
+ let button = null;
+ button != menuButton;
+ button = searchbar.textbox.popup.oneOffButtons.selectedButton
+ ) {
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ }
+
+ // Press the Right arrow key. The submenu should open.
+ promise = promiseEvent(buttonPopup, "popupshown");
+ EventUtils.synthesizeKey("KEY_ArrowRight");
+ await promise;
+
+ Assert.ok(menuButton.open, "Submenu should be open");
+
+ // Press the Esc key. The submenu should close.
+ promise = promiseEvent(buttonPopup, "popuphidden");
+ EventUtils.synthesizeKey("KEY_Escape");
+ await promise;
+
+ Assert.ok(!menuButton.open, "Submenu should be closed");
+
+ gBrowser.removeCurrentTab();
+});
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList = oneOffsContainer.querySelector(".search-add-engines");
+ for (
+ let item = addEngineList.firstElementChild;
+ item;
+ item = item.nextElementSibling
+ ) {
+ os.push(item);
+ }
+
+ return os;
+}
diff --git a/browser/components/search/test/browser/contentSearchUI.html b/browser/components/search/test/browser/contentSearchUI.html
new file mode 100644
index 0000000000..09abe822b2
--- /dev/null
+++ b/browser/components/search/test/browser/contentSearchUI.html
@@ -0,0 +1,22 @@
+<!DOCTYPE html>
+<!-- Any copyright is dedicated to the Public Domain.
+ - http://creativecommons.org/publicdomain/zero/1.0/ -->
+
+<html>
+<head>
+<meta charset="utf-8">
+<script type="application/javascript"
+ src="chrome://browser/content/contentSearchUI.js">
+</script>
+<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css"/>
+<meta http-equiv="Content-Security-Policy" content="default-src data: chrome:; object-src 'none'"/>
+</head>
+<body>
+
+<div id="container"><input type="text" value=""/></div>
+
+<script src="chrome://mochitests/content/browser/browser/components/search/test/browser/contentSearchUI.js">
+</script>
+
+</body>
+</html>
diff --git a/browser/components/search/test/browser/contentSearchUI.js b/browser/components/search/test/browser/contentSearchUI.js
new file mode 100644
index 0000000000..7ccf0b6a6d
--- /dev/null
+++ b/browser/components/search/test/browser/contentSearchUI.js
@@ -0,0 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+/* import-globals-from ../../content/contentSearchUI.js */
+var input = document.querySelector("input");
+var gController = new ContentSearchUIController(
+ input,
+ input.parentNode,
+ "test",
+ "test"
+);
diff --git a/browser/components/search/test/browser/google_codes/browser.ini b/browser/components/search/test/browser/google_codes/browser.ini
new file mode 100644
index 0000000000..34eb85c7b6
--- /dev/null
+++ b/browser/components/search/test/browser/google_codes/browser.ini
@@ -0,0 +1,6 @@
+[DEFAULT]
+prefs =
+ browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar=false
+ browser.search.region='DE'
+
+[../browser_google_behavior.js]
diff --git a/browser/components/search/test/browser/head.js b/browser/components/search/test/browser/head.js
new file mode 100644
index 0000000000..97615b0dad
--- /dev/null
+++ b/browser/components/search/test/browser/head.js
@@ -0,0 +1,233 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ ADLINK_CHECK_TIMEOUT_MS: "resource:///actors/SearchSERPTelemetryChild.jsm",
+ AddonTestUtils: "resource://testing-common/AddonTestUtils.jsm",
+ CustomizableUITestUtils:
+ "resource://testing-common/CustomizableUITestUtils.jsm",
+ FormHistoryTestUtils: "resource://testing-common/FormHistoryTestUtils.jsm",
+ PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.jsm",
+ SearchTestUtils: "resource://testing-common/SearchTestUtils.jsm",
+ SearchUtils: "resource://gre/modules/SearchUtils.jsm",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm",
+ UrlbarSearchUtils: "resource:///modules/UrlbarSearchUtils.jsm",
+});
+
+let gCUITestUtils = new CustomizableUITestUtils(window);
+
+AddonTestUtils.initMochitest(this);
+SearchTestUtils.init(this);
+
+/**
+ * Recursively compare two objects and check that every property of expectedObj has the same value
+ * on actualObj.
+ *
+ * @param {object} expectedObj
+ * The expected object to find.
+ * @param {object} actualObj
+ * The object to inspect.
+ * @param {string} name
+ * The name of the engine, used for test detail logging.
+ */
+function isSubObjectOf(expectedObj, actualObj, name) {
+ for (let prop in expectedObj) {
+ if (typeof expectedObj[prop] == "function") {
+ continue;
+ }
+ if (expectedObj[prop] instanceof Object) {
+ is(
+ actualObj[prop].length,
+ expectedObj[prop].length,
+ name + "[" + prop + "]"
+ );
+ isSubObjectOf(
+ expectedObj[prop],
+ actualObj[prop],
+ name + "[" + prop + "]"
+ );
+ } else {
+ is(actualObj[prop], expectedObj[prop], name + "[" + prop + "]");
+ }
+ }
+}
+
+function getLocale() {
+ return Services.locale.requestedLocale || undefined;
+}
+
+function promiseEvent(aTarget, aEventName, aPreventDefault) {
+ function cancelEvent(event) {
+ if (aPreventDefault) {
+ event.preventDefault();
+ }
+
+ return true;
+ }
+
+ return BrowserTestUtils.waitForEvent(aTarget, aEventName, false, cancelEvent);
+}
+
+/**
+ * Adds a new search engine to the search service and confirms it completes.
+ *
+ * @param {string} basename The file to load that contains the search engine
+ * details.
+ * @param {object} [options] Options for the test:
+ * - {String} [iconURL] The icon to use for the search engine.
+ * - {Boolean} [setAsCurrent] Whether to set the new engine to be the
+ * current engine or not.
+ * - {Boolean} [setAsCurrentPrivate] Whether to set the new engine to be the
+ * current private browsing mode engine or not.
+ * Defaults to false.
+ * - {String} [testPath] Used to override the current test path if this
+ * file is used from a different directory.
+ * @returns {Promise} The promise is resolved once the engine is added, or
+ * rejected if the addition failed.
+ */
+async function promiseNewEngine(basename, options = {}) {
+ // Default the setAsCurrent option to true.
+ let setAsCurrent =
+ options.setAsCurrent == undefined ? true : options.setAsCurrent;
+ info("Waiting for engine to be added: " + basename);
+ let url = getRootDirectory(options.testPath || gTestPath) + basename;
+ let engine = await Services.search.addOpenSearchEngine(
+ url,
+ options.iconURL || ""
+ );
+ info("Search engine added: " + basename);
+ const current = await Services.search.getDefault();
+ if (setAsCurrent) {
+ await Services.search.setDefault(engine);
+ }
+ const currentPrivate = await Services.search.getDefaultPrivate();
+ if (options.setAsCurrentPrivate) {
+ await Services.search.setDefaultPrivate(engine);
+ }
+ registerCleanupFunction(async () => {
+ if (setAsCurrent) {
+ await Services.search.setDefault(current);
+ }
+ if (options.setAsCurrentPrivate) {
+ await Services.search.setDefaultPrivate(currentPrivate);
+ }
+ await Services.search.removeEngine(engine);
+ info("Search engine removed: " + basename);
+ });
+ return engine;
+}
+
+// Get an array of the one-off buttons.
+function getOneOffs() {
+ let oneOffs = [];
+ let searchPopup = document.getElementById("PopupSearchAutoComplete");
+ let oneOffsContainer = searchPopup.searchOneOffsContainer;
+ let oneOff = oneOffsContainer.querySelector(".search-panel-one-offs");
+ for (oneOff = oneOff.firstChild; oneOff; oneOff = oneOff.nextSibling) {
+ if (oneOff.nodeType == Node.ELEMENT_NODE) {
+ oneOffs.push(oneOff);
+ }
+ }
+ return oneOffs;
+}
+
+async function typeInSearchField(browser, text, fieldName) {
+ await SpecialPowers.spawn(browser, [[fieldName, text]], async function([
+ contentFieldName,
+ contentText,
+ ]) {
+ // Put the focus on the search box.
+ let searchInput = content.document.getElementById(contentFieldName);
+ searchInput.focus();
+ searchInput.value = contentText;
+ });
+}
+
+XPCOMUtils.defineLazyGetter(this, "searchCounts", () => {
+ return Services.telemetry.getKeyedHistogramById("SEARCH_COUNTS");
+});
+
+XPCOMUtils.defineLazyGetter(this, "SEARCH_AD_CLICK_SCALARS", () => {
+ const sources = [
+ ...BrowserSearchTelemetry.KNOWN_SEARCH_SOURCES.values(),
+ "unknown",
+ ];
+ return [
+ "browser.search.with_ads",
+ "browser.search.ad_clicks",
+ ...sources.map(v => `browser.search.withads.${v}`),
+ ...sources.map(v => `browser.search.adclicks.${v}`),
+ ];
+});
+
+// Ad links are processed after a small delay. We need to allow tests to wait
+// for that before checking telemetry, otherwise the received values may be
+// too small in some cases.
+function promiseWaitForAdLinkCheck() {
+ return new Promise(resolve =>
+ /* eslint-disable-next-line mozilla/no-arbitrary-setTimeout */
+ setTimeout(resolve, ADLINK_CHECK_TIMEOUT_MS)
+ );
+}
+
+async function assertSearchSourcesTelemetry(
+ expectedHistograms,
+ expectedScalars
+) {
+ let histSnapshot = {};
+ let scalars = {};
+
+ await TestUtils.waitForCondition(() => {
+ histSnapshot = searchCounts.snapshot();
+ return (
+ Object.getOwnPropertyNames(histSnapshot).length ==
+ Object.getOwnPropertyNames(expectedHistograms).length
+ );
+ }, "should have the correct number of histograms");
+
+ if (Object.entries(expectedScalars).length) {
+ await TestUtils.waitForCondition(() => {
+ scalars =
+ Services.telemetry.getSnapshotForKeyedScalars("main", false).parent ||
+ {};
+ return Object.getOwnPropertyNames(expectedScalars).every(
+ scalar => scalar in scalars
+ );
+ }, "should have the expected keyed scalars");
+ }
+
+ Assert.equal(
+ Object.getOwnPropertyNames(histSnapshot).length,
+ Object.getOwnPropertyNames(expectedHistograms).length,
+ "Should only have one key"
+ );
+
+ for (let [key, value] of Object.entries(expectedHistograms)) {
+ Assert.ok(
+ key in histSnapshot,
+ `Histogram should have the expected key: ${key}`
+ );
+ Assert.equal(
+ histSnapshot[key].sum,
+ value,
+ `Should have counted the correct number of visits for ${key}`
+ );
+ }
+
+ for (let [name, value] of Object.entries(expectedScalars)) {
+ Assert.ok(name in scalars, `Scalar ${name} should have been added.`);
+ Assert.deepEqual(
+ scalars[name],
+ value,
+ `Should have counted the correct number of visits for ${name}`
+ );
+ }
+
+ for (let name of SEARCH_AD_CLICK_SCALARS) {
+ Assert.equal(
+ name in scalars,
+ name in expectedScalars,
+ `Should have matched ${name} in scalars and expectedScalars`
+ );
+ }
+}
diff --git a/browser/components/search/test/browser/mozsearch.sjs b/browser/components/search/test/browser/mozsearch.sjs
new file mode 100644
index 0000000000..bde867c93e
--- /dev/null
+++ b/browser/components/search/test/browser/mozsearch.sjs
@@ -0,0 +1,11 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ resp.setHeader("Content-Type", "text/html", false);
+ if (req.hasHeader("Origin") && req.getHeader("Origin") != "null") {
+ resp.write("error");
+ return;
+ }
+ resp.write("hello world");
+}
diff --git a/browser/components/search/test/browser/opensearch.html b/browser/components/search/test/browser/opensearch.html
new file mode 100644
index 0000000000..00620e3bcc
--- /dev/null
+++ b/browser/components/search/test/browser/opensearch.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine_mozsearch.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engineInvalid" href="http://mochi.test:8888/browser/browser/components/search/test/browser/testEngine_404.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/components/search/test/browser/searchSuggestionEngine.sjs b/browser/components/search/test/browser/searchSuggestionEngine.sjs
new file mode 100644
index 0000000000..8b8da60572
--- /dev/null
+++ b/browser/components/search/test/browser/searchSuggestionEngine.sjs
@@ -0,0 +1,50 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+let gTimer;
+
+function handleRequest(req, resp) {
+ // Parse the query params. If the params aren't in the form "foo=bar", then
+ // treat the entire query string as a search string.
+ let params = req.queryString.split("&").reduce((memo, pair) => {
+ let [key, val] = pair.split("=");
+ if (!val) {
+ // This part isn't in the form "foo=bar". Treat it as the search string
+ // (the "query").
+ val = key;
+ key = "query";
+ }
+ memo[decode(key)] = decode(val);
+ return memo;
+ }, {});
+
+ let timeout = parseInt(params.timeout);
+ if (timeout) {
+ // Write the response after a timeout.
+ resp.processAsync();
+ gTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gTimer.init(
+ () => {
+ writeResponse(params, resp);
+ resp.finish();
+ },
+ timeout,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+ return;
+ }
+
+ writeResponse(params, resp);
+}
+
+function writeResponse(params, resp) {
+ // Echo back the search string with "foo" and "bar" appended.
+ let suffixes = ["foo", "bar"];
+ let data = [params.query, suffixes.map(s => params.query + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
+
+function decode(str) {
+ return decodeURIComponent(str.replace(/\+/g, encodeURIComponent(" ")));
+}
diff --git a/browser/components/search/test/browser/searchTelemetry.html b/browser/components/search/test/browser/searchTelemetry.html
new file mode 100644
index 0000000000..1cdc206547
--- /dev/null
+++ b/browser/components/search/test/browser/searchTelemetry.html
@@ -0,0 +1,10 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+ <a href="https://example.com/otherpage">Non ad link</a>
+ <a href="https://example1.com/ad">Matching path prefix, different server</a>
+ <a href="https://mochi.test:8888/otherpage">Non ad link</a>
+</body>
+</html>
diff --git a/browser/components/search/test/browser/searchTelemetryAd.html b/browser/components/search/test/browser/searchTelemetryAd.html
new file mode 100644
index 0000000000..1bdc91857c
--- /dev/null
+++ b/browser/components/search/test/browser/searchTelemetryAd.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html>
+<html>
+<head>
+</head>
+<body>
+ <a id="ad1" href="https://example.com/ad">Ad link</a>
+ <a id="ad2" href="https://example.com/ad2">Second Ad link</a>
+</body>
+</html>
diff --git a/browser/components/search/test/browser/telemetrySearchSuggestions.sjs b/browser/components/search/test/browser/telemetrySearchSuggestions.sjs
new file mode 100644
index 0000000000..1978b4f665
--- /dev/null
+++ b/browser/components/search/test/browser/telemetrySearchSuggestions.sjs
@@ -0,0 +1,9 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+function handleRequest(req, resp) {
+ let suffixes = ["foo", "bar"];
+ let data = [req.queryString, suffixes.map(s => req.queryString + s)];
+ resp.setHeader("Content-Type", "application/json", false);
+ resp.write(JSON.stringify(data));
+}
diff --git a/browser/components/search/test/browser/telemetrySearchSuggestions.xml b/browser/components/search/test/browser/telemetrySearchSuggestions.xml
new file mode 100644
index 0000000000..057fc70bf5
--- /dev/null
+++ b/browser/components/search/test/browser/telemetrySearchSuggestions.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+<ShortName>browser_UsageTelemetry usageTelemetrySearchSuggestions.xml</ShortName>
+<Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/telemetrySearchSuggestions.sjs?{searchTerms}"/>
+<Url type="text/html" method="GET" template="http://example.com" rel="searchform"/>
+</SearchPlugin>
diff --git a/browser/components/search/test/browser/test.html b/browser/components/search/test/browser/test.html
new file mode 100644
index 0000000000..a39bece4ff
--- /dev/null
+++ b/browser/components/search/test/browser/test.html
@@ -0,0 +1,8 @@
+<!DOCTYPE html>
+<html>
+<head>
+ <meta charset="utf-8" />
+ <title>Bug 426329</title>
+</head>
+<body></body>
+</html>
diff --git a/browser/components/search/test/browser/testEngine.xml b/browser/components/search/test/browser/testEngine.xml
new file mode 100644
index 0000000000..9c25993232
--- /dev/null
+++ b/browser/components/search/test/browser/testEngine.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo</ShortName>
+ <Description>Foo Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
+ <moz:Alias>fooalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/testEngine_diacritics.xml b/browser/components/search/test/browser/testEngine_diacritics.xml
new file mode 100644
index 0000000000..340893348d
--- /dev/null
+++ b/browser/components/search/test/browser/testEngine_diacritics.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo &#9825;</ShortName>
+ <Description>Engine whose ShortName contains non-BMP Unicode characters</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
+ <moz:Alias>diacriticalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/testEngine_dupe.xml b/browser/components/search/test/browser/testEngine_dupe.xml
new file mode 100644
index 0000000000..86c4cfadaf
--- /dev/null
+++ b/browser/components/search/test/browser/testEngine_dupe.xml
@@ -0,0 +1,12 @@
+<OpenSearchDescription xmlns="http://a9.com/-/spec/opensearch/1.1/"
+ xmlns:moz="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>FooDupe</ShortName>
+ <Description>Second Engine Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?search">
+ <Param name="test" value="{searchTerms}"/>
+ </Url>
+ <moz:SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</moz:SearchForm>
+ <moz:Alias>secondalias</moz:Alias>
+</OpenSearchDescription>
diff --git a/browser/components/search/test/browser/testEngine_mozsearch.xml b/browser/components/search/test/browser/testEngine_mozsearch.xml
new file mode 100644
index 0000000000..2f285feb4c
--- /dev/null
+++ b/browser/components/search/test/browser/testEngine_mozsearch.xml
@@ -0,0 +1,14 @@
+<SearchPlugin xmlns="http://www.mozilla.org/2006/browser/search/">
+ <ShortName>Foo</ShortName>
+ <Description>Foo Search</Description>
+ <InputEncoding>utf-8</InputEncoding>
+ <Image width="16" height="16">data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAABGklEQVQoz2NgGB6AnZ1dUlJSXl4eSDIyMhLW4Ovr%2B%2Fr168uXL69Zs4YoG%2BLi4i5dusTExMTGxsbNzd3f37937976%2BnpmZmagbHR09J49e5YvX66kpATVEBYW9ubNm2nTphkbG7e2tp44cQLIuHfvXm5urpaWFlDKysqqu7v73LlzECMYIiIiHj58mJCQoKKicvXq1bS0NKBgW1vbjh074uPjgeqAXE1NzSdPnvDz84M0AEUvXLgAsW379u1z5swBen3jxo2zZ892cHB4%2BvQp0KlAfwI1cHJyghQFBwfv2rULokFXV%2FfixYu7d%2B8GGqGgoMDKyrpu3br9%2B%2FcDuXl5eVA%2FAEWBfoWHAdAYoNuAYQ0XAeoUERFhGDYAAPoUaT2dfWJuAAAAAElFTkSuQmCC</Image>
+ <Url type="application/x-suggestions+json" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/?suggestions&amp;locale={moz:locale}&amp;test={searchTerms}"/>
+ <Url type="text/html" method="GET" template="http://mochi.test:8888/browser/browser/components/search/test/browser/">
+ <Param name="test" value="{searchTerms}"/>
+ <Param name="ie" value="utf-8"/>
+ <MozParam name="channel" condition="purpose" purpose="keyword" value="keywordsearch"/>
+ <MozParam name="channel" condition="purpose" purpose="contextmenu" value="contextsearch"/>
+ </Url>
+ <SearchForm>http://mochi.test:8888/browser/browser/components/search/test/browser/</SearchForm>
+</SearchPlugin>
diff --git a/browser/components/search/test/browser/test_search.html b/browser/components/search/test/browser/test_search.html
new file mode 100644
index 0000000000..010d1fdc82
--- /dev/null
+++ b/browser/components/search/test/browser/test_search.html
@@ -0,0 +1 @@
+test%20search
diff --git a/browser/components/search/test/browser/tooManyEnginesOffered.html b/browser/components/search/test/browser/tooManyEnginesOffered.html
new file mode 100644
index 0000000000..64e48d05e9
--- /dev/null
+++ b/browser/components/search/test/browser/tooManyEnginesOffered.html
@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html>
+<head>
+<meta charset="UTF-8">
+<link rel="search" type="application/opensearchdescription+xml" title="engine1" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine1.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine2" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine2.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine3" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine3.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine4" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine4.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine5" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine5.xml">
+<link rel="search" type="application/opensearchdescription+xml" title="engine6" href="http://mochi.test:8888/browser/browser/components/search/test/browser/engine6.xml">
+</head>
+<body></body>
+</html>
diff --git a/browser/components/search/test/marionette/manifest.ini b/browser/components/search/test/marionette/manifest.ini
new file mode 100644
index 0000000000..3ca0ae0eb5
--- /dev/null
+++ b/browser/components/search/test/marionette/manifest.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+run-if = buildapp == 'browser'
+
+[test_engines_on_restart.py]
diff --git a/browser/components/search/test/marionette/test_engines_on_restart.py b/browser/components/search/test/marionette/test_engines_on_restart.py
new file mode 100644
index 0000000000..5f9bfe6334
--- /dev/null
+++ b/browser/components/search/test/marionette/test_engines_on_restart.py
@@ -0,0 +1,41 @@
+# -*- coding: utf-8 -*-
+# 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/.
+
+from __future__ import absolute_import
+import textwrap
+
+from marionette_harness.marionette_test import MarionetteTestCase
+
+
+class TestEnginesOnRestart(MarionetteTestCase):
+ def setUp(self):
+ super(TestEnginesOnRestart, self).setUp()
+ self.marionette.enforce_gecko_prefs(
+ {
+ "browser.search.log": True,
+ }
+ )
+
+ def get_default_search_engine(self):
+ """Retrieve the identifier of the default search engine."""
+
+ script = """\
+ let [resolve] = arguments;
+ let searchService = Components.classes[
+ "@mozilla.org/browser/search-service;1"]
+ .getService(Components.interfaces.nsISearchService);
+ return searchService.init().then(function () {
+ resolve(searchService.defaultEngine.identifier);
+ });
+ """
+
+ with self.marionette.using_context(self.marionette.CONTEXT_CHROME):
+ return self.marionette.execute_async_script(textwrap.dedent(script))
+
+ def test_engines(self):
+ self.assertTrue(self.get_default_search_engine().startswith("google"))
+ self.marionette.set_pref("intl.locale.requested", "kk_KZ")
+ self.marionette.restart(clean=False, in_app=True)
+ self.assertTrue(self.get_default_search_engine().startswith("google"))
diff --git a/browser/components/search/test/unit/test_urlTelemetry.js b/browser/components/search/test/unit/test_urlTelemetry.js
new file mode 100644
index 0000000000..ab2367537a
--- /dev/null
+++ b/browser/components/search/test/unit/test_urlTelemetry.js
@@ -0,0 +1,253 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const { XPCOMUtils } = ChromeUtils.import(
+ "resource://gre/modules/XPCOMUtils.jsm"
+);
+
+XPCOMUtils.defineLazyModuleGetters(this, {
+ BrowserSearchTelemetry: "resource:///modules/BrowserSearchTelemetry.jsm",
+ NetUtil: "resource://gre/modules/NetUtil.jsm",
+ SearchSERPTelemetry: "resource:///modules/SearchSERPTelemetry.jsm",
+ SearchUtils: "resource://gre/modules/SearchUtils.jsm",
+ Services: "resource://gre/modules/Services.jsm",
+ sinon: "resource://testing-common/Sinon.jsm",
+ TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.jsm",
+});
+
+const TESTS = [
+ {
+ title: "Google search access point",
+ trackingUrl:
+ "https://www.google.com/search?q=test&ie=utf-8&oe=utf-8&client=firefox-b-1-ab",
+ expectedSearchCountEntry: "google.in-content:sap:firefox-b-1-ab",
+ expectedAdKey: "google:sap",
+ adUrls: [
+ "https://www.googleadservices.com/aclk=foobar",
+ "https://www.googleadservices.com/pagead/aclk=foobar",
+ "https://www.google.com/aclk=foobar",
+ "https://www.google.com/pagead/aclk=foobar",
+ ],
+ nonAdUrls: [
+ "https://www.googleadservices.com/?aclk=foobar",
+ "https://www.googleadservices.com/bar",
+ "https://www.google.com/image",
+ ],
+ },
+ {
+ title: "Google search access point follow-on",
+ trackingUrl:
+ "https://www.google.com/search?client=firefox-b-1-ab&ei=EI_VALUE&q=test2&oq=test2&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google.in-content:sap-follow-on:firefox-b-1-ab",
+ },
+ {
+ title: "Google organic",
+ trackingUrl:
+ "https://www.google.com/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google.in-content:organic:none",
+ expectedAdKey: "google:organic",
+ adUrls: ["https://www.googleadservices.com/aclk=foobar"],
+ nonAdUrls: ["https://www.googleadservices.com/?aclk=foobar"],
+ },
+ {
+ title: "Google organic UK",
+ trackingUrl:
+ "https://www.google.co.uk/search?source=hp&ei=EI_VALUE&q=test&oq=test&gs_l=GS_L_VALUE",
+ expectedSearchCountEntry: "google.in-content:organic:none",
+ },
+ {
+ title: "Yahoo organic",
+ trackingUrl:
+ "https://search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8",
+ expectedSearchCountEntry: "yahoo.in-content:organic:none",
+ },
+ {
+ title: "Yahoo organic UK",
+ trackingUrl:
+ "https://uk.search.yahoo.com/search?p=test&fr=yfp-t&fp=1&toggle=1&cop=mss&ei=UTF-8",
+ expectedSearchCountEntry: "yahoo.in-content:organic:none",
+ },
+ {
+ title: "Bing search access point",
+ trackingUrl: "https://www.bing.com/search?q=test&pc=MOZI&form=MOZLBR",
+ expectedSearchCountEntry: "bing.in-content:sap:MOZI",
+ expectedAdKey: "bing:sap",
+ adUrls: [
+ "https://www.bing.com/aclick?ld=foo",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=bar&url=https%3A%2F%2Fwww.bing.com%2Faclick",
+ "https://www.bing.com/aclk?ld=foo",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=bar&url=https%3A%2F%2Fwww.bing.com%2Faclk",
+ ],
+ nonAdUrls: [
+ "https://www.bing.com/fd/ls/ls.gif?IG=foo",
+ "https://www.bing.com/fd/ls/l?IG=bar",
+ "https://www.bing.com/aclook?",
+ "https://www.bing.com/fd/ls/GLinkPingPost.aspx?IG=baz&url=%2Fvideos%2Fsearch%3Fq%3Dfoo",
+ ],
+ },
+ {
+ setUp() {
+ Services.cookies.removeAll();
+ Services.cookies.add(
+ "www.bing.com",
+ "/",
+ "SRCHS",
+ "PC=MOZ",
+ false,
+ false,
+ false,
+ Date.now() + 1000 * 60 * 60,
+ {},
+ Ci.nsICookie.SAMESITE_NONE,
+ Ci.nsICookie.SCHEME_HTTPS
+ );
+ },
+ tearDown() {
+ Services.cookies.removeAll();
+ },
+ title: "Bing search access point follow-on",
+ trackingUrl:
+ "https://www.bing.com/search?q=test&qs=n&form=QBRE&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE",
+ expectedSearchCountEntry: "bing.in-content:sap-follow-on:MOZ",
+ },
+ {
+ title: "Bing organic",
+ trackingUrl:
+ "https://www.bing.com/search?q=test&qs=n&form=QBLH&sp=-1&pq=&sc=0-0&sk=&cvid=CVID_VALUE",
+ expectedSearchCountEntry: "bing.in-content:organic:none",
+ expectedAdKey: "bing:organic",
+ adUrls: ["https://www.bing.com/aclick?ld=foo"],
+ nonAdUrls: ["https://www.bing.com/fd/ls/ls.gif?IG=foo"],
+ },
+ {
+ title: "DuckDuckGo search access point",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=ffab",
+ expectedSearchCountEntry: "duckduckgo.in-content:sap:ffab",
+ expectedAdKey: "duckduckgo:sap",
+ adUrls: [
+ "https://duckduckgo.com/y.js?ad_provider=foo",
+ "https://duckduckgo.com/y.js?f=bar&ad_provider=foo",
+ "https://www.amazon.co.uk/foo?tag=duckduckgo-ffab-uk-32-xk",
+ ],
+ nonAdUrls: [
+ "https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images",
+ "https://duckduckgo.com/y.js?ifu=foo",
+ "https://improving.duckduckgo.com/t/bar",
+ ],
+ },
+ {
+ title: "DuckDuckGo organic",
+ trackingUrl: "https://duckduckgo.com/?q=test&t=hi&ia=news",
+ expectedSearchCountEntry: "duckduckgo.in-content:organic:hi",
+ expectedAdKey: "duckduckgo:organic",
+ adUrls: ["https://duckduckgo.com/y.js?ad_provider=foo"],
+ nonAdUrls: ["https://duckduckgo.com/?q=foo&t=ffab&ia=images&iax=images"],
+ },
+ {
+ title: "Baidu search access point",
+ trackingUrl: "https://www.baidu.com/baidu?wd=test&tn=monline_7_dg&ie=utf-8",
+ expectedSearchCountEntry: "baidu.in-content:sap:monline_7_dg",
+ },
+ {
+ title: "Baidu search access point follow-on",
+ trackingUrl:
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&tn=monline_7_dg&wd=test2&oq=test&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn&rsv_enter=1&rsv_sug3=2&rsv_sug2=0&inputT=227&rsv_sug4=397",
+ expectedSearchCountEntry: "baidu.in-content:sap-follow-on:monline_7_dg",
+ },
+ {
+ title: "Baidu organic",
+ trackingUrl:
+ "https://www.baidu.com/s?ie=utf-8&f=8&rsv_bp=1&rsv_idx=1&ch=&tn=baidu&bar=&wd=test&rn=&oq=&rsv_pq=RSV_PQ_VALUE&rsv_t=RSV_T_VALUE&rqlang=cn",
+ expectedSearchCountEntry: "baidu.in-content:organic:baidu",
+ },
+];
+
+/**
+ * This function is primarily for testing the Ad URL regexps that are triggered
+ * when a URL is clicked on. These regexps are also used for the `with_ads`
+ * probe. However, we test the ad_clicks route as that is easier to hit.
+ *
+ * @param {string} serpUrl
+ * The url to simulate where the page the click came from.
+ * @param {string} adUrl
+ * The ad url to simulate being clicked.
+ * @param {string} [expectedAdKey]
+ * The expected key to be logged for the scalar. Omit if no scalar should be
+ * logged.
+ */
+async function testAdUrlClicked(serpUrl, adUrl, expectedAdKey) {
+ info(`Testing Ad URL: ${adUrl}`);
+ let channel = NetUtil.newChannel({
+ uri: NetUtil.newURI(adUrl),
+ triggeringPrincipal: Services.scriptSecurityManager.createContentPrincipal(
+ NetUtil.newURI(serpUrl),
+ {}
+ ),
+ loadUsingSystemPrincipal: true,
+ });
+ SearchSERPTelemetry._contentHandler.observeActivity(
+ channel,
+ Ci.nsIHttpActivityObserver.ACTIVITY_TYPE_HTTP_TRANSACTION,
+ Ci.nsIHttpActivityObserver.ACTIVITY_SUBTYPE_TRANSACTION_CLOSE
+ );
+ // Since the content handler takes a moment to allow the channel information
+ // to settle down, wait the same amount of time here.
+ await new Promise(resolve => Services.tm.dispatchToMainThread(resolve));
+
+ const scalars = TelemetryTestUtils.getProcessScalars("parent", true, true);
+ if (!expectedAdKey) {
+ Assert.ok(
+ !("browser.search.ad_clicks" in scalars),
+ "Should not have recorded an ad click"
+ );
+ } else {
+ TelemetryTestUtils.assertKeyedScalar(
+ scalars,
+ "browser.search.ad_clicks",
+ expectedAdKey,
+ 1
+ );
+ }
+}
+
+add_task(async function setup() {
+ Services.prefs.setBoolPref(SearchUtils.BROWSER_SEARCH_PREF + "log", true);
+ await SearchSERPTelemetry.init();
+ sinon.stub(BrowserSearchTelemetry, "shouldRecordSearchCount").returns(true);
+});
+
+add_task(async function test_parsing_search_urls() {
+ for (const test of TESTS) {
+ info(`Running ${test.title}`);
+ if (test.setUp) {
+ test.setUp();
+ }
+ SearchSERPTelemetry.updateTrackingStatus(
+ {
+ getTabBrowser: () => {},
+ },
+ test.trackingUrl
+ );
+ const hs = Services.telemetry
+ .getKeyedHistogramById("SEARCH_COUNTS")
+ .snapshot();
+ Assert.ok(hs);
+ Assert.ok(
+ test.expectedSearchCountEntry in hs,
+ "The histogram must contain the correct key"
+ );
+
+ if ("adUrls" in test) {
+ for (const adUrl of test.adUrls) {
+ await testAdUrlClicked(test.trackingUrl, adUrl, test.expectedAdKey);
+ }
+ for (const nonAdUrls of test.nonAdUrls) {
+ await testAdUrlClicked(test.trackingUrl, nonAdUrls);
+ }
+ }
+
+ if (test.tearDown) {
+ test.tearDown();
+ }
+ }
+});
diff --git a/browser/components/search/test/unit/xpcshell.ini b/browser/components/search/test/unit/xpcshell.ini
new file mode 100644
index 0000000000..09569354e9
--- /dev/null
+++ b/browser/components/search/test/unit/xpcshell.ini
@@ -0,0 +1,4 @@
+[DEFAULT]
+firefox-appdir = browser
+
+[test_urlTelemetry.js]