Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -252,7 +252,12 @@ extension RunnerTests {
)
case .tap:
if let selectorKey = command.selectorKey, let selectorValue = command.selectorValue {
let match = findElement(app: activeApp, selectorKey: selectorKey, selectorValue: selectorValue)
let match = findElement(
app: activeApp,
selectorKey: selectorKey,
selectorValue: selectorValue,
allowNonHittableFallback: command.allowNonHittableSelectorTap == true
)
if match.isAmbiguous {
return Response(ok: false, error: ErrorPayload(code: "AMBIGUOUS_MATCH", message: "selector matched multiple elements"))
}
Expand All @@ -264,16 +269,24 @@ extension RunnerTests {
var outcome = RunnerInteractionOutcome.performed
let timing = measureGesture {
withTemporaryScrollIdleTimeoutIfSupported(activeApp) {
outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
if match.usedNonHittableFallback {
// Maestro compatibility: RN E2E backdoor controls can be 1x1 and
// reported non-hittable by XCTest, while Maestro still taps their
// resolved bounds. Keep this behind the explicit replay-only flag.
outcome = tapAt(app: activeApp, x: frame.midX, y: frame.midY)
} else {
outcome = activateElement(app: activeApp, element: element, action: "tap by selector")
}
}
}
if let response = unsupportedResponse(for: outcome) {
return response
}
waitForTextEntryReadinessAfterTap(app: activeApp, element: element)
return Response(
ok: true,
data: DataPayload(
message: "tapped",
message: match.usedNonHittableFallback ? "tapped via non-hittable coordinate fallback" : "tapped",
gestureStartUptimeMs: timing.gestureStartUptimeMs,
gestureEndUptimeMs: timing.gestureEndUptimeMs,
x: touchFrame?.x,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ extension RunnerTests {
struct SelectorElementMatch {
let element: XCUIElement?
let isAmbiguous: Bool
let usedNonHittableFallback: Bool
}

enum TextTypingRepairMode {
Expand Down Expand Up @@ -177,10 +178,15 @@ extension RunnerTests {
return element.exists ? element : nil
}

func findElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> SelectorElementMatch {
func findElement(
app: XCUIApplication,
selectorKey: String,
selectorValue: String,
allowNonHittableFallback: Bool = false
) -> SelectorElementMatch {
let value = selectorValue.trimmingCharacters(in: .whitespacesAndNewlines)
guard !value.isEmpty else {
return SelectorElementMatch(element: nil, isAmbiguous: false)
return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
}
let predicate: NSPredicate
switch selectorKey {
Expand All @@ -193,21 +199,47 @@ extension RunnerTests {
case "text":
predicate = NSPredicate(format: "label ==[c] %@ OR identifier ==[c] %@ OR value ==[c] %@", value, value, value)
default:
return SelectorElementMatch(element: nil, isAmbiguous: false)
return SelectorElementMatch(element: nil, isAmbiguous: false, usedNonHittableFallback: false)
}

var matchedElement: XCUIElement?
var nonHittableElement: XCUIElement?
let matches = app.descendants(matching: .any).matching(predicate).allElementsBoundByIndex
for element in matches where element.exists {
guard element.isHittable else {
if !element.isHittable {
if allowNonHittableFallback && hasTappableFrame(app: app, element: element) {
guard nonHittableElement == nil else {
return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
}
nonHittableElement = element
}
continue
}
guard matchedElement == nil else {
return SelectorElementMatch(element: nil, isAmbiguous: true)
return SelectorElementMatch(element: nil, isAmbiguous: true, usedNonHittableFallback: false)
}
matchedElement = element
}
return SelectorElementMatch(element: matchedElement, isAmbiguous: false)
if let matchedElement {
return SelectorElementMatch(element: matchedElement, isAmbiguous: false, usedNonHittableFallback: false)
}
return SelectorElementMatch(
element: nonHittableElement,
isAmbiguous: false,
usedNonHittableFallback: nonHittableElement != nil
)
}

private func hasTappableFrame(app: XCUIApplication, element: XCUIElement) -> Bool {
let frame = element.frame
if frame.isEmpty {
return false
}
let appFrame = app.frame
if appFrame.isEmpty {
return true
}
return appFrame.contains(CGPoint(x: frame.midX, y: frame.midY))
}

func queryElement(app: XCUIApplication, selectorKey: String, selectorValue: String) -> Response {
Expand Down Expand Up @@ -780,6 +812,35 @@ extension RunnerTests {
#endif
}

func waitForTextEntryReadinessAfterTap(app: XCUIApplication, element: XCUIElement) {
#if os(iOS)
switch element.elementType {
case .textField, .secureTextField, .searchField, .textView:
if waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout) != nil {
return
}
let frame = element.frame
if !frame.isEmpty {
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
_ = waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout)
}
default:
return
}
#endif
}

private func waitForFocusedTextInput(app: XCUIApplication, timeout: TimeInterval) -> XCUIElement? {
let deadline = Date().addingTimeInterval(timeout)
while Date() < deadline {
if let focused = focusedTextInput(app: app) {
return focused
}
sleepFor(TextEntryTiming.pollInterval)
}
return focusedTextInput(app: app)
}

private func textEntryRefreshPoint(for element: XCUIElement?) -> CGPoint? {
guard let element else {
return nil
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ struct Command: Codable {
let text: String?
let selectorKey: String?
let selectorValue: String?
let allowNonHittableSelectorTap: Bool?
let delayMs: Int?
let textEntryMode: String?
let clearFirst: Bool?
Expand Down
4 changes: 2 additions & 2 deletions src/compat/__tests__/replay-input.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ test('parseReplayInput routes compat replay scripts through the selected parser'
parsed.actions.map((action) => [action.command, action.positionals]),
[
['open', ['com.callstack.agentdevicelab']],
['click', ['id="submit-order"']],
['__maestroTapOn', ['id="submit-order"']],
],
);
});
Expand Down Expand Up @@ -60,7 +60,7 @@ env:
parsed.actions.map((action) => [action.command, action.positionals]),
[
['open', ['cli-app']],
['click', ['id="shell-button"']],
['__maestroTapOn', ['id="shell-button"']],
],
);
});
Expand Down
Loading
Loading