import ApplicationServices
|
import Foundation
|
|
struct Candidate {
|
let role: String
|
let exactIdentifier: String
|
let selected: Bool?
|
let rect: CGRect?
|
let hasParentWindow: Bool
|
}
|
|
func selectUnique(_ candidates: [Candidate], expected: String) -> Candidate? {
|
let matches = candidates.filter {
|
$0.role == (kAXTextFieldRole as String) && $0.exactIdentifier == expected
|
}
|
guard matches.count == 1,
|
matches[0].hasParentWindow,
|
let rect = matches[0].rect,
|
rect.origin.x.isFinite,
|
rect.origin.y.isFinite,
|
rect.width.isFinite,
|
rect.height.isFinite,
|
rect.width > 0,
|
rect.height > 0 else { return nil }
|
return matches[0]
|
}
|
|
func selfTest() -> Int32 {
|
let rect = CGRect(x: 10, y: 20, width: 30, height: 40)
|
let good = Candidate(role: kAXTextFieldRole as String, exactIdentifier: "fixture", selected: true, rect: rect, hasParentWindow: true)
|
guard selectUnique([good], expected: "fixture") != nil,
|
selectUnique([good, good], expected: "fixture") == nil,
|
selectUnique([good], expected: "fix") == nil,
|
selectUnique([Candidate(role: good.role, exactIdentifier: good.exactIdentifier, selected: true, rect: .zero, hasParentWindow: true)], expected: "fixture") == nil,
|
selectUnique([Candidate(role: good.role, exactIdentifier: good.exactIdentifier, selected: true, rect: rect, hasParentWindow: false)], expected: "fixture") == nil else { return 1 }
|
print("self_test=PASS exact_unique_binding=true minimal_output=true")
|
return 0
|
}
|
|
if CommandLine.arguments == [CommandLine.arguments[0], "--self-test"] {
|
exit(selfTest())
|
}
|
|
guard CommandLine.arguments.count == 3,
|
let pid = pid_t(CommandLine.arguments[1]),
|
!CommandLine.arguments[2].isEmpty else {
|
fputs("usage: AXInspect pid exactSourceIdentifier\n", stderr)
|
exit(64)
|
}
|
let expected = CommandLine.arguments[2]
|
|
enum AXFailure: Error {
|
case operation(String, AXError)
|
}
|
|
func attribute(_ element: AXUIElement, _ name: String) throws -> CFTypeRef? {
|
var value: CFTypeRef?
|
let error = AXUIElementCopyAttributeValue(element, name as CFString, &value)
|
if error == .success { return value }
|
if error == .noValue || error == .attributeUnsupported { return nil }
|
throw AXFailure.operation(name, error)
|
}
|
|
func stringValue(_ value: CFTypeRef?) -> String? {
|
guard let value, CFGetTypeID(value) == CFStringGetTypeID() else { return nil }
|
return value as? String
|
}
|
|
func boolValue(_ value: CFTypeRef?) -> Bool? {
|
guard let value, CFGetTypeID(value) == CFBooleanGetTypeID() else { return nil }
|
return CFBooleanGetValue((value as! CFBoolean))
|
}
|
|
func rect(_ element: AXUIElement) throws -> CGRect? {
|
guard let pv = try attribute(element, kAXPositionAttribute), CFGetTypeID(pv) == AXValueGetTypeID(),
|
let sv = try attribute(element, kAXSizeAttribute), CFGetTypeID(sv) == AXValueGetTypeID() else { return nil }
|
var point = CGPoint.zero
|
var size = CGSize.zero
|
guard AXValueGetValue(pv as! AXValue, .cgPoint, &point),
|
AXValueGetValue(sv as! AXValue, .cgSize, &size) else { return nil }
|
return CGRect(origin: point, size: size)
|
}
|
|
func parentWindow(_ element: AXUIElement) throws -> AXUIElement? {
|
var current = element
|
for _ in 0..<16 {
|
if stringValue(try attribute(current, kAXRoleAttribute)) == (kAXWindowRole as String) {
|
return current
|
}
|
guard let parent = try attribute(current, kAXParentAttribute),
|
CFGetTypeID(parent) == AXUIElementGetTypeID() else { return nil }
|
current = parent as! AXUIElement
|
}
|
return nil
|
}
|
|
func collectMatches(_ element: AXUIElement, depth: Int = 0, into matches: inout [AXUIElement]) throws {
|
let role = stringValue(try attribute(element, kAXRoleAttribute))
|
if role == (kAXTextFieldRole as String),
|
stringValue(try attribute(element, kAXValueAttribute)) == expected {
|
matches.append(element)
|
}
|
guard depth < 14,
|
let children = try attribute(element, kAXChildrenAttribute) as? [AXUIElement] else { return }
|
for child in children { try collectMatches(child, depth: depth + 1, into: &matches) }
|
}
|
|
do {
|
var matches: [AXUIElement] = []
|
try collectMatches(AXUIElementCreateApplication(pid), into: &matches)
|
let descriptors = try matches.map { element in
|
Candidate(
|
role: stringValue(try attribute(element, kAXRoleAttribute)) ?? "",
|
exactIdentifier: stringValue(try attribute(element, kAXValueAttribute)) ?? "",
|
selected: boolValue(try attribute(element, kAXSelectedAttribute)),
|
rect: try rect(element),
|
hasParentWindow: try parentWindow(element) != nil
|
)
|
}
|
guard matches.count == 1,
|
selectUnique(descriptors, expected: expected) != nil,
|
let targetRect = try rect(matches[0]),
|
let window = try parentWindow(matches[0]),
|
let windowRect = try rect(window),
|
targetRect.origin.x.isFinite, targetRect.origin.y.isFinite,
|
targetRect.width.isFinite, targetRect.height.isFinite,
|
targetRect.width > 0, targetRect.height > 0 else {
|
fputs("exact source binding failed\n", stderr)
|
exit(67)
|
}
|
let selected = descriptors[0].selected
|
print("target=<QA_SOURCE> role=AXTextField selected=\(selected.map(String.init) ?? "unknown")")
|
print("target_rect=\(targetRect.origin.x),\(targetRect.origin.y),\(targetRect.width),\(targetRect.height)")
|
print("parent_window=<QA_WINDOW> role=AXWindow")
|
print("window_rect=\(windowRect.origin.x),\(windowRect.origin.y),\(windowRect.width),\(windowRect.height)")
|
} catch {
|
fputs("AX inspection failed\n", stderr)
|
exit(70)
|
}
|