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
3 changes: 1 addition & 2 deletions apps/ios/ModexCompanion/UITests/CompanionFlowTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -441,8 +441,7 @@ final class CompanionFlowTests: XCTestCase {
let secret = " phone-secret-42 "
let token = app.secureTextFields.matching(NSPredicate(format: "identifier == %@", "answer-\(card)-token")).firstMatch
XCTAssertTrue(token.waitForExistence(timeout: UITestBudget.seconds(10)), "A secret question takes a secure field.")
token.tapWhenReady()
XCTAssertTrue(waitUntil(timeout: UITestBudget.seconds(8)) { app.keyboards.firstMatch.exists })
guard token.focusWhenReady(in: app) else { return }
token.typeText(secret)
// A secure field reads back as one bullet per character; wait for the last keystroke.
XCTAssertTrue(waitUntil(timeout: UITestBudget.seconds(10)) { (token.value as? String)?.count == secret.count }, "The secure field received every character.")
Expand Down
26 changes: 17 additions & 9 deletions apps/ios/ModexCompanion/UITests/XCUIElement+Ready.swift
Original file line number Diff line number Diff line change
Expand Up @@ -165,23 +165,31 @@ extension XCUIElement {
func typeTextWhenReady(_ text: String, in app: XCUIApplication, timeout: TimeInterval = UITestBudget.seconds(30),
file: StaticString = #filePath, line: UInt = #line) {
let deadline = Date(timeIntervalSinceNow: timeout)
guard focusWhenReady(in: app, timeout: timeout, file: file, line: line) else { return }
typeText(text)
let observation = observeTypedText(text, timeout: max(1, deadline.timeIntervalSinceNow)) { value as? String }
guard observation.matches else {
XCTFail("The field did not finish receiving the typed text; it holds \(observation.value.map { "\"\($0)\"" } ?? "nothing").", file: file, line: line)
return
}
}

/// Waits for this field's keyboard focus before a caller sends keystrokes. Secure fields use
/// the same focus boundary while keeping their masked-value and exact-delivery assertions.
func focusWhenReady(in app: XCUIApplication, timeout: TimeInterval = UITestBudget.seconds(30),
file: StaticString = #filePath, line: UInt = #line) -> Bool {
let deadline = Date(timeIntervalSinceNow: timeout)
var taps = 0
repeat {
taps += 1
guard waitUntilReady(timeout: max(1, deadline.timeIntervalSinceNow), file: file, line: line) else { return }
guard waitUntilReady(timeout: max(1, deadline.timeIntervalSinceNow), file: file, line: line) else { return false }
tap()
if waitUntil(timeout: min(UITestBudget.seconds(8), max(1, deadline.timeIntervalSinceNow)), { hasKeyboardFocus(in: app) }) { break }
if waitUntil(timeout: min(UITestBudget.seconds(8), max(1, deadline.timeIntervalSinceNow)), { hasKeyboardFocus(in: app) }) { return true }
guard taps < 3, Date() < deadline else {
XCTFail("\(self) did not take keyboard focus after \(taps) taps.", file: file, line: line)
return
return false
}
} while true
typeText(text)
let observation = observeTypedText(text, timeout: max(1, deadline.timeIntervalSinceNow)) { value as? String }
guard observation.matches else {
XCTFail("The field did not finish receiving the typed text; it holds \(observation.value.map { "\"\($0)\"" } ?? "nothing").", file: file, line: line)
return
}
}

/// The element's frame when it exists, is enabled and has a finite, non-empty frame; nil otherwise.
Expand Down
11 changes: 11 additions & 0 deletions docs/reviews/2026-10-30-iphone-secret-focus.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# iPhone secure-answer focus readiness

Hosted PR #453 run 41043591862, iPhone job 124022084672, failed at `CompanionFlowTests.swift:446`: XCTest refused to type into the secure answer field because neither it nor a descendant had keyboard focus. The result bundle reports 41 passed, one unexpected failure, and one expected screenshot failure on iPhone 17 Pro / iOS 26.4.1. The original log and result archive are preserved outside the checkout in the `modex-land-and-prune` evidence directory; the archive SHA-256 is `f01794d52358fe1d48139111e0971c0912da18b4ff4a33cf82f06d483930b9cf`.

The test tapped the secure field and waited for any keyboard to exist. That observation does not prove this field received focus. Ordinary text entry already uses a bounded field-focus wait. This candidate extracts that existing focus step so the secure answer can use it before sending any keystrokes. The masked-character count, exact secret received by the fixture, and no-secret-in-transcript assertions remain unchanged. No application behavior, timeout scale, test selection, or failure policy changes.

The hosted failure is the initial failing regression. The full native UI suite passed on a dedicated iPhone 17 / iOS 27 simulator: 43 tests, zero failures, including the paired flow and secure answer. iOS 26 is not installed locally, so this establishes only the tested configuration; it does not independently explain why the original tap failed or establish physical-phone acceptance. Independent read-only review found no actionable findings. Native unit validation also passed: 60 tests, zero failures. Repository build and unit validation passed: 2,268 passed, 26 skipped, zero failures. Hosted checks and protected delivery remain pending.

A subsequent native unit command failed before tests because the generated Xcode project referenced LocalPairing.json after the E2E script had removed that temporary fixture. The first failure is preserved; regenerating the Xcode project corrected the validation setup, and the unchanged native unit suite then passed. No product source was changed to address that build failure.

The shared focus helper retains its existing compatibility fallback: when XCTest does not expose hasKeyboardFocus, it checks for any keyboard. That fallback has not been qualified for a multi-field screen and is not claimed as field-specific focus evidence.
2 changes: 2 additions & 0 deletions docs/status.md
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,8 @@ The published application source is `2eda138` (v0.4.0). Later source deliveries

## Known rough edges

- **iPhone secure-answer focus readiness.** Hosted #453 failed when typing into a secure field that did not have keyboard focus, despite a keyboard being visible. A separate test-harness candidate reuses the existing field-focus wait while retaining masked-value and exact secret-delivery assertions. The dedicated iOS 27 simulator passed all 43 native UI and 60 native unit tests, and independent review found no actionable findings. Repository build/unit validation passed (2,268 passed, 26 skipped); protected delivery remains pending and the original iOS 26.4.1 failure stays preserved. [Evidence](reviews/2026-10-30-iphone-secret-focus.md).

Fixed ones move to [resolved rough edges](reviews/2026-10-18-resolved-rough-edges.md), with the PR
that fixed each.

Expand Down
Loading