mcp-baepsae 6.1.1 → 6.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README-KR.md +19 -0
- package/README.md +20 -0
- package/bundled/baepsae-native +0 -0
- package/dist/tools/media.d.ts.map +1 -1
- package/dist/tools/media.js +103 -4
- package/dist/tools/media.js.map +1 -1
- package/dist/tools/ui.js +1 -1
- package/dist/tools/ui.js.map +1 -1
- package/dist/utils.d.ts +1 -1
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/native/Sources/Commands/InputCommands.swift +18 -17
- package/native/Sources/Commands/UICommands.swift +44 -32
- package/native/Sources/Commands/WindowCommands.swift +1 -1
- package/native/Sources/Utils.swift +274 -26
- package/native/Sources/Version.swift +1 -1
- package/package.json +6 -1
- package/scripts/dump-tabbar-actions.mjs +312 -0
- package/scripts/research-coordinate-calibration.mjs +276 -0
- package/scripts/research-input-channels.mjs +327 -0
- package/scripts/research-tap-tab-grid.mjs +271 -0
- package/scripts/verify-media-capture.mjs +99 -0
|
@@ -259,6 +259,11 @@ func requireSimulatorUdid(_ target: TargetApp) throws -> String {
|
|
|
259
259
|
return udid
|
|
260
260
|
}
|
|
261
261
|
|
|
262
|
+
func simulatorUdid(from target: TargetApp) -> String? {
|
|
263
|
+
guard case .simulator(let udid) = target else { return nil }
|
|
264
|
+
return udid
|
|
265
|
+
}
|
|
266
|
+
|
|
262
267
|
// MARK: - Accessibility Helpers
|
|
263
268
|
|
|
264
269
|
func ensureAccessibilityTrusted() throws {
|
|
@@ -300,6 +305,24 @@ func simulatorAccessibilityRootElement() throws -> UIElement {
|
|
|
300
305
|
return appElement
|
|
301
306
|
}
|
|
302
307
|
|
|
308
|
+
func shellCaptureCommand(_ command: String, _ arguments: [String]) -> String? {
|
|
309
|
+
let process = Process()
|
|
310
|
+
process.executableURL = URL(fileURLWithPath: command)
|
|
311
|
+
process.arguments = arguments
|
|
312
|
+
let pipe = Pipe()
|
|
313
|
+
process.standardOutput = pipe
|
|
314
|
+
process.standardError = FileHandle.nullDevice
|
|
315
|
+
do {
|
|
316
|
+
try process.run()
|
|
317
|
+
process.waitUntilExit()
|
|
318
|
+
guard process.terminationStatus == 0 else { return nil }
|
|
319
|
+
let data = pipe.fileHandleForReading.readDataToEndOfFile()
|
|
320
|
+
return String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
321
|
+
} catch {
|
|
322
|
+
return nil
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
|
|
303
326
|
func CopyAttributeValue(_ element: UIElement, _ attribute: CFString) -> CFTypeRef? {
|
|
304
327
|
var value: CFTypeRef?
|
|
305
328
|
let status = AXUIElementCopyAttributeValue(element, attribute, &value)
|
|
@@ -434,6 +457,24 @@ func IdentifierAttribute(_ element: UIElement) -> String? {
|
|
|
434
457
|
return StringAttribute(element, "AXIdentifier" as CFString)
|
|
435
458
|
}
|
|
436
459
|
|
|
460
|
+
func performPrimaryAction(on element: UIElement) throws {
|
|
461
|
+
let actions = ActionNames(element)
|
|
462
|
+
if actions.contains(kAXPressAction as String) {
|
|
463
|
+
let status = AXUIElementPerformAction(element, kAXPressAction as CFString)
|
|
464
|
+
if status != .success {
|
|
465
|
+
throw NativeError.commandFailed("Matched accessibility element but AXPress failed with status \(status.rawValue).")
|
|
466
|
+
}
|
|
467
|
+
return
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
if let frame = FrameAttribute(element) {
|
|
471
|
+
sendClick(at: CGPoint(x: frame.midX, y: frame.midY))
|
|
472
|
+
return
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
throw NativeError.commandFailed("Matched accessibility element has no AXPress action or frame for fallback click.")
|
|
476
|
+
}
|
|
477
|
+
|
|
437
478
|
// MARK: - Text Helpers
|
|
438
479
|
|
|
439
480
|
func normalizeText(_ value: String) -> String {
|
|
@@ -540,6 +581,98 @@ func describeAccessibilityElement(_ element: UIElement, includeEmpty: Bool = tru
|
|
|
540
581
|
return parts.joined(separator: " ")
|
|
541
582
|
}
|
|
542
583
|
|
|
584
|
+
func actionableTabBarItems(in tabBar: UIElement, maxDepth: Int = 2) -> [UIElement] {
|
|
585
|
+
var stack: [(element: UIElement, depth: Int)] = Children(tabBar).map { ($0, 1) }.reversed()
|
|
586
|
+
var matches: [UIElement] = []
|
|
587
|
+
|
|
588
|
+
while let current = stack.popLast() {
|
|
589
|
+
let role = StringAttribute(current.element, kAXRoleAttribute as CFString) ?? ""
|
|
590
|
+
let actions = ActionNames(current.element)
|
|
591
|
+
let hasFrame = FrameAttribute(current.element) != nil
|
|
592
|
+
let isLikelyTabItem =
|
|
593
|
+
role == "AXButton" ||
|
|
594
|
+
role == "AXRadioButton" ||
|
|
595
|
+
role == "AXCheckBox" ||
|
|
596
|
+
actions.contains(kAXPressAction as String)
|
|
597
|
+
|
|
598
|
+
if isLikelyTabItem && hasFrame {
|
|
599
|
+
if !matches.contains(where: { elementsAreEqual($0, current.element) }) {
|
|
600
|
+
matches.append(current.element)
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
if current.depth < maxDepth {
|
|
605
|
+
for child in Children(current.element).reversed() {
|
|
606
|
+
stack.append((child, current.depth + 1))
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
return matches.sorted {
|
|
612
|
+
(FrameAttribute($0)?.midX ?? 0) < (FrameAttribute($1)?.midX ?? 0)
|
|
613
|
+
}
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
func semanticProxyTabButtons(in contentRoot: UIElement, excluding excludedElement: UIElement? = nil, expectedCount: Int) -> [UIElement] {
|
|
617
|
+
guard expectedCount > 0 else { return [] }
|
|
618
|
+
guard let contentFrame = FrameAttribute(contentRoot) else { return [] }
|
|
619
|
+
let excludedFrame = excludedElement.flatMap(FrameAttribute)
|
|
620
|
+
|
|
621
|
+
let directChildren = Children(contentRoot)
|
|
622
|
+
let candidates = directChildren.filter { child in
|
|
623
|
+
let role = StringAttribute(child, kAXRoleAttribute as CFString) ?? ""
|
|
624
|
+
let actions = ActionNames(child)
|
|
625
|
+
guard let frame = FrameAttribute(child) else { return false }
|
|
626
|
+
if let excludedFrame, excludedFrame.contains(frame) {
|
|
627
|
+
return false
|
|
628
|
+
}
|
|
629
|
+
guard frame.midY < contentFrame.origin.y + contentFrame.height * 0.55 else {
|
|
630
|
+
return false
|
|
631
|
+
}
|
|
632
|
+
guard frame.width < contentFrame.width * 0.8 else {
|
|
633
|
+
return false
|
|
634
|
+
}
|
|
635
|
+
return role == "AXButton" || actions.contains(kAXPressAction as String)
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
guard !candidates.isEmpty else { return [] }
|
|
639
|
+
|
|
640
|
+
struct RowGroup {
|
|
641
|
+
var meanY: CGFloat
|
|
642
|
+
var elements: [UIElement]
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
var rows: [RowGroup] = []
|
|
646
|
+
let tolerance: CGFloat = 24
|
|
647
|
+
for element in candidates.sorted(by: { (FrameAttribute($0)?.midY ?? 0) < (FrameAttribute($1)?.midY ?? 0) }) {
|
|
648
|
+
let midY = FrameAttribute(element)?.midY ?? 0
|
|
649
|
+
if let rowIndex = rows.firstIndex(where: { abs($0.meanY - midY) <= tolerance }) {
|
|
650
|
+
rows[rowIndex].elements.append(element)
|
|
651
|
+
let count = CGFloat(rows[rowIndex].elements.count)
|
|
652
|
+
rows[rowIndex].meanY = ((rows[rowIndex].meanY * (count - 1)) + midY) / count
|
|
653
|
+
} else {
|
|
654
|
+
rows.append(RowGroup(meanY: midY, elements: [element]))
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
guard let bestRow = rows.sorted(by: {
|
|
659
|
+
if $0.elements.count == $1.elements.count {
|
|
660
|
+
return $0.meanY < $1.meanY
|
|
661
|
+
}
|
|
662
|
+
return $0.elements.count > $1.elements.count
|
|
663
|
+
}).first else {
|
|
664
|
+
return []
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
guard bestRow.elements.count == expectedCount else {
|
|
668
|
+
return []
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
return bestRow.elements.sorted {
|
|
672
|
+
(FrameAttribute($0)?.midX ?? 0) < (FrameAttribute($1)?.midX ?? 0)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
543
676
|
func describeAccessibilityTree(from root: UIElement, options: DescribeOptions = DescribeOptions()) -> [String] {
|
|
544
677
|
var lines: [String] = []
|
|
545
678
|
var stack: [(element: UIElement, depth: Int)] = [(root, 0)]
|
|
@@ -869,7 +1002,57 @@ func findElementBySubrole(from root: UIElement, subrole: String) -> UIElement? {
|
|
|
869
1002
|
return nil
|
|
870
1003
|
}
|
|
871
1004
|
|
|
872
|
-
func
|
|
1005
|
+
func simulatorDeviceName(for udid: String?) -> String? {
|
|
1006
|
+
guard let udid, !udid.isEmpty else { return nil }
|
|
1007
|
+
return shellCaptureCommand("/usr/bin/xcrun", ["simctl", "getenv", udid, "SIMULATOR_DEVICE_NAME"])
|
|
1008
|
+
}
|
|
1009
|
+
|
|
1010
|
+
func simulatorWindowTitle(_ element: UIElement) -> String? {
|
|
1011
|
+
return StringAttribute(element, kAXTitleAttribute as CFString)
|
|
1012
|
+
?? StringAttribute(element, kAXDescriptionAttribute as CFString)
|
|
1013
|
+
?? StringAttribute(element, kAXValueAttribute as CFString)
|
|
1014
|
+
}
|
|
1015
|
+
|
|
1016
|
+
func simulatorWindowElement(from appRoot: UIElement, udid: String? = nil) -> UIElement? {
|
|
1017
|
+
let windows = Children(appRoot).filter { element in
|
|
1018
|
+
let attrs = copyMultipleAttributes(element, [kAXRoleAttribute as String])
|
|
1019
|
+
if let ref = attrs[kAXRoleAttribute as String], let role = stringFromCFTypeRef(ref) {
|
|
1020
|
+
return role == "AXWindow"
|
|
1021
|
+
}
|
|
1022
|
+
return false
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
guard !windows.isEmpty else { return nil }
|
|
1026
|
+
|
|
1027
|
+
let normalizedDeviceName = simulatorDeviceName(for: udid).map(normalizeText)
|
|
1028
|
+
let preferredWindows: [UIElement]
|
|
1029
|
+
if let normalizedDeviceName {
|
|
1030
|
+
let matched = windows.filter { window in
|
|
1031
|
+
guard let title = simulatorWindowTitle(window) else { return false }
|
|
1032
|
+
return normalizeText(title).contains(normalizedDeviceName)
|
|
1033
|
+
}
|
|
1034
|
+
preferredWindows = matched.isEmpty ? windows : matched
|
|
1035
|
+
} else {
|
|
1036
|
+
preferredWindows = windows
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
var bestWindow: UIElement?
|
|
1040
|
+
var bestArea: CGFloat = 0
|
|
1041
|
+
for window in preferredWindows {
|
|
1042
|
+
let area = FrameAttribute(window).map { $0.width * $0.height } ?? 0
|
|
1043
|
+
if bestWindow == nil || area > bestArea {
|
|
1044
|
+
bestWindow = window
|
|
1045
|
+
bestArea = area
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
return bestWindow
|
|
1049
|
+
}
|
|
1050
|
+
|
|
1051
|
+
func simulatorContentRootElement(from appRoot: UIElement, udid: String? = nil) -> UIElement? {
|
|
1052
|
+
if let scopedWindow = simulatorWindowElement(from: appRoot, udid: udid),
|
|
1053
|
+
let scopedContentRoot = findElementBySubrole(from: scopedWindow, subrole: "iOSContentGroup") {
|
|
1054
|
+
return scopedContentRoot
|
|
1055
|
+
}
|
|
873
1056
|
return findElementBySubrole(from: appRoot, subrole: "iOSContentGroup")
|
|
874
1057
|
}
|
|
875
1058
|
|
|
@@ -956,7 +1139,8 @@ func collectWideAuxiliaryGroups(in root: UIElement, contentRootFrame: CGRect? =
|
|
|
956
1139
|
)
|
|
957
1140
|
}
|
|
958
1141
|
|
|
959
|
-
func simulatorAuxiliaryContainerCandidates(from appRoot: UIElement, excluding contentRoot: UIElement? = nil) -> [SimulatorAuxiliaryContainerCandidate] {
|
|
1142
|
+
func simulatorAuxiliaryContainerCandidates(from appRoot: UIElement, excluding contentRoot: UIElement? = nil, udid: String? = nil) -> [SimulatorAuxiliaryContainerCandidate] {
|
|
1143
|
+
let scopeRoot = simulatorWindowElement(from: appRoot, udid: udid) ?? appRoot
|
|
960
1144
|
let contentRootFrame = contentRoot.flatMap(FrameAttribute)
|
|
961
1145
|
let roleCandidates: [(role: String, label: String)] = [
|
|
962
1146
|
("AXTabGroup", "tab bar"),
|
|
@@ -981,20 +1165,20 @@ func simulatorAuxiliaryContainerCandidates(from appRoot: UIElement, excluding co
|
|
|
981
1165
|
}
|
|
982
1166
|
|
|
983
1167
|
for roleCandidate in roleCandidates {
|
|
984
|
-
for element in collectElementsByRole(in:
|
|
1168
|
+
for element in collectElementsByRole(in: scopeRoot, role: roleCandidate.role) {
|
|
985
1169
|
appendCandidate(element, label: roleCandidate.label)
|
|
986
1170
|
}
|
|
987
1171
|
}
|
|
988
1172
|
|
|
989
|
-
for element in collectWideAuxiliaryGroups(in:
|
|
1173
|
+
for element in collectWideAuxiliaryGroups(in: scopeRoot, contentRootFrame: contentRootFrame) {
|
|
990
1174
|
appendCandidate(element, label: "auxiliary group")
|
|
991
1175
|
}
|
|
992
1176
|
|
|
993
1177
|
return candidates
|
|
994
1178
|
}
|
|
995
1179
|
|
|
996
|
-
func simulatorAuxiliaryContainerLabels(from appRoot: UIElement, excluding contentRoot: UIElement? = nil) -> [String] {
|
|
997
|
-
simulatorAuxiliaryContainerCandidates(from: appRoot, excluding: contentRoot).map(\.label)
|
|
1180
|
+
func simulatorAuxiliaryContainerLabels(from appRoot: UIElement, excluding contentRoot: UIElement? = nil, udid: String? = nil) -> [String] {
|
|
1181
|
+
simulatorAuxiliaryContainerCandidates(from: appRoot, excluding: contentRoot, udid: udid).map(\.label)
|
|
998
1182
|
}
|
|
999
1183
|
|
|
1000
1184
|
func formatSimulatorAuxiliaryContainerHint(_ labels: [String]) -> String? {
|
|
@@ -1018,7 +1202,7 @@ func simulatorSelectorNotFoundMessage(selectorText: String, auxiliaryLabels: [St
|
|
|
1018
1202
|
return "No accessibility element matched \(selectorText) in simulator app content. Try --all to include Simulator chrome UI."
|
|
1019
1203
|
}
|
|
1020
1204
|
|
|
1021
|
-
func findTabBarElement(in root: UIElement) -> UIElement? {
|
|
1205
|
+
func findTabBarElement(in root: UIElement, simulatorUdid: String? = nil) -> UIElement? {
|
|
1022
1206
|
// 1st pass: Look for AXTabGroup
|
|
1023
1207
|
var stack: [UIElement] = [root]
|
|
1024
1208
|
var visited = 0
|
|
@@ -1051,7 +1235,59 @@ func findTabBarElement(in root: UIElement) -> UIElement? {
|
|
|
1051
1235
|
}
|
|
1052
1236
|
}
|
|
1053
1237
|
|
|
1054
|
-
// 3rd pass:
|
|
1238
|
+
// 3rd pass: Simulator-specific heuristic — look for a wide bottom group
|
|
1239
|
+
// inside iOSContentGroup. SwiftUI TabView on Simulator frequently exposes
|
|
1240
|
+
// the tab bar as AXGroup text="Tab Bar" rather than AXTabGroup.
|
|
1241
|
+
if let contentRoot = simulatorContentRootElement(from: root, udid: simulatorUdid),
|
|
1242
|
+
let contentFrame = FrameAttribute(contentRoot) {
|
|
1243
|
+
let bottomThresholdY = contentFrame.origin.y + contentFrame.height * 0.65
|
|
1244
|
+
stack = [contentRoot]
|
|
1245
|
+
visited = 0
|
|
1246
|
+
while let current = stack.popLast() {
|
|
1247
|
+
if visited > 800 { break }
|
|
1248
|
+
visited += 1
|
|
1249
|
+
|
|
1250
|
+
let attrs = copyMultipleAttributes(current, [
|
|
1251
|
+
kAXRoleAttribute as String,
|
|
1252
|
+
"AXFrame",
|
|
1253
|
+
"AXLabel",
|
|
1254
|
+
kAXTitleAttribute as String,
|
|
1255
|
+
kAXDescriptionAttribute as String,
|
|
1256
|
+
kAXValueAttribute as String,
|
|
1257
|
+
])
|
|
1258
|
+
|
|
1259
|
+
if let ref = attrs[kAXRoleAttribute as String],
|
|
1260
|
+
let role = stringFromCFTypeRef(ref),
|
|
1261
|
+
role == "AXGroup",
|
|
1262
|
+
let frameRef = attrs["AXFrame"],
|
|
1263
|
+
let frame = frameFromCFTypeRef(frameRef) {
|
|
1264
|
+
let textCandidates = [
|
|
1265
|
+
attrs["AXLabel"],
|
|
1266
|
+
attrs[kAXTitleAttribute as String],
|
|
1267
|
+
attrs[kAXDescriptionAttribute as String],
|
|
1268
|
+
attrs[kAXValueAttribute as String],
|
|
1269
|
+
].compactMap { $0 }.compactMap(stringFromCFTypeRef)
|
|
1270
|
+
|
|
1271
|
+
let hasExplicitTabBarText = textCandidates.contains { candidate in
|
|
1272
|
+
normalizeText(candidate).contains("tab bar")
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
let isWide = frame.width >= contentFrame.width * 0.60
|
|
1276
|
+
let isNearBottom = frame.origin.y >= bottomThresholdY && frame.maxY <= contentFrame.maxY + 8
|
|
1277
|
+
let plausibleBarHeight = frame.height >= 32 && frame.height <= 140
|
|
1278
|
+
|
|
1279
|
+
if hasExplicitTabBarText || (isWide && isNearBottom && plausibleBarHeight) {
|
|
1280
|
+
return current
|
|
1281
|
+
}
|
|
1282
|
+
}
|
|
1283
|
+
|
|
1284
|
+
for child in Children(current).reversed() {
|
|
1285
|
+
stack.append(child)
|
|
1286
|
+
}
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
// 4th pass: Generic heuristic — wide AXGroup in bottom 15% of screen
|
|
1055
1291
|
guard let mainScreen = NSScreen.main else { return nil }
|
|
1056
1292
|
let screenHeight = mainScreen.frame.height
|
|
1057
1293
|
let bottomThreshold = screenHeight * 0.15
|
|
@@ -1086,8 +1322,8 @@ func findTabBarElement(in root: UIElement) -> UIElement? {
|
|
|
1086
1322
|
|
|
1087
1323
|
func windowBounds(for target: TargetApp) -> CGRect? {
|
|
1088
1324
|
switch target {
|
|
1089
|
-
case .simulator:
|
|
1090
|
-
return simulatorWindowBounds()
|
|
1325
|
+
case .simulator(let udid):
|
|
1326
|
+
return simulatorWindowBounds(udid: udid)
|
|
1091
1327
|
case .macApp(let pid, _, _):
|
|
1092
1328
|
guard let windowInfo = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
|
|
1093
1329
|
as? [[String: Any]] else {
|
|
@@ -1119,7 +1355,7 @@ func windowBounds(for target: TargetApp) -> CGRect? {
|
|
|
1119
1355
|
}
|
|
1120
1356
|
}
|
|
1121
1357
|
|
|
1122
|
-
func simulatorWindowBounds() -> CGRect? {
|
|
1358
|
+
func simulatorWindowBounds(udid: String? = nil) -> CGRect? {
|
|
1123
1359
|
guard let windowInfo = CGWindowListCopyWindowInfo([.optionOnScreenOnly, .excludeDesktopElements], kCGNullWindowID)
|
|
1124
1360
|
as? [[String: Any]] else {
|
|
1125
1361
|
return nil
|
|
@@ -1131,9 +1367,21 @@ func simulatorWindowBounds() -> CGRect? {
|
|
|
1131
1367
|
return owner == "Simulator" && (layer ?? 0) == 0
|
|
1132
1368
|
}
|
|
1133
1369
|
|
|
1370
|
+
let normalizedDeviceName = simulatorDeviceName(for: udid).map(normalizeText)
|
|
1371
|
+
let preferredWindows: [[String: Any]]
|
|
1372
|
+
if let normalizedDeviceName {
|
|
1373
|
+
let matched = windows.filter { info in
|
|
1374
|
+
let title = (info[kCGWindowName as String] as? String) ?? ""
|
|
1375
|
+
return normalizeText(title).contains(normalizedDeviceName)
|
|
1376
|
+
}
|
|
1377
|
+
preferredWindows = matched.isEmpty ? windows : matched
|
|
1378
|
+
} else {
|
|
1379
|
+
preferredWindows = windows
|
|
1380
|
+
}
|
|
1381
|
+
|
|
1134
1382
|
var best: CGRect?
|
|
1135
1383
|
var bestArea: CGFloat = 0
|
|
1136
|
-
for info in
|
|
1384
|
+
for info in preferredWindows {
|
|
1137
1385
|
guard let boundsDict = info[kCGWindowBounds as String] as? [String: Any],
|
|
1138
1386
|
let x = boundsDict["X"] as? CGFloat,
|
|
1139
1387
|
let y = boundsDict["Y"] as? CGFloat,
|
|
@@ -1153,8 +1401,8 @@ func simulatorWindowBounds() -> CGRect? {
|
|
|
1153
1401
|
|
|
1154
1402
|
func pointInWindow(x: Double, y: Double, for target: TargetApp) throws -> CGPoint {
|
|
1155
1403
|
switch target {
|
|
1156
|
-
case .simulator:
|
|
1157
|
-
return try pointInSimulatorWindow(x: x, y: y)
|
|
1404
|
+
case .simulator(let udid):
|
|
1405
|
+
return try pointInSimulatorWindow(x: x, y: y, udid: udid)
|
|
1158
1406
|
case .macApp:
|
|
1159
1407
|
guard let bounds = windowBounds(for: target) else {
|
|
1160
1408
|
throw NativeError.commandFailed("Application window not found. Ensure the app is running and visible.")
|
|
@@ -1165,8 +1413,8 @@ func pointInWindow(x: Double, y: Double, for target: TargetApp) throws -> CGPoin
|
|
|
1165
1413
|
}
|
|
1166
1414
|
}
|
|
1167
1415
|
|
|
1168
|
-
func pointInSimulatorWindow(x: Double, y: Double) throws -> CGPoint {
|
|
1169
|
-
guard let bounds = simulatorWindowBounds() else {
|
|
1416
|
+
func pointInSimulatorWindow(x: Double, y: Double, udid: String? = nil) throws -> CGPoint {
|
|
1417
|
+
guard let bounds = simulatorWindowBounds(udid: udid) else {
|
|
1170
1418
|
throw NativeError.commandFailed("Simulator window not found. Ensure Simulator is running and visible.")
|
|
1171
1419
|
}
|
|
1172
1420
|
let targetX = bounds.origin.x + CGFloat(x)
|
|
@@ -1176,19 +1424,19 @@ func pointInSimulatorWindow(x: Double, y: Double) throws -> CGPoint {
|
|
|
1176
1424
|
|
|
1177
1425
|
// MARK: - Simulator Content Bounds
|
|
1178
1426
|
|
|
1179
|
-
func simulatorContentBounds() -> CGRect? {
|
|
1427
|
+
func simulatorContentBounds(udid: String? = nil) -> CGRect? {
|
|
1180
1428
|
guard let appRoot = try? simulatorAccessibilityRootElement() else {
|
|
1181
|
-
return simulatorWindowBounds()
|
|
1429
|
+
return simulatorWindowBounds(udid: udid)
|
|
1182
1430
|
}
|
|
1183
|
-
if let contentGroup = simulatorContentRootElement(from: appRoot),
|
|
1431
|
+
if let contentGroup = simulatorContentRootElement(from: appRoot, udid: udid),
|
|
1184
1432
|
let frame = FrameAttribute(contentGroup) {
|
|
1185
1433
|
return frame
|
|
1186
1434
|
}
|
|
1187
|
-
return simulatorWindowBounds()
|
|
1435
|
+
return simulatorWindowBounds(udid: udid)
|
|
1188
1436
|
}
|
|
1189
1437
|
|
|
1190
|
-
func pointInSimulatorContent(x: Double, y: Double) throws -> CGPoint {
|
|
1191
|
-
guard let bounds = simulatorContentBounds() else {
|
|
1438
|
+
func pointInSimulatorContent(x: Double, y: Double, udid: String? = nil) throws -> CGPoint {
|
|
1439
|
+
guard let bounds = simulatorContentBounds(udid: udid) else {
|
|
1192
1440
|
throw NativeError.commandFailed("Simulator content area not found. Ensure Simulator is running and visible.")
|
|
1193
1441
|
}
|
|
1194
1442
|
let targetX = bounds.origin.x + CGFloat(x)
|
|
@@ -1198,18 +1446,18 @@ func pointInSimulatorContent(x: Double, y: Double) throws -> CGPoint {
|
|
|
1198
1446
|
|
|
1199
1447
|
func pointForInput(x: Double, y: Double, for target: TargetApp) throws -> CGPoint {
|
|
1200
1448
|
switch target {
|
|
1201
|
-
case .simulator:
|
|
1202
|
-
return try pointInSimulatorContent(x: x, y: y)
|
|
1449
|
+
case .simulator(let udid):
|
|
1450
|
+
return try pointInSimulatorContent(x: x, y: y, udid: udid)
|
|
1203
1451
|
case .macApp:
|
|
1204
1452
|
return try pointInWindow(x: x, y: y, for: target)
|
|
1205
1453
|
}
|
|
1206
1454
|
}
|
|
1207
1455
|
|
|
1208
|
-
func simulatorScrollAnchorPoint(x: Double?, y: Double?) throws -> CGPoint {
|
|
1456
|
+
func simulatorScrollAnchorPoint(x: Double?, y: Double?, udid: String? = nil) throws -> CGPoint {
|
|
1209
1457
|
if let x, let y {
|
|
1210
1458
|
return CGPoint(x: x, y: y)
|
|
1211
1459
|
}
|
|
1212
|
-
guard let bounds = simulatorContentBounds() else {
|
|
1460
|
+
guard let bounds = simulatorContentBounds(udid: udid) else {
|
|
1213
1461
|
throw NativeError.commandFailed("Simulator content area not found. Ensure Simulator is running and visible.")
|
|
1214
1462
|
}
|
|
1215
1463
|
return CGPoint(x: bounds.width * 0.5, y: bounds.height * 0.5)
|
|
@@ -1 +1 @@
|
|
|
1
|
-
let BAEPSAE_VERSION = "6.
|
|
1
|
+
let BAEPSAE_VERSION = "6.2.0"
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mcp-baepsae",
|
|
3
|
-
"version": "6.
|
|
3
|
+
"version": "6.2.0",
|
|
4
4
|
"mcpName": "io.github.oozoofrog/baepsae",
|
|
5
5
|
"description": "Local MCP server for iOS Simulator and macOS app automation with a native Swift bridge",
|
|
6
6
|
"author": "oozoofrog",
|
|
@@ -29,6 +29,11 @@
|
|
|
29
29
|
"test:drift": "npm run build && node scripts/generate-tool-manifest.mjs --check && node --test tests/tool.manifest.test.mjs",
|
|
30
30
|
"test:real": "npm run build && node --test tests/mcp.real.test.mjs",
|
|
31
31
|
"test:real:preflight": "npm run build && node --test --test-name-pattern=\"Preflight diagnostics\" tests/mcp.real.test.mjs",
|
|
32
|
+
"test:real:media": "npm run build && node scripts/verify-media-capture.mjs",
|
|
33
|
+
"research:tap-tab": "npm run build && node scripts/research-tap-tab-grid.mjs",
|
|
34
|
+
"research:tabbar-actions": "node scripts/dump-tabbar-actions.mjs",
|
|
35
|
+
"research:coordinate-calibration": "node scripts/research-coordinate-calibration.mjs",
|
|
36
|
+
"research:input-channels": "node scripts/research-input-channels.mjs",
|
|
32
37
|
"test:real:sim": "npm run build && node --test --test-name-pattern=\"^(Preflight diagnostics|Setup:|Phase 1:|Phase 2:|Phase 2b:|Phase 2c:|Phase 3:|cleanup:)\" tests/mcp.real.test.mjs",
|
|
33
38
|
"test:real:mac": "npm run build && node --test --test-name-pattern=\"^Phase 4:\" tests/mcp.real.test.mjs",
|
|
34
39
|
"bundle:native": "bash scripts/bundle-native.sh",
|