// BackgroundLifecycleUITests.swift // T-2.10 — Background lifecycle UI tests. // // Launch args used: // --uitest existing arg: skips live WS, shows placeholder // --uitest-with-stub-connection NEW (T-2.10): impl agent must add handling // in MainTerminalView.initialBootstrap() so that // a stub/in-process connection is established and // the background lifecycle hooks (suspend/resume) // fire and update the status bar. Without this arg // the app uses plain --uitest mode (no connection, // no reconnect status text) and the Reconnecting // assertions below FAIL — which is the intended // failing state for TDD step 1. // // Status bar text expected values (T-2.10 impl): // • During reconnect window: "Reconnecting…" (new) // • After reconnect settled: "● " (existing connected text) // // Currently MainTerminalView shows "Connecting…" for .connecting state (not // "Reconnecting…") and --uitest-with-stub-connection is not handled at all. // All assertions about "Reconnecting…" will therefore FAIL on the TDD branch. import XCTest @MainActor final class BackgroundLifecycleUITests: XCTestCase { override func setUpWithError() throws { continueAfterFailure = false } // ========================================================================= // MARK: 1. Crash-safety smoke test (should PASS — regression guard) // ========================================================================= /// The app must survive a background → foreground transition without /// crashing, regardless of connection state. func test_backgroundForeground_appDoesNotCrash() throws { let app = XCUIApplication() // Use --uitest so no real WS is needed; lifecycle hooks still fire. app.launchArguments = ["--uitest", "--reset-state"] app.launch() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10), "App must reach runningForeground state") // Background the app XCUIDevice.shared.press(.home) // Brief wait in background Thread.sleep(forTimeInterval: 0.5) // Foreground the app app.activate() // Verify the app returned to foreground (no crash / hang) XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5), "App must return to runningForeground after activate() — crash guard") } // ========================================================================= // MARK: 2. Status bar shows "Reconnecting…" after foreground transition // // FAILS until impl adds: // (a) --uitest-with-stub-connection launch arg handling in // MainTerminalView.initialBootstrap() that creates a stub connection // so lifecycle hooks fire and status text updates. // (b) "Reconnecting…" (not "Connecting…") text in the status bar when // SessionConnection re-enters .connecting after a suspend/resume. // (c) The background lifecycle wiring in MainTerminalView (subscribe to // AppState.lifecycleTransitions, call suspend/resume on the active // connection). // ========================================================================= /// After returning to foreground, the status bar must briefly show /// "Reconnecting…" (not just "Connecting…") to indicate this is a /// re-connection of an existing session, not a first-time attach. /// /// FAILS: --uitest-with-stub-connection is not handled → app shows /// "● uitest" (plain --uitest behaviour) → "Reconnecting…" not found. func test_statusBar_showsReconnectingAfterForeground() throws { let app = XCUIApplication() // --uitest-with-stub-connection: impl agent must add this mode. // Until then, the app falls back to --uitest behavior ("● uitest"). app.launchArguments = ["--uitest", "--uitest-with-stub-connection", "--reset-state"] app.launch() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10), "App must launch successfully") // Background the app XCUIDevice.shared.press(.home) Thread.sleep(forTimeInterval: 0.5) // Foreground the app app.activate() // After foregrounding, status bar must show "Reconnecting…" briefly. // Query by text content — the status bar text element has no accessibility // identifier yet. Impl agent should add accessibilityIdentifier // "statusbar.connectionStatus" to the connectionStatus Text in StatusBar.swift. let reconnectingText = app.staticTexts.matching( NSPredicate(format: "label CONTAINS 'Reconnecting'") ).firstMatch XCTAssertTrue( reconnectingText.waitForExistence(timeout: 3.0), "T-2.10: Status bar must show 'Reconnecting…' (or similar) text " + "during the reconnect window after app foreground. " + "MISSING IMPL: (1) add --uitest-with-stub-connection mode, " + "(2) update MainTerminalView status wiring for .connecting during resume, " + "(3) show 'Reconnecting…' not 'Connecting…' when lastSeq != nil." ) } /// After ~2 seconds the status must revert from "Reconnecting…" to the /// normal connected indicator ("● " or "● stub"). /// /// FAILS: prerequisite test_statusBar_showsReconnectingAfterForeground /// also fails; continueAfterFailure=false so this test is skipped. /// When the impl is complete, this guard prevents the reconnect /// state from being stuck forever. func test_statusBar_recoversFromReconnectingWithin2s() throws { let app = XCUIApplication() app.launchArguments = ["--uitest", "--uitest-with-stub-connection", "--reset-state"] app.launch() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10), "App must launch successfully") XCUIDevice.shared.press(.home) Thread.sleep(forTimeInterval: 0.5) app.activate() // First the reconnecting state must appear let reconnectingText = app.staticTexts.matching( NSPredicate(format: "label CONTAINS 'Reconnecting'") ).firstMatch XCTAssertTrue(reconnectingText.waitForExistence(timeout: 3.0), "T-2.10: Must see 'Reconnecting…' state first (prerequisite)") // Then it must disappear (connected text takes over) within 2 s let normalStatus = app.staticTexts.matching( NSPredicate(format: "label BEGINSWITH '●'") ).firstMatch XCTAssertTrue(normalStatus.waitForExistence(timeout: 2.0), "T-2.10: Status bar must revert to '● ' within 2 s " + "after the reconnect completes (P-3 acceptance)") } // ========================================================================= // MARK: 3. Multiple background/foreground cycles are stable // // FAILS: same missing impl as above; smoke guards for future regression. // ========================================================================= /// Two consecutive background→foreground cycles must not crash. func test_repeatedBackgroundForeground_doesNotCrash() throws { let app = XCUIApplication() app.launchArguments = ["--uitest", "--reset-state"] app.launch() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10)) for i in 0..<2 { XCUIDevice.shared.press(.home) Thread.sleep(forTimeInterval: 0.3) app.activate() XCTAssertTrue(app.wait(for: .runningForeground, timeout: 5), "App must survive bg/fg cycle \(i + 1)") } } }