/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ "use strict"; /** * Test the keyboard behavior of PanelViews. */ const { PanelMultiView } = ChromeUtils.import( "resource:///modules/PanelMultiView.jsm" ); const kEmbeddedDocUrl = 'data:text/html,'; 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 gMainTabOrder; let gMainArrowOrder; let gSubView; let gSubButton; let gSubTextarea; let gBrowserView; let gBrowserBrowser; let gIframeView; let gIframeIframe; 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); gMainTabOrder = [ gMainButton1, gMainMenulist, gMainRadiogroup, gMainTextbox, gMainButton2, gMainButton3, gCheckbox, ]; gMainArrowOrder = [gMainButton1, gMainButton2, gMainButton3, gCheckbox]; 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"); let panelHidden = BrowserTestUtils.waitForEvent(gPanel, "popuphidden"); EventUtils.synthesizeKey("KEY_Tab"); await menuHidden; ok(!gMainMenulist.open, "menulist closed after Tab"); // Tab in an open menulist closes the menulist, but also dismisses the panel // above it (bug 1566673). So, we just wait for the panel to hide rather than // using hidePopup(). await panelHidden; }); 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"); let panelHidden = BrowserTestUtils.waitForEvent(gPanel, "popuphidden"); EventUtils.synthesizeKey("KEY_Tab"); await menuHidden; ok(!gMainMenulist.open, "menulist closed after Tab"); // Tab in an open menulist closes the menulist, but also dismisses the panel // above it (bug 1566673). So, we just wait for the panel to hide rather than // using hidePopup(). await panelHidden; }); } // 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(); 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(); });