test(ios): T-2.10 background lifecycle — failing tests (TDD step 1/3)
This commit is contained in:
parent
4472e39310
commit
a4613f932f
|
|
@ -0,0 +1,191 @@
|
||||||
|
// AppStateLifecycleTests.swift
|
||||||
|
// T-2.10 — Background lifecycle: AppState surface for lifecycle events.
|
||||||
|
//
|
||||||
|
// Covers:
|
||||||
|
// • Face-ID gate regression (appDidBackground / appWillForeground existing logic).
|
||||||
|
// • New T-2.10 requirement: AppState must publish lifecycle transitions so
|
||||||
|
// an active SessionConnection can react (suspend on background, resume on
|
||||||
|
// foreground).
|
||||||
|
//
|
||||||
|
// Strategy: stub extension at the bottom provides `lifecycleTransitions` as an
|
||||||
|
// always-empty publisher. Tests expecting emissions will FAIL until the impl
|
||||||
|
// agent adds a real publisher on AppState.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import XCTest
|
||||||
|
@testable import piRemote
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class AppStateLifecycleTests: XCTestCase {
|
||||||
|
|
||||||
|
private nonisolated(unsafe) var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
cancellables.removeAll()
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 1. Face-ID gate regression (existing logic must not regress)
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Within the 60-second grace period, appWillForeground must NOT lock.
|
||||||
|
func test_appWillForeground_withinGracePeriod_doesNotLock() async {
|
||||||
|
let state = AppState.shared
|
||||||
|
// Simulate a recent background event (< 60 s ago)
|
||||||
|
state.appDidBackground()
|
||||||
|
// Immediately foreground — elapsed ≈ 0 s, well within the 60 s window.
|
||||||
|
await state.appWillForeground()
|
||||||
|
XCTAssertFalse(state.isLocked,
|
||||||
|
"Within 60 s grace period, appWillForeground must NOT set isLocked=true")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// appDidBackground() must record the current date in lastForegroundedAt
|
||||||
|
/// so the elapsed time calculation is correct on the next foreground.
|
||||||
|
func test_appDidBackground_updatesLastForegroundedAt() {
|
||||||
|
let state = AppState.shared
|
||||||
|
let before = Date()
|
||||||
|
state.appDidBackground()
|
||||||
|
let after = Date()
|
||||||
|
XCTAssertGreaterThanOrEqual(state.lastForegroundedAt, before,
|
||||||
|
"lastForegroundedAt must be updated to ≥ the call time")
|
||||||
|
XCTAssertLessThanOrEqual(state.lastForegroundedAt, after,
|
||||||
|
"lastForegroundedAt must be updated to ≤ the call time")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 2. T-2.10 — AppState must publish background/foreground transitions
|
||||||
|
//
|
||||||
|
// FAILS until impl agent adds lifecycleTransitions publisher to AppState.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// After appDidBackground(), lifecycleTransitions must emit `true`.
|
||||||
|
///
|
||||||
|
/// FAILS: stub publisher never emits → expectation times out.
|
||||||
|
func test_appDidBackground_publishesBackgroundTransition() {
|
||||||
|
let state = AppState.shared
|
||||||
|
let exp = expectation(description: "lifecycleTransitions emits true on background")
|
||||||
|
|
||||||
|
state.lifecycleTransitions
|
||||||
|
.filter { $0 == true }
|
||||||
|
.first()
|
||||||
|
.sink { _ in exp.fulfill() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
state.appDidBackground()
|
||||||
|
|
||||||
|
// Stub publisher never emits → this wait TIMES OUT → test FAILS.
|
||||||
|
// Impl agent: add an AnyPublisher<Bool, Never> on AppState that sends
|
||||||
|
// `true` from appDidBackground() and `false` from appWillForeground().
|
||||||
|
wait(for: [exp], timeout: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After appWillForeground(), lifecycleTransitions must emit `false`.
|
||||||
|
///
|
||||||
|
/// FAILS: stub publisher never emits → expectation times out.
|
||||||
|
func test_appWillForeground_publishesForegroundTransition() async {
|
||||||
|
let state = AppState.shared
|
||||||
|
// Prime with a recent timestamp so FaceID gate is skipped.
|
||||||
|
state.appDidBackground()
|
||||||
|
|
||||||
|
let exp = expectation(description: "lifecycleTransitions emits false on foreground")
|
||||||
|
|
||||||
|
state.lifecycleTransitions
|
||||||
|
.filter { $0 == false }
|
||||||
|
.first()
|
||||||
|
.sink { _ in exp.fulfill() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
await state.appWillForeground()
|
||||||
|
|
||||||
|
// Stub publisher never emits → this wait TIMES OUT → test FAILS.
|
||||||
|
await fulfillment(of: [exp], timeout: 0.5)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// lifecycleTransitions must emit in the correct order: true then false.
|
||||||
|
///
|
||||||
|
/// FAILS: stub publisher never emits → first expectation times out.
|
||||||
|
func test_lifecycleTransitions_correctOrder_backgroundThenForeground() async {
|
||||||
|
let state = AppState.shared
|
||||||
|
|
||||||
|
var emissions: [Bool] = []
|
||||||
|
let backgroundExp = expectation(description: "background emission (true)")
|
||||||
|
let foregroundExp = expectation(description: "foreground emission (false)")
|
||||||
|
|
||||||
|
state.lifecycleTransitions
|
||||||
|
.prefix(2)
|
||||||
|
.collect()
|
||||||
|
.sink { values in
|
||||||
|
emissions = values
|
||||||
|
if values.first == true { backgroundExp.fulfill() }
|
||||||
|
if values.last == false { foregroundExp.fulfill() }
|
||||||
|
}
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
state.appDidBackground()
|
||||||
|
await state.appWillForeground()
|
||||||
|
|
||||||
|
// Stub publisher never emits → both expectations time out → FAILS.
|
||||||
|
await fulfillment(of: [backgroundExp, foregroundExp], timeout: 0.5)
|
||||||
|
XCTAssertEqual(emissions, [true, false],
|
||||||
|
"T-2.10: lifecycleTransitions must emit [true, false] for " +
|
||||||
|
"background→foreground sequence")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 3. T-2.10 — Lifecycle transitions must be accessible for connection
|
||||||
|
// subscription without requiring access to ContentView internals.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// lifecycleTransitions must be a valid publisher (non-nil) on AppState.
|
||||||
|
/// Tests the structural presence of the publisher surface.
|
||||||
|
///
|
||||||
|
/// PASSES immediately once the property is added; the emission tests above
|
||||||
|
/// verify that it actually fires.
|
||||||
|
func test_lifecycleTransitions_publisherExists() {
|
||||||
|
// This test passes once lifecycleTransitions property exists on AppState.
|
||||||
|
// It always FAILS because the stub (Empty publisher) technically exists,
|
||||||
|
// but we add a stronger check: we verify the publisher has sent at least
|
||||||
|
// one value by checking a side-channel flag.
|
||||||
|
//
|
||||||
|
// Use a simple check: the value we receive after posting a background event
|
||||||
|
// is `true`. Since stub never emits, `receivedValue` stays nil → FAILS.
|
||||||
|
|
||||||
|
let state = AppState.shared
|
||||||
|
var receivedValue: Bool? = nil
|
||||||
|
state.lifecycleTransitions
|
||||||
|
.first()
|
||||||
|
.sink { value in receivedValue = value }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
state.appDidBackground()
|
||||||
|
|
||||||
|
// Stub never emits → receivedValue stays nil → FAILS.
|
||||||
|
XCTAssertNotNil(receivedValue,
|
||||||
|
"T-2.10: AppState.lifecycleTransitions must emit when appDidBackground() is called. " +
|
||||||
|
"Impl agent: add a PassthroughSubject<Bool, Never> to AppState, send(true) in " +
|
||||||
|
"appDidBackground(), send(false) in appWillForeground(). Expose as " +
|
||||||
|
"AnyPublisher<Bool, Never>.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MARK: - Stub extension
|
||||||
|
//
|
||||||
|
// Provides `lifecycleTransitions` so the test file compiles before the impl.
|
||||||
|
// The Empty publisher never emits → all tests that wait for emissions FAIL.
|
||||||
|
//
|
||||||
|
// Impl agent: add a real `lifecycleTransitions: AnyPublisher<Bool, Never>` to
|
||||||
|
// AppState.swift backed by a PassthroughSubject. REMOVE this stub once added.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
extension AppState {
|
||||||
|
|
||||||
|
/// T-2.10: Emits `true` when the app enters background,
|
||||||
|
/// `false` when the app returns to foreground.
|
||||||
|
///
|
||||||
|
/// STUB → Empty publisher, never emits → all T-2.10 emission tests FAIL.
|
||||||
|
var lifecycleTransitions: AnyPublisher<Bool, Never> {
|
||||||
|
Empty<Bool, Never>().eraseToAnyPublisher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,256 @@
|
||||||
|
// SessionConnectionLifecycleTests.swift
|
||||||
|
// T-2.10 — Background lifecycle: suspend / resume / stale-frame-freeze /
|
||||||
|
// keep-alive foreground-only / idempotency / reconnect latency.
|
||||||
|
//
|
||||||
|
// Strategy: tests compile against stub extensions at the bottom of this file.
|
||||||
|
// The stubs return sentinel values that make every T-2.10-specific assertion
|
||||||
|
// fail. The impl agent replaces the stubs with real implementations inside
|
||||||
|
// SessionConnection.swift.
|
||||||
|
|
||||||
|
import Combine
|
||||||
|
import XCTest
|
||||||
|
@testable import piRemote
|
||||||
|
|
||||||
|
// MARK: - Helpers
|
||||||
|
|
||||||
|
private func fakeCredential() -> SidecarCredential {
|
||||||
|
SidecarCredential(
|
||||||
|
sidecarId: "test-sidecar",
|
||||||
|
host: "127.0.0.1",
|
||||||
|
port: 19991, // unreachable localhost port — no real WS needed
|
||||||
|
bearerToken: "test-token",
|
||||||
|
tlsFingerprint: "deadbeef",
|
||||||
|
sidecarName: "test",
|
||||||
|
pairedAt: Date()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MARK: - Test class
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
final class SessionConnectionLifecycleTests: XCTestCase {
|
||||||
|
|
||||||
|
private nonisolated(unsafe) var cancellables = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
override func tearDown() {
|
||||||
|
cancellables.removeAll()
|
||||||
|
super.tearDown()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 1. Suspend / Resume basic state transitions
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// Regression: suspend() always leaves state as .disconnected.
|
||||||
|
func test_suspend_setsConnectionStateToDisconnected() async {
|
||||||
|
let conn = SessionConnection(id: "s1", credential: fakeCredential())
|
||||||
|
XCTAssertEqual(conn.connectionState, .disconnected,
|
||||||
|
"Fresh connection must start .disconnected")
|
||||||
|
await conn.suspend()
|
||||||
|
XCTAssertEqual(conn.connectionState, .disconnected,
|
||||||
|
"suspend() must keep/leave connectionState as .disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calling suspend() twice must not crash or corrupt state.
|
||||||
|
func test_idempotentSuspend_doesNotCrash() async {
|
||||||
|
let conn = SessionConnection(id: "s2", credential: fakeCredential())
|
||||||
|
await conn.suspend()
|
||||||
|
await conn.suspend() // second call — must be a no-op
|
||||||
|
XCTAssertEqual(conn.connectionState, .disconnected,
|
||||||
|
"Double suspend must remain .disconnected")
|
||||||
|
}
|
||||||
|
|
||||||
|
/// resume(from:nil) must transition the connection to .connecting.
|
||||||
|
func test_resume_nil_transitionsToConnecting() async throws {
|
||||||
|
let conn = SessionConnection(id: "s3", credential: fakeCredential())
|
||||||
|
let exp = expectation(description: "connectionState reaches .connecting")
|
||||||
|
|
||||||
|
conn.$connectionState
|
||||||
|
.filter { $0 == .connecting }
|
||||||
|
.first()
|
||||||
|
.sink { _ in exp.fulfill() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
await conn.resume(from: nil)
|
||||||
|
await fulfillment(of: [exp], timeout: 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After an explicit suspend(), resume(from:lastSeq) must reconnect.
|
||||||
|
func test_resumeAfterSuspend_transitionsToConnecting() async throws {
|
||||||
|
let conn = SessionConnection(id: "s4", credential: fakeCredential())
|
||||||
|
await conn.suspend()
|
||||||
|
|
||||||
|
let exp = expectation(description: "resume after suspend reaches .connecting")
|
||||||
|
conn.$connectionState
|
||||||
|
.filter { $0 == .connecting }
|
||||||
|
.first()
|
||||||
|
.sink { _ in exp.fulfill() }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
await conn.resume(from: 42)
|
||||||
|
await fulfillment(of: [exp], timeout: 2.0)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calling resume() twice must not crash and must leave the connection in
|
||||||
|
/// a consistent (suspend-able) state. This is a regression guard — the
|
||||||
|
/// existing resume() implementation calls suspend() first so it is already
|
||||||
|
/// idempotent; this test should PASS and must not regress.
|
||||||
|
func test_idempotentResume_doesNotCrashOrOrphanPublishers() async throws {
|
||||||
|
let conn = SessionConnection(id: "s5", credential: fakeCredential())
|
||||||
|
|
||||||
|
// Count how many times state transitions to .connecting; a correct
|
||||||
|
// double-resume may produce at most 2 transitions (one per call).
|
||||||
|
var connectingCount = 0
|
||||||
|
conn.$connectionState
|
||||||
|
.filter { $0 == .connecting }
|
||||||
|
.sink { _ in connectingCount += 1 }
|
||||||
|
.store(in: &cancellables)
|
||||||
|
|
||||||
|
await conn.resume(from: nil)
|
||||||
|
await conn.resume(from: nil) // second call — must not crash
|
||||||
|
|
||||||
|
// Teardown must always succeed, regardless of double-resume.
|
||||||
|
await conn.suspend()
|
||||||
|
XCTAssertEqual(conn.connectionState, .disconnected,
|
||||||
|
"After suspend following double-resume, state must be .disconnected")
|
||||||
|
|
||||||
|
// At most 2 .connecting transitions (one per resume() call).
|
||||||
|
XCTAssertLessThanOrEqual(connectingCount, 2,
|
||||||
|
"Double resume must not produce more than 2 .connecting transitions")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 2. Stale-frame freeze
|
||||||
|
//
|
||||||
|
// During the reconnect window (after resume(from: nonNil)), the stream
|
||||||
|
// publisher must NOT forward new frames to consumers until the first delta
|
||||||
|
// chunk arrives OR a snapshot completes. This prevents stale/mismatched
|
||||||
|
// content being fed to the terminal.
|
||||||
|
//
|
||||||
|
// FAILS until impl adds SessionConnection.isStreamFrozen.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// After resume(from: nonNil), the stream must be frozen.
|
||||||
|
func test_staleFrameFreeze_isFrozenDuringReconnect() async throws {
|
||||||
|
let conn = SessionConnection(id: "freeze-1", credential: fakeCredential())
|
||||||
|
await conn.resume(from: 100) // non-nil seq → reconnect scenario
|
||||||
|
|
||||||
|
// Allow the async connect sequence to start
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50 ms
|
||||||
|
|
||||||
|
// FAILS: isStreamFrozen stub returns false; real impl must return true
|
||||||
|
// during the reconnect window.
|
||||||
|
XCTAssertTrue(conn.isStreamFrozen,
|
||||||
|
"T-2.10: Stream must be frozen while reconnecting (after resume with non-nil lastSeq). " +
|
||||||
|
"Impl agent: add SessionConnection.isStreamFrozen: Bool that returns true between " +
|
||||||
|
"resume(from: nonNil) and first delta/snapshot arrival.")
|
||||||
|
|
||||||
|
await conn.suspend()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After a fresh attach (resume(from: nil)), freeze should NOT be active —
|
||||||
|
/// the initial snapshot flow handles the cleared screen.
|
||||||
|
func test_staleFrameFreeze_notFrozenOnFreshAttach() async throws {
|
||||||
|
let conn = SessionConnection(id: "freeze-2", credential: fakeCredential())
|
||||||
|
await conn.resume(from: nil) // nil seq → fresh attach, no freeze needed
|
||||||
|
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50 ms
|
||||||
|
|
||||||
|
// The impl may set isStreamFrozen=false on fresh attach. The stub also
|
||||||
|
// returns false, so this assertion PASSES either way — it serves as a
|
||||||
|
// specification of intent and a future regression guard.
|
||||||
|
XCTAssertFalse(conn.isStreamFrozen,
|
||||||
|
"T-2.10: Fresh attach (lastSeq=nil) must NOT freeze the stream.")
|
||||||
|
|
||||||
|
await conn.suspend()
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 3. Keep-alive ping is foreground-only
|
||||||
|
//
|
||||||
|
// FAILS until impl adds SessionConnection.isKeepAliveActive.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
/// After resume(), the keep-alive heartbeat must be scheduled (active).
|
||||||
|
func test_keepAlivePing_activeAfterResume() async throws {
|
||||||
|
let conn = SessionConnection(id: "ping-1", credential: fakeCredential())
|
||||||
|
await conn.resume(from: nil)
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50 ms
|
||||||
|
|
||||||
|
// FAILS: stub returns false; real impl must return true when connected/connecting.
|
||||||
|
XCTAssertTrue(conn.isKeepAliveActive,
|
||||||
|
"T-2.10: Keep-alive heartbeat must be active while the connection is open. " +
|
||||||
|
"Impl agent: add SessionConnection.isKeepAliveActive: Bool and schedule a " +
|
||||||
|
"repeating ping timer on resume().")
|
||||||
|
|
||||||
|
await conn.suspend()
|
||||||
|
}
|
||||||
|
|
||||||
|
/// After suspend(), the keep-alive heartbeat must be cancelled.
|
||||||
|
func test_keepAlivePing_inactiveAfterSuspend() async throws {
|
||||||
|
let conn = SessionConnection(id: "ping-2", credential: fakeCredential())
|
||||||
|
await conn.resume(from: nil)
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // 50 ms
|
||||||
|
|
||||||
|
// First check that it was active — this assertion will FAIL
|
||||||
|
// (see test_keepAlivePing_activeAfterResume). continueAfterFailure=true
|
||||||
|
// so we continue to the suspend check.
|
||||||
|
XCTAssertTrue(conn.isKeepAliveActive,
|
||||||
|
"T-2.10: Keep-alive must be active after resume() (precondition for this test).")
|
||||||
|
|
||||||
|
await conn.suspend()
|
||||||
|
|
||||||
|
XCTAssertFalse(conn.isKeepAliveActive,
|
||||||
|
"T-2.10: Keep-alive must be stopped after suspend().")
|
||||||
|
}
|
||||||
|
|
||||||
|
// =========================================================================
|
||||||
|
// MARK: 4. Reconnect latency P-3
|
||||||
|
//
|
||||||
|
// The suspend→resume roundtrip must have no artificial delays blocking the
|
||||||
|
// reconnect path. P-3 acceptance: <1 s on LAN. Here we check that the
|
||||||
|
// in-process part (excluding actual TCP round-trip) takes <200 ms.
|
||||||
|
// =========================================================================
|
||||||
|
|
||||||
|
func test_reconnectLatency_suspendResumeCycleUnder200ms() async throws {
|
||||||
|
let conn = SessionConnection(id: "latency-1", credential: fakeCredential())
|
||||||
|
await conn.resume(from: nil)
|
||||||
|
try await Task.sleep(nanoseconds: 50_000_000) // let .connecting settle
|
||||||
|
|
||||||
|
let start = Date()
|
||||||
|
await conn.suspend()
|
||||||
|
await conn.resume(from: 0)
|
||||||
|
let elapsed = Date().timeIntervalSince(start)
|
||||||
|
|
||||||
|
XCTAssertLessThan(elapsed, 0.2,
|
||||||
|
"T-2.10 P-3: suspend()+resume() in-process roundtrip must be <200 ms " +
|
||||||
|
"(i.e. no artificial sleep / synchronous blocking in the reconnect path)")
|
||||||
|
|
||||||
|
await conn.suspend()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// =============================================================================
|
||||||
|
// MARK: - Stub extensions
|
||||||
|
//
|
||||||
|
// These STUB properties let the test file COMPILE without the real impl.
|
||||||
|
// Each stub returns a sentinel value that makes the T-2.10-specific assertions
|
||||||
|
// FAIL with a clear message. The impl agent replaces these stubs with real
|
||||||
|
// implementations on SessionConnection in Sources/.
|
||||||
|
//
|
||||||
|
// DO NOT modify these stubs — edit SessionConnection.swift instead.
|
||||||
|
// =============================================================================
|
||||||
|
|
||||||
|
extension SessionConnection {
|
||||||
|
|
||||||
|
/// T-2.10: True while the stream is gated during a reconnect window
|
||||||
|
/// (after resume(from: nonNil) until first delta chunk or snapshot lands).
|
||||||
|
///
|
||||||
|
/// STUB → always false → tests asserting true will FAIL until impl adds this.
|
||||||
|
var isStreamFrozen: Bool { false }
|
||||||
|
|
||||||
|
/// T-2.10: True when a foreground keep-alive heartbeat timer is scheduled.
|
||||||
|
///
|
||||||
|
/// STUB → always false → tests asserting true will FAIL until impl adds this.
|
||||||
|
var isKeepAliveActive: Bool { false }
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,174 @@
|
||||||
|
// 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)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -24,9 +24,12 @@
|
||||||
4334301CFE20E6B66BDE7D19 /* SessionSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C77FC9D24BB5F8DA96386C /* SessionSwitcher.swift */; };
|
4334301CFE20E6B66BDE7D19 /* SessionSwitcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = E8C77FC9D24BB5F8DA96386C /* SessionSwitcher.swift */; };
|
||||||
4877B4085C529C640FBBE6AB /* ThemeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC48E39D19238178A180B30C /* ThemeStore.swift */; };
|
4877B4085C529C640FBBE6AB /* ThemeStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = BC48E39D19238178A180B30C /* ThemeStore.swift */; };
|
||||||
56096DB64F700FC00C4D58CE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFF032BC30D513204211ADA5 /* Assets.xcassets */; };
|
56096DB64F700FC00C4D58CE /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = AFF032BC30D513204211ADA5 /* Assets.xcassets */; };
|
||||||
|
5D6FE414C7ECB0E9146BD388 /* BackgroundLifecycleUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A7B0C4212866DDF358CD483C /* BackgroundLifecycleUITests.swift */; };
|
||||||
|
5E831AF2B2F42B09BF6E6960 /* SessionConnectionLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 30F52BCE08D851960C438B11 /* SessionConnectionLifecycleTests.swift */; };
|
||||||
5F82D50C477F47893FADA8CB /* PasteSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F776605A4109C047E44A89 /* PasteSheet.swift */; };
|
5F82D50C477F47893FADA8CB /* PasteSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = A3F776605A4109C047E44A89 /* PasteSheet.swift */; };
|
||||||
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FA0A0FD737901834AD5705 /* ThemeTests.swift */; };
|
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 99FA0A0FD737901834AD5705 /* ThemeTests.swift */; };
|
||||||
6A52DBC5C2845ADA05B37A35 /* SessionRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959878B4816DD2617038A339 /* SessionRegistry.swift */; };
|
6A52DBC5C2845ADA05B37A35 /* SessionRegistry.swift in Sources */ = {isa = PBXBuildFile; fileRef = 959878B4816DD2617038A339 /* SessionRegistry.swift */; };
|
||||||
|
6DDCCD4DB739E7710DCD9737 /* AppStateLifecycleTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F758E6F77384904725670529 /* AppStateLifecycleTests.swift */; };
|
||||||
734F2FECD358816F695D26CD /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DD6C03615573E339057EBF /* AppState.swift */; };
|
734F2FECD358816F695D26CD /* AppState.swift in Sources */ = {isa = PBXBuildFile; fileRef = 62DD6C03615573E339057EBF /* AppState.swift */; };
|
||||||
7936EDE3DC79D02CF66F8863 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */; };
|
7936EDE3DC79D02CF66F8863 /* QRScannerView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */; };
|
||||||
7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */; };
|
7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */; };
|
||||||
|
|
@ -94,6 +97,7 @@
|
||||||
278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierStateTests.swift; sourceTree = "<group>"; };
|
278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierStateTests.swift; sourceTree = "<group>"; };
|
||||||
2C3C69D7879985E77A45DE76 /* SessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRow.swift; sourceTree = "<group>"; };
|
2C3C69D7879985E77A45DE76 /* SessionRow.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRow.swift; sourceTree = "<group>"; };
|
||||||
2E2370A3190FDC144C822FF6 /* piRemote.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = piRemote.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
2E2370A3190FDC144C822FF6 /* piRemote.app */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.application; path = piRemote.app; sourceTree = BUILT_PRODUCTS_DIR; };
|
||||||
|
30F52BCE08D851960C438B11 /* SessionConnectionLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConnectionLifecycleTests.swift; sourceTree = "<group>"; };
|
||||||
39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewRepresentable.swift; sourceTree = "<group>"; };
|
39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewRepresentable.swift; sourceTree = "<group>"; };
|
||||||
3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTerminalView.swift; sourceTree = "<group>"; };
|
3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainTerminalView.swift; sourceTree = "<group>"; };
|
||||||
5205F823929F91450C58D4CA /* WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = "<group>"; };
|
5205F823929F91450C58D4CA /* WebSocketClient.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WebSocketClient.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -117,6 +121,7 @@
|
||||||
A38B86E930CB34DEB9E4C144 /* PairingFlowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingFlowView.swift; sourceTree = "<group>"; };
|
A38B86E930CB34DEB9E4C144 /* PairingFlowView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingFlowView.swift; sourceTree = "<group>"; };
|
||||||
A3F776605A4109C047E44A89 /* PasteSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteSheet.swift; sourceTree = "<group>"; };
|
A3F776605A4109C047E44A89 /* PasteSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PasteSheet.swift; sourceTree = "<group>"; };
|
||||||
A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewController.swift; sourceTree = "<group>"; };
|
A75BE928FA90D8AF2C56615D /* TerminalViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalViewController.swift; sourceTree = "<group>"; };
|
||||||
|
A7B0C4212866DDF358CD483C /* BackgroundLifecycleUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = BackgroundLifecycleUITests.swift; sourceTree = "<group>"; };
|
||||||
A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameCodecTests.swift; sourceTree = "<group>"; };
|
A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameCodecTests.swift; sourceTree = "<group>"; };
|
||||||
AFF032BC30D513204211ADA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
AFF032BC30D513204211ADA5 /* Assets.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; path = Assets.xcassets; sourceTree = "<group>"; };
|
||||||
B772854E3FADA8998C93DAF5 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
B772854E3FADA8998C93DAF5 /* Keychain.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Keychain.swift; sourceTree = "<group>"; };
|
||||||
|
|
@ -138,6 +143,7 @@
|
||||||
EE5E1A0BE69AF1FEF73E373F /* SmokeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeUITests.swift; sourceTree = "<group>"; };
|
EE5E1A0BE69AF1FEF73E373F /* SmokeUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SmokeUITests.swift; sourceTree = "<group>"; };
|
||||||
F47CA5A1045A264958B360BF /* KeychainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainTests.swift; sourceTree = "<group>"; };
|
F47CA5A1045A264958B360BF /* KeychainTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = KeychainTests.swift; sourceTree = "<group>"; };
|
||||||
F553E905D716538D9DA442E7 /* ModifierBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierBar.swift; sourceTree = "<group>"; };
|
F553E905D716538D9DA442E7 /* ModifierBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierBar.swift; sourceTree = "<group>"; };
|
||||||
|
F758E6F77384904725670529 /* AppStateLifecycleTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppStateLifecycleTests.swift; sourceTree = "<group>"; };
|
||||||
/* End PBXFileReference section */
|
/* End PBXFileReference section */
|
||||||
|
|
||||||
/* Begin PBXFrameworksBuildPhase section */
|
/* Begin PBXFrameworksBuildPhase section */
|
||||||
|
|
@ -264,6 +270,7 @@
|
||||||
B259358DD628B98EC61A6736 /* UITests */ = {
|
B259358DD628B98EC61A6736 /* UITests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
A7B0C4212866DDF358CD483C /* BackgroundLifecycleUITests.swift */,
|
||||||
5C1F7B03B47D7766D72BA3B8 /* Helpers.swift */,
|
5C1F7B03B47D7766D72BA3B8 /* Helpers.swift */,
|
||||||
DF52ADD110456E368EAE217C /* LockScreenUITests.swift */,
|
DF52ADD110456E368EAE217C /* LockScreenUITests.swift */,
|
||||||
1E689590D75F152E90467016 /* ModifierBarUITests.swift */,
|
1E689590D75F152E90467016 /* ModifierBarUITests.swift */,
|
||||||
|
|
@ -279,6 +286,7 @@
|
||||||
C06242078CD1DD2BD7C7A4FA /* CoreTests */ = {
|
C06242078CD1DD2BD7C7A4FA /* CoreTests */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
F758E6F77384904725670529 /* AppStateLifecycleTests.swift */,
|
||||||
83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */,
|
83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */,
|
||||||
A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */,
|
A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */,
|
||||||
F47CA5A1045A264958B360BF /* KeychainTests.swift */,
|
F47CA5A1045A264958B360BF /* KeychainTests.swift */,
|
||||||
|
|
@ -288,6 +296,7 @@
|
||||||
55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */,
|
55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */,
|
||||||
6DE4A325EEA53870390B89D9 /* REVIEW_NOTES.md */,
|
6DE4A325EEA53870390B89D9 /* REVIEW_NOTES.md */,
|
||||||
22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */,
|
22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */,
|
||||||
|
30F52BCE08D851960C438B11 /* SessionConnectionLifecycleTests.swift */,
|
||||||
99FA0A0FD737901834AD5705 /* ThemeTests.swift */,
|
99FA0A0FD737901834AD5705 /* ThemeTests.swift */,
|
||||||
);
|
);
|
||||||
path = CoreTests;
|
path = CoreTests;
|
||||||
|
|
@ -551,6 +560,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
5D6FE414C7ECB0E9146BD388 /* BackgroundLifecycleUITests.swift in Sources */,
|
||||||
A1527F94754A9205576C317C /* Helpers.swift in Sources */,
|
A1527F94754A9205576C317C /* Helpers.swift in Sources */,
|
||||||
17F996CE1F5CF3FF12E5C1AB /* LockScreenUITests.swift in Sources */,
|
17F996CE1F5CF3FF12E5C1AB /* LockScreenUITests.swift in Sources */,
|
||||||
9BDEBAD2C295FDD53946FB8C /* ModifierBarUITests.swift in Sources */,
|
9BDEBAD2C295FDD53946FB8C /* ModifierBarUITests.swift in Sources */,
|
||||||
|
|
@ -566,6 +576,7 @@
|
||||||
isa = PBXSourcesBuildPhase;
|
isa = PBXSourcesBuildPhase;
|
||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
|
6DDCCD4DB739E7710DCD9737 /* AppStateLifecycleTests.swift in Sources */,
|
||||||
C1F266B0DC9D7029E5E5B203 /* DeviceTokenRegistrarTests.swift in Sources */,
|
C1F266B0DC9D7029E5E5B203 /* DeviceTokenRegistrarTests.swift in Sources */,
|
||||||
A1B807C3E8586E99507463B9 /* FrameCodecTests.swift in Sources */,
|
A1B807C3E8586E99507463B9 /* FrameCodecTests.swift in Sources */,
|
||||||
C823749124F98D46FB993247 /* KeychainTests.swift in Sources */,
|
C823749124F98D46FB993247 /* KeychainTests.swift in Sources */,
|
||||||
|
|
@ -573,6 +584,7 @@
|
||||||
05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */,
|
05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */,
|
||||||
16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */,
|
16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */,
|
||||||
3486C15393498F5306C8F43B /* ScrollbackCacheTests.swift in Sources */,
|
3486C15393498F5306C8F43B /* ScrollbackCacheTests.swift in Sources */,
|
||||||
|
5E831AF2B2F42B09BF6E6960 /* SessionConnectionLifecycleTests.swift in Sources */,
|
||||||
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */,
|
5F8F5E6D2D5277CB90FA98A0 /* ThemeTests.swift in Sources */,
|
||||||
);
|
);
|
||||||
runOnlyForDeploymentPostprocessing = 0;
|
runOnlyForDeploymentPostprocessing = 0;
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue