test_list_keyboard.html (11654B)
1 <!-- This Source Code Form is subject to the terms of the Mozilla Public 2 - License, v. 2.0. If a copy of the MPL was not distributed with this 3 - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> 4 <!DOCTYPE HTML> 5 <html> 6 <!-- 7 Test that List component has working keyboard interactions. 8 --> 9 <head> 10 <meta charset="utf-8"> 11 <title>List component keyboard test</title> 12 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> 13 <script src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script> 14 <link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css"> 15 <link rel="stylesheet" href="chrome://devtools/skin/light-theme.css" type="text/css"> 16 </head> 17 <body> 18 <pre id="test"> 19 <script src="head.js" type="application/javascript"></script> 20 <script type="application/javascript"> 21 22 "use strict"; 23 24 window.onload = async function() { 25 try { 26 const { a, button, div } = 27 require("devtools/client/shared/vendor/react-dom-factories"); 28 const React = browserRequire("devtools/client/shared/vendor/react"); 29 const { 30 Simulate, 31 findRenderedDOMComponentWithClass, 32 findRenderedDOMComponentWithTag, 33 scryRenderedDOMComponentsWithTag, 34 } = browserRequire("devtools/client/shared/vendor/react-dom-test-utils"); 35 const { List } = 36 browserRequire("devtools/client/shared/components/List"); 37 38 const testItems = [ 39 { 40 component: div({}, "Test List Item 1"), 41 className: "list-item-1", 42 key: "list-item-1", 43 }, 44 { 45 component: div({}, 46 "Test List Item 2", 47 a({ href: "#" }, "Focusable 1"), 48 button({ }, "Focusable 2")), 49 className: "list-item-2", 50 key: "list-item-2", 51 }, 52 { 53 component: div({}, "Test List Item 3"), 54 className: "list-item-3", 55 key: "list-item-3", 56 }, 57 ]; 58 59 const list = React.createElement(List, { 60 items: testItems, 61 labelledby: "test-labelledby", 62 }); 63 64 const tree = ReactDOM.render(list, document.body); 65 const listEl = findRenderedDOMComponentWithClass(tree, "list"); 66 scryRenderedDOMComponentsWithTag(tree, "li"); 67 const defaultFocus = listEl.ownerDocument.body; 68 69 function blurEl(el) { 70 const onBlur = new Promise(r => { 71 el.addEventListener("blur", r, {once: true}); 72 }) 73 // Simulate.blur does not seem to update the activeElement. 74 el.blur(); 75 return onBlur; 76 } 77 78 function focusEl(el) { 79 const onFocus = new Promise(r => { 80 el.addEventListener("focus", r, {once: true}); 81 }) 82 // Simulate.focus does not seem to update the activeElement. 83 el.focus(); 84 return onFocus; 85 } 86 87 function getExpectedActiveElementForFinalShiftTab() { 88 // When tab focus mode is applied, the "Run Chrome Tests" button is not 89 // focusable, so this Shift+Tab moves the focus to a Chrome UI element 90 // instead of the "Run Chrome Tests" button, which makes the focus to 91 // move to a browsing context that has a different top level browsing context 92 // than the current browsing context. Since top level browsing contexts are 93 // different, the activeElement in the old document is not cleared. Also 94 // this is only the case when e10s is enabled. 95 if (SpecialPowers.getBoolPref("accessibility.tabfocus_applies_to_xul") && 96 SpecialPowers.Services.appinfo.browserTabsRemoteAutostart) { 97 return listEl; 98 } 99 100 // <body> 101 return defaultFocus; 102 } 103 104 const tests = [{ 105 name: "Test default List state. Keyboard focus is set to document body by default.", 106 state: { current: null, active: null }, 107 activeElement: defaultFocus, 108 }, { 109 name: "Current item must be set to the first list item on initial focus. " + 110 "Keyboard focus should be set on list's conatiner (<ul>).", 111 action: () => focusEl(listEl), 112 activeElement: listEl, 113 state: { current: 0 }, 114 }, { 115 name: "Current item should remain set even when the list is blured. " + 116 "Keyboard focus should be set back to document body.", 117 action: () => blurEl(listEl), 118 state: { current: 0 }, 119 activeElement: defaultFocus, 120 }, { 121 name: "Unset list's current state.", 122 action: () => tree.setState({ current: null }), 123 state: { current: null }, 124 }, { 125 name: "Current item must be re-set again to the first list item on initial " + 126 "focus. Keyboard focus should be set on list's conatiner (<ul>).", 127 action: () => focusEl(listEl), 128 activeElement: listEl, 129 state: { current: 0 }, 130 }, { 131 name: "Current item should be updated to next on ArrowDown.", 132 event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }}, 133 state: { current: 1 }, 134 }, { 135 name: "Current item should be updated to last on ArrowDown.", 136 event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }}, 137 state: { current: 2 }, 138 }, { 139 name: "Current item should remain on last on ArrowDown.", 140 event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }}, 141 state: { current: 2 }, 142 }, { 143 name: "Current item should be updated to previous on ArrowUp.", 144 event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }}, 145 state: { current: 1 }, 146 }, { 147 name: "Current item should be updated to first on ArrowUp.", 148 event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }}, 149 state: { current: 0 }, 150 }, { 151 name: "Current item should remain on first on ArrowUp.", 152 event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }}, 153 state: { current: 0 }, 154 }, { 155 name: "Current item should be updated to last on End.", 156 event: { type: "keyDown", el: listEl, options: { key: "End" }}, 157 state: { current: 2 }, 158 }, { 159 name: "Current item should be updated to first on Home.", 160 event: { type: "keyDown", el: listEl, options: { key: "Home" }}, 161 state: { current: 0 }, 162 }, { 163 name: "Current item should be set as active on Enter.", 164 event: { type: "keyDown", el: listEl, options: { key: "Enter" }}, 165 state: { current: 0, active: 0 }, 166 activeElement: listEl, 167 }, { 168 name: "Active item should be unset on Escape.", 169 event: { type: "keyDown", el: listEl, options: { key: "Escape" }}, 170 state: { current: 0, active: null }, 171 }, { 172 name: "Current item should be set as active on Space.", 173 event: { type: "keyDown", el: listEl, options: { key: " " }}, 174 state: { current: 0, active: 0 }, 175 activeElement: listEl, 176 }, { 177 name: "Current item should unset when focus leaves the list.", 178 action: () => blurEl(listEl), 179 state: { current: 0, active: null }, 180 activeElement: defaultFocus, 181 }, { 182 name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.", 183 action: () => focusEl(listEl), 184 activeElement: listEl, 185 }, { 186 name: "Current item should be updated to next on ArrowDown.", 187 event: { type: "keyDown", el: listEl, options: { key: "ArrowDown" }}, 188 state: { current: 1, active: null }, 189 }, { 190 name: "Current item should be set as active on Enter. Keyboard focus should be " + 191 "set on the first focusable element inside the list item, if available.", 192 event: { type: "keyDown", el: listEl, options: { key: "Enter" }}, 193 state: { current: 1, active: 1 }, 194 get activeElement() { 195 // When list item becomes active/inactive, it is replaced with a newly rendered 196 // one. 197 return findRenderedDOMComponentWithTag(tree, "a"); 198 }, 199 }, { 200 name: "Keyboard focus should be set to next tabbable element inside the active " + 201 "list item on Tab.", 202 action() { 203 synthesizeKey("KEY_Tab"); 204 }, 205 state: { current: 1, active: 1 }, 206 get activeElement() { 207 // When list item becomes active/inactive, it is replaced with a newly rendered 208 // one. 209 return findRenderedDOMComponentWithTag(tree, "button"); 210 }, 211 }, { 212 name: "Keyboard focus should wrap inside the list item when focused on last " + 213 "tabbable element.", 214 action() { 215 synthesizeKey("KEY_Tab"); 216 }, 217 state: { current: 1, active: 1 }, 218 get activeElement() { 219 return findRenderedDOMComponentWithTag(tree, "a"); 220 }, 221 }, { 222 name: "Keyboard focus should wrap inside the list item when focused on first " + 223 "tabbable element.", 224 action() { 225 synthesizeKey("KEY_Tab", { shiftKey: true }); 226 }, 227 state: { current: 1, active: 1 }, 228 get activeElement() { 229 return findRenderedDOMComponentWithTag(tree, "button"); 230 }, 231 }, { 232 name: "Active item should be unset on Escape. Focus should move back to the " + 233 "list container.", 234 event: { type: "keyDown", el: listEl, options: { key: "Escape" }}, 235 state: { current: 1, active: null }, 236 activeElement: listEl, 237 }, { 238 name: "Current item should be set as active on Space. Keyboard focus should be " + 239 "set on the first focusable element inside the list item, if available.", 240 event: { type: "keyDown", el: listEl, options: { key: " " }}, 241 state: { current: 1, active: 1 }, 242 get activeElement() { 243 // When list item becomes active/inactive, it is replaced with a newly rendered 244 // one. 245 return findRenderedDOMComponentWithTag(tree, "a"); 246 }, 247 }, { 248 name: "Current item should remain set even when the list is blured. " + 249 "Keyboard focus should be set back to document body.", 250 action: () => blurEl(listEl.ownerDocument.activeElement), 251 state: { current: 1, active: null, }, 252 activeElement: defaultFocus, 253 }, { 254 name: "Keyboard focus should be set on list's conatiner (<ul>) on focus.", 255 action: () => focusEl(listEl), 256 state: { current: 1, active: null }, 257 activeElement: listEl, 258 }, { 259 name: "Current item should be updated to previous on ArrowUp.", 260 event: { type: "keyDown", el: listEl, options: { key: "ArrowUp" }}, 261 state: { current: 0, active: null }, 262 }, { 263 name: "Current item should be set as active on Enter.", 264 event: { type: "keyDown", el: listEl, options: { key: "Enter" }}, 265 state: { current: 0, active: 0 }, 266 activeElement: listEl, 267 }, { 268 name: "Keyboard focus should move to another focusable element outside of the " + 269 "list when there's nothing to focus on inside the list item.", 270 action() { 271 synthesizeKey("KEY_Tab", { shiftKey: true }); 272 }, 273 state: { current: 0, active: null }, 274 activeElement: getExpectedActiveElementForFinalShiftTab(), 275 }]; 276 277 for (const test of tests) { 278 const { action, event, state, name } = test; 279 280 is(listEl, findRenderedDOMComponentWithClass(tree, "list"), "Sanity check"); 281 282 info(name); 283 if (event) { 284 const { type, options, el } = event; 285 Simulate[type](el, options); 286 } else if (action) { 287 await action(); 288 } 289 290 if (test.activeElement) { 291 is(listEl.ownerDocument.activeElement, test.activeElement, 292 "Focus is set correctly."); 293 } 294 295 for (const key in state) { 296 is(tree.state[key], state[key], `${key} state is correct.`); 297 } 298 } 299 } catch (e) { 300 ok(false, "Got an error: " + DevToolsUtils.safeErrorString(e)); 301 } finally { 302 SimpleTest.finish(); 303 } 304 }; 305 </script> 306 </pre> 307 </body> 308 </html>