/* 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(
`<table id=
"customers">
<tbody>
<tr id=
"firstrow"><th>Company</th><th>Contact</th><th>Country</th></tr>
<tr><td>Alfreds Futterkiste</td><td>Maria Anders</td><td>Germany</td></tr>
<tr><td>Centro comercial Moctezuma</td><td>Francisco Chang</td><td>Mexico</td></tr>
<tr><td>Ernst Handel</td><td>Roland Mendel</td><td>Austria</td></tr>
</tbody>
</table>`,
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(
`<table id=
"table">
<tr>
<th colspan=
"2" id=
"header1">Header 1</th>
<th id=
"header2">Header 2</th>
</tr>
<tr>
<td id=
"cell1">one</td>
<td id=
"cell2" rowspan=
"2">two</td>
<td id=
"cell3">three</td>
</tr>
<tr>
<td id=
"cell4">four</td>
<td id=
"cell5">five</td>
</tr>
</table>`,
(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(
`<table id=
"table">
<tr>
<td>Foo</td>
</tr>
</table>`,
(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(
`<div id=
"table" role=
"table">
<span style=
"display: block;">
<div role=
"row">
<div role=
"cell">Cell 1</div>
<div role=
"cell">Cell 2</div>
</div>
</span>
<span style=
"display: block;">
<div role=
"row">
<span style=
"display: block;">
<div role=
"cell">Cell 3</div>
<div role=
"cell">Cell 4</div>
</span>
</div>
</span>
</div>`,
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(
`<table id=
"table" summary=
"example summary">
<tr role=
"presentation">
<td id=
"cellOne">cell1</td>
<td>cell2</td>
</tr>
<tr>
<td id=
"cellThree">cell3</td>
<td>cell4</td>
</tr>
</table>`,
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(
`<table id=
"table">
<tr id=
"rowOne">
<td id=
"cellOne">cell1</td>
<td>cell2</td>
</tr>
<tr>
<td>cell3</td>
<td>cell4</td>
</tr>
</table>`,
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(
`<table id=
"table">
<thead id=
"thead">
<tr><td>head row</td></tr>
</thead>
<tbody id=
"tbody">
<tr><td>body row</td></tr>
<tr><td>another body row</td></tr>
</tbody>
</table>`,
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");
}
);