/* 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"); let gAnchor; let gPanel; let gPanelMultiView; let gMainView; let gMainButton1; let gMainMenulist; let gMainTextbox; let gMainButton2; let gMainButton3; let gMainTabOrder; let gMainArrowOrder; let gSubView; let gSubButton; let gSubTextarea; let gDocView; let gDocBrowser; async function openPopup() { let shown = BrowserTestUtils.waitForEvent(gMainView, "ViewShown"); PanelMultiView.openPopup(gPanel, gAnchor, "bottomcenter 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_task(async function setup() { 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); 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"); 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); gMainTextbox = document.createXULElement("textbox"); 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); gMainTabOrder = [gMainButton1, gMainMenulist, gMainTextbox, gMainButton2, gMainButton3]; gMainArrowOrder = [gMainButton1, gMainButton2, gMainButton3]; 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"; gDocView = document.createXULElement("panelview"); gDocView.id = "testDocView"; gPanelMultiView.appendChild(gDocView); gDocBrowser = document.createXULElement("browser"); gDocBrowser.id = "gDocBrowser"; gDocBrowser.setAttribute("type", "content"); gDocBrowser.setAttribute("src", 'data:text/html,'); gDocBrowser.setAttribute("width", 100); gDocBrowser.setAttribute("height", 100); gDocView.appendChild(gDocBrowser); 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 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. add_task(async function testTabArrowsBrowser() { await openPopup(); await showSubView(gDocView); let backButton = gDocView.querySelector(".subviewbutton-back"); backButton.id = "docBack"; await expectFocusAfterKey("Tab", backButton); let doc = gDocBrowser.contentDocument; // 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 browser 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"); expectFocusAfterKey("Tab", docButton); // Make sure tab leaves the document and reaches the Back button. expectFocusAfterKey("Tab", backButton); await hidePopup(); });