test_statechange.html (19622B)
1 <html> 2 3 <head> 4 <title>Accessible state change event testing</title> 5 6 <link rel="stylesheet" type="text/css" 7 href="chrome://mochikit/content/tests/SimpleTest/test.css" /> 8 9 <script src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script> 10 11 <script type="application/javascript" 12 src="../common.js"></script> 13 <script type="application/javascript" 14 src="../promisified-events.js"></script> 15 <script type="application/javascript" 16 src="../role.js"></script> 17 <script type="application/javascript" 18 src="../states.js"></script> 19 20 <script type="application/javascript"> 21 async function openNode(aIDDetails, aIDSummary, aIsOpen) { 22 let p = waitForStateChange(aIDSummary, STATE_EXPANDED, aIsOpen, false); 23 if (aIsOpen) { 24 getNode(aIDDetails).setAttribute("open", ""); 25 } else { 26 getNode(aIDDetails).removeAttribute("open"); 27 } 28 await p; 29 } 30 31 async function makeEditableDoc(aDocNode) { 32 let p = waitForStateChange(aDocNode, EXT_STATE_EDITABLE, true, true); 33 aDocNode.designMode = "on"; 34 await p; 35 } 36 37 async function invalidInput(aNodeOrID) { 38 let p = waitForStateChange(aNodeOrID, STATE_INVALID, true, false); 39 getNode(aNodeOrID).value = "I am not an email"; 40 await p; 41 } 42 43 async function changeCheckInput(aID, aIsChecked) { 44 let p = waitForStateChange(aID, STATE_CHECKED, aIsChecked, false); 45 getNode(aID).checked = aIsChecked; 46 await p; 47 } 48 49 async function changeRequiredState(aID, aIsRequired) { 50 let p = waitForStateChange(aID, STATE_REQUIRED, aIsRequired, false); 51 getNode(aID).required = aIsRequired; 52 await p; 53 } 54 55 async function stateChangeOnFileInput(aID, aAttr, aValue, 56 aState, aIsExtraState, aIsEnabled) { 57 let fileControlNode = getNode(aID); 58 let browseButton = getAccessible(fileControlNode); 59 let p = waitForStateChange( 60 browseButton, aState, aIsEnabled, aIsExtraState 61 ); 62 fileControlNode.setAttribute(aAttr, aValue); 63 await p; 64 } 65 66 function toggleSentinel() { 67 let sentinel = getNode("sentinel"); 68 if (sentinel.hasAttribute("aria-busy")) { 69 sentinel.removeAttribute("aria-busy"); 70 } else { 71 sentinel.setAttribute("aria-busy", "true"); 72 } 73 } 74 75 async function toggleStateChange(aID, aAttr, aState, aIsExtraState) { 76 let p = waitForEvents([ 77 stateChangeEventArgs(aID, aState, true, aIsExtraState), 78 [EVENT_STATE_CHANGE, "sentinel"] 79 ]); 80 getNode(aID).setAttribute(aAttr, "true"); 81 toggleSentinel(); 82 await p; 83 p = waitForEvents([ 84 stateChangeEventArgs(aID, aState, false, aIsExtraState), 85 [EVENT_STATE_CHANGE, "sentinel"] 86 ]); 87 getNode(aID).setAttribute(aAttr, "false"); 88 toggleSentinel(); 89 await p; 90 } 91 92 async function dupeStateChange(aID, aAttr, aValue, 93 aState, aIsExtraState, aIsEnabled) { 94 let p = waitForEvents([ 95 stateChangeEventArgs(aID, aState, aIsEnabled, aIsExtraState), 96 [EVENT_STATE_CHANGE, "sentinel"] 97 ]); 98 getNode(aID).setAttribute(aAttr, aValue); 99 getNode(aID).setAttribute(aAttr, aValue); 100 toggleSentinel(); 101 await p; 102 } 103 104 async function oppositeStateChange(aID, aAttr, aState, aIsExtraState) { 105 let p = waitForEvents({ 106 expected: [[EVENT_STATE_CHANGE, "sentinel"]], 107 unexpected: [ 108 stateChangeEventArgs(aID, aState, false, aIsExtraState), 109 stateChangeEventArgs(aID, aState, true, aIsExtraState) 110 ] 111 }); 112 getNode(aID).setAttribute(aAttr, "false"); 113 getNode(aID).setAttribute(aAttr, "true"); 114 toggleSentinel(); 115 await p; 116 } 117 118 /** 119 * Change concomitant ARIA and native attribute at once. 120 */ 121 async function echoingStateChange(aID, aARIAAttr, aAttr, aValue, 122 aState, aIsExtraState, aIsEnabled) { 123 let p = waitForStateChange(aID, aState, aIsEnabled, aIsExtraState); 124 if (aValue == null) { 125 getNode(aID).removeAttribute(aARIAAttr); 126 getNode(aID).removeAttribute(aAttr); 127 } else { 128 getNode(aID).setAttribute(aARIAAttr, aValue); 129 getNode(aID).setAttribute(aAttr, aValue); 130 } 131 await p; 132 } 133 134 async function testHasPopup() { 135 let p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false); 136 getNode("popupButton").setAttribute("aria-haspopup", "true"); 137 await p; 138 139 p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false); 140 getNode("popupButton").setAttribute("aria-haspopup", "false"); 141 await p; 142 143 p = waitForStateChange("popupButton", STATE_HASPOPUP, true, false); 144 getNode("popupButton").setAttribute("aria-haspopup", "true"); 145 await p; 146 147 p = waitForStateChange("popupButton", STATE_HASPOPUP, false, false); 148 getNode("popupButton").removeAttribute("aria-haspopup"); 149 await p; 150 } 151 152 async function testDefaultSubmitChange() { 153 testStates("default-button", 154 STATE_DEFAULT, 0, 155 0, 0, 156 "button should have DEFAULT state"); 157 let button = document.createElement("button"); 158 button.textContent = "new default"; 159 let p = waitForStateChange("default-button", STATE_DEFAULT, false, false); 160 getNode("default-button").before(button); 161 await p; 162 testStates("default-button", 163 0, 0, 164 STATE_DEFAULT, 0, 165 "button should not have DEFAULT state"); 166 p = waitForStateChange("default-button", STATE_DEFAULT, true, false); 167 button.remove(); 168 await p; 169 testStates("default-button", 170 STATE_DEFAULT, 0, 171 0, 0, 172 "button should have DEFAULT state"); 173 } 174 175 async function testReadOnly() { 176 let p = waitForStateChange("email", STATE_READONLY, true, false); 177 getNode("email").setAttribute("readonly", "true"); 178 await p; 179 p = waitForStateChange("email", STATE_READONLY, false, false); 180 getNode("email").removeAttribute("readonly"); 181 await p; 182 } 183 184 async function testReadonlyUntilEditable() { 185 testStates("article", 186 STATE_READONLY, 0, 187 0, EXT_STATE_EDITABLE, 188 "article is READONLY and not EDITABLE"); 189 let p = waitForEvents([ 190 stateChangeEventArgs("article", STATE_READONLY, false, false), 191 stateChangeEventArgs("article", EXT_STATE_EDITABLE, true, true)]); 192 getNode("article").contentEditable = "true"; 193 await p; 194 testStates("article", 195 0, EXT_STATE_EDITABLE, 196 STATE_READONLY, 0, 197 "article is EDITABLE and not READONLY"); 198 p = waitForEvents([ 199 stateChangeEventArgs("article", STATE_READONLY, true, false), 200 stateChangeEventArgs("article", EXT_STATE_EDITABLE, false, true)]); 201 getNode("article").contentEditable = "false"; 202 await p; 203 testStates("article", 204 STATE_READONLY, 0, 205 0, EXT_STATE_EDITABLE, 206 "article is READONLY and not EDITABLE"); 207 } 208 209 async function testAnimatedImage() { 210 testStates("animated-image", 211 STATE_ANIMATED, 0, 212 0, 0, 213 "image should be animated 1"); 214 let p = waitForStateChange("animated-image", STATE_ANIMATED, false, false); 215 getNode("animated-image").src = "../animated-gif-finalframe.gif"; 216 await p; 217 testStates("animated-image", 218 0, 0, 219 STATE_ANIMATED, 0, 220 "image should not be animated 2"); 221 p = waitForStateChange("animated-image", STATE_ANIMATED, true, false); 222 getNode("animated-image").src = "../animated-gif.gif"; 223 await p; 224 testStates("animated-image", 225 STATE_ANIMATED, 0, 226 0, 0, 227 "image should be animated 3"); 228 } 229 230 async function testImageLoad() { 231 let img = document.createElement("img"); 232 img.id = "image"; 233 // eslint-disable-next-line @microsoft/sdl/no-insecure-url 234 img.src = "http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs"; 235 let p = waitForEvent(EVENT_SHOW, "image"); 236 getNode("eventdump").before(img); 237 await p; 238 testStates("image", 239 STATE_INVISIBLE, 0, 240 0, 0, 241 "image should be invisible"); 242 p = waitForStateChange("image", STATE_INVISIBLE, false, false); 243 // eslint-disable-next-line @microsoft/sdl/no-insecure-url 244 await fetch("http://example.com/a11y/accessible/tests/mochitest/events/slow_image.sjs?complete"); 245 await p; 246 testStates("image", 247 0, 0, 248 STATE_INVISIBLE, 0, 249 "image should be invisible"); 250 } 251 252 async function testMultiSelectable(aID, aAttribute) { 253 testStates(aID, 254 0, 0, 255 STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, 256 `${aID} should not be multiselectable`); 257 let p = waitForEvents([ 258 stateChangeEventArgs(aID, STATE_MULTISELECTABLE, true, false), 259 stateChangeEventArgs(aID, STATE_EXTSELECTABLE, true, false), 260 ]); 261 getNode(aID).setAttribute(aAttribute, true); 262 await p; 263 testStates(aID, 264 STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, 265 0, 0, 266 `${aID} should not be multiselectable`); 267 p = waitForEvents([ 268 stateChangeEventArgs(aID, STATE_MULTISELECTABLE, false, false), 269 stateChangeEventArgs(aID, STATE_EXTSELECTABLE, false, false), 270 ]); 271 getNode(aID).removeAttribute(aAttribute); 272 await p; 273 testStates(aID, 274 0, 0, 275 STATE_MULTISELECTABLE | STATE_EXTSELECTABLE, 0, 276 `${aID} should not be multiselectable`); 277 } 278 279 async function testAutocomplete() { 280 // A text input will have autocomplete via browser's form autofill... 281 testStates("input", 282 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, 283 0, 0, 284 "input supports autocompletion"); 285 // unless it is explicitly turned off. 286 testStates("input-autocomplete-off", 287 0, 0, 288 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, 289 "input-autocomplete-off does not support autocompletion"); 290 // An input with a datalist will always have autocomplete. 291 testStates("input-list", 292 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, 293 0, 0, 294 "input-list supports autocompletion"); 295 // password fields don't get autocomplete. 296 testStates("input-password", 297 0, 0, 298 0, EXT_STATE_SUPPORTS_AUTOCOMPLETION, 299 "input-autocomplete-off does not support autocompletion"); 300 301 let p = waitForEvents({ 302 expected: [ 303 // Setting the form's autocomplete attribute to "off" will cause 304 // "input" to lost its autocomplete state. 305 stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true) 306 ], 307 unexpected: [ 308 // "input-list" should preserve its autocomplete state regardless of 309 // forms "autocomplete" attribute 310 [EVENT_STATE_CHANGE, "input-list"], 311 // "input-autocomplete-off" already has its autocomplte off, so no state 312 // change here. 313 [EVENT_STATE_CHANGE, "input-autocomplete-off"], 314 // passwords never get autocomplete 315 [EVENT_STATE_CHANGE, "input-password"], 316 ] 317 }); 318 319 getNode("form").setAttribute("autocomplete", "off"); 320 321 await p; 322 323 // Same when we remove the form's autocomplete attribute. 324 p = waitForEvents({ 325 expected: [stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true)], 326 unexpected: [ 327 [EVENT_STATE_CHANGE, "input-list"], 328 [EVENT_STATE_CHANGE, "input-autocomplete-off"], 329 [EVENT_STATE_CHANGE, "input-password"], 330 ] 331 }); 332 333 getNode("form").removeAttribute("autocomplete"); 334 335 await p; 336 337 p = waitForEvents({ 338 expected: [ 339 // Forcing autocomplete off on an input will cause a state change 340 stateChangeEventArgs("input", EXT_STATE_SUPPORTS_AUTOCOMPLETION, false, true), 341 // Associating a datalist with an autocomplete=off input 342 // will give it an autocomplete state, regardless. 343 stateChangeEventArgs("input-autocomplete-off", EXT_STATE_SUPPORTS_AUTOCOMPLETION, true, true), 344 // XXX: datalist inputs also get a HASPOPUP state, the inconsistent 345 // use of that state is inexplicable, but lets make sure we fire state 346 // change events for it anyway. 347 stateChangeEventArgs("input-autocomplete-off", STATE_HASPOPUP, true, false), 348 ], 349 unexpected: [ 350 // Forcing autocomplete off with a dataset input does nothing. 351 [EVENT_STATE_CHANGE, "input-list"], 352 // passwords never get autocomplete 353 [EVENT_STATE_CHANGE, "input-password"], 354 ] 355 }); 356 357 getNode("input").setAttribute("autocomplete", "off"); 358 getNode("input-list").setAttribute("autocomplete", "off"); 359 getNode("input-autocomplete-off").setAttribute("list", "browsers"); 360 getNode("input-password").setAttribute("autocomplete", "off"); 361 362 await p; 363 } 364 365 async function doTests() { 366 // Disable mixed-content upgrading as this test is expecting an HTTP load 367 await SpecialPowers.pushPrefEnv({ 368 set: [["security.mixed_content.upgrade_display_content", false]] 369 }); 370 371 // Test opening details objects 372 await openNode("detailsOpen", "summaryOpen", true); 373 await openNode("detailsOpen", "summaryOpen", false); 374 await openNode("detailsOpen1", "summaryOpen1", true); 375 await openNode("detailsOpen2", "summaryOpen2", true); 376 await openNode("detailsOpen3", "summaryOpen3", true); 377 await openNode("detailsOpen4", "summaryOpen4", true); 378 await openNode("detailsOpen5", "summaryOpen5", true); 379 await openNode("detailsOpen6", "summaryOpen6", true); 380 381 // Test delayed editable state change 382 var doc = document.getElementById("iframe").contentDocument; 383 await makeEditableDoc(doc); 384 385 // invalid state change 386 await invalidInput("email"); 387 388 // checked state change 389 await changeCheckInput("checkbox", true); 390 await changeCheckInput("checkbox", false); 391 await changeCheckInput("radio", true); 392 await changeCheckInput("radio", false); 393 394 // required state change 395 await changeRequiredState("checkbox", true); 396 397 // file input inherited state changes 398 await stateChangeOnFileInput("file", "aria-busy", "true", 399 STATE_BUSY, false, true); 400 await stateChangeOnFileInput("file", "aria-required", "true", 401 STATE_REQUIRED, false, true); 402 await stateChangeOnFileInput("file", "aria-invalid", "true", 403 STATE_INVALID, false, true); 404 405 await dupeStateChange("div", "aria-busy", "true", 406 STATE_BUSY, false, true); 407 await oppositeStateChange("div", "aria-busy", 408 STATE_BUSY, false); 409 410 await echoingStateChange("text1", "aria-disabled", "disabled", "true", 411 EXT_STATE_ENABLED, true, false); 412 await echoingStateChange("text1", "aria-disabled", "disabled", null, 413 EXT_STATE_ENABLED, true, true); 414 415 await testReadOnly(); 416 417 await testReadonlyUntilEditable(); 418 419 await testHasPopup(); 420 421 await toggleStateChange("textbox", "aria-multiline", EXT_STATE_MULTI_LINE, true); 422 423 await testDefaultSubmitChange(); 424 425 await testAnimatedImage(); 426 427 await testImageLoad(); 428 429 await testMultiSelectable("listbox", "aria-multiselectable"); 430 431 await testMultiSelectable("select", "multiple"); 432 433 await testAutocomplete(); 434 435 SimpleTest.finish(); 436 } 437 438 SimpleTest.waitForExplicitFinish(); 439 addA11yLoadEvent(doTests); 440 </script> 441 </head> 442 <style> 443 details.openBefore::before{ 444 content: "before detail content: "; 445 background: blue; 446 } 447 summary.openBefore::before{ 448 content: "before summary content: "; 449 background: green; 450 } 451 details.openAfter::after{ 452 content: " :after detail content"; 453 background: blue; 454 } 455 summary.openAfter::after{ 456 content: " :after summary content"; 457 background: green; 458 } 459 </style> 460 <body> 461 462 <a target="_blank" 463 href="https://bugzilla.mozilla.org/show_bug.cgi?id=564471" 464 title="Make state change events async"> 465 Bug 564471 466 </a> 467 <a target="_blank" 468 href="https://bugzilla.mozilla.org/show_bug.cgi?id=555728" 469 title="Fire a11y event based on HTML5 constraint validation"> 470 Bug 555728 471 </a> 472 <a target="_blank" 473 href="https://bugzilla.mozilla.org/show_bug.cgi?id=699017" 474 title="File input control should be propogate states to descendants"> 475 Bug 699017 476 </a> 477 <a target="_blank" 478 href="https://bugzilla.mozilla.org/show_bug.cgi?id=788389" 479 title="Fire statechange event whenever checked state is changed not depending on focused state"> 480 Bug 788389 481 </a> 482 <a target="_blank" 483 href="https://bugzilla.mozilla.org/show_bug.cgi?id=926812" 484 title="State change event not fired when both disabled and aria-disabled are toggled"> 485 Bug 926812 486 </a> 487 488 <p id="display"></p> 489 <div id="content" style="display: none"></div> 490 <pre id="test"> 491 </pre> 492 493 <!-- open --> 494 <details id="detailsOpen"><summary id="summaryOpen">open</summary>details can be opened</details> 495 <details id="detailsOpen1">order doesn't matter<summary id="summaryOpen1">open</summary></details> 496 <details id="detailsOpen2"><div>additional elements don't matter</div><summary id="summaryOpen2">open</summary></details> 497 <details id="detailsOpen3" class="openBefore"><summary id="summaryOpen3">summary</summary>content</details> 498 <details id="detailsOpen4" class="openAfter"><summary id="summaryOpen4">summary</summary>content</details> 499 <details id="detailsOpen5"><summary id="summaryOpen5" class="openBefore">summary</summary>content</details> 500 <details id="detailsOpen6"><summary id="summaryOpen6" class="openAfter">summary</summary>content</details> 501 502 503 <div id="testContainer"> 504 <iframe id="iframe"></iframe> 505 </div> 506 507 <input id="email" type='email'> 508 509 <input id="checkbox" type="checkbox"> 510 <input id="radio" type="radio"> 511 512 <input id="file" type="file"> 513 514 <div id="div"></div> 515 516 <!-- A sentinal guards from events of interest being fired after it emits a state change --> 517 <div id="sentinel"></div> 518 519 <input id="text1"> 520 521 <div id="textbox" role="textbox" aria-multiline="false">hello</div> 522 523 <form id="form"> 524 <button id="default-button">hello</button> 525 <button>world</button> 526 <input id="input"> 527 <input id="input-autocomplete-off" autocomplete="off"> 528 <input id="input-list" list="browsers"> 529 <input id="input-password" type="password"> 530 <datalist id="browsers"> 531 <option value="Internet Explorer"> 532 <option value="Firefox"> 533 <option value="Google Chrome"> 534 <option value="Opera"> 535 <option value="Safari"> 536 </datalist> 537 </form> 538 539 <div id="article" role="article">hello</div> 540 541 <img id="animated-image" src="../animated-gif.gif"> 542 543 <ul id="listbox" role="listbox"> 544 <li role="option">one</li> 545 <li role="option">two</li> 546 <li role="option">three</li> 547 <li role="option">four</li> 548 <li role="option">five</li> 549 </ul> 550 551 <select id="select" size="2"> 552 <option>one</option> 553 <option>two</option> 554 <option>three</option> 555 <option>four</option> 556 <option>five</option> 557 <option>size</option> 558 </select> 559 560 <div id="eventdump"></div> 561 562 <div id="eventdump"></div> 563 <button id="popupButton">action</button> 564 </body> 565 </html>