browser_aria_activedescendant.js (13169B)
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 5 "use strict"; 6 7 /* import-globals-from ../../mochitest/role.js */ 8 /* import-globals-from ../../mochitest/states.js */ 9 loadScripts( 10 { name: "role.js", dir: MOCHITESTS_DIR }, 11 { name: "states.js", dir: MOCHITESTS_DIR } 12 ); 13 14 async function synthFocus(browser, container, item) { 15 let focusPromise = waitForEvent(EVENT_FOCUS, item); 16 await invokeContentTask(browser, [container], _container => { 17 let elm = ( 18 content.document._testGetElementById || content.document.getElementById 19 ).bind(content.document)(_container); 20 elm.focus(); 21 }); 22 await focusPromise; 23 } 24 25 async function changeARIAActiveDescendant( 26 browser, 27 container, 28 itemId, 29 prevItemId, 30 elementReflection 31 ) { 32 let expectedEvents = [[EVENT_FOCUS, itemId]]; 33 34 if (prevItemId) { 35 info("A state change of the previous item precedes the new one."); 36 expectedEvents.push( 37 stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) 38 ); 39 } 40 41 expectedEvents.push( 42 stateChangeEventArgs(itemId, EXT_STATE_ACTIVE, true, true) 43 ); 44 45 let expectedPromise = waitForEvents(expectedEvents); 46 await invokeContentTask( 47 browser, 48 [container, itemId, elementReflection], 49 (_container, _itemId, _elementReflection) => { 50 let getElm = ( 51 content.document._testGetElementById || content.document.getElementById 52 ).bind(content.document); 53 let elm = getElm(_container); 54 if (_elementReflection) { 55 elm.ariaActiveDescendantElement = getElm(_itemId); 56 } else { 57 elm.setAttribute("aria-activedescendant", _itemId); 58 } 59 } 60 ); 61 62 await expectedPromise; 63 } 64 65 async function clearARIAActiveDescendant( 66 browser, 67 container, 68 prevItemId, 69 defaultId, 70 elementReflection 71 ) { 72 let expectedEvents = [[EVENT_FOCUS, defaultId || container]]; 73 if (prevItemId) { 74 expectedEvents.push( 75 stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) 76 ); 77 } 78 79 if (defaultId) { 80 expectedEvents.push( 81 stateChangeEventArgs(defaultId, EXT_STATE_ACTIVE, true, true) 82 ); 83 } 84 85 let expectedPromise = waitForEvents(expectedEvents); 86 await invokeContentTask( 87 browser, 88 [container, elementReflection], 89 (_container, _elementReflection) => { 90 let elm = ( 91 content.document._testGetElementById || content.document.getElementById 92 ).bind(content.document)(_container); 93 if (_elementReflection) { 94 elm.ariaActiveDescendantElement = null; 95 } else { 96 elm.removeAttribute("aria-activedescendant"); 97 } 98 } 99 ); 100 101 await expectedPromise; 102 } 103 104 async function insertItemNFocus( 105 browser, 106 container, 107 newItemID, 108 prevItemId, 109 elementReflection 110 ) { 111 let expectedEvents = [ 112 [EVENT_SHOW, newItemID], 113 [EVENT_FOCUS, newItemID], 114 ]; 115 116 if (prevItemId) { 117 info("A state change of the previous item precedes the new one."); 118 expectedEvents.push( 119 stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) 120 ); 121 } 122 123 expectedEvents.push( 124 stateChangeEventArgs(newItemID, EXT_STATE_ACTIVE, true, true) 125 ); 126 127 let expectedPromise = waitForEvents(expectedEvents); 128 129 await invokeContentTask( 130 browser, 131 [container, newItemID, elementReflection], 132 (_container, _newItemID, _elementReflection) => { 133 let elm = ( 134 content.document._testGetElementById || content.document.getElementById 135 ).bind(content.document)(_container); 136 let itemElm = content.document.createElement("div"); 137 itemElm.setAttribute("id", _newItemID); 138 itemElm.setAttribute("role", "listitem"); 139 itemElm.textContent = _newItemID; 140 elm.appendChild(itemElm); 141 if (_elementReflection) { 142 elm.ariaActiveDescendantElement = itemElm; 143 } else { 144 elm.setAttribute("aria-activedescendant", _newItemID); 145 } 146 } 147 ); 148 149 await expectedPromise; 150 } 151 152 async function moveARIAActiveDescendantID(browser, fromID, toID) { 153 let expectedEvents = [ 154 [EVENT_FOCUS, toID], 155 stateChangeEventArgs(toID, EXT_STATE_ACTIVE, true, true), 156 ]; 157 158 let expectedPromise = waitForEvents(expectedEvents); 159 await invokeContentTask(browser, [fromID, toID], (_fromID, _toID) => { 160 let orig = ( 161 content.document._testGetElementById || content.document.getElementById 162 ).bind(content.document)(_toID); 163 if (orig) { 164 orig.id = ""; 165 } 166 ( 167 content.document._testGetElementById || content.document.getElementById 168 ).bind(content.document)(_fromID).id = _toID; 169 }); 170 await expectedPromise; 171 } 172 173 async function changeARIAActiveDescendantInvalid( 174 browser, 175 container, 176 invalidID = "invalid", 177 prevItemId = null 178 ) { 179 let expectedEvents = [[EVENT_FOCUS, container]]; 180 if (prevItemId) { 181 expectedEvents.push( 182 stateChangeEventArgs(prevItemId, EXT_STATE_ACTIVE, false, true) 183 ); 184 } 185 186 let expectedPromise = waitForEvents(expectedEvents); 187 await invokeContentTask( 188 browser, 189 [container, invalidID], 190 (_container, _invalidID) => { 191 let elm = ( 192 content.document._testGetElementById || content.document.getElementById 193 ).bind(content.document)(_container); 194 elm.setAttribute("aria-activedescendant", _invalidID); 195 } 196 ); 197 198 await expectedPromise; 199 } 200 201 const LISTBOX_MARKUP = ` 202 <div role="listbox" aria-activedescendant="item1" id="listbox" tabindex="1" 203 aria-owns="item3"> 204 <div role="listitem" id="item1">item1</div> 205 <div role="listitem" id="item2">item2</div> 206 <div role="listitem" id="roaming" data-id="roaming">roaming</div> 207 <div role="listitem" id="roaming2" data-id="roaming2">roaming2</div> 208 </div> 209 <div role="listitem" id="item3">item3</div> 210 <div role="combobox" id="combobox"> 211 <input id="combobox_entry"> 212 <ul> 213 <li role="option" id="combobox_option1">option1</li> 214 <li role="option" id="combobox_option2">option2</li> 215 </ul> 216 </div>`; 217 218 async function basicListboxTest(browser, elementReflection) { 219 await synthFocus(browser, "listbox", "item1"); 220 await changeARIAActiveDescendant( 221 browser, 222 "listbox", 223 "item2", 224 "item1", 225 elementReflection 226 ); 227 await changeARIAActiveDescendant( 228 browser, 229 "listbox", 230 "item3", 231 "item2", 232 elementReflection 233 ); 234 235 info("Focus out of listbox"); 236 await synthFocus(browser, "combobox_entry", "combobox_entry"); 237 await changeARIAActiveDescendant( 238 browser, 239 "combobox", 240 "combobox_option2", 241 null, 242 elementReflection 243 ); 244 await changeARIAActiveDescendant( 245 browser, 246 "combobox", 247 "combobox_option1", 248 null, 249 elementReflection 250 ); 251 252 info("Focus back in listbox"); 253 await synthFocus(browser, "listbox", "item3"); 254 await insertItemNFocus( 255 browser, 256 "listbox", 257 "item4", 258 "item3", 259 elementReflection 260 ); 261 262 await clearARIAActiveDescendant( 263 browser, 264 "listbox", 265 "item4", 266 null, 267 elementReflection 268 ); 269 await changeARIAActiveDescendant( 270 browser, 271 "listbox", 272 "item1", 273 null, 274 elementReflection 275 ); 276 } 277 278 addAccessibleTask( 279 LISTBOX_MARKUP, 280 async function (browser) { 281 info("Test aria-activedescendant content attribute"); 282 await basicListboxTest(browser, false); 283 284 await changeARIAActiveDescendantInvalid( 285 browser, 286 "listbox", 287 "invalid", 288 "item1" 289 ); 290 291 await changeARIAActiveDescendant(browser, "listbox", "roaming"); 292 await moveARIAActiveDescendantID(browser, "roaming2", "roaming"); 293 await changeARIAActiveDescendantInvalid( 294 browser, 295 "listbox", 296 "roaming3", 297 "roaming" 298 ); 299 await moveARIAActiveDescendantID(browser, "roaming", "roaming3"); 300 }, 301 { topLevel: true, chrome: true } 302 ); 303 304 addAccessibleTask( 305 LISTBOX_MARKUP, 306 async function (browser) { 307 info("Test ariaActiveDescendantElement element reflection"); 308 await basicListboxTest(browser, true); 309 }, 310 { topLevel: true, chrome: true } 311 ); 312 313 addAccessibleTask( 314 ` 315 <input id="activedesc_nondesc_input" aria-activedescendant="activedesc_nondesc_option"> 316 <div role="listbox"> 317 <div role="option" id="activedesc_nondesc_option">option</div> 318 </div>`, 319 async function (browser) { 320 info("Test aria-activedescendant non-descendant"); 321 await synthFocus( 322 browser, 323 "activedesc_nondesc_input", 324 "activedesc_nondesc_option" 325 ); 326 }, 327 { topLevel: true, chrome: true } 328 ); 329 330 addAccessibleTask( 331 `<div id="shadow"></div>`, 332 async function (browser) { 333 info("Test aria-activedescendant in shadow root"); 334 335 await invokeContentTask(browser, [], () => { 336 const doc = content.document; 337 338 let host = doc.getElementById("shadow"); 339 let shadow = host.attachShadow({ mode: "open" }); 340 let listbox = doc.createElement("div"); 341 listbox.id = "shadowListbox"; 342 listbox.setAttribute("role", "listbox"); 343 listbox.setAttribute("tabindex", "0"); 344 shadow.appendChild(listbox); 345 let item = doc.createElement("div"); 346 item.id = "shadowItem1"; 347 item.setAttribute("role", "option"); 348 listbox.appendChild(item); 349 listbox.setAttribute("aria-activedescendant", "shadowItem1"); 350 item = doc.createElement("div"); 351 item.id = "shadowItem2"; 352 item.setAttribute("role", "option"); 353 listbox.appendChild(item); 354 355 // We want to retrieve elements using their IDs inside the shadow root, so 356 // we define a custom get element by ID method that our utility functions 357 // above call into if it exists. 358 doc._testGetElementById = id => 359 doc.getElementById("shadow").shadowRoot.getElementById(id); 360 }); 361 362 await synthFocus(browser, "shadowListbox", "shadowItem1"); 363 await changeARIAActiveDescendant( 364 browser, 365 "shadowListbox", 366 "shadowItem2", 367 "shadowItem1" 368 ); 369 info("Do it again with element reflection"); 370 await changeARIAActiveDescendant( 371 browser, 372 "shadowListbox", 373 "shadowItem1", 374 "shadowItem2", 375 true 376 ); 377 }, 378 { topLevel: true, chrome: true } 379 ); 380 381 addAccessibleTask( 382 ` 383 <div id="comboboxWithHiddenList" tabindex="0" role="combobox" aria-owns="hiddenList"> 384 </div> 385 <div id="hiddenList" hidden role="listbox"> 386 <div id="hiddenListOption" role="option"></div> 387 </div>`, 388 async function (browser, docAcc) { 389 info("Test simultaneous insertion, relocation and aria-activedescendant"); 390 await synthFocus( 391 browser, 392 "comboboxWithHiddenList", 393 "comboboxWithHiddenList" 394 ); 395 396 testStates( 397 findAccessibleChildByID(docAcc, "comboboxWithHiddenList"), 398 STATE_FOCUSED 399 ); 400 let evtProm = Promise.all([ 401 waitForEvent(EVENT_FOCUS, "hiddenListOption"), 402 waitForStateChange("hiddenListOption", EXT_STATE_ACTIVE, true, true), 403 ]); 404 await invokeContentTask(browser, [], () => { 405 info("hiddenList is owned, so unhiding causes insertion and relocation."); 406 ( 407 content.document._testGetElementById || content.document.getElementById 408 ).bind(content.document)("hiddenList").hidden = false; 409 content.document 410 .getElementById("comboboxWithHiddenList") 411 .setAttribute("aria-activedescendant", "hiddenListOption"); 412 }); 413 await evtProm; 414 testStates( 415 findAccessibleChildByID(docAcc, "hiddenListOption"), 416 STATE_FOCUSED 417 ); 418 }, 419 { topLevel: true, chrome: true } 420 ); 421 422 addAccessibleTask( 423 ` 424 <custom-listbox id="custom-listbox1"> 425 <div role="listitem" id="l1_1"></div> 426 <div role="listitem" id="l1_2"></div> 427 <div role="listitem" id="l1_3"></div> 428 </custom-listbox> 429 430 <custom-listbox id="custom-listbox2" aria-activedescendant="l2_1"> 431 <div role="listitem" id="l2_1"></div> 432 <div role="listitem" id="l2_2"></div> 433 <div role="listitem" id="l2_3"></div> 434 </custom-listbox> 435 436 <script> 437 customElements.define("custom-listbox", 438 class extends HTMLElement { 439 constructor() { 440 super(); 441 this.tabIndex = "0" 442 this._internals = this.attachInternals(); 443 this._internals.role = "listbox"; 444 this._internals.ariaActiveDescendantElement = this.lastElementChild; 445 } 446 get internals() { 447 return this._internals; 448 } 449 } 450 ); 451 </script>`, 452 async function (browser) { 453 await synthFocus(browser, "custom-listbox1", "l1_3"); 454 455 let evtProm = Promise.all([ 456 waitForEvent(EVENT_FOCUS, "l1_2"), 457 waitForStateChange("l1_3", EXT_STATE_ACTIVE, false, true), 458 waitForStateChange("l1_2", EXT_STATE_ACTIVE, true, true), 459 ]); 460 461 await invokeContentTask(browser, [], () => { 462 content.document.getElementById( 463 "custom-listbox1" 464 ).internals.ariaActiveDescendantElement = 465 content.document.getElementById("l1_2"); 466 }); 467 468 await evtProm; 469 470 evtProm = Promise.all([ 471 waitForEvent(EVENT_FOCUS, "custom-listbox1"), 472 waitForStateChange("l1_2", EXT_STATE_ACTIVE, false, true), 473 ]); 474 475 await invokeContentTask(browser, [], () => { 476 content.document.getElementById( 477 "custom-listbox1" 478 ).internals.ariaActiveDescendantElement = null; 479 }); 480 481 await evtProm; 482 483 await synthFocus(browser, "custom-listbox2", "l2_1"); 484 await clearARIAActiveDescendant(browser, "custom-listbox2", "l2_1", "l2_3"); 485 } 486 );