diff --git a/apps/ios/ModexCompanion/UITests/CompanionFlowTests.swift b/apps/ios/ModexCompanion/UITests/CompanionFlowTests.swift index 761c8143..ab36aba2 100644 --- a/apps/ios/ModexCompanion/UITests/CompanionFlowTests.swift +++ b/apps/ios/ModexCompanion/UITests/CompanionFlowTests.swift @@ -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.") diff --git a/apps/ios/ModexCompanion/UITests/XCUIElement+Ready.swift b/apps/ios/ModexCompanion/UITests/XCUIElement+Ready.swift index a89a8b9e..79d4cb85 100644 --- a/apps/ios/ModexCompanion/UITests/XCUIElement+Ready.swift +++ b/apps/ios/ModexCompanion/UITests/XCUIElement+Ready.swift @@ -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. diff --git a/docs/reviews/2026-10-30-iphone-secret-focus.md b/docs/reviews/2026-10-30-iphone-secret-focus.md new file mode 100644 index 00000000..3b90bd25 --- /dev/null +++ b/docs/reviews/2026-10-30-iphone-secret-focus.md @@ -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. diff --git a/docs/status.md b/docs/status.md index 14037936..46cee33a 100644 --- a/docs/status.md +++ b/docs/status.md @@ -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.