175 lines
8.0 KiB
Swift
175 lines
8.0 KiB
Swift
// 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: "● <sessionId>" (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 ("● <sessionId>" 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 '● <sessionId>' 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)")
|
|
}
|
|
}
|
|
}
|