summaryrefslogtreecommitdiffstats
path: root/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js')
-rw-r--r--browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js449
1 files changed, 449 insertions, 0 deletions
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..e2ddadabd9
--- /dev/null
+++ b/browser/components/search/test/browser/browser_searchbar_smallpanel_keyboard_navigation.js
@@ -0,0 +1,449 @@
+// Tests that keyboard navigation in the search panel works as designed.
+
+const searchPopup = document.getElementById("PopupSearchAutoComplete");
+
+const kValues = ["foo1", "foo2", "foo3"];
+
+function getOpenSearchItems() {
+ let os = [];
+
+ let addEngineList = searchPopup.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_setup(async function () {
+ searchbar = await gCUITestUtils.addSearchBar();
+ registerCleanupFunction(() => {
+ gCUITestUtils.removeSearchBar();
+ });
+ textbox = searchbar.textbox;
+ searchIcon = searchbar.querySelector(".searchbar-search-button");
+
+ await SearchTestUtils.promiseNewSearchEngine({
+ url: getRootDirectory(gTestPath) + "testEngine.xml",
+ setAsDefault: true,
+ });
+
+ // First cleanup the form history in case other tests left things there.
+ info("cleanup the search history");
+ await FormHistory.update({ op: "remove", fieldname: "searchbar-history" });
+
+ info("adding search history values: " + kValues);
+ let addOps = kValues.map(value => {
+ return { op: "add", fieldname: "searchbar-history", value };
+ });
+ await FormHistory.update(addOps);
+
+ registerCleanupFunction(async () => {
+ gCUITestUtils.removeSearchBar();
+ });
+});
+
+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;
+ await TestUtils.waitForCondition(() => {
+ engines = searchPopup.querySelectorAll(
+ ".searchbar-engine-one-off-add-engine"
+ );
+ return engines.length == 3;
+ }, "Should expose three 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("searchbar-engine-one-off-add-engine"),
+ "the button is themed as an add engine"
+ );
+ }
+
+ // Pressing up again should select the last one-off button.
+ EventUtils.synthesizeKey("KEY_ArrowUp");
+ const allOneOffs = getOneOffs();
+ is(
+ textbox.selectedButton,
+ allOneOffs[allOneOffs.length - engines.length - 1],
+ "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 };
+ });
+ FormHistory.update(removeOps);
+});