Skip to content

Commit baaee00

Browse files
committed
feat: add Maestro replay compatibility
1 parent 0bc1b1e commit baaee00

32 files changed

Lines changed: 1417 additions & 282 deletions

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,12 @@ extension RunnerTests {
252252
)
253253
case .tap:
254254
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
255-
let match = findElement(app: activeApp, selectorKey: selectorKey, selectorValue: selectorValue)
255+
let match = findElement(
256+
app: activeApp,
257+
selectorKey: selectorKey,
258+
selectorValue: selectorValue,
259+
allowNonHittableFallback: command.allowNonHittableSelectorTap == true
260+
)
256261
if match.isAmbiguous {
257262
return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
258263
}
@@ -264,7 +269,14 @@ extension RunnerTests {
264269
var outcome = RunnerInteractionOutcome.performed
265270
let timing = measureGesture {
266271
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
267-
outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
272+
if match.usedNonHittableFallback {
273+
// Maestro compatibility: RN E2E backdoor controls can be 1x1 and
274+
// reported non-hittable by XCTest, while Maestro still taps their
275+
// resolved bounds. Keep this behind the explicit replay-only flag.
276+
outcome = tapAt(app: activeApp, x: frame.midX, y: frame.midY)
277+
} else {
278+
outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
279+
}
268280
}
269281
}
270282
if let response = unsupportedResponse(for: outcome) {
@@ -273,7 +285,7 @@ extension RunnerTests {
273285
return Response(
274286
ok: true,
275287
data: DataPayload(
276-
message: "tapped",
288+
message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
277289
gestureStartUptimeMs: timing.gestureStartUptimeMs,
278290
gestureEndUptimeMs: timing.gestureEndUptimeMs,
279291
x: touchFrame?.x,

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift

Lines changed: 35 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ extension RunnerTests {
2727
struct SelectorElementMatch {
2828
let element: XCUIElement?
2929
let isAmbiguous: Bool
30+
let usedNonHittableFallback: Bool
3031
}
3132

3233
enum TextTypingRepairMode {
@@ -177,10 +178,15 @@ extension RunnerTests {
177178
return element.exists ? element : nil
178179
}
179180

180-
func findElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> SelectorElementMatch {
181+
func findElement(
182+
app: XCUIApplication,
183+
selectorKey: String,
184+
selectorValue: String,
185+
allowNonHittableFallback: Bool = false
186+
) -> SelectorElementMatch {
181187
let value = selectorValue.trimmingCharacters(in: .whitespacesAndNewlines)
182188
guard !value.isEmpty else {
183-
return SelectorElementMatch(element: nil, isAmbiguous: false)
189+
return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
184190
}
185191
let predicate: NSPredicate
186192
switch selectorKey {
@@ -193,21 +199,44 @@ extension RunnerTests {
193199
case "text":
194200
predicate = NSPredicate(format: "label ==[c] %@ OR identifier ==[c] %@ OR value ==[c] %@", value, value, value)
195201
default:
196-
return SelectorElementMatch(element: nil, isAmbiguous: false)
202+
return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
197203
}
198204

199205
var matchedElement: XCUIElement?
206+
var nonHittableElement: XCUIElement?
200207
let matches = app.descendants(matching: .any).matching(predicate).allElementsBoundByIndex
201208
for element in matches where element.exists {
202-
guard element.isHittable else {
209+
if !element.isHittable {
210+
if allowNonHittableFallback && hasTappableFrame(app: app, element: element) {
211+
guard nonHittableElement == nil else {
212+
return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
213+
}
214+
nonHittableElement = element
215+
}
203216
continue
204217
}
205218
guard matchedElement == nil else {
206-
return SelectorElementMatch(element: nil, isAmbiguous: true)
219+
return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
207220
}
208221
matchedElement = element
209222
}
210-
return SelectorElementMatch(element: matchedElement, isAmbiguous: false)
223+
if let matchedElement {
224+
return SelectorElementMatch(element: matchedElement, isAmbiguous: false, usedNonHittableFallback: false)
225+
}
226+
return SelectorElementMatch(
227+
element: nonHittableElement,
228+
isAmbiguous: false,
229+
usedNonHittableFallback: nonHittableElement != nil
230+
)
231+
}
232+
233+
private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
234+
let frame = element.frame
235+
if frame.isEmpty {
236+
return false
237+
}
238+
let appFrame = app.frame
239+
return appFrame.isEmpty || appFrame.intersects(frame)
211240
}
212241

213242
func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {

ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ struct Command: Codable {
3737
let text: String?
3838
let selectorKey: String?
3939
let selectorValue: String?
40+
let allowNonHittableSelectorTap: Bool?
4041
let delayMs: Int?
4142
let textEntryMode: String?
4243
let clearFirst: Bool?

src/compat/__tests__/replay-input.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ test('parseReplayInput routes compat replay scripts through the selected parser'
3232
parsed.actions.map((action) => [action.command, action.positionals]),
3333
[
3434
['open', ['com.callstack.agentdevicelab']],
35-
['click', ['id="submit-order"']],
35+
['__maestroTapOn', ['id="submit-order"']],
3636
],
3737
);
3838
});
@@ -60,7 +60,7 @@ env:
6060
parsed.actions.map((action) => [action.command, action.positionals]),
6161
[
6262
['open', ['cli-app']],
63-
['click', ['id="shell-button"']],
63+
['__maestroTapOn', ['id="shell-button"']],
6464
],
6565
);
6666
});

0 commit comments

Comments
 (0)