summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/browser/browser_contentSearchUI.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test/browser/browser_contentSearchUI.js')
-rw-r--r--browser/components/search/test/browser/browser_contentSearchUI.js1158
1 files changed, 1158 insertions, 0 deletions
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..9196b1355c
--- /dev/null
+++ b/browser/components/search/test/browser/browser_contentSearchUI.js
@@ -0,0 +1,1158 @@
+/* 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",
+ id: "other-searchSuggestionEngine1",
+ loadPath: "[addon]searchsuggestionengine1@tests.mozilla.org",
+};
+const TEST_ENGINE2 = {
+ name: "searchSuggestionEngine2",
+ id: "other-searchSuggestionEngine2",
+ loadPath: "[addon]searchsuggestionengine2@tests.mozilla.org",
+};
+
+const TEST_MSG = "ContentSearchUIControllerTest";
+
+ChromeUtils.defineESModuleGetters(this, {
+ ContentSearch: "resource:///actors/ContentSearchParent.sys.mjs",
+ FormHistoryTestUtils:
+ "resource://testing-common/FormHistoryTestUtils.sys.mjs",
+ SearchSuggestionController:
+ "resource://gre/modules/SearchSuggestionController.sys.mjs",
+});
+
+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;
+
+add_setup(async function () {
+ let originalOnMessageSearch = ContentSearch._onMessageSearch;
+ let originalOnMessageManageEngines = ContentSearch._onMessageManageEngines;
+
+ ContentSearch._onMessageSearch = () => {};
+ ContentSearch._onMessageManageEngines = () => {};
+
+ let currentEngines = await Services.search.getVisibleEngines();
+
+ extension1 = await SearchTestUtils.installSearchExtension(
+ {
+ name: TEST_ENGINE1.name,
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ },
+ { setAsDefault: true }
+ );
+ extension2 = await SearchTestUtils.installSearchExtension({
+ name: TEST_ENGINE2.name,
+ suggest_url:
+ "https://example.com/browser/browser/components/search/test/browser/searchSuggestionEngine.sjs",
+ suggest_url_get_params: "query={searchTerms}",
+ });
+
+ 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 });
+
+ Services.telemetry.clearEvents();
+ Services.fog.testResetFOG();
+
+ let p = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await msg("key", { key: "VK_DOWN", modifiers: { accelKey: true } });
+ let newEngine = await p;
+ Assert.equal(
+ newEngine.name,
+ TEST_ENGINE2.name,
+ "Should have correctly cycled the engine"
+ );
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: "user_searchbar",
+ extra: {
+ prev_id: TEST_ENGINE1.id,
+ new_id: TEST_ENGINE2.id,
+ new_name: TEST_ENGINE2.name,
+ new_load_path: TEST_ENGINE2.loadPath,
+ new_sub_url: "",
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ let snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[0].timestamp;
+ Assert.deepEqual(
+ snapshot[0],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ new_load_path: TEST_ENGINE2.loadPath,
+ previous_engine_id: TEST_ENGINE1.id,
+ change_source: "user_searchbar",
+ new_engine_id: TEST_ENGINE2.id,
+ new_display_name: TEST_ENGINE2.name,
+ new_submission_url: "",
+ },
+ },
+ "Should have received the correct event details"
+ );
+
+ p = SearchTestUtils.promiseSearchNotification(
+ "engine-default",
+ "browser-search-engine-modified"
+ );
+ await msg("key", { key: "VK_UP", modifiers: { accelKey: true } });
+ newEngine = await p;
+ Assert.equal(
+ newEngine.name,
+ TEST_ENGINE1.name,
+ "Should have correctly cycled the engine"
+ );
+
+ TelemetryTestUtils.assertEvents(
+ [
+ {
+ object: "change_default",
+ value: "user_searchbar",
+ extra: {
+ prev_id: TEST_ENGINE2.id,
+ new_id: TEST_ENGINE1.id,
+ new_name: TEST_ENGINE1.name,
+ new_load_path: TEST_ENGINE1.loadPath,
+ new_sub_url: "",
+ },
+ },
+ ],
+ { category: "search", method: "engine" }
+ );
+
+ snapshot = await Glean.searchEngineDefault.changed.testGetValue();
+ delete snapshot[1].timestamp;
+ Assert.deepEqual(
+ snapshot[1],
+ {
+ category: "search.engine.default",
+ name: "changed",
+ extra: {
+ new_load_path: TEST_ENGINE1.loadPath,
+ previous_engine_id: TEST_ENGINE2.id,
+ change_source: "user_searchbar",
+ new_engine_id: TEST_ENGINE1.id,
+ new_display_name: TEST_ENGINE1.name,
+ new_submission_url: "",
+ },
+ },
+ "Should have received the correct event details"
+ );
+
+ 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() {
+ Services.search.restoreDefaultEngines();
+});
+
+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;
+}