diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-05-15 03:35:49 +0000 |
commit | d8bbc7858622b6d9c278469aab701ca0b609cddf (patch) | |
tree | eff41dc61d9f714852212739e6b3738b82a2af87 /accessible/tests/browser | |
parent | Releasing progress-linux version 125.0.3-1~progress7.99u1. (diff) | |
download | firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.tar.xz firefox-d8bbc7858622b6d9c278469aab701ca0b609cddf.zip |
Merging upstream version 126.0.
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'accessible/tests/browser')
27 files changed, 977 insertions, 100 deletions
diff --git a/accessible/tests/browser/.eslintrc.js b/accessible/tests/browser/.eslintrc.js index 528797cb91..96c0859468 100644 --- a/accessible/tests/browser/.eslintrc.js +++ b/accessible/tests/browser/.eslintrc.js @@ -18,7 +18,7 @@ module.exports = { "no-proto": "error", "no-return-assign": "error", "no-shadow": "error", - "no-unused-vars": ["error", { vars: "all", args: "none" }], + "no-unused-vars": ["error", { vars: "all", argsIgnorePattern: "^_" }], "one-var": ["error", "never"], radix: "error", strict: ["error", "global"], diff --git a/accessible/tests/browser/bounds/browser.toml b/accessible/tests/browser/bounds/browser.toml index da1fe37a1e..c4c421cdeb 100644 --- a/accessible/tests/browser/bounds/browser.toml +++ b/accessible/tests/browser/bounds/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", "!/accessible/tests/mochitest/letters.gif", ] diff --git a/accessible/tests/browser/e10s/browser.toml b/accessible/tests/browser/e10s/browser.toml index 914f839993..dff9b1c712 100644 --- a/accessible/tests/browser/e10s/browser.toml +++ b/accessible/tests/browser/e10s/browser.toml @@ -10,7 +10,6 @@ support-files = [ "doc_treeupdate_whitespace.html", "fonts/Ahem.sjs", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", "!/accessible/tests/mochitest/events/slow_image.sjs", "!/accessible/tests/mochitest/letters.gif", diff --git a/accessible/tests/browser/e10s/browser_aria_activedescendant.js b/accessible/tests/browser/e10s/browser_aria_activedescendant.js index f58c5aab39..fadda0f964 100644 --- a/accessible/tests/browser/e10s/browser_aria_activedescendant.js +++ b/accessible/tests/browser/e10s/browser_aria_activedescendant.js @@ -277,7 +277,7 @@ async function basicListboxTest(browser, elementReflection) { addAccessibleTask( LISTBOX_MARKUP, - async function (browser, docAcc) { + async function (browser) { info("Test aria-activedescendant content attribute"); await basicListboxTest(browser, false); @@ -303,7 +303,7 @@ addAccessibleTask( addAccessibleTask( LISTBOX_MARKUP, - async function (browser, docAcc) { + async function (browser) { info("Test ariaActiveDescendantElement element reflection"); await basicListboxTest(browser, true); }, @@ -316,7 +316,7 @@ addAccessibleTask( <div role="listbox"> <div role="option" id="activedesc_nondesc_option">option</div> </div>`, - async function (browser, docAcc) { + async function (browser) { info("Test aria-activedescendant non-descendant"); await synthFocus( browser, @@ -348,7 +348,7 @@ addAccessibleTask( item.setAttribute("role", "option"); listbox.appendChild(item); </script>`, - async function (browser, docAcc) { + async function (browser) { info("Test aria-activedescendant in shadow root"); // We want to retrieve elements using their IDs inside the shadow root, so // we define a custom get element by ID method that our utility functions @@ -448,7 +448,7 @@ customElements.define("custom-listbox", } ); </script>`, - async function (browser, docAcc) { + async function (browser) { await synthFocus(browser, "custom-listbox1", "l1_3"); let evtProm = Promise.all([ diff --git a/accessible/tests/browser/e10s/browser_caching_value.js b/accessible/tests/browser/e10s/browser_caching_value.js index 2b968b5948..926110199e 100644 --- a/accessible/tests/browser/e10s/browser_caching_value.js +++ b/accessible/tests/browser/e10s/browser_caching_value.js @@ -46,16 +46,6 @@ const valueTests = [ expected: ["5", 5, 0, 7, 0], }, { - desc: "Value should change when currentValue is called", - id: "slider", - async action(browser, acc) { - acc.QueryInterface(nsIAccessibleValue); - acc.currentValue = 4; - }, - waitFor: EVENT_VALUE_CHANGE, - expected: ["4", 4, 0, 7, 0], - }, - { desc: "Value should change when @aria-valuenow is updated", id: "slider", attrs: [ diff --git a/accessible/tests/browser/events/browser.toml b/accessible/tests/browser/events/browser.toml index 7ec3c3621a..ffbb96cfbb 100644 --- a/accessible/tests/browser/events/browser.toml +++ b/accessible/tests/browser/events/browser.toml @@ -4,12 +4,13 @@ support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", "!/accessible/tests/mochitest/*.js", - "!/accessible/tests/browser/*.jsm", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] ["browser_alert.js"] +["browser_content_prompt.js"] + ["browser_test_A11yUtils_announce.js"] ["browser_test_caret_move_granularity.js"] diff --git a/accessible/tests/browser/events/browser_content_prompt.js b/accessible/tests/browser/events/browser_content_prompt.js new file mode 100644 index 0000000000..7677c0258a --- /dev/null +++ b/accessible/tests/browser/events/browser_content_prompt.js @@ -0,0 +1,48 @@ +/* Any copyright is dedicated to the Public Domain. + https://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Want to test relations. +/* import-globals-from ../../mochitest/name.js */ +/* import-globals-from ../../mochitest/relations.js */ +/* import-globals-from ../../mochitest/role.js */ +loadScripts( + { name: "relations.js", dir: MOCHITESTS_DIR }, + { name: "name.js", dir: MOCHITESTS_DIR }, + { name: "role.js", dir: MOCHITESTS_DIR } +); + +addAccessibleTask(``, async function (browser) { + info("Showing alert"); + let shown = waitForEvent( + EVENT_SHOW, + evt => evt.accessible.role == ROLE_INTERNAL_FRAME + ); + // Let's make sure the dialog content gets focus. + // On macOS, we unfortunately focus the label. We focus the OK button on + // all other platforms. See https://phabricator.services.mozilla.com/D204908 + // for more discussion. + let expectedRole = + AppConstants.platform == "macosx" ? ROLE_LABEL : ROLE_PUSHBUTTON; + let focused = waitForEvent(EVENT_FOCUS, evt => { + return evt.accessible.role == expectedRole; + }); + await invokeContentTask(browser, [], () => { + // Use setTimeout to avoid blocking the return of the content task + // on the alert, which is otherwise synchronous. + content.setTimeout(() => content.alert("test"), 0); + }); + const frame = (await shown).accessible; + const focusedEl = (await focused).accessible; + ok(true, "Dialog shown and something got focused"); + let dialog = getAccessible(focusedEl.DOMNode.ownerDocument); + testRole(dialog, ROLE_DIALOG); + let infoBody = focusedEl.DOMNode.ownerDocument.getElementById("infoBody"); + testRelation(dialog, RELATION_DESCRIBED_BY, infoBody); + testDescr(dialog, "test "); + info("Dismissing alert"); + let hidden = waitForEvent(EVENT_HIDE, frame); + EventUtils.synthesizeKey("KEY_Escape", {}, frame.DOMNode.contentWindow); + await hidden; +}); diff --git a/accessible/tests/browser/fission/browser.toml b/accessible/tests/browser/fission/browser.toml index 0332573db9..f795ec6b3d 100644 --- a/accessible/tests/browser/fission/browser.toml +++ b/accessible/tests/browser/fission/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/hittest/browser.toml b/accessible/tests/browser/hittest/browser.toml index 7f82a6ff49..894bf05c39 100644 --- a/accessible/tests/browser/hittest/browser.toml +++ b/accessible/tests/browser/hittest/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", "!/accessible/tests/mochitest/letters.gif", ] diff --git a/accessible/tests/browser/mac/browser.toml b/accessible/tests/browser/mac/browser.toml index b180a42ab7..227575fb4d 100644 --- a/accessible/tests/browser/mac/browser.toml +++ b/accessible/tests/browser/mac/browser.toml @@ -9,7 +9,6 @@ support-files = [ "doc_menulist.xhtml", "doc_tree.xhtml", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", "!/accessible/tests/mochitest/letters.gif", "!/accessible/tests/mochitest/moz.png", @@ -29,6 +28,8 @@ https_first_disabled = true ["browser_aria_haspopup.js"] +["browser_aria_placeholder.js"] + ["browser_aria_setsize.js"] ["browser_attributed_text.js"] diff --git a/accessible/tests/browser/mac/browser_aria_placeholder.js b/accessible/tests/browser/mac/browser_aria_placeholder.js new file mode 100644 index 0000000000..e5102fd093 --- /dev/null +++ b/accessible/tests/browser/mac/browser_aria_placeholder.js @@ -0,0 +1,70 @@ +/* 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"; + +/** + * Test that inputs with placeholder text expose it via AXPlaceholderValue and + * correctly _avoid_ exposing it via AXTItle, AXValue, or AXDescription. Verify AXPlaceholderValue is not exposed when the placeholder is used as the name. + */ +addAccessibleTask( + ` + <input id="input" placeholder="Name"><br> + <label for="input2">Enter Name</label><input id="input2" placeholder="Name" value="Elmer Fudd"> + `, + (browser, accDoc) => { + let input = getNativeInterface(accDoc, "input"); + let input2 = getNativeInterface(accDoc, "input2"); + + is( + input.getAttributeValue("AXPlaceholderValue"), + null, + "Placeholder is used as name, so no AXPlaceholderValue is stored." + ); + is(input.getAttributeValue("AXDescription"), "Name"); + is(input.getAttributeValue("AXTitle"), "", "Correct title"); + is(input.getAttributeValue("AXValue"), "", "Correct value"); + + is( + input2.getAttributeValue("AXPlaceholderValue"), + "Name", + "Correct placeholder value in presence of value" + ); + is( + input2.getAttributeValue("AXDescription"), + "Enter Name", + "Correct label" + ); + is(input2.getAttributeValue("AXTitle"), "", "Correct title"); + is(input2.getAttributeValue("AXValue"), "Elmer Fudd", "Correct value"); + } +); + +/** + * Test that aria-placeholder gets exposed via AXPlaceholderValue and correctly + * contributes to AXValue (but not title or description). + */ +addAccessibleTask( + ` + <span id="date-of-birth">Birthday</span> + <div + id="bday" + contenteditable + role="textbox" + aria-labelledby="date-of-birth" + aria-placeholder="MM-DD-YYYY">MM-DD-YYYY</div> + `, + (browser, accDoc) => { + let bday = getNativeInterface(accDoc, "bday"); + + is( + bday.getAttributeValue("AXPlaceholderValue"), + "MM-DD-YYYY", + "Correct placeholder value" + ); + is(bday.getAttributeValue("AXDescription"), "Birthday", "Correct label"); + is(bday.getAttributeValue("AXTitle"), "", "Correct title"); + is(bday.getAttributeValue("AXValue"), "MM-DD-YYYY", "Correct value"); + } +); diff --git a/accessible/tests/browser/mac/browser_roles_elements.js b/accessible/tests/browser/mac/browser_roles_elements.js index b6049e7afd..65caf308c4 100644 --- a/accessible/tests/browser/mac/browser_roles_elements.js +++ b/accessible/tests/browser/mac/browser_roles_elements.js @@ -73,8 +73,8 @@ addAccessibleTask( <div id="switch" role="switch"></div> <div id="timer" role="timer"></div> <div id="tooltip" role="tooltip"></div> - <input type="radio" role="menuitemradio" id="menuitemradio"> - <input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox"> + <div role="menu"><input type="radio" role="menuitemradio" id="menuitemradio"></div> + <div role="menu"><input type="checkbox" role="menuitemcheckbox" id="menuitemcheckbox"></div> <input type="datetime-local" id="datetime"> <!-- text entries --> diff --git a/accessible/tests/browser/mac/browser_selectables.js b/accessible/tests/browser/mac/browser_selectables.js index af88c2e136..cc489e566d 100644 --- a/accessible/tests/browser/mac/browser_selectables.js +++ b/accessible/tests/browser/mac/browser_selectables.js @@ -300,9 +300,6 @@ addAccessibleTask( async (browser, accDoc) => { let select = getNativeInterface(accDoc, "select"); let one = getNativeInterface(accDoc, "one"); - let two = getNativeInterface(accDoc, "two"); - let three = getNativeInterface(accDoc, "three"); - let four = getNativeInterface(accDoc, "four"); is( select.getAttributeValue("AXTitle"), @@ -339,25 +336,5 @@ addAccessibleTask( }); await evt; is(select.getAttributeValue("AXSelectedChildren").length, 0); - evt = waitForMacEvent("AXSelectedChildrenChanged"); - three.setAttributeValue("AXSelected", true); - await evt; - is(select.getAttributeValue("AXSelectedChildren").length, 1); - ok(getSelectedIds(select).includes("three"), "'three' is selected"); - evt = waitForMacEvent("AXSelectedChildrenChanged"); - select.setAttributeValue("AXSelectedChildren", [one, two]); - await evt; - await untilCacheOk(() => { - let ids = getSelectedIds(select); - return ids[0] == "one" && ids[1] == "two"; - }, "Got correct selected children"); - - evt = waitForMacEvent("AXSelectedChildrenChanged"); - select.setAttributeValue("AXSelectedChildren", [three, two, four]); - await evt; - await untilCacheOk(() => { - let ids = getSelectedIds(select); - return ids[0] == "two" && ids[1] == "three"; - }, "Got correct selected children"); } ); diff --git a/accessible/tests/browser/role/browser_computedARIARole.js b/accessible/tests/browser/role/browser_computedARIARole.js index 50cfe43c98..a87d0a0eae 100644 --- a/accessible/tests/browser/role/browser_computedARIARole.js +++ b/accessible/tests/browser/role/browser_computedARIARole.js @@ -14,7 +14,9 @@ addAccessibleTask( <div id="ariaDirectory" role="directory">ARIA directory</div> <div id="ariaAlertdialog" role="alertdialog">ARIA alertdialog</div> <div id="ariaFeed" role="feed">ARIA feed</div> -<div id="ariaRowgroup" role="rowgroup">ARIA rowgroup</div> +<div role="table"> + <div id="ariaRowgroup" role="rowgroup">ARIA rowgroup</div> +</div> <div id="ariaSearchbox" role="searchbox">ARIA searchbox</div> <div id="ariaUnknown" role="unknown">unknown ARIA role</div> <button id="htmlButton">HTML button</button> diff --git a/accessible/tests/browser/scroll/browser.toml b/accessible/tests/browser/scroll/browser.toml index ef637fe9a5..390e07db8e 100644 --- a/accessible/tests/browser/scroll/browser.toml +++ b/accessible/tests/browser/scroll/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/selectable/browser.toml b/accessible/tests/browser/selectable/browser.toml index 38a13c25f4..144555b6a0 100644 --- a/accessible/tests/browser/selectable/browser.toml +++ b/accessible/tests/browser/selectable/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/selectable/browser_test_aria_select.js b/accessible/tests/browser/selectable/browser_test_aria_select.js index f52603d1cb..dbc36956f8 100644 --- a/accessible/tests/browser/selectable/browser_test_aria_select.js +++ b/accessible/tests/browser/selectable/browser_test_aria_select.js @@ -72,16 +72,16 @@ addAccessibleTask( // role="tablist" aria-multiselectable addAccessibleTask( `<div role="tablist" id="tablist" aria-multiselectable="true"> - <div role="tab" id="tab_multi1">tab1</div> + <div role="tab" id="tab_multi1" aria-selected="true">tab1</div> <div role="tab" id="tab_multi2">tab2</div> + <div role="tab" id="tab_multi3" aria-selected="true">tab3</div> </div>`, async function (browser, docAcc) { info('role="tablist" aria-multiselectable'); let tablist = findAccessibleChildByID(docAcc, "tablist", [ nsIAccessibleSelectable, ]); - - await testMultiSelectable(tablist, ["tab_multi1", "tab_multi2"]); + testSelectableSelection(tablist, ["tab_multi1", "tab_multi3"]); }, { chrome: true, @@ -95,16 +95,16 @@ addAccessibleTask( // role="listbox" aria-multiselectable addAccessibleTask( `<div role="listbox" id="listbox" aria-multiselectable="true"> - <div role="option" id="listbox2_item1">item1</div> + <div role="option" id="listbox2_item1" aria-selected="true">item1</div> <div role="option" id="listbox2_item2">item2</div> + <div role="option" id="listbox2_item3" aria-selected="true">item2</div> </div>`, async function (browser, docAcc) { info('role="listbox" aria-multiselectable'); let listbox = findAccessibleChildByID(docAcc, "listbox", [ nsIAccessibleSelectable, ]); - - await testMultiSelectable(listbox, ["listbox2_item1", "listbox2_item2"]); + testSelectableSelection(listbox, ["listbox2_item1", "listbox2_item3"]); }, { chrome: true, @@ -122,7 +122,7 @@ addAccessibleTask( <thead> <tr> <th tabindex="-1" role="columnheader" id="grid_colhead1" - style="width:6em">Entry #</th> + style="width:6em" aria-selected="true">Entry #</th> <th tabindex="-1" role="columnheader" id="grid_colhead2" style="width:10em">Date</th> <th tabindex="-1" role="columnheader" id="grid_colhead3" @@ -134,7 +134,7 @@ addAccessibleTask( <td tabindex="-1" role="rowheader" id="grid_rowhead" aria-readonly="true">1</td> <td tabindex="-1" role="gridcell" id="grid_cell1" - aria-selected="false">03/14/05</td> + aria-selected="true">03/14/05</td> <td tabindex="-1" role="gridcell" id="grid_cell2" aria-selected="false">Conference Fee</td> </tr> @@ -145,15 +145,7 @@ addAccessibleTask( let grid = findAccessibleChildByID(docAcc, "grid", [ nsIAccessibleSelectable, ]); - - await testMultiSelectable(grid, [ - "grid_colhead1", - "grid_colhead2", - "grid_colhead3", - "grid_rowhead", - "grid_cell1", - "grid_cell2", - ]); + testSelectableSelection(grid, ["grid_colhead1", "grid_cell1"]); }, { chrome: true, diff --git a/accessible/tests/browser/states/browser.toml b/accessible/tests/browser/states/browser.toml index fe29597d27..6b2d20ae14 100644 --- a/accessible/tests/browser/states/browser.toml +++ b/accessible/tests/browser/states/browser.toml @@ -4,7 +4,6 @@ support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", "!/accessible/tests/mochitest/*.js", - "!/accessible/tests/browser/*.jsm", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/text/browser.toml b/accessible/tests/browser/text/browser.toml index c04531b126..4bc64866fc 100644 --- a/accessible/tests/browser/text/browser.toml +++ b/accessible/tests/browser/text/browser.toml @@ -3,7 +3,6 @@ subsuite = "a11y" support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", - "!/accessible/tests/browser/*.jsm", "!/accessible/tests/mochitest/*.js", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/tree/browser.toml b/accessible/tests/browser/tree/browser.toml index 64be6853d1..a7d9894fcb 100644 --- a/accessible/tests/browser/tree/browser.toml +++ b/accessible/tests/browser/tree/browser.toml @@ -4,7 +4,6 @@ support-files = [ "head.js", "!/accessible/tests/browser/shared-head.js", "!/accessible/tests/mochitest/*.js", - "!/accessible/tests/browser/*.jsm", ] prefs = ["javascript.options.asyncstack_capture_debuggee_only=false"] diff --git a/accessible/tests/browser/windows/a11y_setup.py b/accessible/tests/browser/windows/a11y_setup.py index 726eea07a4..860364d99b 100644 --- a/accessible/tests/browser/windows/a11y_setup.py +++ b/accessible/tests/browser/windows/a11y_setup.py @@ -61,9 +61,6 @@ uiaClient = comtypes.CoCreateInstance( interface=uiaMod.IUIAutomation, clsctx=comtypes.CLSCTX_INPROC_SERVER, ) -TreeScope_Descendants = uiaMod.TreeScope_Descendants -UIA_AutomationIdPropertyId = uiaMod.UIA_AutomationIdPropertyId -del uiaMod def AccessibleObjectFromWindow(hwnd, objectID=OBJID_CLIENT): @@ -173,7 +170,7 @@ class WaitForWinEvent: """ def __init__(self, eventId, match): - """event is the event id to wait for. + """eventId is the event id to wait for. match is either None to match any object, an str containing the DOM id of the desired object, or a function taking a WinEvent which should return True if this is the requested event. @@ -235,6 +232,7 @@ class WaitForWinEvent: raise finally: user32.UnhookWinEvent(self._hook) + ctypes.windll.kernel32.CloseHandle(self._signal) self._proc = None if isinstance(self._matched, Exception): raise self._matched from self._matched @@ -243,17 +241,153 @@ class WaitForWinEvent: def getDocUia(): """Get the IUIAutomationElement for the document being tested.""" - # We start with IAccessible2 because there's no efficient way to - # find the document we want with UIA. - ia2 = getDocIa2() - return uiaClient.ElementFromIAccessible(ia2, CHILDID_SELF) + # There's no efficient way to find the document we want with UIA. We can't + # get the IA2 and then get UIA from that because that will always use the + # IA2 -> UIA proxy, but we don't want that if we're trying to test our + # native implementation. For now, we just search the tree. In future, we + # could perhaps implement a custom property. + hwnd = getFirefoxHwnd() + root = uiaClient.ElementFromHandle(hwnd) + doc = findUiaByDomId(root, "body") + if not doc: + # Sometimes, when UIA is disabled, we can't find the document for some + # unknown reason. Since this only happens when UIA is disabled, we want + # the IA2 -> UIA proxy anyway, so we can start with IA2 in this case. + info("getUiaDoc: Falling back to IA2") # noqa: F821 + ia2 = getDocIa2() + return uiaClient.ElementFromIAccessible(ia2, CHILDID_SELF) + child = uiaClient.RawViewWalker.GetFirstChildElement(doc) + if child and child.CurrentAutomationId == "default-iframe-id": + # This is an iframe or remoteIframe test. + doc = uiaClient.RawViewWalker.GetFirstChildElement(child) + return doc def findUiaByDomId(root, id): - cond = uiaClient.CreatePropertyCondition(UIA_AutomationIdPropertyId, id) + cond = uiaClient.CreatePropertyCondition(uiaMod.UIA_AutomationIdPropertyId, id) # FindFirst ignores elements in the raw tree, so we have to use # FindFirstBuildCache to override that, even though we don't want to cache # anything. request = uiaClient.CreateCacheRequest() request.TreeFilter = uiaClient.RawViewCondition - return root.FindFirstBuildCache(TreeScope_Descendants, cond, request) + el = root.FindFirstBuildCache(uiaMod.TreeScope_Descendants, cond, request) + if not el: + return None + # We need to test things that were introduced after UIA was initially + # introduced in Windows 7. + return el.QueryInterface(uiaMod.IUIAutomationElement9) + + +class WaitForUiaEvent(comtypes.COMObject): + """Wait for a UIA event. + This should be used as follows: + 1. Create an instance to wait for the desired event. + 2. Perform the action that should fire the event. + 3. Call wait() on the instance you created in 1) to wait for the event. + """ + + # This tells comtypes which COM interfaces we implement. It will then call + # either `ISomeInterface_SomeMethod` or just `SomeMethod` on this instance + # when that method is called using COM. We use the shorter convention, since + # we don't anticipate method name conflicts with UIA interfaces. + _com_interfaces_ = [ + uiaMod.IUIAutomationFocusChangedEventHandler, + uiaMod.IUIAutomationPropertyChangedEventHandler, + uiaMod.IUIAutomationEventHandler, + ] + + def __init__(self, *, eventId=None, property=None, match=None): + """eventId is the event id to wait for. Alternatively, you can pass + property to wait for a particular property to change. + match is either None to match any object, an str containing the DOM id + of the desired object, or a function taking a IUIAutomationElement which + should return True if this is the requested event. + """ + self._match = match + self._matched = None + # A kernel event used to signal when we get the desired event. + self._signal = ctypes.windll.kernel32.CreateEventW(None, True, False, None) + if eventId == uiaMod.UIA_AutomationFocusChangedEventId: + uiaClient.AddFocusChangedEventHandler(None, self) + elif eventId: + # Generic automation event. + uiaClient.AddAutomationEventHandler( + eventId, + uiaClient.GetRootElement(), + uiaMod.TreeScope_Subtree, + None, + self, + ) + elif property: + uiaClient.AddPropertyChangedEventHandler( + uiaClient.GetRootElement(), + uiaMod.TreeScope_Subtree, + None, + self, + [property], + ) + else: + raise ValueError("No supported event specified") + + def _checkMatch(self, sender): + if isinstance(self._match, str): + try: + if sender.CurrentAutomationId == self._match: + self._matched = sender + except comtypes.COMError: + pass + elif callable(self._match): + try: + if self._match(sender): + self._matched = sender + except Exception as e: + self._matched = e + else: + self._matched = sender + if self._matched: + ctypes.windll.kernel32.SetEvent(self._signal) + + def HandleFocusChangedEvent(self, sender): + self._checkMatch(sender) + + def HandlePropertyChangedEvent(self, sender, propertyId, newValue): + self._checkMatch(sender) + + def HandleAutomationEvent(self, sender, eventId): + self._checkMatch(sender) + + def wait(self): + """Wait for and return the IUIAutomationElement which sent the desired + event.""" + # Pump Windows messages until we get the desired event, which will be + # signalled using a kernel event. + handles = (ctypes.c_void_p * 1)(self._signal) + index = ctypes.wintypes.DWORD() + TIMEOUT = 10000 + try: + ctypes.oledll.ole32.CoWaitForMultipleHandles( + COWAIT_DEFAULT, TIMEOUT, 1, handles, ctypes.byref(index) + ) + except WindowsError as e: + if e.winerror == RPC_S_CALLPENDING: + raise TimeoutError("Timeout before desired event received") + raise + finally: + uiaClient.RemoveAllEventHandlers() + ctypes.windll.kernel32.CloseHandle(self._signal) + if isinstance(self._matched, Exception): + raise self._matched from self._matched + return self._matched + + +def getUiaPattern(element, patternName): + """Get a control pattern interface from an IUIAutomationElement.""" + patternId = getattr(uiaMod, f"UIA_{patternName}PatternId") + unknown = element.GetCurrentPattern(patternId) + if not unknown: + return None + # GetCurrentPattern returns an IUnknown. We have to QI to the real + # interface. + # Get the comtypes interface object. + interface = getattr(uiaMod, f"IUIAutomation{patternName}Pattern") + return unknown.QueryInterface(interface) diff --git a/accessible/tests/browser/windows/uia/browser.toml b/accessible/tests/browser/windows/uia/browser.toml index d1513c1822..75728f56d7 100644 --- a/accessible/tests/browser/windows/uia/browser.toml +++ b/accessible/tests/browser/windows/uia/browser.toml @@ -10,4 +10,9 @@ support-files = ["head.js"] ["browser_elementFromPoint.js"] +["browser_focus.js"] +["browser_generalProps.js"] + +["browser_simplePatterns.js"] + ["browser_tree.js"] diff --git a/accessible/tests/browser/windows/uia/browser_focus.js b/accessible/tests/browser/windows/uia/browser_focus.js new file mode 100644 index 0000000000..f26c9e1d1b --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_focus.js @@ -0,0 +1,61 @@ +/* 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"; + +async function testIsFocusable(pyVar, isFocusable) { + const result = await runPython(`${pyVar}.CurrentIsKeyboardFocusable`); + if (isFocusable) { + ok(result, `${pyVar} is focusable`); + } else { + ok(!result, `${pyVar} isn't focusable`); + } +} + +async function testHasFocus(pyVar, hasFocus) { + const result = await runPython(`${pyVar}.CurrentHasKeyboardFocus`); + if (hasFocus) { + ok(result, `${pyVar} has focus`); + } else { + ok(!result, `${pyVar} doesn't have focus`); + } +} + +addUiaTask( + ` +<button id="button1">button1</button> +<p id="p">p</p> +<button id="button2">button2</button> + `, + async function (browser) { + await definePyVar("doc", `getDocUia()`); + await testIsFocusable("doc", true); + await testHasFocus("doc", true); + + await assignPyVarToUiaWithId("button1"); + await testIsFocusable("button1", true); + await testHasFocus("button1", false); + info("Focusing button1"); + await setUpWaitForUiaEvent("AutomationFocusChanged", "button1"); + await invokeFocus(browser, "button1"); + await waitForUiaEvent(); + ok(true, "Got AutomationFocusChanged event on button1"); + await testHasFocus("button1", true); + + await assignPyVarToUiaWithId("p"); + await testIsFocusable("p", false); + await testHasFocus("p", false); + + await assignPyVarToUiaWithId("button2"); + await testIsFocusable("button2", true); + await testHasFocus("button2", false); + info("Focusing button2"); + await setUpWaitForUiaEvent("AutomationFocusChanged", "button2"); + await invokeFocus(browser, "button2"); + await waitForUiaEvent(); + ok(true, "Got AutomationFocusChanged event on button2"); + await testHasFocus("button2", true); + await testHasFocus("button1", false); + } +); diff --git a/accessible/tests/browser/windows/uia/browser_generalProps.js b/accessible/tests/browser/windows/uia/browser_generalProps.js new file mode 100644 index 0000000000..5cfda226d0 --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_generalProps.js @@ -0,0 +1,105 @@ +/* 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"; + +/** + * Test the Name property. + */ +addUiaTask( + ` +<button id="button">before</button> +<div id="div">div</div> + `, + async function testName(browser) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentName`), + "before", + "button has correct name" + ); + await assignPyVarToUiaWithId("div"); + is(await runPython(`div.CurrentName`), "", "div has no name"); + + info("Setting aria-label on button"); + await setUpWaitForUiaPropEvent("Name", "button"); + await invokeSetAttribute(browser, "button", "aria-label", "after"); + await waitForUiaEvent(); + ok(true, "Got Name prop change event on button"); + is( + await runPython(`button.CurrentName`), + "after", + "button has correct name" + ); + } +); + +/** + * Test the FullDescription property. + */ +addUiaTask( + ` +<button id="button" aria-description="before">button</button> +<div id="div">div</div> + `, + async function testFullDescription(browser) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("button"); + is( + await runPython(`button.CurrentFullDescription`), + "before", + "button has correct FullDescription" + ); + await assignPyVarToUiaWithId("div"); + is( + await runPython(`div.CurrentFullDescription`), + "", + "div has no FullDescription" + ); + + info("Setting aria-description on button"); + await setUpWaitForUiaPropEvent("FullDescription", "button"); + await invokeSetAttribute(browser, "button", "aria-description", "after"); + await waitForUiaEvent(); + ok(true, "Got FullDescription prop change event on button"); + is( + await runPython(`button.CurrentFullDescription`), + "after", + "button has correct FullDescription" + ); + }, + // The IA2 -> UIA proxy doesn't support FullDescription. + { uiaEnabled: true, uiaDisabled: false } +); + +/** + * Test the IsEnabled property. + */ +addUiaTask( + ` +<button id="button">button</button> +<p id="p">p</p> + `, + async function testIsEnabled(browser) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("button"); + ok(await runPython(`button.CurrentIsEnabled`), "button has IsEnabled true"); + // The IA2 -> UIA proxy doesn't fire IsEnabled prop change events. + if (gIsUiaEnabled) { + info("Setting disabled on button"); + await setUpWaitForUiaPropEvent("IsEnabled", "button"); + await invokeSetAttribute(browser, "button", "disabled", true); + await waitForUiaEvent(); + ok(true, "Got IsEnabled prop change event on button"); + ok( + !(await runPython(`button.CurrentIsEnabled`)), + "button has IsEnabled false" + ); + } + + await assignPyVarToUiaWithId("p"); + ok(await runPython(`p.CurrentIsEnabled`), "p has IsEnabled true"); + } +); diff --git a/accessible/tests/browser/windows/uia/browser_simplePatterns.js b/accessible/tests/browser/windows/uia/browser_simplePatterns.js new file mode 100644 index 0000000000..f464db0e13 --- /dev/null +++ b/accessible/tests/browser/windows/uia/browser_simplePatterns.js @@ -0,0 +1,445 @@ +/* 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 */ +/* import-globals-from ../../../mochitest/states.js */ +loadScripts( + { name: "role.js", dir: MOCHITESTS_DIR }, + { name: "states.js", dir: MOCHITESTS_DIR } +); + +/* eslint-disable camelcase */ +const ExpandCollapseState_Collapsed = 0; +const ExpandCollapseState_Expanded = 1; +const ToggleState_Off = 0; +const ToggleState_On = 1; +const ToggleState_Indeterminate = 2; +/* eslint-enable camelcase */ + +/** + * Test the Invoke pattern. + */ +addUiaTask( + ` +<button id="button">button</button> +<p id="p">p</p> +<input id="checkbox" type="checkbox"> + `, + async function testInvoke() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("button"); + await definePyVar("pattern", `getUiaPattern(button, "Invoke")`); + ok(await runPython(`bool(pattern)`), "button has Invoke pattern"); + info("Calling Invoke on button"); + // The button will get focus when it is clicked. + let focused = waitForEvent(EVENT_FOCUS, "button"); + // The UIA -> IA2 proxy doesn't fire the Invoked event. + if (gIsUiaEnabled) { + await setUpWaitForUiaEvent("Invoke_Invoked", "button"); + } + await runPython(`pattern.Invoke()`); + await focused; + ok(true, "button got focus"); + if (gIsUiaEnabled) { + await waitForUiaEvent(); + ok(true, "button got Invoked event"); + } + + await testPatternAbsent("p", "Invoke"); + // The Microsoft IA2 -> UIA proxy doesn't follow Microsoft's own rules. + if (gIsUiaEnabled) { + // Check boxes expose the Toggle pattern, so they should not expose the + // Invoke pattern. + await testPatternAbsent("checkbox", "Invoke"); + } + } +); + +/** + * Test the Toggle pattern. + */ +addUiaTask( + ` +<input id="checkbox" type="checkbox" checked> +<button id="toggleButton" aria-pressed="false">toggle</button> +<button id="button">button</button> +<p id="p">p</p> + +<script> + // When checkbox is clicked and it is not checked, make it indeterminate. + document.getElementById("checkbox").addEventListener("click", evt => { + // Within the event listener, .checked is reversed and you can't set + // .indeterminate. Work around this by deferring and handling the changes + // ourselves. + evt.preventDefault(); + const target = evt.target; + setTimeout(() => { + if (target.checked) { + target.checked = false; + } else { + target.indeterminate = true; + } + }, 0); + }); + + // When toggleButton is clicked, set aria-pressed to true. + document.getElementById("toggleButton").addEventListener("click", evt => { + evt.target.ariaPressed = "true"; + }); +</script> + `, + async function testToggle() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("checkbox"); + await definePyVar("pattern", `getUiaPattern(checkbox, "Toggle")`); + ok(await runPython(`bool(pattern)`), "checkbox has Toggle pattern"); + is( + await runPython(`pattern.CurrentToggleState`), + ToggleState_On, + "checkbox has ToggleState_On" + ); + // The IA2 -> UIA proxy doesn't fire ToggleState prop change events. + if (gIsUiaEnabled) { + info("Calling Toggle on checkbox"); + await setUpWaitForUiaPropEvent("ToggleToggleState", "checkbox"); + await runPython(`pattern.Toggle()`); + await waitForUiaEvent(); + ok(true, "Got ToggleState prop change event on checkbox"); + is( + await runPython(`pattern.CurrentToggleState`), + ToggleState_Off, + "checkbox has ToggleState_Off" + ); + info("Calling Toggle on checkbox"); + await setUpWaitForUiaPropEvent("ToggleToggleState", "checkbox"); + await runPython(`pattern.Toggle()`); + await waitForUiaEvent(); + ok(true, "Got ToggleState prop change event on checkbox"); + is( + await runPython(`pattern.CurrentToggleState`), + ToggleState_Indeterminate, + "checkbox has ToggleState_Indeterminate" + ); + } + + await assignPyVarToUiaWithId("toggleButton"); + await definePyVar("pattern", `getUiaPattern(toggleButton, "Toggle")`); + ok(await runPython(`bool(pattern)`), "toggleButton has Toggle pattern"); + is( + await runPython(`pattern.CurrentToggleState`), + ToggleState_Off, + "toggleButton has ToggleState_Off" + ); + if (gIsUiaEnabled) { + info("Calling Toggle on toggleButton"); + await setUpWaitForUiaPropEvent("ToggleToggleState", "toggleButton"); + await runPython(`pattern.Toggle()`); + await waitForUiaEvent(); + ok(true, "Got ToggleState prop change event on toggleButton"); + is( + await runPython(`pattern.CurrentToggleState`), + ToggleState_On, + "toggleButton has ToggleState_Off" + ); + } + + await testPatternAbsent("button", "Toggle"); + await testPatternAbsent("p", "Toggle"); + } +); + +/** + * Test the ExpandCollapse pattern. + */ +addUiaTask( + ` +<details> + <summary id="summary">summary</summary> + details +</details> +<button id="popup" aria-haspopup="true">popup</button> +<button id="button">button</button> +<script> + // When popup is clicked, set aria-expanded to true. + document.getElementById("popup").addEventListener("click", evt => { + evt.target.ariaExpanded = "true"; + }); +</script> + `, + async function testExpandCollapse() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("summary"); + await definePyVar("pattern", `getUiaPattern(summary, "ExpandCollapse")`); + ok(await runPython(`bool(pattern)`), "summary has ExpandCollapse pattern"); + is( + await runPython(`pattern.CurrentExpandCollapseState`), + ExpandCollapseState_Collapsed, + "summary has ExpandCollapseState_Collapsed" + ); + // The IA2 -> UIA proxy doesn't fire ToggleState prop change events, nor + // does it fail when Expand/Collapse is called on a control which is + // already in the desired state. + if (gIsUiaEnabled) { + info("Calling Expand on summary"); + await setUpWaitForUiaPropEvent( + "ExpandCollapseExpandCollapseState", + "summary" + ); + await runPython(`pattern.Expand()`); + await waitForUiaEvent(); + ok( + true, + "Got ExpandCollapseExpandCollapseState prop change event on summary" + ); + is( + await runPython(`pattern.CurrentExpandCollapseState`), + ExpandCollapseState_Expanded, + "summary has ExpandCollapseState_Expanded" + ); + info("Calling Expand on summary"); + await testPythonRaises(`pattern.Expand()`, "Expand on summary failed"); + info("Calling Collapse on summary"); + await setUpWaitForUiaPropEvent( + "ExpandCollapseExpandCollapseState", + "summary" + ); + await runPython(`pattern.Collapse()`); + await waitForUiaEvent(); + ok( + true, + "Got ExpandCollapseExpandCollapseState prop change event on summary" + ); + is( + await runPython(`pattern.CurrentExpandCollapseState`), + ExpandCollapseState_Collapsed, + "summary has ExpandCollapseState_Collapsed" + ); + info("Calling Collapse on summary"); + await testPythonRaises( + `pattern.Collapse()`, + "Collapse on summary failed" + ); + } + + await assignPyVarToUiaWithId("popup"); + // Initially, popup has aria-haspopup but not aria-expanded. That should + // be exposed as collapsed. + await definePyVar("pattern", `getUiaPattern(popup, "ExpandCollapse")`); + ok(await runPython(`bool(pattern)`), "popup has ExpandCollapse pattern"); + // The IA2 -> UIA proxy doesn't expose ExpandCollapseState_Collapsed for + // aria-haspopup without aria-expanded. + if (gIsUiaEnabled) { + is( + await runPython(`pattern.CurrentExpandCollapseState`), + ExpandCollapseState_Collapsed, + "popup has ExpandCollapseState_Collapsed" + ); + info("Calling Expand on popup"); + await setUpWaitForUiaPropEvent( + "ExpandCollapseExpandCollapseState", + "popup" + ); + await runPython(`pattern.Expand()`); + await waitForUiaEvent(); + ok( + true, + "Got ExpandCollapseExpandCollapseState prop change event on popup" + ); + is( + await runPython(`pattern.CurrentExpandCollapseState`), + ExpandCollapseState_Expanded, + "popup has ExpandCollapseState_Expanded" + ); + } + + await testPatternAbsent("button", "ExpandCollapse"); + } +); + +/** + * Test the ScrollItem pattern. + */ +addUiaTask( + ` +<hr style="height: 100vh;"> +<button id="button">button</button> + `, + async function testScrollItem(browser, docAcc) { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("button"); + await definePyVar("pattern", `getUiaPattern(button, "ScrollItem")`); + ok(await runPython(`bool(pattern)`), "button has ScrollItem pattern"); + const button = findAccessibleChildByID(docAcc, "button"); + testStates(button, STATE_OFFSCREEN); + info("Calling ScrollIntoView on button"); + // UIA doesn't have an event for this. + let scrolled = waitForEvent(EVENT_SCROLLING_END, docAcc); + await runPython(`pattern.ScrollIntoView()`); + await scrolled; + ok(true, "Document scrolled"); + testStates(button, 0, 0, STATE_OFFSCREEN); + } +); + +/** + * Test the Value pattern. + */ +addUiaTask( + ` +<input id="text" value="before"> +<input id="textRo" readonly value="textRo"> +<input id="textDis" disabled value="textDis"> +<select id="select"><option selected>a</option><option>b</option></select> +<progress id="progress" value="0.5"></progress> +<input id="range" type="range" aria-valuetext="02:00:00"> +<a id="link" href="https://example.com/">Link</a> +<div id="ariaTextbox" contenteditable role="textbox">before</div> +<button id="button">button</button> + `, + async function testValue() { + await definePyVar("doc", `getDocUia()`); + await assignPyVarToUiaWithId("text"); + await definePyVar("pattern", `getUiaPattern(text, "Value")`); + ok(await runPython(`bool(pattern)`), "text has Value pattern"); + ok( + !(await runPython(`pattern.CurrentIsReadOnly`)), + "text has IsReadOnly false" + ); + is( + await runPython(`pattern.CurrentValue`), + "before", + "text has correct Value" + ); + info("SetValue on text"); + await setUpWaitForUiaPropEvent("ValueValue", "text"); + await runPython(`pattern.SetValue("after")`); + await waitForUiaEvent(); + is( + await runPython(`pattern.CurrentValue`), + "after", + "text has correct Value" + ); + + await assignPyVarToUiaWithId("textRo"); + await definePyVar("pattern", `getUiaPattern(textRo, "Value")`); + ok(await runPython(`bool(pattern)`), "textRo has Value pattern"); + ok( + await runPython(`pattern.CurrentIsReadOnly`), + "textRo has IsReadOnly true" + ); + is( + await runPython(`pattern.CurrentValue`), + "textRo", + "textRo has correct Value" + ); + info("SetValue on textRo"); + await testPythonRaises( + `pattern.SetValue("after")`, + "SetValue on textRo failed" + ); + + await assignPyVarToUiaWithId("textDis"); + await definePyVar("pattern", `getUiaPattern(textDis, "Value")`); + ok(await runPython(`bool(pattern)`), "textDis has Value pattern"); + ok( + !(await runPython(`pattern.CurrentIsReadOnly`)), + "textDis has IsReadOnly false" + ); + is( + await runPython(`pattern.CurrentValue`), + "textDis", + "textDis has correct Value" + ); + // The IA2 -> UIA proxy doesn't fail SetValue for a disabled element. + if (gIsUiaEnabled) { + info("SetValue on textDis"); + await testPythonRaises( + `pattern.SetValue("after")`, + "SetValue on textDis failed" + ); + } + + await assignPyVarToUiaWithId("select"); + await definePyVar("pattern", `getUiaPattern(select, "Value")`); + ok(await runPython(`bool(pattern)`), "select has Value pattern"); + ok( + !(await runPython(`pattern.CurrentIsReadOnly`)), + "select has IsReadOnly false" + ); + is( + await runPython(`pattern.CurrentValue`), + "a", + "select has correct Value" + ); + info("SetValue on select"); + await testPythonRaises( + `pattern.SetValue("b")`, + "SetValue on select failed" + ); + + await assignPyVarToUiaWithId("progress"); + await definePyVar("pattern", `getUiaPattern(progress, "Value")`); + ok(await runPython(`bool(pattern)`), "progress has Value pattern"); + // Gecko a11y doesn't treat progress bars as read only, but it probably + // should. + todo( + await runPython(`pattern.CurrentIsReadOnly`), + "progress has IsReadOnly true" + ); + is( + await runPython(`pattern.CurrentValue`), + "50%", + "progress has correct Value" + ); + info("SetValue on progress"); + await testPythonRaises( + `pattern.SetValue("60%")`, + "SetValue on progress failed" + ); + + await assignPyVarToUiaWithId("range"); + await definePyVar("pattern", `getUiaPattern(range, "Value")`); + ok(await runPython(`bool(pattern)`), "range has Value pattern"); + is( + await runPython(`pattern.CurrentValue`), + "02:00:00", + "range has correct Value" + ); + + await assignPyVarToUiaWithId("link"); + await definePyVar("pattern", `getUiaPattern(link, "Value")`); + ok(await runPython(`bool(pattern)`), "link has Value pattern"); + is( + await runPython(`pattern.CurrentValue`), + "https://example.com/", + "link has correct Value" + ); + + await assignPyVarToUiaWithId("ariaTextbox"); + await definePyVar("pattern", `getUiaPattern(ariaTextbox, "Value")`); + ok(await runPython(`bool(pattern)`), "ariaTextbox has Value pattern"); + ok( + !(await runPython(`pattern.CurrentIsReadOnly`)), + "ariaTextbox has IsReadOnly false" + ); + is( + await runPython(`pattern.CurrentValue`), + "before", + "ariaTextbox has correct Value" + ); + info("SetValue on ariaTextbox"); + await setUpWaitForUiaPropEvent("ValueValue", "ariaTextbox"); + await runPython(`pattern.SetValue("after")`); + await waitForUiaEvent(); + is( + await runPython(`pattern.CurrentValue`), + "after", + "ariaTextbox has correct Value" + ); + + await testPatternAbsent("button", "Value"); + } +); diff --git a/accessible/tests/browser/windows/uia/browser_tree.js b/accessible/tests/browser/windows/uia/browser_tree.js index 778609bedb..c63045b6d1 100644 --- a/accessible/tests/browser/windows/uia/browser_tree.js +++ b/accessible/tests/browser/windows/uia/browser_tree.js @@ -13,24 +13,6 @@ async function testIsControl(pyVar, isControl) { } } -/** - * Define a global Python variable and assign it to a given Python expression. - */ -function definePyVar(varName, expression) { - return runPython(` - global ${varName} - ${varName} = ${expression} - `); -} - -/** - * Get the UIA element with the given id and assign it to a global Python - * variable using the id as the variable name. - */ -function assignPyVarToUiaWithId(id) { - return definePyVar(id, `findUiaByDomId(doc, "${id}")`); -} - addUiaTask( ` <p id="p">paragraph</p> @@ -46,7 +28,7 @@ addUiaTask( <div id="editable" contenteditable>editable</div> <table id="table"><tr><th>th</th></tr></table> `, - async function (browser, docAcc) { + async function () { await definePyVar("doc", `getDocUia()`); await assignPyVarToUiaWithId("p"); await testIsControl("p", false); diff --git a/accessible/tests/browser/windows/uia/head.js b/accessible/tests/browser/windows/uia/head.js index e659354c7c..5b453ce6fe 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 */ +/* exported gIsUiaEnabled, addUiaTask, definePyVar, assignPyVarToUiaWithId, setUpWaitForUiaEvent, setUpWaitForUiaPropEvent, waitForUiaEvent, testPatternAbsent, testPythonRaises */ // Load the shared-head file first. Services.scriptloader.loadSubScript( @@ -53,3 +53,76 @@ function addUiaTask(doc, task, options = {}) { addTask(false); } } + +/** + * Define a global Python variable and assign it to a given Python expression. + */ +function definePyVar(varName, expression) { + return runPython(` + global ${varName} + ${varName} = ${expression} + `); +} + +/** + * Get the UIA element with the given id and assign it to a global Python + * variable using the id as the variable name. + */ +function assignPyVarToUiaWithId(id) { + return definePyVar(id, `findUiaByDomId(doc, "${id}")`); +} + +/** + * Set up to wait for a UIA event. You must await this before performing the + * action which fires the event. + */ +function setUpWaitForUiaEvent(eventName, id) { + return definePyVar( + "onEvent", + `WaitForUiaEvent(eventId=UIA_${eventName}EventId, match="${id}")` + ); +} + +/** + * Set up to wait for a UIA property change event. You must await this before + * performing the action which fires the event. + */ +function setUpWaitForUiaPropEvent(propName, id) { + return definePyVar( + "onEvent", + `WaitForUiaEvent(property=UIA_${propName}PropertyId, match="${id}")` + ); +} + +/** + * Wait for the event requested in setUpWaitForUia*Event. + */ +function waitForUiaEvent() { + return runPython(` + onEvent.wait() + `); +} + +/** + * Verify that a UIA element does *not* support the given control pattern. + */ +async function testPatternAbsent(id, patternName) { + const hasPattern = await runPython(` + el = findUiaByDomId(doc, "${id}") + return bool(getUiaPattern(el, "${patternName}")) + `); + ok(!hasPattern, `${id} doesn't have ${patternName} pattern`); +} + +/** + * Verify that a Python expression raises an exception. + */ +async function testPythonRaises(expression, message) { + let failed = false; + try { + await runPython(expression); + } catch { + failed = true; + } + ok(failed, message); +} |