Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Windows IA2 support #10

Merged
merged 4 commits into from
Jul 19, 2024
Merged
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Next Next commit
Add IA2 testing to testdriver.js
spectranaut committed Jul 19, 2024

Verified

This commit was created on GitHub.com and signed with GitHub’s verified signature. The key has expired.
commit 8f0352d57722ce1ca09cad336a47888cb8d47de4
3 changes: 3 additions & 0 deletions core-aam/acacia/all_apis_example.html
Original file line number Diff line number Diff line change
@@ -20,6 +20,9 @@
else if (node.API == 'axapi') {
assert_equals(node.role, 'AXButton', 'AX API role');
}
else if (node.API == 'windows') {
assert_equals(node.msaa_role, 'ROLE_SYSTEM_PUSHBUTTON', 'MSAA Role');
}
else {
assert_unreached(`Unknown API: ${node.API}`)
}
4 changes: 2 additions & 2 deletions resources/testdriver.js
Original file line number Diff line number Diff line change
@@ -1076,7 +1076,7 @@
* rejected in the cases of failures.
*/
get_accessibility_api_node: async function(dom_id) {
return window.test_driver_internal.get_accessibility_api_node(dom_id)
return window.test_driver_internal.get_accessibility_api_node(document.title, dom_id)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we sent location.href here instead of document.title, we could match based on the URL rather than the (potentially ambiguous) title.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

good idea!

.then((jsonresult) => {
return JSON.parse(jsonresult);
});
@@ -1270,7 +1270,7 @@
throw new Error("clear_device_posture() is not implemented by testdriver-vendor.js");
},

async get_accessibility_api_node(dom_id) {
async get_accessibility_api_node(title, dom_id) {
throw new Error("get_accessibility_api_node() is not available.");
}
};
3 changes: 2 additions & 1 deletion tools/wptrunner/wptrunner/executors/actions.py
Original file line number Diff line number Diff line change
@@ -472,8 +472,9 @@ def __init__(self, logger, protocol):
self.protocol = protocol

def __call__(self, payload):
title = payload["title"]
dom_id = payload["dom_id"]
return self.protocol.platform_accessibility.get_accessibility_api_node(dom_id)
return self.protocol.platform_accessibility.get_accessibility_api_node(title, dom_id)


actions = [ClickAction,
3 changes: 2 additions & 1 deletion tools/wptrunner/wptrunner/executors/executoratspi.py
Original file line number Diff line number Diff line change
@@ -109,7 +109,8 @@ def setup(self, product_name, logger):
f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Accessibility API queries will not succeeded."
)

def get_accessibility_api_node(self, dom_id):

def get_accessibility_api_node(self, dom_id, url):
if not self.root:
raise Exception(
f"Couldn't find browser {self.product_name} in accessibility API ATSPI. Did you turn on accessibility?"
2 changes: 1 addition & 1 deletion tools/wptrunner/wptrunner/executors/executoraxapi.py
Original file line number Diff line number Diff line change
@@ -91,7 +91,7 @@ def setup(self, product_name):
raise Exception(f"Couldn't find application: {product_name}")


def get_accessibility_api_node(self, dom_id):
def get_accessibility_api_node(self, title, dom_id):
tab = find_active_tab(self.root)
node = find_node(tab, "AXDOMIdentifier", dom_id)
if not node:
Original file line number Diff line number Diff line change
@@ -5,18 +5,19 @@

linux = False
mac = False
windows = False
if platform == "linux":
linux = True
from .executoratspi import *
if platform == "darwin":
mac = True
from .executoraxapi import *

if platform == "win32":
windows = True
from .executorwindowsaccessibility import WindowsAccessibilityExecutorImpl

class PlatformAccessibilityProtocolPart(ProtocolPart):
"""Protocol part for platform accessibility introspection"""
__metaclass__ = ABCMeta

name = "platform_accessibility"

def setup(self):
@@ -28,6 +29,9 @@ def setup(self):
if mac:
self.impl = AXAPIExecutorImpl()
self.impl.setup(self.product_name)
if windows:
self.impl = WindowsAccessibilityExecutorImpl()
self.impl.setup(self.product_name)

def get_accessibility_api_node(self, dom_id):
return self.impl.get_accessibility_api_node(dom_id)
def get_accessibility_api_node(self, title, dom_id):
return self.impl.get_accessibility_api_node(title, dom_id)
130 changes: 130 additions & 0 deletions tools/wptrunner/wptrunner/executors/executorwindowsaccessibility.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
from ..executors.ia2.constants import *

import json
import sys
import time

import ctypes
from ctypes import POINTER, byref
from ctypes.wintypes import BOOL, HWND, LPARAM, POINT

import comtypes.client
from comtypes import COMError, IServiceProvider

CHILDID_SELF = 0
OBJID_CLIENT = -4

user32 = ctypes.windll.user32
oleacc = ctypes.oledll.oleacc
oleaccMod = comtypes.client.GetModule("oleacc.dll")
IAccessible = oleaccMod.IAccessible
del oleaccMod

## CoCreateInstance of UIA also initialized IA2
uiaMod = comtypes.client.GetModule("UIAutomationCore.dll")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

So we're not using uiaMod/uiaClient anywhere; they're just a means to the end of initializing IA2?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that is right. I think because of the IA2->UIA bridge that windows currently ships with.

uiaClient = comtypes.CoCreateInstance(
uiaMod.CUIAutomation._reg_clsid_,
interface=uiaMod.IUIAutomation,
clsctx=comtypes.CLSCTX_INPROC_SERVER,
)

def accessible_object_from_window(hwnd, objectID=OBJID_CLIENT):
p = POINTER(IAccessible)()
oleacc.AccessibleObjectFromWindow(
hwnd, objectID, byref(IAccessible._iid_), byref(p)
)
return p

def name_from_hwnd(hwnd):
MAX_CHARS = 257
buffer = ctypes.create_unicode_buffer(MAX_CHARS)
user32.GetWindowTextW(hwnd, buffer, MAX_CHARS)
return buffer.value

def get_browser_hwnd(product_name):
found = []

@ctypes.WINFUNCTYPE(BOOL, HWND, LPARAM)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this do?

def callback(hwnd, lParam):
name = name_from_hwnd(hwnd)
if product_name not in name.lower():
return True
found.append(hwnd)
return False

user32.EnumWindows(callback, LPARAM(0))
if not found:
raise LookupError(f"Couldn't find {product_name} HWND")
return found[0]

def to_ia2(node):
serv = node.QueryInterface(IServiceProvider)
return serv.QueryService(IAccessible2._iid_, IAccessible2)

def find_browser(product_name):
hwnd = get_browser_hwnd(product_name)
root = accessible_object_from_window(hwnd)
return to_ia2(root)

def poll_for_active_tab(title, root):
active_tab = find_active_tab(title, root)
iterations = 0
while not active_tab:
time.sleep(0.01)
active_tab = find_active_tab(title, root)
iterations += 1

print(f"found active tab in {iterations} iterations", file=sys.stderr)
return active_tab

def find_active_tab(title, root):
for i in range(1, root.accChildCount + 1):
child = to_ia2(root.accChild(i))
if child.accRole(CHILDID_SELF) == ROLE_SYSTEM_DOCUMENT:
if child.accName(CHILDID_SELF) == title:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if child.accName(CHILDID_SELF) == title:
if child.accValue(CHILDID_SELF) == url:

(Not an actual suggestion to be applied directly, just documenting how we get the URL, if we wanted to do that)

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

return child
# No need to search within documents.
return
descendant = find_active_tab(title, child)
if descendant:
return descendant

def find_ia2_node(root, id):
search = f"id:{id};"
for i in range(1, root.accChildCount + 1):
child = to_ia2(root.accChild(i))
if child.attributes and search in child.attributes:
return child
descendant = find_ia2_node(child, id)
if descendant:
return descendant

def serialize_node(node):
node_dictionary = {}
node_dictionary["API"] = "windows"

# MSAA properties
node_dictionary["name"] = node.accName(CHILDID_SELF)
node_dictionary["msaa_role"] = role_to_string[node.accRole(CHILDID_SELF)]
node_dictionary["msaa_states"] = get_msaa_state_list(node.accState(CHILDID_SELF))

# IAccessible2 properties
node_dictionary["ia2_role"] = role_to_string[node.role()]
node_dictionary["ia2_states"] = get_state_list(node.states)

return node_dictionary

class WindowsAccessibilityExecutorImpl:
def setup(self, product_name):
self.product_name = product_name

def get_accessibility_api_node(self, title, dom_id):
self.root = find_browser(self.product_name)
if not self.root:
raise Exception(f"Couldn't find browser {self.product_name}.")

active_tab = poll_for_active_tab(title, self.root)
node = find_ia2_node(active_tab, dom_id)
if not node:
raise Exception(f"Couldn't find node with ID {dom_id}.")
return json.dumps(serialize_node(node))
Loading