From 16d87f95ded335d0a008719cd5031daf45ca82ea Mon Sep 17 00:00:00 2001 From: creatixchu Date: Fri, 6 Nov 2026 12:05:18 +0800 Subject: [PATCH] Keep incomplete AX action metadata local to the observed element --- .../native/Sources/CuaNative/AXAccess.swift | 20 +++++++++++------ .../native/Sources/CuaNative/AXWorker.swift | 2 +- .../native/tests/swift/AXFailureTests.swift | 22 +++++++++++++++++++ 3 files changed, 36 insertions(+), 8 deletions(-) diff --git a/packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift index 254951d524..8376f1a312 100644 --- a/packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift +++ b/packages/experimental/cua-macos/native/Sources/CuaNative/AXAccess.swift @@ -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") } diff --git a/packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift b/packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift index f94ae91121..5221631791 100644 --- a/packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift +++ b/packages/experimental/cua-macos/native/Sources/CuaNative/AXWorker.swift @@ -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)])) } diff --git a/packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift b/packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift index 3734a60801..2c25c943e4 100644 --- a/packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift +++ b/packages/experimental/cua-macos/native/tests/swift/AXFailureTests.swift @@ -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))