Keep incomplete AX action metadata local to the observed element
This commit is contained in:
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))
|
||||
|
||||
Reference in new issue
Block a user