/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; /* import-globals-from ../../mochitest/role.js */ loadScripts({ name: "role.js", dir: MOCHITESTS_DIR }); /* import-globals-from ../../mochitest/attributes.js */ loadScripts({ name: "attributes.js", dir: MOCHITESTS_DIR }); /** * Helper function to test table consistency. */ function testTableConsistency(table, expectedRowCount, expectedColumnCount) { is(table.getAttributeValue("AXRole"), "AXTable", "Correct role for table"); let tableChildren = table.getAttributeValue("AXChildren"); // XXX: Should be expectedRowCount+ExpectedColumnCount+1 children, rows (incl headers) + cols + headers // if we're trying to match Safari. is( tableChildren.length, expectedRowCount + expectedColumnCount, "Table has children = rows (4) + cols (3)" ); for (let i = 0; i < tableChildren.length; i++) { let currChild = tableChildren[i]; if (i < expectedRowCount) { is( currChild.getAttributeValue("AXRole"), "AXRow", "Correct role for row" ); } else { is( currChild.getAttributeValue("AXRole"), "AXColumn", "Correct role for col" ); is( currChild.getAttributeValue("AXRoleDescription"), "column", "Correct role desc for col" ); } } is( table.getAttributeValue("AXColumnCount"), expectedColumnCount, "Table has correct column count." ); is( table.getAttributeValue("AXRowCount"), expectedRowCount, "Table has correct row count." ); let cols = table.getAttributeValue("AXColumns"); is(cols.length, expectedColumnCount, "Table has col list of correct length"); for (let i = 0; i < cols.length; i++) { let currCol = cols[i]; let currChildren = currCol.getAttributeValue("AXChildren"); is( currChildren.length, expectedRowCount, "Column has correct number of cells" ); for (let j = 0; j < currChildren.length; j++) { let currChild = currChildren[j]; is( currChild.getAttributeValue("AXRole"), "AXCell", "Column child is cell" ); } } let rows = table.getAttributeValue("AXRows"); is(rows.length, expectedRowCount, "Table has row list of correct length"); for (let i = 0; i < rows.length; i++) { let currRow = rows[i]; let currChildren = currRow.getAttributeValue("AXChildren"); is( currChildren.length, expectedColumnCount, "Row has correct number of cells" ); for (let j = 0; j < currChildren.length; j++) { let currChild = currChildren[j]; is(currChild.getAttributeValue("AXRole"), "AXCell", "Row child is cell"); } } } /** * Test table, columns, rows */ addAccessibleTask( `
CompanyContactCountry
Alfreds FutterkisteMaria AndersGermany
Centro comercial MoctezumaFrancisco ChangMexico
Ernst HandelRoland MendelAustria
`, async (browser, accDoc) => { let table = getNativeInterface(accDoc, "customers"); testTableConsistency(table, 4, 3); const rowText = [ "Madrigal Electromotive GmbH", "Lydia Rodarte-Quayle", "Germany", ]; let reorder = waitForEvent(EVENT_REORDER, "customers"); await SpecialPowers.spawn(browser, [rowText], _rowText => { let tr = content.document.createElement("tr"); for (let t of _rowText) { let td = content.document.createElement("td"); td.textContent = t; tr.appendChild(td); } content.document.getElementById("customers").appendChild(tr); }); await reorder; let cols = table.getAttributeValue("AXColumns"); is(cols.length, 3, "Table has col list of correct length"); for (let i = 0; i < cols.length; i++) { let currCol = cols[i]; let currChildren = currCol.getAttributeValue("AXChildren"); is(currChildren.length, 5, "Column has correct number of cells"); let lastCell = currChildren[currChildren.length - 1]; let cellChildren = lastCell.getAttributeValue("AXChildren"); is(cellChildren.length, 1, "Cell has a single text child"); is( cellChildren[0].getAttributeValue("AXRole"), "AXStaticText", "Correct role for cell child" ); is( cellChildren[0].getAttributeValue("AXValue"), rowText[i], "Correct text for cell" ); } reorder = waitForEvent(EVENT_REORDER, "firstrow"); await SpecialPowers.spawn(browser, [], () => { let td = content.document.createElement("td"); td.textContent = "Ticker"; content.document.getElementById("firstrow").appendChild(td); }); await reorder; cols = table.getAttributeValue("AXColumns"); is(cols.length, 4, "Table has col list of correct length"); is( cols[cols.length - 1].getAttributeValue("AXChildren").length, 1, "Last column has single child" ); reorder = waitForEvent( EVENT_REORDER, e => e.accessible.role == ROLE_DOCUMENT ); await SpecialPowers.spawn(browser, [], () => { content.document.getElementById("customers").remove(); }); await reorder; try { cols[0].getAttributeValue("AXChildren"); ok(false, "Getting children from column of expired table should fail"); } catch (e) { ok(true, "Getting children from column of expired table should fail"); } } ); addAccessibleTask( `
Header 1 Header 2
one two three
four five
`, (browser, accDoc) => { let table = getNativeInterface(accDoc, "table"); let getCellAt = (col, row) => table.getParameterizedAttributeValue("AXCellForColumnAndRow", [col, row]); function testCell(cell, expectedId, expectedColRange, expectedRowRange) { is( cell.getAttributeValue("AXDOMIdentifier"), expectedId, "Correct DOM Identifier" ); Assert.deepEqual( cell.getAttributeValue("AXColumnIndexRange"), expectedColRange, "Correct column range" ); Assert.deepEqual( cell.getAttributeValue("AXRowIndexRange"), expectedRowRange, "Correct row range" ); } testCell(getCellAt(0, 0), "header1", [0, 2], [0, 1]); testCell(getCellAt(1, 0), "header1", [0, 2], [0, 1]); testCell(getCellAt(2, 0), "header2", [2, 1], [0, 1]); testCell(getCellAt(0, 1), "cell1", [0, 1], [1, 1]); testCell(getCellAt(1, 1), "cell2", [1, 1], [1, 2]); testCell(getCellAt(2, 1), "cell3", [2, 1], [1, 1]); testCell(getCellAt(0, 2), "cell4", [0, 1], [2, 1]); testCell(getCellAt(1, 2), "cell2", [1, 1], [1, 2]); testCell(getCellAt(2, 2), "cell5", [2, 1], [2, 1]); let colHeaders = table.getAttributeValue("AXColumnHeaderUIElements"); Assert.deepEqual( colHeaders.map(c => c.getAttributeValue("AXDOMIdentifier")), ["header1", "header1", "header2"], "Correct column headers" ); } ); addAccessibleTask( `
Foo
`, (browser, accDoc) => { // Make sure we guess this table to be a layout table. testAttrs( findAccessibleChildByID(accDoc, "table"), { "layout-guess": "true" }, true ); let table = getNativeInterface(accDoc, "table"); is( table.getAttributeValue("AXRole"), "AXGroup", "Correct role (AXGroup) for layout table" ); let children = table.getAttributeValue("AXChildren"); is( children.length, 1, "Layout table has single child (no additional columns)" ); } ); addAccessibleTask( `
Cell 1
Cell 2
Cell 3
Cell 4
`, async (browser, accDoc) => { let table = getNativeInterface(accDoc, "table"); testTableConsistency(table, 2, 2); } ); /* * After executing function 'change' which operates on 'elem', verify the specified * 'event' (if not null) is fired on elem. After the event, check if the given * native accessible 'table' is a layout or data table by role using 'isLayout'. */ async function testIsLayout(table, elem, event, change, isLayout) { info( "Changing " + elem + ", expecting table change to " + (isLayout ? "AXGroup" : "AXTable") ); const toWait = event ? waitForEvent(event, elem) : null; await change(); if (toWait) { await toWait; } let intendedRole = isLayout ? "AXGroup" : "AXTable"; await untilCacheIs( () => table.getAttributeValue("AXRole"), intendedRole, "Table role correct after change" ); } /* * The following attributes should fire an attribute changed * event, which in turn invalidates the layout-table cache * associated with the given table. After adding and removing * each attr, verify the table is a data or layout table, * appropriately. Attrs: summary, abbr, scope, headers */ addAccessibleTask( `
cell1 cell2
cell3 cell4
`, async (browser, accDoc) => { let table = getNativeInterface(accDoc, "table"); // summary attr should take precedence over role="presentation" to make this // a data table is(table.getAttributeValue("AXRole"), "AXTable", "Table is data table"); info("Removing summary attr"); // after summary is removed, we should have a layout table await testIsLayout( table, "table", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document.getElementById("table").removeAttribute("summary"); }); }, true ); info("Setting abbr attr"); // after abbr is set we should have a data table again await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellThree") .setAttribute("abbr", "hello world"); }); }, false ); info("Removing abbr attr"); // after abbr is removed we should have a layout table again await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document.getElementById("cellThree").removeAttribute("abbr"); }); }, true ); info("Setting scope attr"); // after scope is set we should have a data table again await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellThree") .setAttribute("scope", "col"); }); }, false ); info("Removing scope attr"); // remove scope should give layout await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document.getElementById("cellThree").removeAttribute("scope"); }); }, true ); info("Setting headers attr"); // add headers attr should give data await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellThree") .setAttribute("headers", "cellOne"); }); }, false ); info("Removing headers attr"); // remove headers attr should give layout await testIsLayout( table, "cellThree", EVENT_OBJECT_ATTRIBUTE_CHANGED, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellThree") .removeAttribute("headers"); }); }, true ); } ); /* * The following style changes should fire a table style changed * event, which in turn invalidates the layout-table cache * associated with the given table. */ addAccessibleTask( `
cell1 cell2
cell3 cell4
`, async (browser, accDoc) => { let table = getNativeInterface(accDoc, "table"); // we should start as a layout table is(table.getAttributeValue("AXRole"), "AXGroup", "Table is layout table"); info("Adding cell border"); // after cell border added, we should have a data table await testIsLayout( table, "cellOne", null, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellOne") .style.setProperty("border", "5px solid green"); }); }, false ); info("Removing cell border"); // after cell border removed, we should have a layout table await testIsLayout( table, "cellOne", null, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("cellOne") .style.removeProperty("border"); }); }, true ); info("Adding row background"); // after row background added, we should have a data table await testIsLayout( table, "rowOne", null, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("rowOne") .style.setProperty("background-color", "green"); }); }, false ); info("Removing row background"); // after row background removed, we should have a layout table await testIsLayout( table, "rowOne", null, async () => { await SpecialPowers.spawn(browser, [], () => { content.document .getElementById("rowOne") .style.removeProperty("background-color"); }); }, true ); } ); /* * thead/tbody elements with click handlers should: * (a) render as AXGroup elements * (b) expose their rows as part of their parent table's AXRows array */ addAccessibleTask( `
head row
body row
another body row
`, async (browser, accDoc) => { let table = getNativeInterface(accDoc, "table"); // No click handlers present on thead/tbody let tableChildren = table.getAttributeValue("AXChildren"); let tableRows = table.getAttributeValue("AXRows"); is(tableChildren.length, 4, "Table has four children (3 row + 1 col)"); is(tableRows.length, 3, "Table has three rows"); for (let i = 0; i < tableChildren.length; i++) { const child = tableChildren[i]; if (i < 3) { is( child.getAttributeValue("AXRole"), "AXRow", "Table's first 3 children are rows" ); } else { is( child.getAttributeValue("AXRole"), "AXColumn", "Table's last child is a column" ); } } const reorder = waitForEvent(EVENT_REORDER); await invokeContentTask(browser, [], () => { const head = content.document.getElementById("thead"); const body = content.document.getElementById("tbody"); head.addEventListener("click", function () {}); body.addEventListener("click", function () {}); }); await reorder; // Click handlers present tableChildren = table.getAttributeValue("AXChildren"); is(tableChildren.length, 3, "Table has three children (2 groups + 1 col)"); is( tableChildren[0].getAttributeValue("AXRole"), "AXGroup", "Child one is a group" ); is( tableChildren[0].getAttributeValue("AXChildren").length, 1, "Child one has one child" ); is( tableChildren[1].getAttributeValue("AXRole"), "AXGroup", "Child two is a group" ); is( tableChildren[1].getAttributeValue("AXChildren").length, 2, "Child two has two children" ); is( tableChildren[2].getAttributeValue("AXRole"), "AXColumn", "Child three is a col" ); tableRows = table.getAttributeValue("AXRows"); is(tableRows.length, 3, "Table has three rows"); } );