Keep incomplete AX action metadata local to the observed element

This commit is contained in:
creatixchu committed 2026-11-06 12:05:18 +08:00
1 parent 5d8dd640d4
commit 16d87f95de
3 files changed
+36 -8

No files matched your search

@@ -157,11 +157,14 @@ final class AXAccess {
let error = AXUIElementSetAttributeValue(element, name as CFString, kCFBooleanFalse)
guard error == .success else { throw failure(error, "clear \(name)") }
}
func actions(_ element: AXUIElement) throws -> [String] {
func actions(_ element: AXUIElement, requireKnown: Bool = false) throws -> [String] {
try prepare(element)
var names: CFArray?
let error = AXUIElementCopyActionNames(element, &names)
if absent(error) || error == .actionUnsupported { return [] }
return try actionNames(names, error: error, requireKnown: requireKnown)
}
func actionNames(_ names: CFArray?, error: AXError, requireKnown: Bool) throws -> [String] {
if absent(error) || error == .actionUnsupported || (!requireKnown && error == .failure) { return [] }
guard error == .success else { throw failure(error, "read actions") }
guard let names else { return [] }
guard let result = names as? [String] else { throw NativeFailure(code: "NATIVE_ERROR", message: "Accessibility actions are not strings") }
@@ -189,18 +192,21 @@ final class AXAccess {
guard error == .success else { throw failure(error, "read attribute names") }
guard let names else { return [] }
guard let result = names as? [String] else { throw NativeFailure(code: "NATIVE_ERROR", message: "Accessibility attributes are not strings") }
return try Self.writableAttributeNames(result) { try writable(element, $0) }
return try Self.writableAttributeNames(result) { try writable(element, $0, forDiscovery: true) }
}
static func writableAttributeNames(_ names: [String], isWritable: (String) throws -> Bool?) rethrows -> [String] {
try names.filter { try isWritable($0) == true }
}
func writable(_ element: AXUIElement, _ name: String) throws -> Bool? {
func writable(_ element: AXUIElement, _ name: String, forDiscovery: Bool = false) throws -> Bool? {
try prepare(element)
var result: DarwinBoolean = false
let error = AXUIElementIsAttributeSettable(element, name as CFString, &result)
if absent(error) { return nil }
guard error == .success else { throw failure(error, "read attribute writability") }
return result.boolValue
return try attributeWritability(result.boolValue, error: error, name: name, forDiscovery: forDiscovery)
}
func attributeWritability(_ value: Bool, error: AXError, name: String, forDiscovery: Bool) throws -> Bool? {
if absent(error) || (forDiscovery && error == .failure) { return nil }
guard error == .success else { throw failure(error, "read writability of \(name)") }
return value
}
func requireEnabled(_ element: AXUIElement) throws {
if try boolean(element, "AXEnabled") == false { throw NativeFailure(code: "NATIVE_ERROR", message: "Accessibility element is disabled") }
@@ -327,7 +327,7 @@ actor AXWorker {
try ax().performAction(element(handle, params), action: params.string("action"), argument: params.values["argument"])
case "invoke":
let target = try element(handle, params)
let advertised = try ax().actions(target)
let advertised = try ax().actions(target, requireKnown: true)
guard let action = Self.defaultAction(advertised) else {
return .result(.object(["dispatched": .bool(false)]))
}
@@ -17,6 +17,28 @@ import Testing
}
}
@Test func unavailableActionMetadataDoesNotInterruptObservation() throws {
let access = AXAccess(timeout: 1, cancellation: CancellationLatch())
#expect(try access.actionNames(nil, error: .failure, requireKnown: false).isEmpty)
#expect(throws: NativeFailure.self) { try access.actionNames(nil, error: .failure, requireKnown: true) }
for strict in [false, true] {
#expect(try access.actionNames(nil, error: .actionUnsupported, requireKnown: strict).isEmpty)
for error in [AXError.apiDisabled, .cannotComplete, .invalidUIElement, .illegalArgument] {
#expect(throws: NativeFailure.self) { try access.actionNames(nil, error: error, requireKnown: strict) }
}
}
}
@Test func listingSkipsOnlyTheAttributeWhoseWritabilityIsUnknown() throws {
let access = AXAccess(timeout: 1, cancellation: CancellationLatch())
let writable = try AXAccess.writableAttributeNames(["AXValue", "AXFocused"]) { name in
try access.attributeWritability(true, error: name == "AXValue" ? .failure : .success, name: name, forDiscovery: true)
}
#expect(writable == ["AXFocused"])
#expect(throws: NativeFailure.self) { try access.attributeWritability(false, error: .failure, name: "AXValue", forDiscovery: false) }
#expect(throws: NativeFailure.self) { try access.attributeWritability(false, error: .apiDisabled, name: "AXValue", forDiscovery: true) }
}
private func wrappedAXError(_ code: AXError) throws -> AXValue {
var error = code
return try #require(AXValueCreate(.axError, &error))