Skip to content
Merged
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 @@ -335,168 +335,6 @@ extension RunnerTests {
&& point.y <= frame.maxY + tolerance
}

func isKeyboardVisible(app: XCUIApplication) -> Bool {
return visibleKeyboardFrame(app: app) != nil
}

func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
let wasVisible = isKeyboardVisible(app: app)
guard wasVisible else {
return (wasVisible: false, dismissed: false, visible: false)
}

#if os(tvOS)
_ = pressTvRemote(.menu)
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
#else
if tapKeyboardDismissControl(app: app) {
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
}

return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
#endif
}

func pressKeyboardReturn(app: XCUIApplication) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
#if os(tvOS)
return (wasVisible: false, pressed: pressTvRemote(.select), visible: false)
#elseif os(iOS)
let wasVisible = isKeyboardVisible(app: app)
if tapKeyboardReturnControl(app: app) {
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
}

var typed = false
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
app.typeText(XCUIKeyboardKey.return.rawValue)
typed = true
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_IGNORED_EXCEPTION=%@",
exceptionMessage
)
if let singleTarget = singleTextEntryElement(app: app) {
return pressKeyboardReturn(on: singleTarget, app: app, wasVisible: wasVisible)
}
return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
}
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: typed, visible: isKeyboardVisible(app: app))
#else
return (wasVisible: false, pressed: false, visible: false)
#endif
}

private func pressKeyboardReturn(
on element: XCUIElement,
app: XCUIApplication,
wasVisible: Bool
) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
#if os(iOS)
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
element.tap()
element.typeText(XCUIKeyboardKey.return.rawValue)
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TARGET_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
}
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
#else
return (wasVisible: wasVisible, pressed: false, visible: false)
#endif
}

private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
#if os(iOS)
let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
guard element.exists else { return false }
switch element.elementType {
case .textField, .secureTextField, .searchField, .textView:
return true
default:
return false
}
}
}
return matches.count == 1 ? matches[0] : nil
#else
return nil
#endif
}

private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
#if os(tvOS)
return false
#else
guard let keyboardFrame = visibleKeyboardFrame(app: app) else {
return false
}
for label in ["Hide keyboard", "Dismiss keyboard", "Done"] {
let candidates = [
app.keyboards.buttons[label],
app.keyboards.keys[label],
app.keyboards.toolbars.buttons[label],
]
if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
hittable.tap()
return true
}

let toolbarButtonPredicate = NSPredicate(
format: "label == %@ OR identifier == %@",
label,
label
)
let toolbarButtons = app.descendants(matching: .button)
.matching(toolbarButtonPredicate)
.allElementsBoundByIndex
if let hittable = toolbarButtons.first(where: {
$0.exists && $0.isHittable && isKeyboardAccessoryControl($0, keyboardFrame: keyboardFrame)
}) {
hittable.tap()
return true
}
}
return false
#endif
}

private func tapKeyboardReturnControl(app: XCUIApplication) -> Bool {
#if os(iOS)
for label in ["return", "Return", "Enter", "Go", "Search", "Next", "Done", "Send", "Join"] {
let candidates = [
app.keyboards.buttons[label],
app.keyboards.keys[label],
]
if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
hittable.tap()
return true
}
}
#endif
return false
}

private func isKeyboardAccessoryControl(_ element: XCUIElement, keyboardFrame: CGRect) -> Bool {
let frame = element.frame
guard !frame.isEmpty && !keyboardFrame.isEmpty else {
return false
}
return frame.intersects(keyboardFrame) || abs(frame.maxY - keyboardFrame.minY) <= 80
}

private func readableText(for element: XCUIElement) -> String? {
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
Expand Down Expand Up @@ -1005,20 +843,6 @@ extension RunnerTests {
#endif
}

private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
#if os(iOS)
return safely("KEYBOARD_FRAME") {
let keyboard = app.keyboards.firstMatch
guard keyboard.exists else { return nil }
let keyboardFrame = keyboard.frame
guard !keyboardFrame.isEmpty else { return nil }
return keyboardFrame
}
#else
return nil
#endif
}

func axFreeSynthesizedDragPlan(
app: XCUIApplication,
x: Double,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
import XCTest

private enum KeyboardDismissObservationTiming {
static let timeout: TimeInterval = 2
}

extension RunnerTests {
func isKeyboardVisible(app: XCUIApplication) -> Bool {
return visibleKeyboardFrame(app: app) != nil
}

func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
let keyboard = app.keyboards.firstMatch
let wasVisible = isKeyboardVisible(app: app)
guard wasVisible else {
return (wasVisible: false, dismissed: false, visible: false)
}

#if os(tvOS)
_ = pressTvRemote(.menu)
sleepFor(0.2)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
#else
if tapKeyboardDismissControl(app: app) {
_ = keyboard.waitForNonExistence(timeout: KeyboardDismissObservationTiming.timeout)
let visible = isKeyboardVisible(app: app)
return (wasVisible: true, dismissed: !visible, visible: visible)
}

return (wasVisible: true, dismissed: false, visible: isKeyboardVisible(app: app))
#endif
}

func pressKeyboardReturn(app: XCUIApplication) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
#if os(tvOS)
return (wasVisible: false, pressed: pressTvRemote(.select), visible: false)
#elseif os(iOS)
let wasVisible = isKeyboardVisible(app: app)
if tapKeyboardReturnControl(app: app) {
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
}

var typed = false
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
app.typeText(XCUIKeyboardKey.return.rawValue)
typed = true
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_IGNORED_EXCEPTION=%@",
exceptionMessage
)
if let singleTarget = singleTextEntryElement(app: app) {
return pressKeyboardReturn(on: singleTarget, app: app, wasVisible: wasVisible)
}
return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
}
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: typed, visible: isKeyboardVisible(app: app))
#else
return (wasVisible: false, pressed: false, visible: false)
#endif
}

func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
#if os(iOS)
return safely("KEYBOARD_FRAME") {
let keyboard = app.keyboards.firstMatch
guard keyboard.exists else { return nil }
let keyboardFrame = keyboard.frame
guard !keyboardFrame.isEmpty else { return nil }
return keyboardFrame
}
#else
return nil
#endif
}

private func pressKeyboardReturn(
on element: XCUIElement,
app: XCUIApplication,
wasVisible: Bool
) -> (wasVisible: Bool, pressed: Bool, visible: Bool) {
#if os(iOS)
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
element.tap()
element.typeText(XCUIKeyboardKey.return.rawValue)
})
if let exceptionMessage {
NSLog(
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TARGET_IGNORED_EXCEPTION=%@",
exceptionMessage
)
return (wasVisible: wasVisible, pressed: false, visible: isKeyboardVisible(app: app))
}
sleepFor(0.2)
return (wasVisible: wasVisible, pressed: true, visible: isKeyboardVisible(app: app))
#else
return (wasVisible: wasVisible, pressed: false, visible: false)
#endif
}

private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
#if os(iOS)
let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
guard element.exists else { return false }
switch element.elementType {
case .textField, .secureTextField, .searchField, .textView:
return true
default:
return false
}
}
}
return matches.count == 1 ? matches[0] : nil
#else
return nil
#endif
}

private func tapKeyboardDismissControl(app: XCUIApplication) -> Bool {
#if os(tvOS)
return false
#else
guard let keyboardFrame = visibleKeyboardFrame(app: app) else {
return false
}
for label in ["Hide keyboard", "Dismiss keyboard", "Done"] {
let candidates = [
app.keyboards.buttons[label],
app.keyboards.keys[label],
app.keyboards.toolbars.buttons[label],
]
if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
hittable.tap()
return true
}

let toolbarButtonPredicate = NSPredicate(
format: "label == %@ OR identifier == %@",
label,
label
)
let toolbarButtons = app.descendants(matching: .button)
.matching(toolbarButtonPredicate)
.allElementsBoundByIndex
if let hittable = toolbarButtons.first(where: {
$0.exists && $0.isHittable && isKeyboardAccessoryControl($0, keyboardFrame: keyboardFrame)
}) {
hittable.tap()
return true
}
}
return false
#endif
}

private func tapKeyboardReturnControl(app: XCUIApplication) -> Bool {
#if os(iOS)
for label in ["return", "Return", "Enter", "Go", "Search", "Next", "Done", "Send", "Join"] {
let candidates = [
app.keyboards.buttons[label],
app.keyboards.keys[label],
]
if let hittable = candidates.first(where: { $0.exists && $0.isHittable }) {
hittable.tap()
return true
}
}
#endif
return false
}

private func isKeyboardAccessoryControl(_ element: XCUIElement, keyboardFrame: CGRect) -> Bool {
let frame = element.frame
guard !frame.isEmpty && !keyboardFrame.isEmpty else {
return false
}
return frame.intersects(keyboardFrame) || abs(frame.maxY - keyboardFrame.minY) <= 80
}
}
4 changes: 2 additions & 2 deletions src/core/dispatch-context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ export type DispatchContext = ScreenshotDispatchFlags & {
activity?: string;
launchConsole?: string;
launchArgs?: string[];
// iOS simulator only: relaunch via a single `simctl launch
// --terminate-running-process` instead of a separate terminate + launch.
// iOS simulator only: terminate the current app inside the platform open,
// either during `simctl launch` or immediately before `simctl openurl`.
terminateRunningApp?: boolean;
clearAppState?: boolean;
verbose?: boolean;
Expand Down
Loading
Loading