diff options
Diffstat (limited to 'accessible/tests/browser/windows/uia')
7 files changed, 997 insertions, 1 deletions
diff --git a/accessible/tests/browser/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml index 75728f56d7..13b1c12cc0 100644 --- a/accessible/tests/browser/windows/uia/browser.toml +++ b/accessible/tests/browser/windows/uia/browser.toml @@ -11,8 +11,15 @@ support-files = ["head.js"] ["browser_elementFromPoint.js"] ["browser_focus.js"] + ["browser_generalProps.js"] +["browser_gridPatterns.js"] + +["browser_relationProps.js"] + +["browser_selectionPatterns.js"] + ["browser_simplePatterns.js"] ["browser_tree.js"] diff --git a/accessible/tests/browser/windows/uia/browser_generalProps.js b/accessible/tests/browser/windows/uia/browser_generalProps.js index 5cfda226d0..244c9e4b1b 100644 --- a/accessible/tests/browser/windows/uia/browser_generalProps.js +++ b/accessible/tests/browser/windows/uia/browser_generalProps.js @@ -4,6 +4,12 @@ "use strict"; +/* eslint-disable camelcase */ +// From https://learn.microsoft.com/en-us/windows/win32/winauto/landmark-type-identifiers +const UIA_CustomLandmarkTypeId = 80000; +const UIA_MainLandmarkTypeId = 80002; +/* eslint-enable camelcase */ + /** * Test the Name property. */ @@ -103,3 +109,347 @@ addUiaTask( ok(await runPython(`p.CurrentIsEnabled`), "p has IsEnabled true"); } ); + +async function testGroupPos(id, level, pos, size) { + await assignPyVarToUiaWithId(id); + is(await runPython(`${id}.CurrentLevel`), level, `${id} Level correct`); + is( + await runPython(`${id}.CurrentPositionInSet`), + pos, + `${id} PositionInSet correct` + ); + is( + await runPython(`${id}.CurrentSizeOfSet`), + size, + `${id} SizeOfSet correct` + ); +} + +/** + * Test the Level, PositionInSet and SizeOfSet properties. + */ +addUiaTask( + ` +<ul> + <li id="li1">li1<ul id="ul1"> + <li id="li2a">li2a</li> + <li id="li2b" hidden>li2b</li> + <li id="li2c">li2c</li> + </ul></li> +</ul> +<h2 id="h2">h2</h2> +<button id="button">button</button> + `, + async function testGroupPosProps(browser) { + await definePyVar("doc", `getDocUia()`); + await testGroupPos("li1", 1, 1, 1); + await testGroupPos("li2a", 2, 1, 2); + await testGroupPos("li2c", 2, 2, 2); + info("Showing li2b"); + // There aren't events in any API for a change to group position properties + // because this would be too spammy and isn't particularly useful given + // how frequently these can change. + let shown = waitForEvent(EVENT_SHOW, "li2b"); + await invokeContentTask(browser, [], () => { + content.document.getElementById("li2b").hidden = false; + }); + await shown; + await testGroupPos("li2a", 2, 1, 3); + await testGroupPos("li2b", 2, 2, 3); + await testGroupPos("li2c", 2, 3, 3); + + // The IA2 -> UIA proxy doesn't map heading level to the Level property. + if (gIsUiaEnabled) { + await testGroupPos("h2", 2, 0, 0); + } + await testGroupPos("button", 0, 0, 0); + } +); + +/** + * Test the FrameworkId property. + */ +addUiaTask( + `<button id="button">button</button>`, + async function testFrameworkId() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`doc.CurrentFrameworkId`), + "Gecko", + "doc FrameworkId is correct" + ); + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentFrameworkId`), + "Gecko", + "button FrameworkId is correct" + ); + } +); + +/** + * Test the ClassName property. + */ +addUiaTask( + ` +<p id="p">p</p> +<button id="button" class="c1">button</button> + `, + async function testClassName(browser, docAcc) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("p"); + ok(!(await runPython(`p.CurrentClassName`)), "p has no ClassName"); + + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentClassName`), + "c1", + "button has correct ClassName" + ); + info("Changing button class"); + await invokeSetAttribute(browser, "button", "class", "c2 c3"); + // Gecko doesn't fire an event for class changes, as this isn't useful for + // clients. + const button = findAccessibleChildByID(docAcc, "button"); + await untilCacheIs( + () => button.attributes.getStringProperty("class"), + "c2 c3", + "button class updated" + ); + is( + await runPython(`button.CurrentClassName`), + "c2 c3", + "button has correct ClassName" + ); + }, + // The IA2 -> UIA proxy doesn't support ClassName. + { uiaEnabled: true, uiaDisabled: false } +); + +/** + * Test the AriaRole property. + */ +addUiaTask( + ` +<div id="button" role="button">button</div> +<div id="main" role="main">main</div> +<div id="unknown" role="unknown">unknown</div> +<button id="computedButton">computedButton</button> +<h1 id="computedHeading">computedHeading</h1> +<main id="computedMain">computedMain</main> +<div id="generic">generic</div> + `, + async function testAriaRole() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`findUiaByDomId(doc, "button").CurrentAriaRole`), + "button", + "button has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "main").CurrentAriaRole`), + "main", + "main has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "unknown").CurrentAriaRole`), + "unknown", + "unknown has correct AriaRole" + ); + // The IA2 -> UIA proxy doesn't compute ARIA roles. + if (gIsUiaEnabled) { + is( + await runPython( + `findUiaByDomId(doc, "computedButton").CurrentAriaRole` + ), + "button", + "computedButton has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "computedMain").CurrentAriaRole`), + "main", + "computedMain has correct AriaRole" + ); + is( + await runPython( + `findUiaByDomId(doc, "computedHeading").CurrentAriaRole` + ), + "heading", + "computedHeading has correct AriaRole" + ); + is( + await runPython(`findUiaByDomId(doc, "generic").CurrentAriaRole`), + "generic", + "generic has correct AriaRole" + ); + } + } +); + +/** + * Test the LocalizedControlType property. We don't support this ourselves, but + * the system provides it based on ControlType and AriaRole. + */ +addUiaTask( + ` +<button id="button">button</button> +<h1 id="h1">h1</h1> +<main id="main">main</main> + `, + async function testLocalizedControlType() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython( + `findUiaByDomId(doc, "button").CurrentLocalizedControlType` + ), + "button", + "button has correct LocalizedControlType" + ); + // The IA2 -> UIA proxy doesn't compute ARIA roles, so it can't compute the + // correct LocalizedControlType for these either. + if (gIsUiaEnabled) { + is( + await runPython( + `findUiaByDomId(doc, "h1").CurrentLocalizedControlType` + ), + "heading", + "h1 has correct LocalizedControlType" + ); + is( + await runPython( + `findUiaByDomId(doc, "main").CurrentLocalizedControlType` + ), + "main", + "main has correct LocalizedControlType" + ); + } + } +); + +/** + * Test the LandmarkType property. + */ +addUiaTask( + ` +<div id="main" role="main">main</div> +<main id="htmlMain">htmlMain</main> +<div id="banner" role="banner">banner</div> +<header id="header">header</header> +<div id="region" role="region" aria-label="region">region</div> +<div id="unnamedRegion" role="region">unnamedRegion</div> +<main id="mainBanner" role="banner">mainBanner</main> +<div id="none">none</div> + `, + async function testLandmarkType() { + await definePyVar("doc", `getDocUia()`); + is( + await runPython(`findUiaByDomId(doc, "main").CurrentLandmarkType`), + UIA_MainLandmarkTypeId, + "main has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "htmlMain").CurrentLandmarkType`), + UIA_MainLandmarkTypeId, + "htmlMain has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "banner").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "banner has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "header").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "header has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "region").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "region has correct LandmarkType" + ); + is( + await runPython( + `findUiaByDomId(doc, "unnamedRegion").CurrentLandmarkType` + ), + 0, + "unnamedRegion has correct LandmarkType" + ); + // ARIA role takes precedence. + is( + await runPython(`findUiaByDomId(doc, "mainBanner").CurrentLandmarkType`), + UIA_CustomLandmarkTypeId, + "mainBanner has correct LandmarkType" + ); + is( + await runPython(`findUiaByDomId(doc, "none").CurrentLandmarkType`), + 0, + "none has correct LandmarkType" + ); + } +); + +/** + * Test the LocalizedLandmarkType property. + */ +addUiaTask( + ` +<div id="main" role="main">main</div> +<div id="contentinfo" role="contentinfo">contentinfo</div> +<div id="region" role="region" aria-label="region">region</div> +<div id="unnamedRegion" role="region">unnamedRegion</div> +<main id="mainBanner" role="banner">mainBanner</main> +<div id="none">none</div> + `, + async function testLocalizedLandmarkType() { + await definePyVar("doc", `getDocUia()`); + // Provided by the system. + is( + await runPython( + `findUiaByDomId(doc, "main").CurrentLocalizedLandmarkType` + ), + "main", + "main has correct LocalizedLandmarkType" + ); + // The IA2 -> UIA proxy doesn't follow the Core AAM spec for this role. + if (gIsUiaEnabled) { + // Provided by us. + is( + await runPython( + `findUiaByDomId(doc, "contentinfo").CurrentLocalizedLandmarkType` + ), + "content information", + "contentinfo has correct LocalizedLandmarkType" + ); + } + is( + await runPython( + `findUiaByDomId(doc, "region").CurrentLocalizedLandmarkType` + ), + "region", + "region has correct LocalizedLandmarkType" + ); + // Invalid landmark. + is( + await runPython( + `findUiaByDomId(doc, "unnamedRegion").CurrentLocalizedLandmarkType` + ), + "", + "unnamedRegion has correct LocalizedLandmarkType" + ); + // ARIA role takes precedence. + is( + await runPython( + `findUiaByDomId(doc, "mainBanner").CurrentLocalizedLandmarkType` + ), + "banner", + "mainBanner has correct LocalizedLandmarkType" + ); + is( + await runPython( + `findUiaByDomId(doc, "none").CurrentLocalizedLandmarkType` + ), + "", + "none has correct LocalizedLandmarkType" + ); + } +); diff --git a/accessible/tests/browser/windows/uia/browser_gridPatterns.js b/accessible/tests/browser/windows/uia/browser_gridPatterns.js new file mode 100644 index 0000000000..24c80a6340 --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_gridPatterns.js @@ -0,0 +1,161 @@ +/* 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"; + +/* eslint-disable camelcase */ +const RowOrColumnMajor_RowMajor = 0; +/* eslint-enable camelcase */ + +const SNIPPET = ` +<table id="table"> + <tr><th id="a">a</th><th id="b">b</th><th id="c">c</th></tr> + <tr><th id="dg" rowspan="2">dg</th><td id="ef" colspan="2" headers="b c">ef</td></tr> + <tr><th id="h">h</th><td id="i" headers="dg h">i</td></tr> + <tr><td id="jkl" colspan="3" headers="a b c">jkl</td></tr> +</table> +<button id="button">button</button> +`; + +async function testGridGetItem(row, col, cellId) { + is( + await runPython(`pattern.GetItem(${row}, ${col}).CurrentAutomationId`), + cellId, + `GetItem with row ${row} and col ${col} returned ${cellId}` + ); +} + +async function testGridItemProps(id, row, col, rowSpan, colSpan, gridId) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "GridItem")`); + ok(await runPython(`bool(pattern)`), `${id} has GridItem pattern`); + is(await runPython(`pattern.CurrentRow`), row, `${id} has correct Row`); + is(await runPython(`pattern.CurrentColumn`), col, `${id} has correct Column`); + is( + await runPython(`pattern.CurrentRowSpan`), + rowSpan, + `${id} has correct RowSpan` + ); + is( + await runPython(`pattern.CurrentColumnSpan`), + colSpan, + `${id} has correct ColumnSpan` + ); + is( + await runPython(`pattern.CurrentContainingGrid.CurrentAutomationId`), + gridId, + `${id} ContainingGridItem is ${gridId}` + ); +} + +async function testTableItemProps(id, rowHeaders, colHeaders) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "TableItem")`); + ok(await runPython(`bool(pattern)`), `${id} has TableItem pattern`); + await isUiaElementArray( + `pattern.GetCurrentRowHeaderItems()`, + rowHeaders, + `${id} has correct RowHeaderItems` + ); + await isUiaElementArray( + `pattern.GetCurrentColumnHeaderItems()`, + colHeaders, + `${id} has correct ColumnHeaderItems` + ); +} + +/** + * Test the Grid pattern. + */ +addUiaTask(SNIPPET, async function testGrid() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("table"); + await definePyVar("pattern", `getUiaPattern(table, "Grid")`); + ok(await runPython(`bool(pattern)`), "table has Grid pattern"); + is( + await runPython(`pattern.CurrentRowCount`), + 4, + "table has correct RowCount" + ); + is( + await runPython(`pattern.CurrentColumnCount`), + 3, + "table has correct ColumnCount" + ); + await testGridGetItem(0, 0, "a"); + await testGridGetItem(0, 1, "b"); + await testGridGetItem(0, 2, "c"); + await testGridGetItem(1, 0, "dg"); + await testGridGetItem(1, 1, "ef"); + await testGridGetItem(1, 2, "ef"); + await testGridGetItem(2, 0, "dg"); + await testGridGetItem(2, 1, "h"); + await testGridGetItem(2, 2, "i"); + + await testPatternAbsent("button", "Grid"); +}); + +/** + * Test the GridItem pattern. + */ +addUiaTask(SNIPPET, async function testGridItem() { + await definePyVar("doc", `getDocUia()`); + await testGridItemProps("a", 0, 0, 1, 1, "table"); + await testGridItemProps("b", 0, 1, 1, 1, "table"); + await testGridItemProps("c", 0, 2, 1, 1, "table"); + await testGridItemProps("dg", 1, 0, 2, 1, "table"); + await testGridItemProps("ef", 1, 1, 1, 2, "table"); + await testGridItemProps("jkl", 3, 0, 1, 3, "table"); + + await testPatternAbsent("button", "GridItem"); +}); + +/** + * Test the Table pattern. + */ +addUiaTask( + SNIPPET, + async function testTable() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("table"); + await definePyVar("pattern", `getUiaPattern(table, "Table")`); + ok(await runPython(`bool(pattern)`), "table has Table pattern"); + await isUiaElementArray( + `pattern.GetCurrentRowHeaders()`, + ["dg", "h"], + "table has correct RowHeaders" + ); + await isUiaElementArray( + `pattern.GetCurrentColumnHeaders()`, + ["a", "b", "c"], + "table has correct ColumnHeaders" + ); + is( + await runPython(`pattern.CurrentRowOrColumnMajor`), + RowOrColumnMajor_RowMajor, + "table has correct RowOrColumnMajor" + ); + + await testPatternAbsent("button", "Table"); + }, + // The IA2 -> UIA proxy doesn't support the Row/ColumnHeaders properties. + { uiaEnabled: true, uiaDisabled: false } +); + +/** + * Test the TableItem pattern. + */ +addUiaTask(SNIPPET, async function testTableItem() { + await definePyVar("doc", `getDocUia()`); + await testTableItemProps("a", [], []); + await testTableItemProps("b", [], []); + await testTableItemProps("c", [], []); + await testTableItemProps("dg", [], ["a"]); + await testTableItemProps("ef", ["dg"], ["b", "c"]); + await testTableItemProps("h", ["dg"], ["b"]); + await testTableItemProps("i", ["dg", "h"], ["c"]); + await testTableItemProps("jkl", [], ["a", "b", "c"]); + + await testPatternAbsent("button", "TableItem"); +}); diff --git a/accessible/tests/browser/windows/uia/browser_relationProps.js b/accessible/tests/browser/windows/uia/browser_relationProps.js new file mode 100644 index 0000000000..ff4059f99e --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_relationProps.js @@ -0,0 +1,143 @@ +/* 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"; + +function testUiaRelationArray(id, prop, targets) { + return isUiaElementArray( + `findUiaByDomId(doc, "${id}").Current${prop}`, + targets, + `${id} has correct ${prop} targets` + ); +} + +/** + * Test the ControllerFor property. + */ +addUiaTask( + ` +<input id="controls" aria-controls="t1 t2"> +<input id="error" aria-errormessage="t3 t4" aria-invalid="true"> +<input id="controlsError" aria-controls="t1 t2" aria-errormessage="t3 t4" aria-invalid="true"> +<div id="t1">t1</div> +<div id="t2">t2</div> +<div id="t3">t3</div> +<div id="t4">t4</div> +<button id="none">none</button> + `, + async function testControllerFor() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("controls", "ControllerFor", ["t1", "t2"]); + // The IA2 -> UIA proxy doesn't support IA2_RELATION_ERROR. + if (gIsUiaEnabled) { + await testUiaRelationArray("error", "ControllerFor", ["t3", "t4"]); + await testUiaRelationArray("controlsError", "ControllerFor", [ + "t1", + "t2", + "t3", + "t4", + ]); + } + await testUiaRelationArray("none", "ControllerFor", []); + } +); + +/** + * Test the DescribedBy property. + */ +addUiaTask( + ` +<input id="describedby" aria-describedby="t1 t2"> +<input id="details" aria-details="t3 t4"> +<input id="describedbyDetails" aria-describedby="t1 t2" aria-details="t3 t4" aria-invalid="true"> +<div id="t1">t1</div> +<div id="t2">t2</div> +<div id="t3">t3</div> +<div id="t4">t4</div> +<button id="none">none</button> + `, + async function testDescribedBy() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("describedby", "DescribedBy", ["t1", "t2"]); + // The IA2 -> UIA proxy doesn't support IA2_RELATION_DETAILS. + if (gIsUiaEnabled) { + await testUiaRelationArray("details", "DescribedBy", ["t3", "t4"]); + await testUiaRelationArray("describedbyDetails", "DescribedBy", [ + "t1", + "t2", + "t3", + "t4", + ]); + } + await testUiaRelationArray("none", "DescribedBy", []); + } +); + +/** + * Test the FlowsFrom and FlowsTo properties. + */ +addUiaTask( + ` +<div id="t1" aria-flowto="t2">t1</div> +<div id="t2">t2</div> +<button id="none">none</button> + `, + async function testFlows() { + await definePyVar("doc", `getDocUia()`); + await testUiaRelationArray("t1", "FlowsTo", ["t2"]); + await testUiaRelationArray("t2", "FlowsFrom", ["t1"]); + await testUiaRelationArray("none", "FlowsFrom", []); + await testUiaRelationArray("none", "FlowsTo", []); + } +); + +/** + * Test the LabeledBy property. + */ +addUiaTask( + ` +<label id="label">label</label> +<input id="input" aria-labelledby="label"> +<label id="wrappingLabel"> + <input id="wrappedInput" value="wrappedInput"> + <p id="wrappingLabelP">wrappingLabel</p> +</label> +<button id="button" aria-labelledby="label">content</button> +<button id="noLabel">noLabel</button> + `, + async function testLabeledBy() { + await definePyVar("doc", `getDocUia()`); + // input's LabeledBy should be label's text leaf. + let result = await runPython(` + input = findUiaByDomId(doc, "input") + label = findUiaByDomId(doc, "label") + labelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(label) + return uiaClient.CompareElements(input.CurrentLabeledBy, labelLeaf) + `); + ok(result, "input has correct LabeledBy"); + // wrappedInput's LabeledBy should be wrappingLabelP's text leaf. + result = await runPython(` + wrappedInput = findUiaByDomId(doc, "wrappedInput") + wrappingLabelP = findUiaByDomId(doc, "wrappingLabelP") + wrappingLabelLeaf = uiaClient.RawViewWalker.GetFirstChildElement(wrappingLabelP) + return uiaClient.CompareElements(wrappedInput.CurrentLabeledBy, wrappingLabelLeaf) + `); + ok(result, "wrappedInput has correct LabeledBy"); + // button has aria-labelledby, but UIA prohibits LabeledBy on buttons. + ok( + !(await runPython( + `bool(findUiaByDomId(doc, "button").CurrentLabeledBy)` + )), + "button has no LabeledBy" + ); + ok( + !(await runPython( + `bool(findUiaByDomId(doc, "noLabel").CurrentLabeledBy)` + )), + "noLabel has no LabeledBy" + ); + }, + // The IA2 -> UIA proxy doesn't expose LabeledBy properly. + { uiaEnabled: true, uiaDisabled: false } +); diff --git a/accessible/tests/browser/windows/uia/browser_selectionPatterns.js b/accessible/tests/browser/windows/uia/browser_selectionPatterns.js new file mode 100644 index 0000000000..a1f70b886a --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_selectionPatterns.js @@ -0,0 +1,226 @@ +/* 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"; + +const SNIPPET = ` +<select id="selectList" size="2"> + <option id="sl1" selected>sl1</option> + <option id="sl2">sl2</option> +</select> +<select id="selectRequired" size="2" required> + <option id="sr1">sr1</option> +</select> +<select id="selectMulti" size="2" multiple> + <option id="sm1" selected>sm1</option> + <option id="sm2">sm2</option> + <option id="sm3" selected>sm3</option> + <option id="sm4">sm4</option> + <option id="sm5">sm5</option> + <option id="sm6">sm6</option> +</select> +<select id="selectCombo" size="1"> + <option>sc1</option> +</select> +<div id="ariaListbox" role="listbox"> + <div id="al1" role="option" aria-selected="true">al1</div> + <div id="al2" role="option">al2</div> +</div> +<div id="tablist" role="tablist"> + <div id="t1" role="tab">t1</div> + <div id="t2" role="tab" aria-selected="true">t2</div> +</div> +<table id="grid" role="grid" aria-multiselectable="true"> + <tr> + <td id="g1">g1</td> + <td id="g2" role="gridcell" aria-selected="true">g2</td> + </tr> + <tr> + <td id="g3">g3</td> + <td id="g4" role="gridcell" aria-selected="true">g4</td> + </tr> +</table> +<div id="radiogroup" role="radiogroup"> + <label><input id="r1" type="radio" name="r" checked>r1</label> + <label><input id="r2" type="radio" name="r">r2</label> +</div> +<div id="menu" role="menu"> + <div id="m1" role="menuitem">m1</div> + <div id="m2" role="menuitemradio">m2</div> + <div id="m3" role="menuitemradio" aria-checked="true">m3</div> +</div> +<button id="button">button</button> +`; + +async function testSelectionProps(id, selection, multiple, required) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "Selection")`); + ok(await runPython(`bool(pattern)`), `${id} has Selection pattern`); + await isUiaElementArray( + `pattern.GetCurrentSelection()`, + selection, + `${id} has correct Selection` + ); + is( + !!(await runPython(`pattern.CurrentCanSelectMultiple`)), + multiple, + `${id} has correct CanSelectMultiple` + ); + // The IA2 -> UIA proxy doesn't reflect the required state correctly. + if (gIsUiaEnabled) { + is( + !!(await runPython(`pattern.CurrentIsSelectionRequired`)), + required, + `${id} has correct IsSelectionRequired` + ); + } +} + +async function testSelectionItemProps(id, selected, container) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "SelectionItem")`); + ok(await runPython(`bool(pattern)`), `${id} has SelectionItem pattern`); + is( + !!(await runPython(`pattern.CurrentIsSelected`)), + selected, + `${id} has correct IsSelected` + ); + if (container) { + is( + await runPython(`pattern.CurrentSelectionContainer.CurrentAutomationId`), + container, + `${id} has correct SelectionContainer` + ); + } else { + ok( + !(await runPython(`bool(pattern.CurrentSelectionContainer)`)), + `${id} has no SelectionContainer` + ); + } +} + +/** + * Test the Selection pattern. + */ +addUiaTask(SNIPPET, async function testSelection(browser) { + await definePyVar("doc", `getDocUia()`); + await testSelectionProps("selectList", ["sl1"], false, false); + await testSelectionProps("selectRequired", [], false, true); + + await testSelectionProps("selectMulti", ["sm1", "sm3"], true, false); + // The Selection pattern only has an event for a complete invalidation of the + // container's selection, which only happens when there are too many selection + // events. Smaller selection changes fire events in the SelectionItem pattern. + info("Changing selectMulti selection"); + await setUpWaitForUiaEvent("Selection_Invalidated", "selectMulti"); + await invokeContentTask(browser, [], () => { + const multi = content.document.getElementById("selectMulti"); + multi[0].selected = false; + multi[1].selected = true; + multi[2].selected = false; + multi[3].selected = true; + multi[4].selected = true; + multi[5].selected = true; + }); + await waitForUiaEvent(); + ok(true, "select got Invalidated event"); + await testSelectionProps( + "selectMulti", + ["sm2", "sm4", "sm5", "sm6"], + true, + false + ); + + await testPatternAbsent("selectCombo", "Selection"); + + await testSelectionProps("ariaListbox", ["al1"], false, false); + await testSelectionProps("tablist", ["t2"], false, false); + // The IA2 -> UIA proxy doesn't expose the Selection pattern on grids. + if (gIsUiaEnabled) { + await testSelectionProps("grid", ["g2", "g4"], true, false); + } + + // radio gets the SelectionItem pattern, but radiogroup doesn't get the + // Selection pattern for now. Same for menu/menuitemradio. + await testPatternAbsent("radiogroup", "Selection"); + await testPatternAbsent("menu", "Selection"); + + await testPatternAbsent("button", "Selection"); +}); + +/** + * Test the SelectionItem pattern. + */ +addUiaTask(SNIPPET, async function testSelection() { + await definePyVar("doc", `getDocUia()`); + await testPatternAbsent("selectList", "SelectionItem"); + await testSelectionItemProps("sl1", true, "selectList"); + await testSelectionItemProps("sl2", false, "selectList"); + info("Calling Select on sl2"); + await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "sl2"); + await runPython(`pattern.Select()`); + await waitForUiaEvent(); + ok(true, "sl2 got ElementSelected event"); + await testSelectionItemProps("sl1", false, "selectList"); + await testSelectionItemProps("sl2", true, "selectList"); + + await testSelectionItemProps("sr1", false, "selectRequired"); + + await testSelectionItemProps("sm1", true, "selectMulti"); + await testSelectionItemProps("sm2", false, "selectMulti"); + info("Calling AddToSelection on sm2"); + await setUpWaitForUiaEvent("SelectionItem_ElementAddedToSelection", "sm2"); + await runPython(`pattern.AddToSelection()`); + await waitForUiaEvent(); + ok(true, "sm2 got ElementAddedToSelection event"); + await testSelectionItemProps("sm2", true, "selectMulti"); + await testSelectionItemProps("sm3", true, "selectMulti"); + info("Calling RemoveFromSelection on sm3"); + await setUpWaitForUiaEvent( + "SelectionItem_ElementRemovedFromSelection", + "sm3" + ); + await runPython(`pattern.RemoveFromSelection()`); + await waitForUiaEvent(); + ok(true, "sm3 got ElementRemovedFromSelection event"); + await testSelectionItemProps("sm3", false, "selectMulti"); + + await testSelectionItemProps("t1", false, "tablist"); + await testSelectionItemProps("t2", true, "tablist"); + + // The IA2 -> UIA proxy doesn't expose the SelectionItem pattern on grid + // cells. + if (gIsUiaEnabled) { + await testSelectionItemProps("g1", false, "grid"); + await testSelectionItemProps("g2", true, "grid"); + } + + await testSelectionItemProps("r1", true, null); + await testSelectionItemProps("r2", false, null); + // The IA2 -> UIA proxy doesn't fire correct events for radio buttons. + if (gIsUiaEnabled) { + info("Calling Select on r2"); + await setUpWaitForUiaEvent("SelectionItem_ElementSelected", "r2"); + await runPython(`pattern.Select()`); + await waitForUiaEvent(); + ok(true, "r2 got ElementSelected event"); + await testSelectionItemProps("r1", false, null); + await testSelectionItemProps("r2", true, null); + info("Calling RemoveFromSelection on r2"); + await testPythonRaises( + `pattern.RemoveFromSelection()`, + "RemoveFromSelection failed on r2" + ); + } + + await testPatternAbsent("m1", "SelectionItem"); + // The IA2 -> UIA proxy doesn't expose the SelectionItem pattern for radio + // menu items. + if (gIsUiaEnabled) { + await testSelectionItemProps("m2", false, null); + await testSelectionItemProps("m3", true, null); + } + + await testPatternAbsent("button", "SelectionItem"); +}); diff --git a/accessible/tests/browser/windows/uia/browser_simplePatterns.js b/accessible/tests/browser/windows/uia/browser_simplePatterns.js index f464db0e13..484d217af2 100644 --- a/accessible/tests/browser/windows/uia/browser_simplePatterns.js +++ b/accessible/tests/browser/windows/uia/browser_simplePatterns.js @@ -27,6 +27,7 @@ addUiaTask( <button id="button">button</button> <p id="p">p</p> <input id="checkbox" type="checkbox"> +<input id="radio" type="radio"> `, async function testInvoke() { await definePyVar("doc", `getDocUia()`); @@ -54,6 +55,8 @@ addUiaTask( // Check boxes expose the Toggle pattern, so they should not expose the // Invoke pattern. await testPatternAbsent("checkbox", "Invoke"); + // Ditto for radio buttons. + await testPatternAbsent("radio", "Invoke"); } } ); @@ -317,6 +320,7 @@ addUiaTask( await setUpWaitForUiaPropEvent("ValueValue", "text"); await runPython(`pattern.SetValue("after")`); await waitForUiaEvent(); + ok(true, "Got ValueValue prop change event on text"); is( await runPython(`pattern.CurrentValue`), "after", @@ -434,6 +438,7 @@ addUiaTask( await setUpWaitForUiaPropEvent("ValueValue", "ariaTextbox"); await runPython(`pattern.SetValue("after")`); await waitForUiaEvent(); + ok(true, "Got ValueValue prop change event on ariaTextbox"); is( await runPython(`pattern.CurrentValue`), "after", @@ -443,3 +448,95 @@ addUiaTask( await testPatternAbsent("button", "Value"); } ); + +async function testRangeValueProps(id, ro, val, min, max, small, large) { + await assignPyVarToUiaWithId(id); + await definePyVar("pattern", `getUiaPattern(${id}, "RangeValue")`); + ok(await runPython(`bool(pattern)`), `${id} has RangeValue pattern`); + is( + !!(await runPython(`pattern.CurrentIsReadOnly`)), + ro, + `${id} has IsReadOnly ${ro}` + ); + is(await runPython(`pattern.CurrentValue`), val, `${id} has correct Value`); + is( + await runPython(`pattern.CurrentMinimum`), + min, + `${id} has correct Minimum` + ); + is( + await runPython(`pattern.CurrentMaximum`), + max, + `${id} has correct Maximum` + ); + // IA2 doesn't support small/large change, so the IA2 -> UIA proxy can't + // either. + if (gIsUiaEnabled) { + is( + await runPython(`pattern.CurrentSmallChange`), + small, + `${id} has correct SmallChange` + ); + is( + await runPython(`pattern.CurrentLargeChange`), + large, + `${id} has correct LargeChange` + ); + } +} + +/** + * Test the RangeValue pattern. + */ +addUiaTask( + ` +<input id="range" type="range"> +<input id="rangeBig" type="range" max="1000"> +<progress id="progress" value="0.5"></progress> +<input id="numberRo" type="number" min="0" max="10" value="5" readonly> +<div id="ariaSlider" role="slider">slider</div> +<button id="button">button</button> + `, + async function testRangeValue(browser) { + await definePyVar("doc", `getDocUia()`); + await testRangeValueProps("range", false, 50, 0, 100, 1, 10); + info("SetValue on range"); + await setUpWaitForUiaPropEvent("RangeValueValue", "range"); + await runPython(`pattern.SetValue(20)`); + await waitForUiaEvent(); + ok(true, "Got RangeValueValue prop change event on range"); + is(await runPython(`pattern.CurrentValue`), 20, "range has correct Value"); + + await testRangeValueProps("rangeBig", false, 500, 0, 1000, 1, 100); + + // Gecko a11y doesn't expose progress bars as read only, but it probably + // should. + await testRangeValueProps("progress", false, 0.5, 0, 1, 0, 0.1); + info("Calling SetValue on progress"); + await testPythonRaises( + `pattern.SetValue(0.6)`, + "SetValue on progress failed" + ); + + await testRangeValueProps("numberRo", true, 5, 0, 10, 1, 1); + info("Calling SetValue on numberRo"); + await testPythonRaises( + `pattern.SetValue(6)`, + "SetValue on numberRo failed" + ); + + await testRangeValueProps("ariaSlider", false, 50, 0, 100, null, null); + info("Setting aria-valuenow on ariaSlider"); + await setUpWaitForUiaPropEvent("RangeValueValue", "ariaSlider"); + await invokeSetAttribute(browser, "ariaSlider", "aria-valuenow", "60"); + await waitForUiaEvent(); + ok(true, "Got RangeValueValue prop change event on ariaSlider"); + is( + await runPython(`pattern.CurrentValue`), + 60, + "ariaSlider has correct Value" + ); + + await testPatternAbsent("button", "RangeValue"); + } +); diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js index 5b453ce6fe..217b8cb844 100644 --- a/accessible/tests/browser/windows/uia/head.js +++ b/accessible/tests/browser/windows/uia/head.js @@ -4,7 +4,7 @@ "use strict"; -/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises */ +/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises, isUiaElementArray */ // Load the shared-head file first. Services.scriptloader.loadSubScript( @@ -126,3 +126,15 @@ async function testPythonRaises(expression, message) { } ok(failed, message); } + +/** + * Verify that an array of UIA elements contains (only) elements with the given + * DOM ids. + */ +async function isUiaElementArray(pyExpr, ids, message) { + const result = await runPython(` + uias = (${pyExpr}) + return [uias.GetElement(i).CurrentAutomationId for i in range(uias.Length)] + `); + SimpleTest.isDeeply(result, ids, message); +} |