browser_table.js (17840B)
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 loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); 9 10 /* import-globals-from ../../mochitest/attributes.js */ 11 loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); 12 13 /** 14 * Helper function to test table consistency. 15 */ 16 function testTableConsistency(table, expectedRowCount, expectedColumnCount) { 17 is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table"); 18 19 let tableChildren = table.getAttributeValue("AXChildren"); 20 // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers 21 // if we're trying to match Safari. 22 is( 23 tableChildren.length, 24 expectedRowCount + expectedColumnCount, 25 "Table has children = rows (4) + cols (3)" 26 ); 27 for (let i = 0; i < tableChildren.length; i++) { 28 let currChild = tableChildren[i]; 29 if (i < expectedRowCount) { 30 is( 31 currChild.getAttributeValue("AXRole"), 32 "AXRow", 33 "Correct role for row" 34 ); 35 } else { 36 is( 37 currChild.getAttributeValue("AXRole"), 38 "AXColumn", 39 "Correct role for col" 40 ); 41 is( 42 currChild.getAttributeValue("AXRoleDescription"), 43 "column", 44 "Correct role desc for col" 45 ); 46 } 47 } 48 49 is( 50 table.getAttributeValue("AXColumnCount"), 51 expectedColumnCount, 52 "Table has correct column count." 53 ); 54 is( 55 table.getAttributeValue("AXRowCount"), 56 expectedRowCount, 57 "Table has correct row count." 58 ); 59 60 let cols = table.getAttributeValue("AXColumns"); 61 is(cols.length, expectedColumnCount, "Table has col list of correct length"); 62 for (let i = 0; i < cols.length; i++) { 63 let currCol = cols[i]; 64 let currChildren = currCol.getAttributeValue("AXChildren"); 65 is( 66 currChildren.length, 67 expectedRowCount, 68 "Column has correct number of cells" 69 ); 70 for (let j = 0; j < currChildren.length; j++) { 71 let currChild = currChildren[j]; 72 is( 73 currChild.getAttributeValue("AXRole"), 74 "AXCell", 75 "Column child is cell" 76 ); 77 } 78 } 79 80 let rows = table.getAttributeValue("AXRows"); 81 is(rows.length, expectedRowCount, "Table has row list of correct length"); 82 for (let i = 0; i < rows.length; i++) { 83 let currRow = rows[i]; 84 let currChildren = currRow.getAttributeValue("AXChildren"); 85 is( 86 currChildren.length, 87 expectedColumnCount, 88 "Row has correct number of cells" 89 ); 90 for (let j = 0; j < currChildren.length; j++) { 91 let currChild = currChildren[j]; 92 is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell"); 93 } 94 } 95 } 96 97 add_setup(async function () { 98 await SpecialPowers.pushPrefEnv({ 99 set: [["test.wait300msAfterTabSwitch", true]], 100 }); 101 }); 102 103 /** 104 * Test table, columns, rows 105 */ 106 addAccessibleTask( 107 `<table id="customers"> 108 <tbody> 109 <tr id="firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr> 110 <tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr> 111 <tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr> 112 <tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr> 113 </tbody> 114 </table>`, 115 async (browser, accDoc) => { 116 let table = getNativeInterface(accDoc, "customers"); 117 testTableConsistency(table, 4, 3); 118 119 const rowText = [ 120 "Madrigal Electromotive GmbH", 121 "Lydia Rodarte-Quayle", 122 "Germany", 123 ]; 124 let reorder = waitForEvent(EVENT_REORDER, "customers"); 125 await SpecialPowers.spawn(browser, [rowText], _rowText => { 126 let tr = content.document.createElement("tr"); 127 for (let t of _rowText) { 128 let td = content.document.createElement("td"); 129 td.textContent = t; 130 tr.appendChild(td); 131 } 132 content.document.getElementById("customers").appendChild(tr); 133 }); 134 await reorder; 135 136 let cols = table.getAttributeValue("AXColumns"); 137 is(cols.length, 3, "Table has col list of correct length"); 138 for (let i = 0; i < cols.length; i++) { 139 let currCol = cols[i]; 140 let currChildren = currCol.getAttributeValue("AXChildren"); 141 is(currChildren.length, 5, "Column has correct number of cells"); 142 let lastCell = currChildren[currChildren.length - 1]; 143 let cellChildren = lastCell.getAttributeValue("AXChildren"); 144 is(cellChildren.length, 1, "Cell has a single text child"); 145 is( 146 cellChildren[0].getAttributeValue("AXRole"), 147 "AXStaticText", 148 "Correct role for cell child" 149 ); 150 is( 151 cellChildren[0].getAttributeValue("AXValue"), 152 rowText[i], 153 "Correct text for cell" 154 ); 155 } 156 157 reorder = waitForEvent(EVENT_REORDER, "firstrow"); 158 await SpecialPowers.spawn(browser, [], () => { 159 let td = content.document.createElement("td"); 160 td.textContent = "Ticker"; 161 content.document.getElementById("firstrow").appendChild(td); 162 }); 163 await reorder; 164 165 cols = table.getAttributeValue("AXColumns"); 166 is(cols.length, 4, "Table has col list of correct length"); 167 is( 168 cols[cols.length - 1].getAttributeValue("AXChildren").length, 169 1, 170 "Last column has single child" 171 ); 172 173 reorder = waitForEvent( 174 EVENT_REORDER, 175 e => e.accessible.role == ROLE_DOCUMENT 176 ); 177 await SpecialPowers.spawn(browser, [], () => { 178 content.document.getElementById("customers").remove(); 179 }); 180 await reorder; 181 182 try { 183 cols[0].getAttributeValue("AXChildren"); 184 ok(false, "Getting children from column of expired table should fail"); 185 } catch (e) { 186 ok(true, "Getting children from column of expired table should fail"); 187 } 188 } 189 ); 190 191 addAccessibleTask( 192 `<table id="table"> 193 <tr> 194 <th colspan="2" id="header1">Header 1</th> 195 <th id="header2">Header 2</th> 196 </tr> 197 <tr> 198 <td id="cell1">one</td> 199 <td id="cell2" rowspan="2">two</td> 200 <td id="cell3">three</td> 201 </tr> 202 <tr> 203 <td id="cell4">four</td> 204 <td id="cell5">five</td> 205 </tr> 206 </table>`, 207 (browser, accDoc) => { 208 let table = getNativeInterface(accDoc, "table"); 209 210 let getCellAt = (col, row) => 211 table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]); 212 213 function testCell(cell, expectedId, expectedColRange, expectedRowRange) { 214 is( 215 cell.getAttributeValue("AXDOMIdentifier"), 216 expectedId, 217 "Correct DOM Identifier" 218 ); 219 Assert.deepEqual( 220 cell.getAttributeValue("AXColumnIndexRange"), 221 expectedColRange, 222 "Correct column range" 223 ); 224 Assert.deepEqual( 225 cell.getAttributeValue("AXRowIndexRange"), 226 expectedRowRange, 227 "Correct row range" 228 ); 229 } 230 231 testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]); 232 testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]); 233 testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]); 234 235 testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]); 236 testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]); 237 testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]); 238 239 testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]); 240 testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]); 241 testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]); 242 243 let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements"); 244 Assert.deepEqual( 245 colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")), 246 ["header1", "header1", "header2"], 247 "Correct column headers" 248 ); 249 } 250 ); 251 252 addAccessibleTask( 253 `<table id="table"> 254 <tr> 255 <td>Foo</td> 256 </tr> 257 </table>`, 258 (browser, accDoc) => { 259 // Make sure we guess this table to be a layout table. 260 testAttrs( 261 findAccessibleChildByID(accDoc, "table"), 262 { "layout-guess": "true" }, 263 true 264 ); 265 266 let table = getNativeInterface(accDoc, "table"); 267 is( 268 table.getAttributeValue("AXRole"), 269 "AXGroup", 270 "Correct role (AXGroup) for layout table" 271 ); 272 273 let children = table.getAttributeValue("AXChildren"); 274 is( 275 children.length, 276 1, 277 "Layout table has single child (no additional columns)" 278 ); 279 } 280 ); 281 282 addAccessibleTask( 283 `<div id="table" role="table"> 284 <span style="display: block;"> 285 <div role="row"> 286 <div role="cell">Cell 1</div> 287 <div role="cell">Cell 2</div> 288 </div> 289 </span> 290 <span style="display: block;"> 291 <div role="row"> 292 <span style="display: block;"> 293 <div role="cell">Cell 3</div> 294 <div role="cell">Cell 4</div> 295 </span> 296 </div> 297 </span> 298 </div>`, 299 async (browser, accDoc) => { 300 let table = getNativeInterface(accDoc, "table"); 301 testTableConsistency(table, 2, 2); 302 } 303 ); 304 305 /* 306 * After executing function 'change' which operates on 'elem', verify the specified 307 * 'event' (if not null) is fired on elem. After the event, check if the given 308 * native accessible 'table' is a layout or data table by role using 'isLayout'. 309 */ 310 async function testIsLayout(table, elem, event, change, isLayout) { 311 info( 312 "Changing " + 313 elem + 314 ", expecting table change to " + 315 (isLayout ? "AXGroup" : "AXTable") 316 ); 317 const toWait = event ? waitForEvent(event, elem) : null; 318 await change(); 319 if (toWait) { 320 await toWait; 321 } 322 let intendedRole = isLayout ? "AXGroup" : "AXTable"; 323 await untilCacheIs( 324 () => table.getAttributeValue("AXRole"), 325 intendedRole, 326 "Table role correct after change" 327 ); 328 } 329 330 /* 331 * The following attributes should fire an attribute changed 332 * event, which in turn invalidates the layout-table cache 333 * associated with the given table. After adding and removing 334 * each attr, verify the table is a data or layout table, 335 * appropriately. Attrs: summary, abbr, scope, headers 336 */ 337 addAccessibleTask( 338 `<table id="table" summary="example summary"> 339 <tr role="presentation"> 340 <td id="cellOne">cell1</td> 341 <td>cell2</td> 342 </tr> 343 <tr> 344 <td id="cellThree">cell3</td> 345 <td>cell4</td> 346 </tr> 347 </table>`, 348 async (browser, accDoc) => { 349 let table = getNativeInterface(accDoc, "table"); 350 // summary attr should take precedence over role="presentation" to make this 351 // a data table 352 is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table"); 353 354 info("Removing summary attr"); 355 // after summary is removed, we should have a layout table 356 await testIsLayout( 357 table, 358 "table", 359 EVENT_OBJECT_ATTRIBUTE_CHANGED, 360 async () => { 361 await SpecialPowers.spawn(browser, [], () => { 362 content.document.getElementById("table").removeAttribute("summary"); 363 }); 364 }, 365 true 366 ); 367 368 info("Setting abbr attr"); 369 // after abbr is set we should have a data table again 370 await testIsLayout( 371 table, 372 "cellThree", 373 EVENT_OBJECT_ATTRIBUTE_CHANGED, 374 async () => { 375 await SpecialPowers.spawn(browser, [], () => { 376 content.document 377 .getElementById("cellThree") 378 .setAttribute("abbr", "hello world"); 379 }); 380 }, 381 false 382 ); 383 384 info("Removing abbr attr"); 385 // after abbr is removed we should have a layout table again 386 await testIsLayout( 387 table, 388 "cellThree", 389 EVENT_OBJECT_ATTRIBUTE_CHANGED, 390 async () => { 391 await SpecialPowers.spawn(browser, [], () => { 392 content.document.getElementById("cellThree").removeAttribute("abbr"); 393 }); 394 }, 395 true 396 ); 397 398 info("Setting scope attr"); 399 // after scope is set we should have a data table again 400 await testIsLayout( 401 table, 402 "cellThree", 403 EVENT_OBJECT_ATTRIBUTE_CHANGED, 404 async () => { 405 await SpecialPowers.spawn(browser, [], () => { 406 content.document 407 .getElementById("cellThree") 408 .setAttribute("scope", "col"); 409 }); 410 }, 411 false 412 ); 413 414 info("Removing scope attr"); 415 // remove scope should give layout 416 await testIsLayout( 417 table, 418 "cellThree", 419 EVENT_OBJECT_ATTRIBUTE_CHANGED, 420 async () => { 421 await SpecialPowers.spawn(browser, [], () => { 422 content.document.getElementById("cellThree").removeAttribute("scope"); 423 }); 424 }, 425 true 426 ); 427 428 info("Setting headers attr"); 429 // add headers attr should give data 430 await testIsLayout( 431 table, 432 "cellThree", 433 EVENT_OBJECT_ATTRIBUTE_CHANGED, 434 async () => { 435 await SpecialPowers.spawn(browser, [], () => { 436 content.document 437 .getElementById("cellThree") 438 .setAttribute("headers", "cellOne"); 439 }); 440 }, 441 false 442 ); 443 444 info("Removing headers attr"); 445 // remove headers attr should give layout 446 await testIsLayout( 447 table, 448 "cellThree", 449 EVENT_OBJECT_ATTRIBUTE_CHANGED, 450 async () => { 451 await SpecialPowers.spawn(browser, [], () => { 452 content.document 453 .getElementById("cellThree") 454 .removeAttribute("headers"); 455 }); 456 }, 457 true 458 ); 459 } 460 ); 461 462 /* 463 * The following style changes should fire a table style changed 464 * event, which in turn invalidates the layout-table cache 465 * associated with the given table. 466 */ 467 addAccessibleTask( 468 `<table id="table"> 469 <tr id="rowOne"> 470 <td id="cellOne">cell1</td> 471 <td>cell2</td> 472 </tr> 473 <tr> 474 <td>cell3</td> 475 <td>cell4</td> 476 </tr> 477 </table>`, 478 async (browser, accDoc) => { 479 let table = getNativeInterface(accDoc, "table"); 480 // we should start as a layout table 481 is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table"); 482 483 info("Adding cell border"); 484 // after cell border added, we should have a data table 485 await testIsLayout( 486 table, 487 "cellOne", 488 null, 489 async () => { 490 await SpecialPowers.spawn(browser, [], () => { 491 content.document 492 .getElementById("cellOne") 493 .style.setProperty("border", "5px solid green"); 494 }); 495 }, 496 false 497 ); 498 499 info("Removing cell border"); 500 // after cell border removed, we should have a layout table 501 await testIsLayout( 502 table, 503 "cellOne", 504 null, 505 async () => { 506 await SpecialPowers.spawn(browser, [], () => { 507 content.document 508 .getElementById("cellOne") 509 .style.removeProperty("border"); 510 }); 511 }, 512 true 513 ); 514 515 info("Adding row background"); 516 // after row background added, we should have a data table 517 await testIsLayout( 518 table, 519 "rowOne", 520 null, 521 async () => { 522 await SpecialPowers.spawn(browser, [], () => { 523 content.document 524 .getElementById("rowOne") 525 .style.setProperty("background-color", "green"); 526 }); 527 }, 528 false 529 ); 530 531 info("Removing row background"); 532 // after row background removed, we should have a layout table 533 await testIsLayout( 534 table, 535 "rowOne", 536 null, 537 async () => { 538 await SpecialPowers.spawn(browser, [], () => { 539 content.document 540 .getElementById("rowOne") 541 .style.removeProperty("background-color"); 542 }); 543 }, 544 true 545 ); 546 } 547 ); 548 549 /* 550 * thead/tbody elements with click handlers should: 551 * (a) render as AXGroup elements 552 * (b) expose their rows as part of their parent table's AXRows array 553 */ 554 addAccessibleTask( 555 `<table id="table"> 556 <thead id="thead"> 557 <tr><td>head row</td></tr> 558 </thead> 559 <tbody id="tbody"> 560 <tr><td>body row</td></tr> 561 <tr><td>another body row</td></tr> 562 </tbody> 563 </table>`, 564 async (browser, accDoc) => { 565 let table = getNativeInterface(accDoc, "table"); 566 567 // No click handlers present on thead/tbody 568 let tableChildren = table.getAttributeValue("AXChildren"); 569 let tableRows = table.getAttributeValue("AXRows"); 570 571 is(tableChildren.length, 4, "Table has four children (3 row + 1 col)"); 572 is(tableRows.length, 3, "Table has three rows"); 573 574 for (let i = 0; i < tableChildren.length; i++) { 575 const child = tableChildren[i]; 576 if (i < 3) { 577 is( 578 child.getAttributeValue("AXRole"), 579 "AXRow", 580 "Table's first 3 children are rows" 581 ); 582 } else { 583 is( 584 child.getAttributeValue("AXRole"), 585 "AXColumn", 586 "Table's last child is a column" 587 ); 588 } 589 } 590 const reorder = waitForEvent(EVENT_REORDER); 591 await invokeContentTask(browser, [], () => { 592 const head = content.document.getElementById("thead"); 593 const body = content.document.getElementById("tbody"); 594 595 head.addEventListener("click", function () {}); 596 body.addEventListener("click", function () {}); 597 }); 598 await reorder; 599 600 // Click handlers present 601 tableChildren = table.getAttributeValue("AXChildren"); 602 603 is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)"); 604 is( 605 tableChildren[0].getAttributeValue("AXRole"), 606 "AXGroup", 607 "Child one is a group" 608 ); 609 is( 610 tableChildren[0].getAttributeValue("AXChildren").length, 611 1, 612 "Child one has one child" 613 ); 614 615 is( 616 tableChildren[1].getAttributeValue("AXRole"), 617 "AXGroup", 618 "Child two is a group" 619 ); 620 is( 621 tableChildren[1].getAttributeValue("AXChildren").length, 622 2, 623 "Child two has two children" 624 ); 625 626 is( 627 tableChildren[2].getAttributeValue("AXRole"), 628 "AXColumn", 629 "Child three is a col" 630 ); 631 632 tableRows = table.getAttributeValue("AXRows"); 633 is(tableRows.length, 3, "Table has three rows"); 634 } 635 );