Skip to content

Commit 7922ba6

Browse files
committed
fix(ios): preserve verified and Maestro fill paths
1 parent b588a48 commit 7922ba6

29 files changed

Lines changed: 482 additions & 165 deletions

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.h

Lines changed: 17 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,30 @@
22

33
NS_ASSUME_NONNULL_BEGIN
44

5+
typedef NS_ENUM(NSInteger, RunnerSynthesizedTextEntryStatus) {
6+
RunnerSynthesizedTextEntryStatusSucceeded,
7+
RunnerSynthesizedTextEntryStatusUnavailable,
8+
RunnerSynthesizedTextEntryStatusFailed,
9+
};
10+
11+
@interface RunnerSynthesizedTextEntryResult : NSObject
12+
13+
@property(nonatomic, readonly) RunnerSynthesizedTextEntryStatus status;
14+
@property(nonatomic, readonly, nullable) NSString *message;
15+
16+
@end
17+
518
@interface RunnerSynthesizedTextEntry : NSObject
619

720
// Synthesizes keyboard input for the current first responder without resolving an
821
// XCUIElement or serializing the application's accessibility tree.
9-
+ (NSString * _Nullable)synthesizeTextWithApplication:(id)application
10-
text:(NSString *)text;
22+
+ (RunnerSynthesizedTextEntryResult *)synthesizeTextWithApplication:(id)application
23+
text:(NSString *)text;
1124

1225
// Replaces the current first responder's contents using one synthesized
1326
// Command-A, Delete, and text-input event sequence.
14-
+ (NSString * _Nullable)replaceTextWithApplication:(id)application
15-
text:(NSString *)text;
27+
+ (RunnerSynthesizedTextEntryResult *)replaceTextWithApplication:(id)application
28+
text:(NSString *)text;
1629

1730
@end
1831

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerSynthesizedTextEntry.m

Lines changed: 69 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -13,27 +13,42 @@
1313

1414
static NSString * _Nullable RunnerRequireTextClass(Class cls, NSString *name);
1515
static NSString * _Nullable RunnerRequireTextSelector(Class cls, SEL selector, NSString *name);
16-
static NSString * _Nullable RunnerSynthesizeTextWithMode(
16+
static RunnerSynthesizedTextEntryResult *RunnerTextEntryResult(
17+
RunnerSynthesizedTextEntryStatus status,
18+
NSString * _Nullable message
19+
);
20+
static RunnerSynthesizedTextEntryResult *RunnerSynthesizeTextWithMode(
1721
id application,
1822
NSString *text,
1923
BOOL replace
2024
);
2125

26+
@interface RunnerSynthesizedTextEntryResult ()
27+
28+
@property(nonatomic, readwrite) RunnerSynthesizedTextEntryStatus status;
29+
@property(nonatomic, readwrite, nullable) NSString *message;
30+
31+
@end
32+
33+
34+
@implementation RunnerSynthesizedTextEntryResult
35+
@end
36+
2237
@implementation RunnerSynthesizedTextEntry
2338

24-
+ (NSString * _Nullable)synthesizeTextWithApplication:(id)application
25-
text:(NSString *)text {
39+
+ (RunnerSynthesizedTextEntryResult *)synthesizeTextWithApplication:(id)application
40+
text:(NSString *)text {
2641
return RunnerSynthesizeTextWithMode(application, text, NO);
2742
}
2843

29-
+ (NSString * _Nullable)replaceTextWithApplication:(id)application
30-
text:(NSString *)text {
44+
+ (RunnerSynthesizedTextEntryResult *)replaceTextWithApplication:(id)application
45+
text:(NSString *)text {
3146
return RunnerSynthesizeTextWithMode(application, text, YES);
3247
}
3348

3449
@end
3550

36-
static NSString * _Nullable RunnerSynthesizeTextWithMode(
51+
static RunnerSynthesizedTextEntryResult *RunnerSynthesizeTextWithMode(
3752
id application,
3853
NSString *text,
3954
BOOL replace
@@ -51,33 +66,39 @@ + (NSString * _Nullable)replaceTextWithApplication:(id)application
5166
SEL processID = NSSelectorFromString(@"processID");
5267

5368
NSString *missing = RunnerRequireTextClass(recordClass, @"XCSynthesizedEventRecord");
54-
if (missing != nil) return missing;
69+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
5570
missing = RunnerRequireTextClass(pathClass, @"XCPointerEventPath");
56-
if (missing != nil) return missing;
71+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
5772
missing = RunnerRequireTextSelector(recordClass, initRecord, @"initWithName:");
58-
if (missing != nil) return missing;
73+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
5974
missing = RunnerRequireTextSelector(recordClass, addPath, @"addPointerEventPath:");
60-
if (missing != nil) return missing;
75+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
6176
missing = RunnerRequireTextSelector(recordClass, setTargetProcessID, @"setTargetProcessID:");
62-
if (missing != nil) return missing;
77+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
6378
missing = RunnerRequireTextSelector(recordClass, synthesize, @"synthesizeWithError:");
64-
if (missing != nil) return missing;
79+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
6580
missing = RunnerRequireTextSelector(pathClass, initPath, @"initForTextInput");
66-
if (missing != nil) return missing;
81+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
6782
missing = RunnerRequireTextSelector(pathClass, typeText, @"typeText:atOffset:typingSpeed:shouldRedact:");
68-
if (missing != nil) return missing;
83+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
6984
if (replace) {
7085
missing = RunnerRequireTextSelector(pathClass, typeKey, @"typeKey:modifiers:atOffset:");
71-
if (missing != nil) return missing;
86+
if (missing != nil) return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusUnavailable, missing);
7287
}
7388
if (![application respondsToSelector:processID]) {
74-
return @"private XCTest text synthesis unavailable: XCUIApplication missing processID";
89+
return RunnerTextEntryResult(
90+
RunnerSynthesizedTextEntryStatusUnavailable,
91+
@"private XCTest text synthesis unavailable: XCUIApplication missing processID"
92+
);
7593
}
7694

7795
NSInteger targetProcessID =
7896
((RunnerTextMsgSendInteger)objc_msgSend)(application, processID);
7997
if (targetProcessID <= 0) {
80-
return @"private XCTest text synthesis unavailable: could not resolve target process ID";
98+
return RunnerTextEntryResult(
99+
RunnerSynthesizedTextEntryStatusUnavailable,
100+
@"private XCTest text synthesis unavailable: could not resolve target process ID"
101+
);
81102
}
82103

83104
if (replace) {
@@ -86,7 +107,10 @@ + (NSString * _Nullable)replaceTextWithApplication:(id)application
86107
);
87108
id selectionPath = ((RunnerTextMsgSendInitPath)objc_msgSend)([pathClass alloc], initPath);
88109
if (selectionRecord == nil || selectionPath == nil) {
89-
return @"private XCTest text synthesis failed: could not create the selection event";
110+
return RunnerTextEntryResult(
111+
RunnerSynthesizedTextEntryStatusFailed,
112+
@"private XCTest text synthesis failed: could not create the selection event"
113+
);
90114
}
91115
((RunnerTextMsgSendSetInteger)objc_msgSend)(
92116
selectionRecord, setTargetProcessID, targetProcessID
@@ -106,10 +130,10 @@ + (NSString * _Nullable)replaceTextWithApplication:(id)application
106130
);
107131
if (!selected) {
108132
NSString *detail = selectionError.localizedDescription ?: @"synthesizeWithError returned false";
109-
return [NSString stringWithFormat:
110-
@"private XCTest text selection failed: %@",
111-
detail
112-
];
133+
return RunnerTextEntryResult(
134+
RunnerSynthesizedTextEntryStatusFailed,
135+
[NSString stringWithFormat:@"private XCTest text selection failed: %@", detail]
136+
);
113137
}
114138
}
115139

@@ -118,7 +142,10 @@ + (NSString * _Nullable)replaceTextWithApplication:(id)application
118142
);
119143
id path = ((RunnerTextMsgSendInitPath)objc_msgSend)([pathClass alloc], initPath);
120144
if (record == nil || path == nil) {
121-
return @"private XCTest text synthesis failed: could not create the text event";
145+
return RunnerTextEntryResult(
146+
RunnerSynthesizedTextEntryStatusFailed,
147+
@"private XCTest text synthesis failed: could not create the text event"
148+
);
122149
}
123150
((RunnerTextMsgSendSetInteger)objc_msgSend)(record, setTargetProcessID, targetProcessID);
124151
((RunnerTextMsgSendType)objc_msgSend)(path, typeText, text, 0.0, 60, YES);
@@ -128,16 +155,32 @@ + (NSString * _Nullable)replaceTextWithApplication:(id)application
128155
BOOL ok = ((RunnerTextMsgSendSynthesize)objc_msgSend)(record, synthesize, &error);
129156
if (!ok) {
130157
NSString *detail = error.localizedDescription ?: @"synthesizeWithError returned false";
131-
return [NSString stringWithFormat:@"private XCTest text synthesis failed: %@", detail];
158+
return RunnerTextEntryResult(
159+
RunnerSynthesizedTextEntryStatusFailed,
160+
[NSString stringWithFormat:@"private XCTest text synthesis failed: %@", detail]
161+
);
132162
}
133-
return nil;
163+
return RunnerTextEntryResult(RunnerSynthesizedTextEntryStatusSucceeded, nil);
134164
} @catch (NSException *exception) {
135165
NSString *name = exception.name ?: @"NSException";
136166
NSString *reason = exception.reason ?: @"private XCTest text synthesis failed";
137-
return [NSString stringWithFormat:@"%@: %@", name, reason];
167+
return RunnerTextEntryResult(
168+
RunnerSynthesizedTextEntryStatusFailed,
169+
[NSString stringWithFormat:@"%@: %@", name, reason]
170+
);
138171
}
139172
}
140173

174+
static RunnerSynthesizedTextEntryResult *RunnerTextEntryResult(
175+
RunnerSynthesizedTextEntryStatus status,
176+
NSString * _Nullable message
177+
) {
178+
RunnerSynthesizedTextEntryResult *result = [RunnerSynthesizedTextEntryResult new];
179+
result.status = status;
180+
result.message = message;
181+
return result;
182+
}
183+
141184
static NSString * _Nullable RunnerRequireTextClass(Class cls, NSString *name) {
142185
if (cls == Nil) {
143186
return [NSString stringWithFormat:@"private XCTest text synthesis unavailable: missing %@", name];

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -2465,14 +2465,23 @@ extension RunnerTests {
24652465
let target: TextEntryTarget
24662466
var resolvedCoordinateContext: SynthesizedCoordinateContext?
24672467
var maestroNonHittableCoordinateFallbackUsed: Bool?
2468+
if command.allowNonHittableCoordinateFallback == true,
2469+
command.x != nil,
2470+
command.y != nil
2471+
{
2472+
// The shared runtime has already resolved this node as non-hittable and
2473+
// deliberately selected Maestro's coordinate compatibility route.
2474+
maestroNonHittableCoordinateFallbackUsed = true
2475+
}
24682476
let focusStartedAt = Date()
24692477
#if os(iOS)
2478+
let xCTestChannelPenalized = isSnapshotXCTestChannelPenalized(bundleId: currentBundleId)
24702479
var resolvedCoordinateTarget: TextEntryTarget?
24712480
if Self.shouldUseResolvedCoordinateTextEntryRoute(
24722481
repairMode: textEntryMode,
24732482
hasX: command.x != nil,
24742483
hasY: command.y != nil,
2475-
resolvedTextInputTarget: command.resolvedTextInputTarget == true
2484+
xCTestChannelPenalized: xCTestChannelPenalized
24762485
), let x = command.x, let y = command.y {
24772486
let policyKind = SynthesizedGesturePolicyKind.coordinateTap
24782487
let context = synthesizedCoordinateContext(
@@ -2482,27 +2491,35 @@ extension RunnerTests {
24822491
let (_, outcome) = performGesture(activeApp, idleTimeout: false) {
24832492
synthesizedTapAt(app: activeApp, x: x, y: y, context: context)
24842493
}
2485-
if let response = unsupportedResponse(for: outcome) {
2486-
return response
2494+
if Self.shouldFallbackFromSynthesizedTextEntryFocus(outcome) {
2495+
logSynthesizedGesturePolicyDecision(
2496+
kind: policyKind,
2497+
context: context,
2498+
fallbackAttempted: true
2499+
)
2500+
} else {
2501+
logSynthesizedGesturePolicyDecision(
2502+
kind: policyKind,
2503+
context: context,
2504+
fallbackAttempted: false
2505+
)
2506+
resolvedCoordinateContext = context
2507+
resolvedCoordinateTarget = TextEntryTarget(
2508+
element: nil,
2509+
refreshPoint: CGPoint(x: x, y: y),
2510+
prefersFocusedElement: false
2511+
)
24872512
}
2488-
logSynthesizedGesturePolicyDecision(
2489-
kind: policyKind,
2490-
context: context,
2491-
fallbackAttempted: false
2492-
)
2493-
resolvedCoordinateContext = context
2494-
resolvedCoordinateTarget = TextEntryTarget(
2495-
element: nil,
2496-
refreshPoint: CGPoint(x: x, y: y),
2497-
prefersFocusedElement: false
2498-
)
24992513
}
25002514
#else
2515+
let xCTestChannelPenalized = false
25012516
let resolvedCoordinateTarget: TextEntryTarget? = nil
25022517
#endif
25032518
if let resolvedCoordinateTarget {
25042519
target = resolvedCoordinateTarget
25052520
} else if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
2521+
// Released daemons may still send selector-keyed type commands even though current
2522+
// daemons resolve fill selectors through the runtime tree before reaching the runner.
25062523
let match = findElement(
25072524
app: activeApp,
25082525
selectorKey: selectorKey,
@@ -2537,7 +2554,7 @@ extension RunnerTests {
25372554
let canReplaceResolvedFirstResponder = Self.shouldUseSynthesizedFirstResponderReplacement(
25382555
hasResolvedElement: target.element != nil,
25392556
hasRefreshPoint: target.refreshPoint != nil,
2540-
resolvedTextInputTarget: command.resolvedTextInputTarget == true
2557+
xCTestChannelPenalized: xCTestChannelPenalized
25412558
)
25422559
#else
25432560
let canReplaceResolvedFirstResponder = false
@@ -2556,7 +2573,8 @@ extension RunnerTests {
25562573
text: text,
25572574
delaySeconds: delaySeconds,
25582575
repairMode: textEntryMode,
2559-
resolvedTextInputTarget: command.resolvedTextInputTarget == true,
2576+
xCTestChannelPenalized: xCTestChannelPenalized,
2577+
synthesizer: PrivateXCTestTextEntrySynthesizer(),
25602578
commandId: command.commandId
25612579
)
25622580
if textResult.verified == false {

apple/runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,6 @@ struct Command: Codable {
120120
let allowNonHittableCoordinateFallback: Bool?
121121
let delayMs: Int?
122122
let textEntryMode: String?
123-
let resolvedTextInputTarget: Bool?
124123
let action: String?
125124
let x: Double?
126125
let y: Double?

0 commit comments

Comments
 (0)