From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../browser_searchbar_keyboard_navigation.js | 644 +++++++++++++++++++++ 1 file changed, 644 insertions(+) create mode 100644 browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js (limited to 'browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js') 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..de78beabd6 --- /dev/null +++ b/browser/components/search/test/browser/browser_searchbar_keyboard_navigation.js @@ -0,0 +1,644 @@ +// Tests that keyboard navigation in the search panel works as designed. + +const searchPopup = document.getElementById("PopupSearchAutoComplete"); + +const kValues = ["foo1", "foo2", "foo3"]; +const kUserValue = "foo"; + +function getOpenSearchItems() { + let os = []; + + let addEngineList = searchPopup.searchOneOffsContainer.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_setup(async function () { + searchbar = await gCUITestUtils.addSearchBar(); + textbox = searchbar.textbox; + + 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); + + textbox.value = kUserValue; + + registerCleanupFunction(async () => { + gCUITestUtils.removeSearchBar(); + }); +}); + +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 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 = searchPopup.querySelectorAll( + ".searchbar-engine-one-off-add-engine" + ); + 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("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 }; + }); + await FormHistory.update(removeOps); + + textbox.value = ""; +}); -- cgit v1.2.3