summaryrefslogtreecommitdiffstats
path: root/browser/components/customizableui/test/browser_PanelMultiView_keyboard.js
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/customizableui/test/browser_PanelMultiView_keyboard.js')
-rw-r--r--browser/components/customizableui/test/browser_PanelMultiView_keyboard.js582
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();
+});