agent-device 0.16.8 → 0.16.10
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.md +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.apk → agent-device-android-multitouch-helper-0.16.10.apk} +0 -0
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.10.apk.sha256 +1 -0
- package/android-multitouch-helper/dist/{agent-device-android-multitouch-helper-0.16.8.manifest.json → agent-device-android-multitouch-helper-0.16.10.manifest.json} +4 -4
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.apk → agent-device-android-snapshot-helper-0.16.10.apk} +0 -0
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.10.apk.sha256 +1 -0
- package/android-snapshot-helper/dist/{agent-device-android-snapshot-helper-0.16.8.manifest.json → agent-device-android-snapshot-helper-0.16.10.manifest.json} +6 -6
- package/dist/src/2415.js +19 -19
- package/dist/src/8114.js +3 -3
- package/dist/src/apps.js +2 -2
- package/dist/src/generic.js +4 -3
- package/dist/src/input-actions.js +1 -1
- package/dist/src/session.js +2 -2
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandExecution.swift +197 -232
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+CommandJournal.swift +282 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Exceptions.swift +29 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Interaction.swift +8 -771
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Models.swift +30 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Snapshot.swift +2 -20
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+SystemModal.swift +10 -50
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+TextEntry.swift +723 -0
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests+Transport.swift +64 -22
- package/ios-runner/AgentDeviceRunner/AgentDeviceRunnerUITests/RunnerTests.swift +7 -4
- package/package.json +1 -1
- package/server.json +2 -2
- package/android-multitouch-helper/dist/agent-device-android-multitouch-helper-0.16.8.apk.sha256 +0 -1
- package/android-snapshot-helper/dist/agent-device-android-snapshot-helper-0.16.8.apk.sha256 +0 -1
|
@@ -30,48 +30,6 @@ extension RunnerTests {
|
|
|
30
30
|
let usedNonHittableFallback: Bool
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
enum TextTypingRepairMode {
|
|
34
|
-
case none
|
|
35
|
-
case append
|
|
36
|
-
case replacement
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
enum TextEntryTiming {
|
|
40
|
-
static let focusTimeout: TimeInterval = 0.4
|
|
41
|
-
static let repairReadinessTimeout: TimeInterval = 1.0
|
|
42
|
-
static let readinessTimeout: TimeInterval = 2.0
|
|
43
|
-
static let hardwareKeyboardFallbackTimeout: TimeInterval = 0.35
|
|
44
|
-
static let pollInterval: TimeInterval = 0.02
|
|
45
|
-
static let warmupValueTimeout: TimeInterval = 0.4
|
|
46
|
-
static let verificationStabilityWindow: TimeInterval = 0.2
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
struct TextEntryResult {
|
|
50
|
-
let verified: Bool?
|
|
51
|
-
let repaired: Bool
|
|
52
|
-
let expectedText: String?
|
|
53
|
-
let observedText: String?
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
struct TextEntryTarget {
|
|
57
|
-
let element: XCUIElement?
|
|
58
|
-
let refreshPoint: CGPoint?
|
|
59
|
-
let prefersFocusedElement: Bool
|
|
60
|
-
|
|
61
|
-
func withElement(_ nextElement: XCUIElement?) -> TextEntryTarget {
|
|
62
|
-
guard let nextElement else {
|
|
63
|
-
return self
|
|
64
|
-
}
|
|
65
|
-
let frame = nextElement.frame
|
|
66
|
-
let point = frame.isEmpty ? refreshPoint : CGPoint(x: frame.midX, y: frame.midY)
|
|
67
|
-
return TextEntryTarget(
|
|
68
|
-
element: nextElement,
|
|
69
|
-
refreshPoint: point,
|
|
70
|
-
prefersFocusedElement: prefersFocusedElement
|
|
71
|
-
)
|
|
72
|
-
}
|
|
73
|
-
}
|
|
74
|
-
|
|
75
33
|
// MARK: - Navigation Gestures
|
|
76
34
|
|
|
77
35
|
func tapInAppBackControl(app: XCUIApplication) -> Bool {
|
|
@@ -322,40 +280,18 @@ extension RunnerTests {
|
|
|
322
280
|
return nil
|
|
323
281
|
}
|
|
324
282
|
|
|
325
|
-
func clearTextInput(_ element: XCUIElement) {
|
|
326
|
-
// Skip the clear (delete burst + moveCaretToEnd edge-tap) ONLY when we can confirm the
|
|
327
|
-
// field is empty. Why skip: the edge-tap computes a point from the element frame, which can
|
|
328
|
-
// be stale after the field repositions on focus (e.g. the Settings search bar jumps
|
|
329
|
-
// bottom->top and reveals a "Suggestions" list) — tapping there navigates away instead of
|
|
330
|
-
// clearing; and replacing into an already-empty field is a no-op anyway.
|
|
331
|
-
// editableTextValue returns nil for secure (and unknown) fields, where we CANNOT confirm
|
|
332
|
-
// emptiness — those must still be cleared, or replace would concatenate stale + new text.
|
|
333
|
-
// So distinguish nil (clear) from "" (skip).
|
|
334
|
-
if let existing = editableTextValue(for: element, treatingPlaceholderAsEmpty: true),
|
|
335
|
-
existing.isEmpty {
|
|
336
|
-
return
|
|
337
|
-
}
|
|
338
|
-
#if !os(tvOS)
|
|
339
|
-
moveCaretToEnd(element: element)
|
|
340
|
-
#endif
|
|
341
|
-
let count = estimatedDeleteCount(for: element)
|
|
342
|
-
let deletes = String(repeating: XCUIKeyboardKey.delete.rawValue, count: count)
|
|
343
|
-
element.typeText(deletes)
|
|
344
|
-
}
|
|
345
|
-
|
|
346
283
|
func textInputAt(app: XCUIApplication, x: Double, y: Double) -> XCUIElement? {
|
|
347
284
|
return textInputCandidatesAt(app: app, point: CGPoint(x: x, y: y)).first
|
|
348
285
|
}
|
|
349
286
|
|
|
350
287
|
private func textInputCandidatesAt(app: XCUIApplication, point: CGPoint) -> [XCUIElement] {
|
|
351
|
-
|
|
352
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
288
|
+
safely("TEXT_INPUT_AT_POINT", []) {
|
|
353
289
|
// Query the text-input element types directly instead of enumerating the entire tree
|
|
354
290
|
// (app.descendants(.any).allElementsBoundByIndex snapshots every element and is ~10x
|
|
355
291
|
// slower — it dominated fill latency because resolveTextEntryElement re-runs this on
|
|
356
292
|
// each verify/repair poll once the focused field reference goes stale).
|
|
357
293
|
// Prefer the smallest matching field so nested editable controls win over large containers.
|
|
358
|
-
|
|
294
|
+
[
|
|
359
295
|
app.textFields,
|
|
360
296
|
app.secureTextFields,
|
|
361
297
|
app.searchFields,
|
|
@@ -381,15 +317,7 @@ extension RunnerTests {
|
|
|
381
317
|
}
|
|
382
318
|
return left.elementType.rawValue < right.elementType.rawValue
|
|
383
319
|
}
|
|
384
|
-
})
|
|
385
|
-
if let exceptionMessage {
|
|
386
|
-
NSLog(
|
|
387
|
-
"AGENT_DEVICE_RUNNER_TEXT_INPUT_AT_POINT_IGNORED_EXCEPTION=%@",
|
|
388
|
-
exceptionMessage
|
|
389
|
-
)
|
|
390
|
-
return []
|
|
391
320
|
}
|
|
392
|
-
return candidates
|
|
393
321
|
}
|
|
394
322
|
|
|
395
323
|
private func frameContainsPoint(_ frame: CGRect, _ point: CGPoint, tolerance: CGFloat) -> Bool {
|
|
@@ -399,610 +327,10 @@ extension RunnerTests {
|
|
|
399
327
|
&& point.y <= frame.maxY + tolerance
|
|
400
328
|
}
|
|
401
329
|
|
|
402
|
-
func focusedTextInput(app: XCUIApplication) -> XCUIElement? {
|
|
403
|
-
#if os(iOS)
|
|
404
|
-
// iOS focus predicates can return stale or misleading text-input matches
|
|
405
|
-
// under XCUITest, so text entry readiness is driven by tap/keyboard state.
|
|
406
|
-
return nil
|
|
407
|
-
#else
|
|
408
|
-
var focused: XCUIElement?
|
|
409
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
410
|
-
let candidates = app
|
|
411
|
-
.descendants(matching: .any)
|
|
412
|
-
.matching(NSPredicate(format: "hasKeyboardFocus == 1"))
|
|
413
|
-
.allElementsBoundByIndex
|
|
414
|
-
for candidate in candidates where candidate.exists {
|
|
415
|
-
switch candidate.elementType {
|
|
416
|
-
case .textField, .secureTextField, .searchField, .textView:
|
|
417
|
-
focused = candidate
|
|
418
|
-
return
|
|
419
|
-
default:
|
|
420
|
-
continue
|
|
421
|
-
}
|
|
422
|
-
}
|
|
423
|
-
})
|
|
424
|
-
if let exceptionMessage {
|
|
425
|
-
NSLog(
|
|
426
|
-
"AGENT_DEVICE_RUNNER_FOCUSED_INPUT_QUERY_IGNORED_EXCEPTION=%@",
|
|
427
|
-
exceptionMessage
|
|
428
|
-
)
|
|
429
|
-
return nil
|
|
430
|
-
}
|
|
431
|
-
return focused
|
|
432
|
-
#endif
|
|
433
|
-
}
|
|
434
|
-
|
|
435
|
-
func stabilizeTextInputBeforeTyping(app: XCUIApplication, target: XCUIElement?) -> XCUIElement? {
|
|
436
|
-
#if os(tvOS)
|
|
437
|
-
return target
|
|
438
|
-
#else
|
|
439
|
-
let latest = target
|
|
440
|
-
let keyboardVisibleAtEntry = isKeyboardVisible(app: app)
|
|
441
|
-
let deadline = Date().addingTimeInterval(TextEntryTiming.focusTimeout)
|
|
442
|
-
while Date() < deadline {
|
|
443
|
-
if let focused = focusedTextInput(app: app) {
|
|
444
|
-
return focused
|
|
445
|
-
}
|
|
446
|
-
// focusedTextInput is intentionally nil on iOS; treat the keyboard transitioning to
|
|
447
|
-
// visible after our tap as the focus-moved signal. Don't fast-path when it was already up.
|
|
448
|
-
if keyboardBecameVisible(app: app, wasVisibleAtEntry: keyboardVisibleAtEntry) {
|
|
449
|
-
return latest
|
|
450
|
-
}
|
|
451
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
452
|
-
}
|
|
453
|
-
return latest
|
|
454
|
-
#endif
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
func focusTextInputForTextEntry(app: XCUIApplication, x: Double?, y: Double?) -> TextEntryTarget {
|
|
458
|
-
guard let x, let y else {
|
|
459
|
-
let focused = waitForTextEntryReadiness(
|
|
460
|
-
app: app,
|
|
461
|
-
target: TextEntryTarget(
|
|
462
|
-
element: focusedTextInput(app: app),
|
|
463
|
-
refreshPoint: nil,
|
|
464
|
-
prefersFocusedElement: true
|
|
465
|
-
)
|
|
466
|
-
)
|
|
467
|
-
return TextEntryTarget(element: focused, refreshPoint: nil, prefersFocusedElement: true)
|
|
468
|
-
}
|
|
469
|
-
|
|
470
|
-
let target = textInputAt(app: app, x: x, y: y)
|
|
471
|
-
let requestedPoint = CGPoint(x: x, y: y)
|
|
472
|
-
if let target {
|
|
473
|
-
let frame = target.frame
|
|
474
|
-
if !frame.isEmpty {
|
|
475
|
-
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
476
|
-
} else {
|
|
477
|
-
_ = tapAt(app: app, x: x, y: y)
|
|
478
|
-
}
|
|
479
|
-
} else {
|
|
480
|
-
_ = tapAt(app: app, x: x, y: y)
|
|
481
|
-
}
|
|
482
|
-
let stabilized = stabilizeTextInputBeforeTyping(app: app, target: target)
|
|
483
|
-
let element = waitForTextEntryReadiness(
|
|
484
|
-
app: app,
|
|
485
|
-
target: TextEntryTarget(
|
|
486
|
-
element: stabilized ?? target,
|
|
487
|
-
refreshPoint: requestedPoint,
|
|
488
|
-
prefersFocusedElement: false
|
|
489
|
-
)
|
|
490
|
-
) ?? stabilized ?? target
|
|
491
|
-
return TextEntryTarget(
|
|
492
|
-
element: element,
|
|
493
|
-
refreshPoint: textEntryRefreshPoint(for: element) ?? requestedPoint,
|
|
494
|
-
prefersFocusedElement: false
|
|
495
|
-
)
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
func focusTextInputForTextEntry(app: XCUIApplication, element: XCUIElement) -> TextEntryTarget {
|
|
499
|
-
let point = textEntryRefreshPoint(for: element)
|
|
500
|
-
if let point {
|
|
501
|
-
_ = tapAt(app: app, x: point.x, y: point.y)
|
|
502
|
-
}
|
|
503
|
-
let stabilized = stabilizeTextInputBeforeTyping(app: app, target: element)
|
|
504
|
-
let resolved = waitForTextEntryReadiness(
|
|
505
|
-
app: app,
|
|
506
|
-
target: TextEntryTarget(
|
|
507
|
-
element: stabilized ?? element,
|
|
508
|
-
refreshPoint: point,
|
|
509
|
-
prefersFocusedElement: false
|
|
510
|
-
)
|
|
511
|
-
) ?? stabilized ?? element
|
|
512
|
-
return TextEntryTarget(
|
|
513
|
-
element: resolved,
|
|
514
|
-
refreshPoint: textEntryRefreshPoint(for: resolved) ?? point,
|
|
515
|
-
prefersFocusedElement: false
|
|
516
|
-
)
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
func isTextEntryElement(_ element: XCUIElement) -> Bool {
|
|
520
|
-
switch element.elementType {
|
|
521
|
-
case .textField, .secureTextField, .searchField, .textView:
|
|
522
|
-
return true
|
|
523
|
-
default:
|
|
524
|
-
return false
|
|
525
|
-
}
|
|
526
|
-
}
|
|
527
|
-
|
|
528
|
-
func resolveTextEntryMode(_ command: Command) -> TextTypingRepairMode {
|
|
529
|
-
switch command.textEntryMode {
|
|
530
|
-
case "append":
|
|
531
|
-
return .append
|
|
532
|
-
case "replace":
|
|
533
|
-
return .replacement
|
|
534
|
-
default:
|
|
535
|
-
return command.clearFirst == true ? .replacement : .none
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
|
|
539
|
-
func typeTextReliably(
|
|
540
|
-
app: XCUIApplication,
|
|
541
|
-
target: TextEntryTarget,
|
|
542
|
-
text: String,
|
|
543
|
-
delaySeconds: Double,
|
|
544
|
-
repairMode: TextTypingRepairMode = .none
|
|
545
|
-
) -> TextEntryResult {
|
|
546
|
-
guard !text.isEmpty else {
|
|
547
|
-
return TextEntryResult(verified: true, repaired: false, expectedText: "", observedText: "")
|
|
548
|
-
}
|
|
549
|
-
var activeTarget = target
|
|
550
|
-
let initialTarget = resolveTextEntryElement(app: app, target: activeTarget)
|
|
551
|
-
activeTarget = activeTarget.withElement(initialTarget)
|
|
552
|
-
let currentText = editableTextValue(for: initialTarget, treatingPlaceholderAsEmpty: true)
|
|
553
|
-
let initialText = repairMode == .append ? currentText : nil
|
|
554
|
-
let expectedText = expectedTextEntryValue(typedText: text, mode: repairMode, initialText: initialText)
|
|
555
|
-
|
|
556
|
-
if repairMode == .replacement {
|
|
557
|
-
guard let replacementTarget = initialTarget else {
|
|
558
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
|
|
559
|
-
}
|
|
560
|
-
if currentText == nil || currentText?.isEmpty == false {
|
|
561
|
-
clearTextInput(replacementTarget)
|
|
562
|
-
activeTarget = activeTarget.withElement(replacementTarget)
|
|
563
|
-
}
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
func typeIntoCurrentTarget(_ value: String) -> XCUIElement? {
|
|
567
|
-
if let currentTarget = resolveTextEntryElement(app: app, target: activeTarget) {
|
|
568
|
-
app.typeText(value)
|
|
569
|
-
return currentTarget
|
|
570
|
-
} else {
|
|
571
|
-
app.typeText(value)
|
|
572
|
-
return resolveTextEntryElement(app: app, target: activeTarget)
|
|
573
|
-
}
|
|
574
|
-
}
|
|
575
|
-
|
|
576
|
-
func waitForWarmupValue(_ expectedValue: String?, target: TextEntryTarget) {
|
|
577
|
-
guard let expectedValue else {
|
|
578
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
579
|
-
return
|
|
580
|
-
}
|
|
581
|
-
let deadline = Date().addingTimeInterval(TextEntryTiming.warmupValueTimeout)
|
|
582
|
-
while Date() < deadline {
|
|
583
|
-
if editableTextValue(for: resolveTextEntryElement(app: app, target: target)) == expectedValue {
|
|
584
|
-
return
|
|
585
|
-
}
|
|
586
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
|
|
590
|
-
let characters = Array(text)
|
|
591
|
-
if delaySeconds > 0 && characters.count > 1 {
|
|
592
|
-
var typedTarget: XCUIElement?
|
|
593
|
-
for (index, character) in characters.enumerated() {
|
|
594
|
-
typedTarget = typeIntoCurrentTarget(String(character)) ?? typedTarget
|
|
595
|
-
if index + 1 < characters.count {
|
|
596
|
-
sleepFor(delaySeconds)
|
|
597
|
-
}
|
|
598
|
-
}
|
|
599
|
-
if repairMode == .none {
|
|
600
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
|
|
601
|
-
}
|
|
602
|
-
let repairResult = repairTextEntryIfNeeded(
|
|
603
|
-
app: app,
|
|
604
|
-
target: activeTarget.withElement(typedTarget),
|
|
605
|
-
expectedText: expectedText,
|
|
606
|
-
repairMode: repairMode
|
|
607
|
-
)
|
|
608
|
-
return verifyTextEntry(
|
|
609
|
-
app: app,
|
|
610
|
-
target: activeTarget.withElement(typedTarget),
|
|
611
|
-
expectedText: expectedText,
|
|
612
|
-
repaired: repairResult.repaired
|
|
613
|
-
)
|
|
614
|
-
}
|
|
615
|
-
|
|
616
|
-
let typedTarget: XCUIElement?
|
|
617
|
-
if repairMode != .none && characters.count > 1 {
|
|
618
|
-
let firstCharacter = String(characters[0])
|
|
619
|
-
var firstTypedTarget = typeIntoCurrentTarget(firstCharacter)
|
|
620
|
-
activeTarget = activeTarget.withElement(firstTypedTarget)
|
|
621
|
-
let warmupExpectedText = expectedTextEntryValue(
|
|
622
|
-
typedText: firstCharacter,
|
|
623
|
-
mode: repairMode,
|
|
624
|
-
initialText: initialText
|
|
625
|
-
)
|
|
626
|
-
waitForWarmupValue(warmupExpectedText, target: activeTarget)
|
|
627
|
-
let remainingText = String(characters.dropFirst())
|
|
628
|
-
firstTypedTarget = typeIntoCurrentTarget(remainingText) ?? firstTypedTarget
|
|
629
|
-
typedTarget = firstTypedTarget
|
|
630
|
-
} else {
|
|
631
|
-
typedTarget = typeIntoCurrentTarget(text)
|
|
632
|
-
}
|
|
633
|
-
if repairMode == .none {
|
|
634
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: nil)
|
|
635
|
-
}
|
|
636
|
-
let repairResult = repairTextEntryIfNeeded(
|
|
637
|
-
app: app,
|
|
638
|
-
target: activeTarget.withElement(typedTarget),
|
|
639
|
-
expectedText: expectedText,
|
|
640
|
-
repairMode: repairMode
|
|
641
|
-
)
|
|
642
|
-
return verifyTextEntry(
|
|
643
|
-
app: app,
|
|
644
|
-
target: activeTarget.withElement(typedTarget),
|
|
645
|
-
expectedText: expectedText,
|
|
646
|
-
repaired: repairResult.repaired
|
|
647
|
-
)
|
|
648
|
-
}
|
|
649
|
-
|
|
650
|
-
private func repairTextEntryIfNeeded(
|
|
651
|
-
app: XCUIApplication,
|
|
652
|
-
target: TextEntryTarget,
|
|
653
|
-
expectedText: String?,
|
|
654
|
-
repairMode: TextTypingRepairMode
|
|
655
|
-
) -> TextEntryResult {
|
|
656
|
-
#if os(iOS)
|
|
657
|
-
guard let targetElement = resolveTextEntryElement(app: app, target: target) else {
|
|
658
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
|
|
659
|
-
}
|
|
660
|
-
guard let expectedText else {
|
|
661
|
-
let observedText = editableTextValue(for: targetElement)
|
|
662
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: nil, observedText: observedText)
|
|
663
|
-
}
|
|
664
|
-
guard shouldRepairTextEntry(
|
|
665
|
-
app: app,
|
|
666
|
-
target: target,
|
|
667
|
-
expectedText: expectedText,
|
|
668
|
-
repairMode: repairMode
|
|
669
|
-
) else {
|
|
670
|
-
return verifyTextEntry(app: app, target: target, expectedText: expectedText, repaired: false)
|
|
671
|
-
}
|
|
672
|
-
|
|
673
|
-
guard let repairTarget = resolveTextEntryElement(app: app, target: target) else {
|
|
674
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
|
|
675
|
-
}
|
|
676
|
-
let observedText = editableTextValue(for: repairTarget) ?? ""
|
|
677
|
-
NSLog(
|
|
678
|
-
"AGENT_DEVICE_RUNNER_REPAIR_TEXT_ENTRY expectedLength=%d observedLength=%d",
|
|
679
|
-
expectedText.count,
|
|
680
|
-
observedText.count
|
|
681
|
-
)
|
|
682
|
-
clearTextInput(repairTarget)
|
|
683
|
-
app.typeText(expectedText)
|
|
684
|
-
return verifyTextEntry(app: app, target: target, expectedText: expectedText, repaired: true)
|
|
685
|
-
#else
|
|
686
|
-
return TextEntryResult(verified: nil, repaired: false, expectedText: expectedText, observedText: nil)
|
|
687
|
-
#endif
|
|
688
|
-
}
|
|
689
|
-
|
|
690
|
-
private func verifyTextEntry(
|
|
691
|
-
app: XCUIApplication,
|
|
692
|
-
target: TextEntryTarget,
|
|
693
|
-
expectedText: String?,
|
|
694
|
-
repaired: Bool
|
|
695
|
-
) -> TextEntryResult {
|
|
696
|
-
let targetElement = resolveTextEntryElement(app: app, target: target)
|
|
697
|
-
guard let expectedText else {
|
|
698
|
-
return TextEntryResult(
|
|
699
|
-
verified: nil,
|
|
700
|
-
repaired: repaired,
|
|
701
|
-
expectedText: nil,
|
|
702
|
-
observedText: editableTextValue(for: targetElement)
|
|
703
|
-
)
|
|
704
|
-
}
|
|
705
|
-
guard let observedText = editableTextValue(for: targetElement) else {
|
|
706
|
-
return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
|
|
707
|
-
}
|
|
708
|
-
guard textEntryValueMatchesExpected(targetElement, observedText: observedText, expectedText: expectedText) else {
|
|
709
|
-
return TextEntryResult(
|
|
710
|
-
verified: false,
|
|
711
|
-
repaired: repaired,
|
|
712
|
-
expectedText: expectedText,
|
|
713
|
-
observedText: observedText
|
|
714
|
-
)
|
|
715
|
-
}
|
|
716
|
-
let stableDeadline = Date().addingTimeInterval(TextEntryTiming.verificationStabilityWindow)
|
|
717
|
-
var latestObservedText = observedText
|
|
718
|
-
while Date() < stableDeadline {
|
|
719
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
720
|
-
guard let nextObservedText = editableTextValue(for: resolveTextEntryElement(app: app, target: target)) else {
|
|
721
|
-
return TextEntryResult(verified: nil, repaired: repaired, expectedText: expectedText, observedText: nil)
|
|
722
|
-
}
|
|
723
|
-
latestObservedText = nextObservedText
|
|
724
|
-
guard textEntryValueMatchesExpected(
|
|
725
|
-
resolveTextEntryElement(app: app, target: target),
|
|
726
|
-
observedText: nextObservedText,
|
|
727
|
-
expectedText: expectedText
|
|
728
|
-
) else {
|
|
729
|
-
return TextEntryResult(
|
|
730
|
-
verified: false,
|
|
731
|
-
repaired: repaired,
|
|
732
|
-
expectedText: expectedText,
|
|
733
|
-
observedText: nextObservedText
|
|
734
|
-
)
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
return TextEntryResult(
|
|
738
|
-
verified: true,
|
|
739
|
-
repaired: repaired,
|
|
740
|
-
expectedText: expectedText,
|
|
741
|
-
observedText: latestObservedText
|
|
742
|
-
)
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
private func textEntryValueMatchesExpected(
|
|
746
|
-
_ element: XCUIElement?,
|
|
747
|
-
observedText: String,
|
|
748
|
-
expectedText: String
|
|
749
|
-
) -> Bool {
|
|
750
|
-
if observedText == expectedText {
|
|
751
|
-
return true
|
|
752
|
-
}
|
|
753
|
-
guard hasTextEntrySubmitSuffix(expectedText), element?.elementType != .textView else {
|
|
754
|
-
return false
|
|
755
|
-
}
|
|
756
|
-
var submittedText = expectedText
|
|
757
|
-
while hasTextEntrySubmitSuffix(submittedText) {
|
|
758
|
-
submittedText.removeLast()
|
|
759
|
-
}
|
|
760
|
-
return observedText == submittedText
|
|
761
|
-
}
|
|
762
|
-
|
|
763
|
-
private func hasTextEntrySubmitSuffix(_ text: String) -> Bool {
|
|
764
|
-
text.hasSuffix("\n") || text.hasSuffix("\r")
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
private func expectedTextEntryValue(
|
|
768
|
-
typedText: String,
|
|
769
|
-
mode: TextTypingRepairMode,
|
|
770
|
-
initialText: String?
|
|
771
|
-
) -> String? {
|
|
772
|
-
switch mode {
|
|
773
|
-
case .none:
|
|
774
|
-
return nil
|
|
775
|
-
case .append:
|
|
776
|
-
guard let initialText else {
|
|
777
|
-
return nil
|
|
778
|
-
}
|
|
779
|
-
return initialText + typedText
|
|
780
|
-
case .replacement:
|
|
781
|
-
return typedText
|
|
782
|
-
}
|
|
783
|
-
}
|
|
784
|
-
|
|
785
|
-
private func shouldRepairTextEntry(
|
|
786
|
-
app: XCUIApplication,
|
|
787
|
-
target: TextEntryTarget,
|
|
788
|
-
expectedText: String,
|
|
789
|
-
repairMode: TextTypingRepairMode
|
|
790
|
-
) -> Bool {
|
|
791
|
-
#if os(iOS)
|
|
792
|
-
var latestObservedText: String?
|
|
793
|
-
let deadline = Date().addingTimeInterval(TextEntryTiming.verificationStabilityWindow)
|
|
794
|
-
repeat {
|
|
795
|
-
guard let observedText = editableTextValue(for: resolveTextEntryElement(app: app, target: target)) else {
|
|
796
|
-
return false
|
|
797
|
-
}
|
|
798
|
-
if textEntryValueMatchesExpected(
|
|
799
|
-
resolveTextEntryElement(app: app, target: target),
|
|
800
|
-
observedText: observedText,
|
|
801
|
-
expectedText: expectedText
|
|
802
|
-
) {
|
|
803
|
-
return false
|
|
804
|
-
}
|
|
805
|
-
latestObservedText = observedText
|
|
806
|
-
if !isRepairableTextEntryMismatch(
|
|
807
|
-
observedText: observedText,
|
|
808
|
-
expectedText: expectedText,
|
|
809
|
-
repairMode: repairMode
|
|
810
|
-
) {
|
|
811
|
-
return false
|
|
812
|
-
}
|
|
813
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
814
|
-
} while Date() < deadline
|
|
815
|
-
|
|
816
|
-
guard let latestObservedText else {
|
|
817
|
-
return false
|
|
818
|
-
}
|
|
819
|
-
guard !textEntryValueMatchesExpected(
|
|
820
|
-
resolveTextEntryElement(app: app, target: target),
|
|
821
|
-
observedText: latestObservedText,
|
|
822
|
-
expectedText: expectedText
|
|
823
|
-
) else {
|
|
824
|
-
return false
|
|
825
|
-
}
|
|
826
|
-
return isRepairableTextEntryMismatch(
|
|
827
|
-
observedText: latestObservedText,
|
|
828
|
-
expectedText: expectedText,
|
|
829
|
-
repairMode: repairMode
|
|
830
|
-
)
|
|
831
|
-
#else
|
|
832
|
-
return false
|
|
833
|
-
#endif
|
|
834
|
-
}
|
|
835
|
-
|
|
836
|
-
private func isRepairableTextEntryMismatch(
|
|
837
|
-
observedText: String,
|
|
838
|
-
expectedText: String,
|
|
839
|
-
repairMode: TextTypingRepairMode
|
|
840
|
-
) -> Bool {
|
|
841
|
-
guard observedText != expectedText else {
|
|
842
|
-
return false
|
|
843
|
-
}
|
|
844
|
-
if repairMode == .replacement {
|
|
845
|
-
return true
|
|
846
|
-
}
|
|
847
|
-
return observedText.isEmpty || isLikelyDroppedCharacterTextEntryMismatch(
|
|
848
|
-
observedText: observedText,
|
|
849
|
-
expectedText: expectedText
|
|
850
|
-
)
|
|
851
|
-
}
|
|
852
|
-
|
|
853
|
-
private func isLikelyDroppedCharacterTextEntryMismatch(observedText: String, expectedText: String) -> Bool {
|
|
854
|
-
guard observedText.count < expectedText.count else {
|
|
855
|
-
return false
|
|
856
|
-
}
|
|
857
|
-
let missingCharacterCount = expectedText.count - observedText.count
|
|
858
|
-
guard missingCharacterCount <= max(2, expectedText.count / 4) else {
|
|
859
|
-
return false
|
|
860
|
-
}
|
|
861
|
-
var expectedIndex = expectedText.startIndex
|
|
862
|
-
for character in observedText {
|
|
863
|
-
guard let matchIndex = expectedText[expectedIndex...].firstIndex(of: character) else {
|
|
864
|
-
return false
|
|
865
|
-
}
|
|
866
|
-
expectedIndex = expectedText.index(after: matchIndex)
|
|
867
|
-
}
|
|
868
|
-
return true
|
|
869
|
-
}
|
|
870
|
-
|
|
871
|
-
private func resolveTextEntryElement(app: XCUIApplication, target: TextEntryTarget) -> XCUIElement? {
|
|
872
|
-
if target.prefersFocusedElement {
|
|
873
|
-
if let focused = focusedTextInput(app: app) {
|
|
874
|
-
return focused
|
|
875
|
-
}
|
|
876
|
-
if let element = target.element, element.exists {
|
|
877
|
-
return element
|
|
878
|
-
}
|
|
879
|
-
} else {
|
|
880
|
-
if let element = target.element, element.exists {
|
|
881
|
-
return element
|
|
882
|
-
}
|
|
883
|
-
}
|
|
884
|
-
if let refreshPoint = target.refreshPoint,
|
|
885
|
-
let refreshed = textInputAt(app: app, x: refreshPoint.x, y: refreshPoint.y) {
|
|
886
|
-
return refreshed
|
|
887
|
-
}
|
|
888
|
-
if let focused = focusedTextInput(app: app) {
|
|
889
|
-
return focused
|
|
890
|
-
}
|
|
891
|
-
return nil
|
|
892
|
-
}
|
|
893
|
-
|
|
894
|
-
private func waitForTextEntryReadiness(
|
|
895
|
-
app: XCUIApplication,
|
|
896
|
-
target: TextEntryTarget,
|
|
897
|
-
timeout: TimeInterval = TextEntryTiming.readinessTimeout
|
|
898
|
-
) -> XCUIElement? {
|
|
899
|
-
#if os(iOS)
|
|
900
|
-
var latest = resolveTextEntryElement(app: app, target: target)
|
|
901
|
-
let keyboardVisibleAtEntry = isKeyboardVisible(app: app)
|
|
902
|
-
let deadline = Date().addingTimeInterval(timeout)
|
|
903
|
-
let hardwareKeyboardFallback = Date().addingTimeInterval(
|
|
904
|
-
min(TextEntryTiming.hardwareKeyboardFallbackTimeout, timeout)
|
|
905
|
-
)
|
|
906
|
-
var sawSoftwareKeyboard = false
|
|
907
|
-
while Date() < deadline {
|
|
908
|
-
if let focused = focusedTextInput(app: app) {
|
|
909
|
-
latest = focused
|
|
910
|
-
if isKeyboardVisible(app: app) {
|
|
911
|
-
return focused
|
|
912
|
-
}
|
|
913
|
-
}
|
|
914
|
-
// Fast-path on a keyboard hidden->visible transition: our tapped field gained focus, so
|
|
915
|
-
// return immediately instead of burning the full readinessTimeout (warmup-first-char echo
|
|
916
|
-
// + post-type verify/repair remain as drop safety nets). When the keyboard was ALREADY up
|
|
917
|
-
// (back-to-back fills), this isn't a focus signal — fall through to the settle/timeout so
|
|
918
|
-
// text isn't sent to the previously-focused field.
|
|
919
|
-
if keyboardBecameVisible(app: app, wasVisibleAtEntry: keyboardVisibleAtEntry) {
|
|
920
|
-
return latest
|
|
921
|
-
}
|
|
922
|
-
sawSoftwareKeyboard = sawSoftwareKeyboard || keyboardElementExists(app: app)
|
|
923
|
-
if !sawSoftwareKeyboard && Date() >= hardwareKeyboardFallback && latest != nil {
|
|
924
|
-
return latest
|
|
925
|
-
}
|
|
926
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
927
|
-
}
|
|
928
|
-
return focusedTextInput(app: app) ?? latest
|
|
929
|
-
#else
|
|
930
|
-
return resolveTextEntryElement(app: app, target: target)
|
|
931
|
-
#endif
|
|
932
|
-
}
|
|
933
|
-
|
|
934
|
-
func waitForTextEntryReadinessAfterTap(app: XCUIApplication, element: XCUIElement) {
|
|
935
|
-
#if os(iOS)
|
|
936
|
-
switch element.elementType {
|
|
937
|
-
case .textField, .secureTextField, .searchField, .textView:
|
|
938
|
-
if waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout) != nil {
|
|
939
|
-
return
|
|
940
|
-
}
|
|
941
|
-
let frame = element.frame
|
|
942
|
-
if !frame.isEmpty {
|
|
943
|
-
_ = tapAt(app: app, x: frame.midX, y: frame.midY)
|
|
944
|
-
_ = waitForFocusedTextInput(app: app, timeout: TextEntryTiming.readinessTimeout)
|
|
945
|
-
}
|
|
946
|
-
default:
|
|
947
|
-
return
|
|
948
|
-
}
|
|
949
|
-
#endif
|
|
950
|
-
}
|
|
951
|
-
|
|
952
|
-
private func waitForFocusedTextInput(app: XCUIApplication, timeout: TimeInterval) -> XCUIElement? {
|
|
953
|
-
let deadline = Date().addingTimeInterval(timeout)
|
|
954
|
-
while Date() < deadline {
|
|
955
|
-
if let focused = focusedTextInput(app: app) {
|
|
956
|
-
return focused
|
|
957
|
-
}
|
|
958
|
-
sleepFor(TextEntryTiming.pollInterval)
|
|
959
|
-
}
|
|
960
|
-
return focusedTextInput(app: app)
|
|
961
|
-
}
|
|
962
|
-
|
|
963
|
-
private func textEntryRefreshPoint(for element: XCUIElement?) -> CGPoint? {
|
|
964
|
-
guard let element else {
|
|
965
|
-
return nil
|
|
966
|
-
}
|
|
967
|
-
let frame = element.frame
|
|
968
|
-
guard !frame.isEmpty else {
|
|
969
|
-
return nil
|
|
970
|
-
}
|
|
971
|
-
return CGPoint(x: frame.midX, y: frame.midY)
|
|
972
|
-
}
|
|
973
|
-
|
|
974
330
|
func isKeyboardVisible(app: XCUIApplication) -> Bool {
|
|
975
331
|
return visibleKeyboardFrame(app: app) != nil
|
|
976
332
|
}
|
|
977
333
|
|
|
978
|
-
/// A focus-moved signal for iOS text entry, where `focusedTextInput` is intentionally nil.
|
|
979
|
-
/// The software keyboard TRANSITIONING from hidden (at entry) to visible means the field we
|
|
980
|
-
/// just tapped gained first-responder. If the keyboard was ALREADY up (e.g. back-to-back
|
|
981
|
-
/// fills into different fields), its visibility is not evidence focus moved to the new field,
|
|
982
|
-
/// so callers must keep waiting rather than typing into the previously-focused field.
|
|
983
|
-
private func keyboardBecameVisible(app: XCUIApplication, wasVisibleAtEntry: Bool) -> Bool {
|
|
984
|
-
return !wasVisibleAtEntry && isKeyboardVisible(app: app)
|
|
985
|
-
}
|
|
986
|
-
|
|
987
|
-
private func keyboardElementExists(app: XCUIApplication) -> Bool {
|
|
988
|
-
#if os(iOS)
|
|
989
|
-
var exists = false
|
|
990
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
991
|
-
exists = app.keyboards.firstMatch.exists
|
|
992
|
-
})
|
|
993
|
-
if let exceptionMessage {
|
|
994
|
-
NSLog(
|
|
995
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_EXISTS_IGNORED_EXCEPTION=%@",
|
|
996
|
-
exceptionMessage
|
|
997
|
-
)
|
|
998
|
-
return false
|
|
999
|
-
}
|
|
1000
|
-
return exists
|
|
1001
|
-
#else
|
|
1002
|
-
return false
|
|
1003
|
-
#endif
|
|
1004
|
-
}
|
|
1005
|
-
|
|
1006
334
|
func dismissKeyboard(app: XCUIApplication) -> (wasVisible: Bool, dismissed: Bool, visible: Bool) {
|
|
1007
335
|
let wasVisible = isKeyboardVisible(app: app)
|
|
1008
336
|
guard wasVisible else {
|
|
@@ -1094,9 +422,8 @@ extension RunnerTests {
|
|
|
1094
422
|
|
|
1095
423
|
private func singleTextEntryElement(app: XCUIApplication) -> XCUIElement? {
|
|
1096
424
|
#if os(iOS)
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
matches = app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
|
|
425
|
+
let matches = safely("KEYBOARD_RETURN_TEXT_ENTRY_QUERY", []) {
|
|
426
|
+
app.descendants(matching: .any).allElementsBoundByIndex.filter { element in
|
|
1100
427
|
guard element.exists else { return false }
|
|
1101
428
|
switch element.elementType {
|
|
1102
429
|
case .textField, .secureTextField, .searchField, .textView:
|
|
@@ -1105,13 +432,6 @@ extension RunnerTests {
|
|
|
1105
432
|
return false
|
|
1106
433
|
}
|
|
1107
434
|
}
|
|
1108
|
-
})
|
|
1109
|
-
if let exceptionMessage {
|
|
1110
|
-
NSLog(
|
|
1111
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_RETURN_TEXT_ENTRY_QUERY_IGNORED_EXCEPTION=%@",
|
|
1112
|
-
exceptionMessage
|
|
1113
|
-
)
|
|
1114
|
-
return nil
|
|
1115
435
|
}
|
|
1116
436
|
return matches.count == 1 ? matches[0] : nil
|
|
1117
437
|
#else
|
|
@@ -1198,80 +518,6 @@ extension RunnerTests {
|
|
|
1198
518
|
return frame.intersects(keyboardFrame) || abs(frame.maxY - keyboardFrame.minY) <= 80
|
|
1199
519
|
}
|
|
1200
520
|
|
|
1201
|
-
private func moveCaretToEnd(element: XCUIElement) {
|
|
1202
|
-
#if os(tvOS)
|
|
1203
|
-
return
|
|
1204
|
-
#else
|
|
1205
|
-
let frame = element.frame
|
|
1206
|
-
guard !frame.isEmpty else {
|
|
1207
|
-
element.tap()
|
|
1208
|
-
return
|
|
1209
|
-
}
|
|
1210
|
-
let origin = element.coordinate(withNormalizedOffset: CGVector(dx: 0, dy: 0))
|
|
1211
|
-
let target = origin.withOffset(
|
|
1212
|
-
CGVector(dx: max(2, frame.width - 4), dy: max(2, frame.height / 2))
|
|
1213
|
-
)
|
|
1214
|
-
target.tap()
|
|
1215
|
-
#endif
|
|
1216
|
-
}
|
|
1217
|
-
|
|
1218
|
-
private func estimatedDeleteCount(for element: XCUIElement) -> Int {
|
|
1219
|
-
let valueText = normalizedElementText(element.value)
|
|
1220
|
-
let base = valueText.isEmpty ? 24 : (valueText.count + 8)
|
|
1221
|
-
return max(24, min(120, base))
|
|
1222
|
-
}
|
|
1223
|
-
|
|
1224
|
-
private func normalizedElementText(_ value: Any?) -> String {
|
|
1225
|
-
String(describing: value ?? "")
|
|
1226
|
-
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1227
|
-
}
|
|
1228
|
-
|
|
1229
|
-
private func editableTextValue(
|
|
1230
|
-
for element: XCUIElement?,
|
|
1231
|
-
treatingPlaceholderAsEmpty: Bool = false
|
|
1232
|
-
) -> String? {
|
|
1233
|
-
guard let element else {
|
|
1234
|
-
return nil
|
|
1235
|
-
}
|
|
1236
|
-
switch element.elementType {
|
|
1237
|
-
case .textField, .searchField, .textView:
|
|
1238
|
-
let value = String(describing: element.value ?? "")
|
|
1239
|
-
if treatingPlaceholderAsEmpty && isPlaceholderValue(value, for: element) {
|
|
1240
|
-
return ""
|
|
1241
|
-
}
|
|
1242
|
-
return value
|
|
1243
|
-
case .secureTextField:
|
|
1244
|
-
return nil
|
|
1245
|
-
default:
|
|
1246
|
-
return nil
|
|
1247
|
-
}
|
|
1248
|
-
}
|
|
1249
|
-
|
|
1250
|
-
private func isPlaceholderValue(_ value: String, for element: XCUIElement) -> Bool {
|
|
1251
|
-
let normalizedValue = value.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1252
|
-
guard !normalizedValue.isEmpty else {
|
|
1253
|
-
return false
|
|
1254
|
-
}
|
|
1255
|
-
let placeholder = element.placeholderValue?.trimmingCharacters(in: .whitespacesAndNewlines) ?? ""
|
|
1256
|
-
if !placeholder.isEmpty && normalizedValue == placeholder {
|
|
1257
|
-
return true
|
|
1258
|
-
}
|
|
1259
|
-
if isGenericTextInputLabel(normalizedValue) {
|
|
1260
|
-
return true
|
|
1261
|
-
}
|
|
1262
|
-
let normalizedLabel = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1263
|
-
return normalizedLabel == normalizedValue && isGenericTextInputLabel(normalizedLabel)
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
private func isGenericTextInputLabel(_ value: String) -> Bool {
|
|
1267
|
-
switch value {
|
|
1268
|
-
case "Text input field":
|
|
1269
|
-
return true
|
|
1270
|
-
default:
|
|
1271
|
-
return false
|
|
1272
|
-
}
|
|
1273
|
-
}
|
|
1274
|
-
|
|
1275
521
|
private func readableText(for element: XCUIElement) -> String? {
|
|
1276
522
|
let label = element.label.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
1277
523
|
let identifier = element.identifier.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
@@ -1521,22 +767,13 @@ extension RunnerTests {
|
|
|
1521
767
|
|
|
1522
768
|
private func visibleKeyboardFrame(app: XCUIApplication) -> CGRect? {
|
|
1523
769
|
#if os(iOS)
|
|
1524
|
-
|
|
1525
|
-
let exceptionMessage = RunnerObjCExceptionCatcher.catchException({
|
|
770
|
+
return safely("KEYBOARD_FRAME") {
|
|
1526
771
|
let keyboard = app.keyboards.firstMatch
|
|
1527
|
-
guard keyboard.exists else { return }
|
|
772
|
+
guard keyboard.exists else { return nil }
|
|
1528
773
|
let keyboardFrame = keyboard.frame
|
|
1529
|
-
guard !keyboardFrame.isEmpty else { return }
|
|
1530
|
-
|
|
1531
|
-
})
|
|
1532
|
-
if let exceptionMessage {
|
|
1533
|
-
NSLog(
|
|
1534
|
-
"AGENT_DEVICE_RUNNER_KEYBOARD_FRAME_IGNORED_EXCEPTION=%@",
|
|
1535
|
-
exceptionMessage
|
|
1536
|
-
)
|
|
1537
|
-
return nil
|
|
774
|
+
guard !keyboardFrame.isEmpty else { return nil }
|
|
775
|
+
return keyboardFrame
|
|
1538
776
|
}
|
|
1539
|
-
return frame
|
|
1540
777
|
#else
|
|
1541
778
|
return nil
|
|
1542
779
|
#endif
|