diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 19:33:14 +0000 |
commit | 36d22d82aa202bb199967e9512281e9a53db42c9 (patch) | |
tree | 105e8c98ddea1c1e4784a60a5a6410fa416be2de /browser/components/customizableui/test/browser_PanelMultiView_keyboard.js | |
parent | Initial commit. (diff) | |
download | firefox-esr-upstream.tar.xz firefox-esr-upstream.zip |
Adding upstream version 115.7.0esr.upstream/115.7.0esrupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'browser/components/customizableui/test/browser_PanelMultiView_keyboard.js')
-rw-r--r-- | browser/components/customizableui/test/browser_PanelMultiView_keyboard.js | 582 |
1 files changed, 582 insertions, 0 deletions
diff --git a/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js new file mode 100644 index 0000000000..baaa38c224 --- /dev/null +++ b/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js @@ -0,0 +1,582 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +/** + * Test the keyboard behavior of PanelViews. + */ + +const kEmbeddedDocUrl = + 'data:text/html,<textarea id="docTextarea">value</textarea><button id="docButton"></button>'; + +let gAnchor; +let gPanel; +let gPanelMultiView; +let gMainView; +let gMainContext; +let gMainButton1; +let gMainMenulist; +let gMainRadiogroup; +let gMainTextbox; +let gMainButton2; +let gMainButton3; +let gCheckbox; +let gNamespacedLink; +let gLink; +let gMainTabOrder; +let gMainArrowOrder; +let gSubView; +let gSubButton; +let gSubTextarea; +let gBrowserView; +let gBrowserBrowser; +let gIframeView; +let gIframeIframe; +let gToggle; + +async function openPopup() { + let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); + PanelMultiView.openPopup(gPanel, gAnchor, "bottomright topright"); + await shown; +} + +async function hidePopup() { + let hidden = BrowserTestUtils.waitForEvent(gPanel, "popuphidden"); + PanelMultiView.hidePopup(gPanel); + await hidden; +} + +async function showSubView(view = gSubView) { + let shown = BrowserTestUtils.waitForEvent(view, "ViewShown"); + // We must show with an anchor so the Back button is generated. + gPanelMultiView.showSubView(view, gMainButton1); + await shown; +} + +async function expectFocusAfterKey(aKey, aFocus) { + let res = aKey.match(/^(Shift\+)?(.+)$/); + let shift = Boolean(res[1]); + let key; + if (res[2].length == 1) { + key = res[2]; // Character. + } else { + key = "KEY_" + res[2]; // Tab, ArrowRight, etc. + } + info("Waiting for focus on " + aFocus.id); + let focused = BrowserTestUtils.waitForEvent(aFocus, "focus"); + EventUtils.synthesizeKey(key, { shiftKey: shift }); + await focused; + ok(true, aFocus.id + " focused after " + aKey + " pressed"); +} + +add_setup(async function () { + // This shouldn't be necessary - but it is, because we use same-process frames. + // https://bugzilla.mozilla.org/show_bug.cgi?id=1565276 covers improving this. + await SpecialPowers.pushPrefEnv({ + set: [["security.allow_unsafe_parent_loads", true]], + }); + let navBar = document.getElementById("nav-bar"); + gAnchor = document.createXULElement("toolbarbutton"); + navBar.appendChild(gAnchor); + gPanel = document.createXULElement("panel"); + navBar.appendChild(gPanel); + gPanelMultiView = document.createXULElement("panelmultiview"); + gPanelMultiView.setAttribute("mainViewId", "testMainView"); + gPanel.appendChild(gPanelMultiView); + + gMainView = document.createXULElement("panelview"); + gMainView.id = "testMainView"; + gPanelMultiView.appendChild(gMainView); + gMainContext = document.createXULElement("menupopup"); + gMainContext.id = "gMainContext"; + gMainView.appendChild(gMainContext); + gMainContext.appendChild(document.createXULElement("menuitem")); + gMainButton1 = document.createXULElement("button"); + gMainButton1.id = "gMainButton1"; + gMainView.appendChild(gMainButton1); + // We use this for anchoring subviews, so it must have a label. + gMainButton1.setAttribute("label", "gMainButton1"); + gMainButton1.setAttribute("context", "gMainContext"); + gMainMenulist = document.createXULElement("menulist"); + gMainMenulist.id = "gMainMenulist"; + gMainView.appendChild(gMainMenulist); + let menuPopup = document.createXULElement("menupopup"); + gMainMenulist.appendChild(menuPopup); + let item = document.createXULElement("menuitem"); + item.setAttribute("value", "1"); + item.setAttribute("selected", "true"); + menuPopup.appendChild(item); + item = document.createXULElement("menuitem"); + item.setAttribute("value", "2"); + menuPopup.appendChild(item); + gMainRadiogroup = document.createXULElement("radiogroup"); + gMainRadiogroup.id = "gMainRadiogroup"; + gMainView.appendChild(gMainRadiogroup); + let radio = document.createXULElement("radio"); + radio.setAttribute("value", "1"); + radio.setAttribute("selected", "true"); + gMainRadiogroup.appendChild(radio); + radio = document.createXULElement("radio"); + radio.setAttribute("value", "2"); + gMainRadiogroup.appendChild(radio); + gMainTextbox = document.createElement("input"); + gMainTextbox.id = "gMainTextbox"; + gMainView.appendChild(gMainTextbox); + gMainTextbox.setAttribute("value", "value"); + gMainButton2 = document.createXULElement("button"); + gMainButton2.id = "gMainButton2"; + gMainView.appendChild(gMainButton2); + gMainButton3 = document.createXULElement("button"); + gMainButton3.id = "gMainButton3"; + gMainView.appendChild(gMainButton3); + gCheckbox = document.createXULElement("checkbox"); + gCheckbox.id = "gCheckbox"; + gMainView.appendChild(gCheckbox); + + // moz-support-links in XUL documents are created with the + // <html:a> tag and so we need to test this separately from + // <a> tags. + gNamespacedLink = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "html:a" + ); + gNamespacedLink.href = "www.mozilla.org"; + gNamespacedLink.innerText = "gNamespacedLink"; + gNamespacedLink.id = "gNamespacedLink"; + gMainView.appendChild(gNamespacedLink); + gLink = document.createElement("a"); + gLink.href = "www.mozilla.org"; + gLink.innerText = "gLink"; + gLink.id = "gLink"; + gMainView.appendChild(gLink); + await window.ensureCustomElements("moz-toggle"); + gToggle = document.createElement("moz-toggle"); + gMainView.appendChild(gToggle); + + gMainTabOrder = [ + gMainButton1, + gMainMenulist, + gMainRadiogroup, + gMainTextbox, + gMainButton2, + gMainButton3, + gCheckbox, + gNamespacedLink, + gLink, + gToggle, + ]; + gMainArrowOrder = [ + gMainButton1, + gMainButton2, + gMainButton3, + gCheckbox, + gNamespacedLink, + gLink, + gToggle, + ]; + + gSubView = document.createXULElement("panelview"); + gSubView.id = "testSubView"; + gPanelMultiView.appendChild(gSubView); + gSubButton = document.createXULElement("button"); + gSubView.appendChild(gSubButton); + gSubTextarea = document.createElementNS( + "http://www.w3.org/1999/xhtml", + "textarea" + ); + gSubTextarea.id = "gSubTextarea"; + gSubView.appendChild(gSubTextarea); + gSubTextarea.value = "value"; + + gBrowserView = document.createXULElement("panelview"); + gBrowserView.id = "testBrowserView"; + gPanelMultiView.appendChild(gBrowserView); + gBrowserBrowser = document.createXULElement("browser"); + gBrowserBrowser.id = "GBrowserBrowser"; + gBrowserBrowser.setAttribute("type", "content"); + gBrowserBrowser.setAttribute("src", kEmbeddedDocUrl); + gBrowserBrowser.style.minWidth = gBrowserBrowser.style.minHeight = "100px"; + gBrowserView.appendChild(gBrowserBrowser); + + gIframeView = document.createXULElement("panelview"); + gIframeView.id = "testIframeView"; + gPanelMultiView.appendChild(gIframeView); + gIframeIframe = document.createXULElement("iframe"); + gIframeIframe.id = "gIframeIframe"; + gIframeIframe.setAttribute("src", kEmbeddedDocUrl); + gIframeView.appendChild(gIframeIframe); + + registerCleanupFunction(() => { + gAnchor.remove(); + gPanel.remove(); + }); +}); + +// Test that the tab key focuses all expected controls. +add_task(async function testTab() { + await openPopup(); + for (let elem of gMainTabOrder) { + await expectFocusAfterKey("Tab", elem); + } + // Wrap around. + await expectFocusAfterKey("Tab", gMainTabOrder[0]); + await hidePopup(); +}); + +// Test that the shift+tab key focuses all expected controls. +add_task(async function testShiftTab() { + await openPopup(); + for (let i = gMainTabOrder.length - 1; i >= 0; --i) { + await expectFocusAfterKey("Shift+Tab", gMainTabOrder[i]); + } + // Wrap around. + await expectFocusAfterKey( + "Shift+Tab", + gMainTabOrder[gMainTabOrder.length - 1] + ); + await hidePopup(); +}); + +// Test that the down arrow key skips menulists and textboxes. +add_task(async function testDownArrow() { + await openPopup(); + for (let elem of gMainArrowOrder) { + await expectFocusAfterKey("ArrowDown", elem); + } + // Wrap around. + await expectFocusAfterKey("ArrowDown", gMainArrowOrder[0]); + await hidePopup(); +}); + +// Test that the up arrow key skips menulists and textboxes. +add_task(async function testUpArrow() { + await openPopup(); + for (let i = gMainArrowOrder.length - 1; i >= 0; --i) { + await expectFocusAfterKey("ArrowUp", gMainArrowOrder[i]); + } + // Wrap around. + await expectFocusAfterKey( + "ArrowUp", + gMainArrowOrder[gMainArrowOrder.length - 1] + ); + await hidePopup(); +}); + +// Test that the home/end keys move to the first/last controls. +add_task(async function testHomeEnd() { + await openPopup(); + await expectFocusAfterKey("Home", gMainArrowOrder[0]); + await expectFocusAfterKey("End", gMainArrowOrder[gMainArrowOrder.length - 1]); + await hidePopup(); +}); + +// Test that the up/down arrow keys work as expected in menulists. +add_task(async function testArrowsMenulist() { + await openPopup(); + gMainMenulist.focus(); + is(document.activeElement, gMainMenulist, "menulist focused"); + is(gMainMenulist.value, "1", "menulist initial value 1"); + if (AppConstants.platform == "macosx") { + // On Mac, down/up arrows just open the menulist. + let popup = gMainMenulist.menupopup; + for (let key of ["ArrowDown", "ArrowUp"]) { + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + EventUtils.synthesizeKey("KEY_" + key); + await shown; + ok(gMainMenulist.open, "menulist open after " + key); + let hidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Escape"); + await hidden; + ok(!gMainMenulist.open, "menulist closed after Escape"); + } + } else { + // On other platforms, down/up arrows change the value without opening the + // menulist. + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement, + gMainMenulist, + "menulist still focused after ArrowDown" + ); + is(gMainMenulist.value, "2", "menulist value 2 after ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement, + gMainMenulist, + "menulist still focused after ArrowUp" + ); + is(gMainMenulist.value, "1", "menulist value 1 after ArrowUp"); + } + await hidePopup(); +}); + +// Test that the tab key closes an open menu list. +add_task(async function testTabOpenMenulist() { + await openPopup(); + gMainMenulist.focus(); + is(document.activeElement, gMainMenulist, "menulist focused"); + let popup = gMainMenulist.menupopup; + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + gMainMenulist.open = true; + await shown; + ok(gMainMenulist.open, "menulist open"); + let menuHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Tab"); + await menuHidden; + ok(!gMainMenulist.open, "menulist closed after Tab"); + is(gPanel.state, "open", "Panel should be open"); + await hidePopup(); +}); + +if (AppConstants.platform == "macosx") { + // Test that using the mouse to open a menulist still allows keyboard navigation + // inside it. + add_task(async function testNavigateMouseOpenedMenulist() { + await openPopup(); + let popup = gMainMenulist.menupopup; + let shown = BrowserTestUtils.waitForEvent(popup, "popupshown"); + gMainMenulist.open = true; + await shown; + ok(gMainMenulist.open, "menulist open"); + let oldFocus = document.activeElement; + let oldSelectedItem = gMainMenulist.selectedItem; + ok( + oldSelectedItem.hasAttribute("_moz-menuactive"), + "Selected item should show up as active" + ); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await TestUtils.waitForCondition( + () => !oldSelectedItem.hasAttribute("_moz-menuactive") + ); + is(oldFocus, document.activeElement, "Focus should not move on mac"); + ok( + !oldSelectedItem.hasAttribute("_moz-menuactive"), + "Selected item should change" + ); + + let menuHidden = BrowserTestUtils.waitForEvent(popup, "popuphidden"); + EventUtils.synthesizeKey("KEY_Tab"); + await menuHidden; + ok(!gMainMenulist.open, "menulist closed after Tab"); + is(gPanel.state, "open", "Panel should be open"); + await hidePopup(); + }); +} + +// Test that the up/down arrow keys work as expected in radiogroups. +add_task(async function testArrowsRadiogroup() { + await openPopup(); + gMainRadiogroup.focus(); + is(document.activeElement, gMainRadiogroup, "radiogroup focused"); + is(gMainRadiogroup.value, "1", "radiogroup initial value 1"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + is( + document.activeElement, + gMainRadiogroup, + "radiogroup still focused after ArrowDown" + ); + is(gMainRadiogroup.value, "2", "radiogroup value 2 after ArrowDown"); + EventUtils.synthesizeKey("KEY_ArrowUp"); + is( + document.activeElement, + gMainRadiogroup, + "radiogroup still focused after ArrowUp" + ); + is(gMainRadiogroup.value, "1", "radiogroup value 1 after ArrowUp"); + await hidePopup(); +}); + +// Test that pressing space in a textbox inserts a space (instead of trying to +// activate the control). +add_task(async function testSpaceTextbox() { + await openPopup(); + gMainTextbox.focus(); + gMainTextbox.selectionStart = gMainTextbox.selectionEnd = 0; + EventUtils.synthesizeKey(" "); + is(gMainTextbox.value, " value", "Space typed into textbox"); + gMainTextbox.value = "value"; + await hidePopup(); +}); + +// Tests that the left arrow key normally moves back to the previous view. +add_task(async function testLeftArrow() { + await openPopup(); + await showSubView(); + let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + await shown; + ok("Moved to previous view after ArrowLeft"); + await hidePopup(); +}); + +// Tests that the left arrow key moves the caret in a textarea in a subview +// (instead of going back to the previous view). +add_task(async function testLeftArrowTextarea() { + await openPopup(); + await showSubView(); + gSubTextarea.focus(); + is(document.activeElement, gSubTextarea, "textarea focused"); + EventUtils.synthesizeKey("KEY_End"); + is(gSubTextarea.selectionStart, 5, "selectionStart 5 after End"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + is(gSubTextarea.selectionStart, 4, "selectionStart 4 after ArrowLeft"); + is(document.activeElement, gSubTextarea, "textarea still focused"); + await hidePopup(); +}); + +// Test navigation to a button which is initially disabled and later enabled. +add_task(async function testDynamicButton() { + gMainButton2.disabled = true; + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + await expectFocusAfterKey("ArrowDown", gMainButton3); + gMainButton2.disabled = false; + await expectFocusAfterKey("ArrowUp", gMainButton2); + await hidePopup(); +}); + +add_task(async function testActivation() { + function checkActivated(elem, activationFn, reason) { + let activated = false; + elem.onclick = function () { + activated = true; + }; + activationFn(); + ok(activated, "Should have activated button after " + reason); + elem.onclick = null; + } + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey("KEY_Enter"), + "pressing enter" + ); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey(" "), + "pressing space" + ); + checkActivated( + gMainButton1, + () => EventUtils.synthesizeKey("KEY_Enter", { code: "NumpadEnter" }), + "pressing numpad enter" + ); + await hidePopup(); +}); + +// Test that keyboard activation works for buttons responding to mousedown +// events (instead of command or click). The Library button does this, for +// example. +add_task(async function testActivationMousedown() { + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + let activated = false; + gMainButton1.onmousedown = function () { + activated = true; + }; + EventUtils.synthesizeKey(" "); + ok(activated, "mousedown activated after space"); + gMainButton1.onmousedown = null; + await hidePopup(); +}); + +// Test that tab and the arrow keys aren't overridden in embedded documents. +async function testTabArrowsEmbeddedDoc(aView, aEmbedder) { + await openPopup(); + await showSubView(aView); + let doc = aEmbedder.contentDocument; + if (doc.readyState != "complete" || doc.location.href != kEmbeddedDocUrl) { + info(`Embedded doc readyState ${doc.readyState}, location ${doc.location}`); + info("Waiting for load on embedder"); + // Browsers don't fire load events, and iframes don't fire load events in + // typeChrome windows. We can handle both by using a capturing event + // listener to capture the load event from the child document. + await BrowserTestUtils.waitForEvent(aEmbedder, "load", true); + // The original doc might have been a temporary about:blank, so fetch it + // again. + doc = aEmbedder.contentDocument; + } + is(doc.location.href, kEmbeddedDocUrl, "Embedded doc has correct URl"); + let backButton = aView.querySelector(".subviewbutton-back"); + backButton.id = "docBack"; + await expectFocusAfterKey("Tab", backButton); + // Documents don't have an id property, but expectFocusAfterKey wants one. + doc.id = "doc"; + await expectFocusAfterKey("Tab", doc); + // Make sure tab/arrows aren't overridden within the embedded document. + let textarea = doc.getElementById("docTextarea"); + // Tab should really focus the textarea, but default tab handling seems to + // skip everything inside the embedder element when run in this test. This + // behaves as expected in real panels, though. Force focus to the textarea + // and then test from there. + textarea.focus(); + is(doc.activeElement, textarea, "textarea focused"); + is(textarea.selectionStart, 0, "selectionStart initially 0"); + EventUtils.synthesizeKey("KEY_ArrowRight"); + is(textarea.selectionStart, 1, "selectionStart 1 after ArrowRight"); + EventUtils.synthesizeKey("KEY_ArrowLeft"); + is(textarea.selectionStart, 0, "selectionStart 0 after ArrowLeft"); + is(doc.activeElement, textarea, "textarea still focused"); + let docButton = doc.getElementById("docButton"); + await expectFocusAfterKey("Tab", docButton); + await hidePopup(); +} + +// Test that tab and the arrow keys aren't overridden in embedded browsers. +add_task(async function testTabArrowsBrowser() { + await testTabArrowsEmbeddedDoc(gBrowserView, gBrowserBrowser); +}); + +// Test that tab and the arrow keys aren't overridden in embedded iframes. +add_task(async function testTabArrowsIframe() { + await testTabArrowsEmbeddedDoc(gIframeView, gIframeIframe); +}); + +// Test that the arrow keys aren't overridden in context menus. +add_task(async function testArowsContext() { + await openPopup(); + await expectFocusAfterKey("ArrowDown", gMainButton1); + let shown = BrowserTestUtils.waitForEvent(gMainContext, "popupshown"); + // There's no cross-platform way to open a context menu from the keyboard. + gMainContext.openPopup(gMainButton1); + await shown; + let item = gMainContext.children[0]; + ok( + !item.getAttribute("_moz-menuactive"), + "First context menu item initially inactive" + ); + let active = BrowserTestUtils.waitForEvent(item, "DOMMenuItemActive"); + EventUtils.synthesizeKey("KEY_ArrowDown"); + await active; + ok( + item.getAttribute("_moz-menuactive"), + "First context menu item active after ArrowDown" + ); + is( + document.activeElement, + gMainButton1, + "gMainButton1 still focused after ArrowDown" + ); + let hidden = BrowserTestUtils.waitForEvent(gMainContext, "popuphidden"); + gMainContext.hidePopup(); + await hidden; + await hidePopup(); +}); + +add_task(async function testMozToggle() { + await openPopup(); + is(gToggle.pressed, false, "The toggle is not pressed initially."); + // Focus the toggle via keyboard navigation. + while (document.activeElement !== gToggle) { + EventUtils.synthesizeKey("KEY_Tab"); + } + EventUtils.synthesizeKey(" "); + await gToggle.updateComplete; + is(gToggle.pressed, true, "Toggle pressed state changes via spacebar."); + EventUtils.synthesizeKey("KEY_Enter"); + await gToggle.updateComplete; + is(gToggle.pressed, false, "Toggle pressed state changes via enter."); + await hidePopup(); +}); |