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
1 change: 1 addition & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ jobs:
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testTypeWithoutResolvedInputReturnsTypedFailureBeforeDispatchingText \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testBareTypeUsesTappedInputWhenSoftwareKeyboardIsHidden \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testBareDelayedTypeFailsWhenTappedInputDisappearsMidCommand \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testSynthesizedTextCommitProgressWalksExpectedPrefixOnly \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testTextEntryTapWitnessIsBoundToTargetIdentity \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testActivateTargetSkipsForegroundAndActivatesNonForegroundApplication \
-only-testing:AgentDeviceRunnerUITests/RunnerTests/testMissingBundleCommandInvalidatesCompleteCachedTargetState \
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,63 @@ extension RunnerTests {
repairMode == .none && fromTapWitness && !softwareKeyboardVisible
}

enum SynthesizedTextCommitProgress: Equatable {
case committed
case pending
case diverged
}

// The private synthesize call returns once the event record is posted, not once the target
// app has committed the characters, so intermediate reads walk prefix-by-prefix toward the
// expected value. Anything off that prefix path means the app transformed the input
// (formatter, mid-text caret, autocomplete) and the runner must not second-guess it. An
// unreadable value — secure field, or the element stopped resolving — ends the wait the
// same way.
static func synthesizedTextCommitProgress(
observedText: String?,
expectedText: String
) -> SynthesizedTextCommitProgress {
guard let observedText else {
return .diverged
}
if observedText == expectedText {
return .committed
}
return expectedText.hasPrefix(observedText) ? .pending : .diverged
}

/// Blocks until the synthesized bare-type text is observable in the target field, so `type`
/// cannot report ok while trailing characters are still uncommitted on a slow simulator.
///
/// Observation only. A stalled prefix cannot be told apart from a suffix still queued in the
/// event stream, so re-synthesizing the difference risks committing it twice after the command
/// already reported success. Text carrying a submit key is skipped outright: the app may clear
/// or rewrite the field on submit, so `textBefore + typedText` is not the value to wait for.
func awaitSynthesizedFirstResponderCommit(
app: XCUIApplication,
target: TextEntryTarget,
textBefore: String?,
typedText: String
) {
guard let textBefore, !typedText.contains("\n"), !typedText.contains("\r") else {
return
}
let expectedText = textBefore + typedText
let deadline = Date().addingTimeInterval(TextEntryTiming.synthesizedCommitTimeout)
while Date() < deadline {
let observedText = editableTextValue(
for: resolveTextEntryElement(app: app, target: target),
treatingPlaceholderAsEmpty: true
)
switch Self.synthesizedTextCommitProgress(observedText: observedText, expectedText: expectedText) {
case .committed, .diverged:
return
case .pending:
sleepFor(TextEntryTiming.pollInterval)
}
}
}

static func shouldUseResolvedCoordinateTextEntryRoute(
repairMode: TextTypingRepairMode,
hasX: Bool,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ extension RunnerTests {
static let pollInterval: TimeInterval = 0.02
static let warmupValueTimeout: TimeInterval = 0.4
static let verificationStabilityWindow: TimeInterval = 0.2
static let synthesizedCommitTimeout: TimeInterval = 3.0
}

struct TextEntryResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,35 @@ extension RunnerTests {
}
}

func testSynthesizedTextCommitProgressWalksExpectedPrefixOnly() {
let expected = "hardware-keyboard"
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: "hardware-keyboard", expectedText: expected),
.committed
)
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: "", expectedText: expected),
.pending
)
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: "hardware-keyboa", expectedText: expected),
.pending
)
// Transformed input (formatter, mid-text caret, autocomplete) must stop the wait.
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: "hardwarX", expectedText: expected),
.diverged
)
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: "hardware-keyboards", expectedText: expected),
.diverged
)
XCTAssertEqual(
Self.synthesizedTextCommitProgress(observedText: nil, expectedText: expected),
.diverged
)
}

#if os(iOS)
func testSynthesizedTextEntryFallsBackOnlyWhenPrivateSynthesisIsUnavailable() {
XCTAssertEqual(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,8 +111,22 @@ extension RunnerTests {
{
textEntryRoute = "synthesized-first-responder"
NSLog("AGENT_DEVICE_RUNNER_TEXT_ENTRY_ROUTE route=synthesized-first-responder")
let textBefore = editableTextValue(for: currentTarget, treatingPlaceholderAsEmpty: true)
switch synthesizer.enterText(app: app, text: value, replacingExistingText: false) {
case .continueTyping:
// No refresh point: like the tap-witness target itself, the commit wait must observe
// only the element the tap selected, never rediscover a different field.
awaitSynthesizedFirstResponderCommit(
app: app,
target: TextEntryTarget(
element: currentTarget,
refreshPoint: nil,
prefersFocusedElement: false,
fromTapWitness: true
),
textBefore: textBefore,
typedText: value
)
return (currentTarget, true, nil)
case .fallback:
return (nil, false, .synthesisUnavailable)
Expand Down
Loading