Compare commits

..

No commits in common. "main" and "docs/sim-automation" have entirely different histories.

23 changed files with 12 additions and 1729 deletions

View File

@ -1,6 +1,5 @@
// AppState.swift global app state, credential lifecycle // AppState.swift global app state, credential lifecycle
import Combine
import SwiftUI import SwiftUI
@MainActor @MainActor
@ -11,47 +10,9 @@ final class AppState: ObservableObject {
@Published var isLocked = false @Published var isLocked = false
@Published var lastForegroundedAt: Date = Date() @Published var lastForegroundedAt: Date = Date()
// T-2.10: background/foreground lifecycle publisher
// true = app entered background
// false = app returned to foreground
private let _lifecycleTransitions = PassthroughSubject<Bool, Never>()
var lifecycleTransitions: AnyPublisher<Bool, Never> {
_lifecycleTransitions.eraseToAnyPublisher()
}
private init() { private init() {
#if DEBUG
// Test-only launch-argument overrides (gated: never active in Release).
let args = ProcessInfo.processInfo.arguments
if args.contains("--reset-state") {
Keychain.shared.delete(key: "piremote.credential")
UserDefaults.standard.removeObject(forKey: "faceid.enabled")
}
if args.contains("--enable-faceid") {
UserDefaults.standard.set(true, forKey: "faceid.enabled")
}
if args.contains("--force-lock") {
isLocked = true
}
#endif
// Try loading persisted credential on launch // Try loading persisted credential on launch
credential = try? Keychain.shared.load(key: "piremote.credential") credential = try? Keychain.shared.load(key: "piremote.credential")
#if DEBUG
// T-2.10: inject stub credential for UI tests that need MainTerminalView
if args.contains("--uitest-with-stub-connection") {
credential = SidecarCredential(
sidecarId: "stub",
host: "127.0.0.1",
port: 19991,
bearerToken: "stub-token",
tlsFingerprint: "stub",
sidecarName: "stub",
pairedAt: Date()
)
}
#endif
} }
func didPair(credential: SidecarCredential) { func didPair(credential: SidecarCredential) {
@ -68,19 +29,13 @@ final class AppState: ObservableObject {
func appDidBackground() { func appDidBackground() {
lastForegroundedAt = Date() lastForegroundedAt = Date()
_lifecycleTransitions.send(true) // T-2.10
} }
func appWillForeground() async { func appWillForeground() async {
_lifecycleTransitions.send(false) // T-2.10: emit before Face-ID gate
let elapsed = Date().timeIntervalSince(lastForegroundedAt) let elapsed = Date().timeIntervalSince(lastForegroundedAt)
guard elapsed > 60 else { return } // within 60s no re-auth guard elapsed > 60 else { return } // within 60s no re-auth
isLocked = true isLocked = true
let ok = await FaceIDGate.authenticate() let ok = await FaceIDGate.authenticate()
isLocked = !ok isLocked = !ok
// N-2: after successful Face-ID auth the first emission fired when
// isLocked=true (so MainTerminalView skipped the reconnect). Re-emit
// now that isLocked=false so the connection actually resumes.
if ok { _lifecycleTransitions.send(false) }
} }
} }

View File

@ -15,17 +15,6 @@ struct piRemoteApp: App {
.onAppear { .onAppear {
notificationDelegate.setup() notificationDelegate.setup()
UIApplication.shared.registerForRemoteNotifications() UIApplication.shared.registerForRemoteNotifications()
#if DEBUG
// Test-only: auto-pair if argument present (never active in Release).
if let pairArgIndex = ProcessInfo.processInfo.arguments.firstIndex(of: "--pair-with-url"),
pairArgIndex + 1 < ProcessInfo.processInfo.arguments.count {
let urlString = ProcessInfo.processInfo.arguments[pairArgIndex + 1]
if let url = URL(string: urlString) {
handlePairingURL(url)
}
}
#endif
} }
.onOpenURL { url in .onOpenURL { url in
handlePairingURL(url) handlePairingURL(url)

View File

@ -42,23 +42,6 @@ public final class SessionConnection: ObservableObject {
/// Tracks the WebSocket lifecycle. /// Tracks the WebSocket lifecycle.
@Published public private(set) var connectionState: ConnectionState = .disconnected @Published public private(set) var connectionState: ConnectionState = .disconnected
// MARK: - T-2.10 lifecycle flags
/// True while the stream is gated during a reconnect window
/// i.e. after `resume(from: nonNil)` until the first delta or snapshot lands.
public private(set) var isStreamFrozen = false
/// True when the foreground keep-alive heartbeat task is running.
public var isKeepAliveActive: Bool { keepAliveTask != nil }
// MARK: - Internal test/UI-test hook
#if DEBUG
/// Set to `true` in UI-test stub mode to bypass the real WebSocket and
/// drive connection-state transitions in-process.
internal var stubMode = false
#endif
// MARK: - Scrollback // MARK: - Scrollback
/// Persistent rolling ANSI cache for this session. /// Persistent rolling ANSI cache for this session.
@ -67,23 +50,17 @@ public final class SessionConnection: ObservableObject {
// MARK: - Private // MARK: - Private
private let credential: SidecarCredential private let credential: SidecarCredential
private let cursor: ResumeCursor
private var client: WebSocketClient? private var client: WebSocketClient?
private var cancellables = Set<AnyCancellable>() private var cancellables = Set<AnyCancellable>()
private var keepAliveTask: Task<Void, Never>?
// MARK: - Init // MARK: - Init
/// Creates a `SessionConnection` for `id` authenticated with `credential`. /// Creates a `SessionConnection` for `id` authenticated with `credential`.
/// ///
/// Does **not** open a WebSocket. Call `resume(from:)` to connect. /// Does **not** open a WebSocket. Call `resume(from:)` to connect.
/// init(id: String, credential: SidecarCredential) {
/// - Parameter cursor: Injected `ResumeCursor` (defaults to the shared
/// UserDefaults-backed instance; override in tests for isolation).
init(id: String, credential: SidecarCredential, cursor: ResumeCursor = ResumeCursor()) {
self.id = id self.id = id
self.credential = credential self.credential = credential
self.cursor = cursor
self.scrollback = ScrollbackCache(sessionId: id) self.scrollback = ScrollbackCache(sessionId: id)
} }
@ -94,45 +71,16 @@ public final class SessionConnection: ObservableObject {
/// - Parameter lastSeq: The last acknowledged sequence number, or `nil` /// - Parameter lastSeq: The last acknowledged sequence number, or `nil`
/// to request replay from the beginning. /// to request replay from the beginning.
public func resume(from lastSeq: UInt64?) async { public func resume(from lastSeq: UInt64?) async {
// Tear down any existing connection cleanly before reconnecting.
await suspend()
// T-2.10: freeze stream if this is a reconnect (lastSeq != nil).
isStreamFrozen = (lastSeq != nil)
// T-2.10: start foreground-only keep-alive heartbeat.
keepAliveTask = Task {
while !Task.isCancelled {
do { try await Task.sleep(nanoseconds: 30_000_000_000) } // 30 s
catch { break }
}
}
#if DEBUG
// T-2.10: stub mode drive states in-process without a real WebSocket.
if stubMode {
connectionState = .connecting
Task { @MainActor [weak self] in
// 800 ms: long enough for XCUI to detect "Reconnecting",
// short enough to recover within the 2 s test window.
try? await Task.sleep(nanoseconds: 800_000_000)
guard let self, self.keepAliveTask != nil else { return } // suspended?
self.isStreamFrozen = false
self.connectionState = .connected
}
return
}
#endif
guard let url = streamURL else { guard let url = streamURL else {
#if DEBUG #if DEBUG
print("[SessionConnection] Could not construct stream URL for session \(id) — aborting resume.") print("[SessionConnection] Could not construct stream URL for session \(id) — aborting resume.")
#endif #endif
keepAliveTask?.cancel()
keepAliveTask = nil
return return
} }
// Tear down any existing connection cleanly before reconnecting.
await suspend()
let ws = WebSocketClient() let ws = WebSocketClient()
client = ws client = ws
@ -144,11 +92,12 @@ public final class SessionConnection: ObservableObject {
} }
.store(in: &cancellables) .store(in: &cancellables)
// Binary frames scrollback + cursor + downstream `stream` subject. // Binary frames scrollback + downstream `stream` subject.
ws.incomingBinary ws.incomingBinary
.sink { [weak self] frame in .sink { [weak self] frame in
guard let self else { return } guard let self else { return }
self.handleBinaryFrame(frame) self.scrollback.append(frame.data)
self.stream.send(frame.data)
} }
.store(in: &cancellables) .store(in: &cancellables)
@ -157,7 +106,6 @@ public final class SessionConnection: ObservableObject {
.sink { [weak self] frame in .sink { [weak self] frame in
guard let self else { return } guard let self else { return }
if case .snapshot(_, let base64) = frame { if case .snapshot(_, let base64) = frame {
self.isStreamFrozen = false // T-2.10: snapshot clears freeze
// Decode base64 text, prepend clear+home, normalise line endings. // Decode base64 text, prepend clear+home, normalise line endings.
if let raw = Data(base64Encoded: base64), if let raw = Data(base64Encoded: base64),
let text = String(data: raw, encoding: .utf8) { let text = String(data: raw, encoding: .utf8) {
@ -207,50 +155,12 @@ public final class SessionConnection: ObservableObject {
/// Closes the WebSocket but keeps local state (scrollback + cursor). /// Closes the WebSocket but keeps local state (scrollback + cursor).
public func suspend() async { public func suspend() async {
// T-2.10: cancel keep-alive heartbeat and clear freeze flag
keepAliveTask?.cancel()
keepAliveTask = nil
isStreamFrozen = false
client?.disconnect() client?.disconnect()
client = nil client = nil
cancellables.removeAll() cancellables.removeAll()
connectionState = .disconnected connectionState = .disconnected
} }
// MARK: - Binary frame processing
/// Central handler for incoming binary frames.
///
/// Called from the `incomingBinary` sink (production) and from the
/// `#if DEBUG` test helper below (unit tests).
///
/// **CG-3 decision informational, not blocking:**
/// `isStreamFrozen` is cleared on the first binary frame and the frame IS
/// forwarded to `stream`. In the IC-1 protocol the server only starts
/// sending data after it has processed our `resume` frame, so there are no
/// "stale" bytes that arrive while frozen the first binary frame IS the
/// first meaningful delta. Stream delivery is therefore never blocked;
/// `isStreamFrozen` serves the status bar ("Reconnecting") and the
/// `isStreamFrozen` unit tests, not as a gate on `stream`.
private func handleBinaryFrame(_ frame: BinaryFrame) {
isStreamFrozen = false // T-2.10: first delta thaws freeze
scrollback.append(frame.data)
cursor.update(sessionId: id, seq: frame.seq) // B-1: persist seq
stream.send(frame.data)
}
#if DEBUG
/// Test-only: simulate a binary frame arriving via the full production
/// code path (`handleBinaryFrame`), without needing a real WebSocket.
///
/// This drives the same `cursor.update` + `stream.send` logic that the
/// `incomingBinary` sink uses, so a regression in either is caught by
/// both production and test paths.
func _testOnly_receiveBinaryFrame(_ frame: BinaryFrame) {
handleBinaryFrame(frame)
}
#endif
// MARK: - URL construction // MARK: - URL construction
/// Builds `ws://<host>:<port>/sessions/<id>/stream?token=<bearerToken>`. /// Builds `ws://<host>:<port>/sessions/<id>/stream?token=<bearerToken>`.

View File

@ -104,16 +104,6 @@ private struct SessionItem: Decodable {
let id: String let id: String
let name: String let name: String
let state: String let state: String
enum CodingKeys: String, CodingKey { case id, name, state }
init(from decoder: Decoder) throws {
let c = try decoder.container(keyedBy: CodingKeys.self)
id = try c.decode(String.self, forKey: .id)
name = try c.decode(String.self, forKey: .name)
// Defensive: older sidecar versions omit `state` from POST /sessions.
state = (try? c.decode(String.self, forKey: .state)) ?? "idle"
}
} }
// MARK: - Errors // MARK: - Errors

View File

@ -187,8 +187,6 @@ private struct BarButton: View {
) )
} }
.buttonStyle(.plain) // prevent SwiftUI from wrapping in extra chrome .buttonStyle(.plain) // prevent SwiftUI from wrapping in extra chrome
.accessibilityLabel(title)
.accessibilityIdentifier("modbar.\(title)")
} }
} }
@ -240,11 +238,6 @@ struct RepeatingBarButton: View {
} }
} }
) )
// Make it discoverable for XCUITest (Text + gesture isn't a button)
.accessibilityElement()
.accessibilityLabel(title)
.accessibilityIdentifier("modbar.\(title)")
.accessibilityAddTraits(.isButton)
} }
// MARK: Helpers // MARK: Helpers

View File

@ -55,8 +55,6 @@ struct SessionSwitcher: View {
} label: { } label: {
Image(systemName: "plus") Image(systemName: "plus")
} }
.accessibilityLabel("New Session")
.accessibilityIdentifier("sessionswitcher.new")
} }
} }
.alert("New Session", isPresented: $showNewSessionAlert) { .alert("New Session", isPresented: $showNewSessionAlert) {

View File

@ -39,8 +39,6 @@ struct StatusBar: View {
Image(systemName: "list.bullet") Image(systemName: "list.bullet")
.font(.caption) .font(.caption)
} }
.accessibilityLabel("Switcher")
.accessibilityIdentifier("statusbar.switcher")
} }
if onSettings != nil { if onSettings != nil {
@ -50,8 +48,6 @@ struct StatusBar: View {
Image(systemName: "gear") Image(systemName: "gear")
.font(.caption) .font(.caption)
} }
.accessibilityLabel("Settings")
.accessibilityIdentifier("statusbar.settings")
} }
if onUnpair != nil { if onUnpair != nil {
@ -62,8 +58,6 @@ struct StatusBar: View {
.font(.caption) .font(.caption)
.foregroundStyle(.red) .foregroundStyle(.red)
} }
.accessibilityLabel("Unpair")
.accessibilityIdentifier("statusbar.unpair")
} }
} }
} }
@ -100,7 +94,6 @@ struct StatusBar: View {
Text(connectionStatus) Text(connectionStatus)
.font(.caption.monospaced()) .font(.caption.monospaced())
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
.accessibilityIdentifier("statusbar.connectionStatus")
} }
} }
} }

View File

@ -17,36 +17,10 @@ struct MainTerminalView: View {
@State private var showSwitcher = false @State private var showSwitcher = false
@State private var showSettings = false // T-2.11 @State private var showSettings = false // T-2.11
@State private var activeSessionId: String? = nil // T-2.6 @State private var activeSessionId: String? = nil // T-2.6
@State private var lastCapturedSeq: UInt64? = nil // T-2.10: seq captured on background
@State private var cancellables = Set<AnyCancellable>() @State private var cancellables = Set<AnyCancellable>()
@StateObject private var registry = SessionRegistry() // T-2.6 @StateObject private var registry = SessionRegistry() // T-2.6
@EnvironmentObject var appState: AppState @EnvironmentObject var appState: AppState
/// True when running under XCUITest. Skips the live SwiftTerm view + WS
/// connection, which otherwise keep the app non-idle and cause every
/// XCUITest interaction to block for ~120 s.
///
/// Always `false` in Release builds (test hooks are `#if DEBUG` only).
private var isUITest: Bool {
#if DEBUG
return ProcessInfo.processInfo.arguments.contains("--uitest")
#else
return false
#endif
}
/// True when the stub-connection mode is active (uitest + background lifecycle).
///
/// Always `false` in Release builds.
private var isUITestStub: Bool {
#if DEBUG
let args = ProcessInfo.processInfo.arguments
return args.contains("--uitest") && args.contains("--uitest-with-stub-connection")
#else
return false
#endif
}
var body: some View { var body: some View {
VStack(spacing: 0) { VStack(spacing: 0) {
// Status bar // Status bar
@ -60,14 +34,8 @@ struct MainTerminalView: View {
) )
// Terminal // Terminal
if isUITest { TerminalViewRepresentable(controller: terminalVC)
Color.black .ignoresSafeArea(edges: .bottom)
.overlay(Text("UITest mode").foregroundStyle(.white).font(.caption))
.accessibilityIdentifier("terminal.placeholder")
} else {
TerminalViewRepresentable(controller: terminalVC)
.ignoresSafeArea(edges: .bottom)
}
Divider() Divider()
@ -82,28 +50,8 @@ struct MainTerminalView: View {
} }
.task { await initialBootstrap() } .task { await initialBootstrap() }
.task { await registry.refresh(credential: credential) } // T-2.6 .task { await registry.refresh(credential: credential) } // T-2.6
.onReceive(appState.lifecycleTransitions) { isBackground in // T-2.10
Task { @MainActor in
if isBackground {
guard let conn = connection else { return }
if isUITestStub {
lastCapturedSeq = 1 // stub sentinel: ensures "Reconnecting"
} else {
lastCapturedSeq = conn.scrollback.sizeBytes > 0
? ResumeCursor().lastSeq(for: conn.id)
: nil
}
await conn.suspend()
} else {
guard !appState.isLocked, let conn = connection else { return }
await conn.resume(from: lastCapturedSeq)
}
}
}
.onChange(of: activeSessionId) { _, newId in .onChange(of: activeSessionId) { _, newId in
guard let newId else { return } guard let newId else { return }
// UI-test mode: no terminal view, no WS just update the label.
if isUITest { sessionName = newId; return }
// Avoid reconnect storm if id already matches the current connection. // Avoid reconnect storm if id already matches the current connection.
if connection?.id == newId { return } if connection?.id == newId { return }
Task { Task {
@ -130,21 +78,6 @@ struct MainTerminalView: View {
// MARK: - Bootstrap // MARK: - Bootstrap
private func initialBootstrap() async { private func initialBootstrap() async {
#if DEBUG
// UI-test mode: skip the live WebSocket. SwiftTerm's constant redraw
// on incoming frames prevents XCUITest's idle wait from completing,
// making every .tap() block for ~120 s. Status bar, modifier bar, and
// both sheets still render normally.
if ProcessInfo.processInfo.arguments.contains("--uitest") {
if isUITestStub {
await bootstrapStubConnection()
} else {
statusText = "● uitest"
sessionName = "uitest"
}
return
}
#endif
statusText = "Looking for sessions…" statusText = "Looking for sessions…"
do { do {
let sessionId = try await resolveSession() let sessionId = try await resolveSession()
@ -179,13 +112,12 @@ struct MainTerminalView: View {
.store(in: &cancellables) .store(in: &cancellables)
// Wire connection state status text // Wire connection state status text
// T-2.10: show "Reconnecting" if isStreamFrozen (lastSeq was non-nil)
conn.$connectionState conn.$connectionState
.receive(on: DispatchQueue.main) .receive(on: DispatchQueue.main)
.sink { [weak conn] state in .sink { state in
switch state { switch state {
case .connected: statusText = "\(sessionId)" case .connected: statusText = "\(sessionId)"
case .connecting: statusText = conn?.isStreamFrozen == true ? "Reconnecting…" : "Connecting…" case .connecting: statusText = "Connecting…"
case .disconnected: statusText = "Disconnected" case .disconnected: statusText = "Disconnected"
} }
} }
@ -256,36 +188,6 @@ struct MainTerminalView: View {
await conn.resume(from: lastSeq) await conn.resume(from: lastSeq)
} }
// MARK: - Stub connection (uitest-with-stub-connection)
#if DEBUG
/// Creates an in-process stub SessionConnection for UI tests that need
/// to exercise the background/foreground lifecycle without a real sidecar.
///
/// Gated with `#if DEBUG` never compiled into Release builds.
private func bootstrapStubConnection() async {
let stubId = "stub"
sessionName = stubId
activeSessionId = stubId
let conn = SessionConnection(id: stubId, credential: credential)
conn.stubMode = true
conn.$connectionState
.receive(on: DispatchQueue.main)
.sink { [weak conn] state in
switch state {
case .connected: statusText = "\(stubId)"
case .connecting: statusText = conn?.isStreamFrozen == true ? "Reconnecting…" : "Connecting…"
case .disconnected: statusText = "Disconnected"
}
}
.store(in: &cancellables)
connection = conn
await conn.resume(from: nil)
}
#endif
// MARK: - Session resolution // MARK: - Session resolution
/// Returns the first existing session id, or creates one named "pi". /// Returns the first existing session id, or creates one named "pi".

View File

@ -70,9 +70,8 @@ public final class TerminalViewController: UIViewController {
/// SwiftTerm's `feed(byteArray:)` is thread-safe; this wrapper is /// SwiftTerm's `feed(byteArray:)` is thread-safe; this wrapper is
/// intentionally @MainActor so callers remain on the main actor. /// intentionally @MainActor so callers remain on the main actor.
public func feed(data: Data) { public func feed(data: Data) {
guard let tv = terminalView else { return }
let bytes = [UInt8](data) let bytes = [UInt8](data)
tv.feed(byteArray: bytes[...]) terminalView.feed(byteArray: bytes[...])
} }
/// Feed a base64-encoded ANSI snapshot (e.g. from the sidecar's /// Feed a base64-encoded ANSI snapshot (e.g. from the sidecar's

View File

@ -1,252 +0,0 @@
// 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 var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
// =========================================================================
// 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 removed
// lifecycleTransitions is now implemented on AppState directly (T-2.10 impl).
// MARK: - CG-2: Post-Face-ID reconnect signal
@MainActor
final class PostFaceIDReconnectTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
/// CG-2 (depends on N-2): When elapsed > 60 s and Face-ID succeeds,
/// appWillForeground must emit TWO `false` transitions:
/// 1. The first emission fires before Face-ID (isLocked=true at that
/// point MainTerminalView skips the reconnect via the guard).
/// 2. After successful auth isLocked=false, a second emission fires
/// so MainTerminalView actually re-resumes the connection.
///
/// Face-ID is disabled in this test (faceid.enabled not set), so
/// FaceIDGate.authenticate() returns `true` immediately without UI.
func test_cg2_postFaceID_emitsForegroundTwiceOnAuthSuccess() async throws {
let state = AppState.shared
// Ensure Face ID is disabled FaceIDGate.authenticate() returns true.
UserDefaults.standard.removeObject(forKey: "faceid.enabled")
// Prime a "recent" background event, then rewind the timestamp so
// elapsed > 60 s (bypasses the grace-period guard).
state.appDidBackground()
state.lastForegroundedAt = Date().addingTimeInterval(-70)
var falseEmissions = 0
let exp = expectation(description: "two false emissions after Face-ID success")
exp.expectedFulfillmentCount = 2
state.lifecycleTransitions
.filter { $0 == false }
.sink { _ in
falseEmissions += 1
if falseEmissions <= 2 { exp.fulfill() }
}
.store(in: &cancellables)
await state.appWillForeground()
await fulfillment(of: [exp], timeout: 3.0)
XCTAssertEqual(falseEmissions, 2,
"CG-2 / N-2: exactly two false emissions expected " +
"(one pre-auth, one post-auth-success)")
XCTAssertFalse(state.isLocked,
"CG-2: isLocked must be false after successful Face-ID auth")
}
/// CG-2 guard: within the 60 s grace period only ONE false emission occurs
/// (no spurious second emission from the N-2 fix).
func test_cg2_withinGracePeriod_emitsForegroundOnce() async throws {
let state = AppState.shared
UserDefaults.standard.removeObject(forKey: "faceid.enabled")
state.appDidBackground()
// Do NOT rewind timestamp elapsed 0 s, within grace period.
var falseCount = 0
state.lifecycleTransitions
.filter { $0 == false }
.sink { _ in falseCount += 1 }
.store(in: &cancellables)
await state.appWillForeground()
// Allow any async propagation to settle.
try await Task.sleep(nanoseconds: 100_000_000)
XCTAssertEqual(falseCount, 1,
"CG-2 guard: within grace period must emit exactly ONE false " +
"(no spurious second emission from N-2 fix)")
}
}

View File

@ -1,458 +0,0 @@
// 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 var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
// =========================================================================
// 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 suspendresume 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: - Stubs removed
// isStreamFrozen and isKeepAliveActive are now implemented on SessionConnection
// directly (T-2.10 impl). The extension stubs above would cause a redeclaration
// error and are therefore removed.
// MARK: - CG-1: End-to-end real-seq path
@MainActor
final class EndToEndSeqPathTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
/// CG-1: Verifies the full production path:
/// binary frame received
/// ResumeCursor.update (B-1 fix)
/// background (lastCapturedSeq = cursor.lastSeq)
/// foreground resume(from: nonNil)
/// isStreamFrozen = true
/// first delta received isStreamFrozen = false, forwarded to stream
///
/// Uses no hardcoded seq sentinels and exercises the real cursor update
/// path via `_testOnly_receiveBinaryFrame` (which calls `handleBinaryFrame`,
/// the same private method the production `incomingBinary` sink invokes).
func test_cg1_endToEnd_realSeqFlowsThrough() async throws {
// Isolated cursor so this test doesn't pollute real UserDefaults.
let suiteName = "CG1.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
let cursor = ResumeCursor(defaults: defaults)
defer { defaults.removePersistentDomain(forName: suiteName) }
let conn = SessionConnection(
id: "cg1-session",
credential: fakeCredential(),
cursor: cursor
)
// Step 1: receive a binary frame with a real seq
let seq1: UInt64 = 42
let frame1 = BinaryFrame(seq: seq1, data: Data("hello".utf8))
conn._testOnly_receiveBinaryFrame(frame1)
// Step 2: cursor is now updated (what MainTerminalView reads on bg)
let persistedSeq = cursor.lastSeq(for: "cg1-session")
XCTAssertEqual(persistedSeq, seq1,
"CG-1 / B-1: ResumeCursor must be updated with the frame's seq.")
// Step 3: simulate background capture the cursor value
let lastCapturedSeq = persistedSeq // non-nil because B-1 is fixed
XCTAssertNotNil(lastCapturedSeq,
"CG-1: lastCapturedSeq must be non-nil after a real binary frame")
// Step 4: foreground resume from captured seq
await conn.resume(from: lastCapturedSeq)
try await Task.sleep(nanoseconds: 50_000_000) // 50 ms
XCTAssertTrue(conn.isStreamFrozen,
"CG-1: isStreamFrozen must be true immediately after resume(from: nonNil)")
// Step 5: first delta arrives freeze clears, stream receives data
var receivedData: [Data] = []
let streamExp = expectation(description: "first delta forwarded to stream")
conn.stream
.first()
.sink { data in
receivedData.append(data)
streamExp.fulfill()
}
.store(in: &cancellables)
let frame2 = BinaryFrame(seq: seq1 + 1, data: Data("world".utf8))
conn._testOnly_receiveBinaryFrame(frame2)
await fulfillment(of: [streamExp], timeout: 1.0)
XCTAssertFalse(conn.isStreamFrozen,
"CG-1: isStreamFrozen must clear on first binary frame (first delta IS forwarded)")
XCTAssertEqual(receivedData.count, 1,
"CG-1: Exactly one frame must reach stream")
XCTAssertEqual(receivedData.first, Data("world".utf8),
"CG-1: The delta payload must be forwarded unchanged")
XCTAssertEqual(cursor.lastSeq(for: "cg1-session"), seq1 + 1,
"CG-1: Cursor must be updated to the new seq after the first delta")
await conn.suspend()
}
}
// MARK: - CG-3: isStreamFrozen gating behaviour
@MainActor
final class StreamFrozenGatingTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
/// CG-3 decision: INFORMATIONAL (not a hard gate on stream delivery).
///
/// In the IC-1 protocol the server only starts sending bytes after it
/// processes our `resume` frame, so there are no stale bytes that could
/// arrive while `isStreamFrozen == true`. The first binary frame IS
/// the first meaningful delta and IS forwarded to `stream`.
///
/// `isStreamFrozen` drives the status-bar label ("Reconnecting" vs
/// "Connecting") and exposes the freeze state for observability, but
/// does not technically block bytes from reaching `stream.send()`.
///
/// This test verifies:
/// - After resume(from: nonNil): isStreamFrozen = true
/// - After the first binary frame: isStreamFrozen = false
/// - The first binary frame IS forwarded to stream (first delta IS forwarded)
func test_cg3_firstDeltaThawsAndIsForwarded() async throws {
let conn = SessionConnection(id: "cg3", credential: fakeCredential())
await conn.resume(from: 50) // non-nil freeze
try await Task.sleep(nanoseconds: 50_000_000)
XCTAssertTrue(conn.isStreamFrozen, "Must be frozen after resume(from: nonNil)")
var received: [Data] = []
conn.stream
.sink { received.append($0) }
.store(in: &cancellables)
// First delta arrives while frozen: must thaw AND be forwarded.
let firstDelta = BinaryFrame(seq: 51, data: Data("delta".utf8))
conn._testOnly_receiveBinaryFrame(firstDelta)
XCTAssertFalse(conn.isStreamFrozen,
"CG-3: First binary frame must clear isStreamFrozen")
XCTAssertEqual(received.count, 1,
"CG-3: First delta must be forwarded to stream (not dropped)")
// Subsequent frames also flow normally.
conn._testOnly_receiveBinaryFrame(BinaryFrame(seq: 52, data: Data("more".utf8)))
XCTAssertEqual(received.count, 2,
"CG-3: Subsequent frames must also be forwarded")
await conn.suspend()
}
/// Additional CG-3 guard: fresh attach (nil seq) must never set isStreamFrozen.
func test_cg3_freshAttach_neverFreezes() async throws {
let conn = SessionConnection(id: "cg3-fresh", credential: fakeCredential())
await conn.resume(from: nil) // nil no freeze
try await Task.sleep(nanoseconds: 50_000_000)
XCTAssertFalse(conn.isStreamFrozen,
"CG-3: Fresh attach must not freeze stream")
await conn.suspend()
}
}
// MARK: - CG-4: Multi-cycle reconnect with seq progression
@MainActor
final class MultiCycleSeqTests: XCTestCase {
private var cancellables = Set<AnyCancellable>()
override func tearDown() async throws {
cancellables.removeAll()
}
/// CG-4: Two full suspend/resume cycles with seq advancing on each.
/// Cycle 1: receive seq=100 suspend resume(from:100) frozen
/// Cycle 2: receive seq=120 suspend resume(from:120) frozen
/// Verifies cursor advances and isStreamFrozen toggles correctly.
func test_cg4_multiCycle_seqProgression() async throws {
let suiteName = "CG4.\(UUID().uuidString)"
let defaults = UserDefaults(suiteName: suiteName)!
let cursor = ResumeCursor(defaults: defaults)
defer { defaults.removePersistentDomain(forName: suiteName) }
let conn = SessionConnection(
id: "cg4-session",
credential: fakeCredential(),
cursor: cursor
)
// Cycle 1
// Receive a frame with seq=100.
conn._testOnly_receiveBinaryFrame(BinaryFrame(seq: 100, data: Data("a".utf8)))
XCTAssertEqual(cursor.lastSeq(for: "cg4-session"), 100,
"CG-4 cycle 1: cursor must be 100 after receiving seq=100")
// Suspend (isStreamFrozen cleared by suspend).
await conn.suspend()
XCTAssertFalse(conn.isStreamFrozen, "suspend() must clear isStreamFrozen")
// Resume from captured seq=100 frozen.
await conn.resume(from: 100)
try await Task.sleep(nanoseconds: 50_000_000)
XCTAssertTrue(conn.isStreamFrozen,
"CG-4 cycle 1: must be frozen after resume(from: 100)")
// Receive seq=120 thaws.
conn._testOnly_receiveBinaryFrame(BinaryFrame(seq: 120, data: Data("b".utf8)))
XCTAssertFalse(conn.isStreamFrozen,
"CG-4 cycle 1: first delta must clear freeze")
XCTAssertEqual(cursor.lastSeq(for: "cg4-session"), 120,
"CG-4 cycle 1: cursor must advance to 120")
// Cycle 2
await conn.suspend()
XCTAssertFalse(conn.isStreamFrozen, "suspend() must clear isStreamFrozen")
// Resume from captured seq=120 frozen.
await conn.resume(from: 120)
try await Task.sleep(nanoseconds: 50_000_000)
XCTAssertTrue(conn.isStreamFrozen,
"CG-4 cycle 2: must be frozen after resume(from: 120)")
// One more frame thaws and cursor advances.
conn._testOnly_receiveBinaryFrame(BinaryFrame(seq: 150, data: Data("c".utf8)))
XCTAssertFalse(conn.isStreamFrozen,
"CG-4 cycle 2: second cycle delta must also clear freeze")
XCTAssertEqual(cursor.lastSeq(for: "cg4-session"), 150,
"CG-4 cycle 2: cursor must advance to 150")
await conn.suspend()
}
}

View File

@ -1,174 +0,0 @@
// 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 backgroundforeground 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)")
}
}
}

View File

@ -1,74 +0,0 @@
import Foundation
import XCTest
@MainActor
extension XCUIApplication {
/// Launch with a freshly-fetched pairing URL so the test starts from a
/// known paired state. Hits `/pair-qr` on the sidecar to obtain a
/// one-time-use token, then passes it to the app via launch args.
/// Fails the current test if the sidecar is unreachable.
func launchPaired(file: StaticString = #file, line: UInt = #line) {
guard let url = TestSidecar.freshPairURL() else {
XCTFail("Could not fetch fresh pair URL from sidecar — is it running?",
file: file, line: line)
return
}
launchArguments = ["--uitest", "--reset-state", "--pair-with-url", url]
launch()
}
/// Launch with state reset so the PairingFlowView is shown.
func launchUnpaired() {
launchArguments = ["--uitest", "--reset-state"]
launch()
}
/// Launch with a specific pairing URL (for the explicit deep-link test).
func launchWithPairing(url: String) {
launchArguments = ["--uitest", "--reset-state", "--pair-with-url", url]
launch()
}
/// Launch directly into the lock screen (faceid enabled + forced lock).
/// Resets state first so the test isn't polluted by previous runs.
func launchLocked() {
launchArguments = ["--uitest", "--reset-state", "--enable-faceid", "--force-lock"]
launch()
}
}
// MARK: - Sidecar helpers
enum TestSidecar {
static let baseURL = "http://10.13.37.2:17373"
static let bearer = "GeoZytsPPGwItCHRNu8EwoZGlHH5iUAx"
/// Synchronously fetch a fresh pairing URL from the sidecar.
static func freshPairURL() -> String? {
let url = URL(string: "\(baseURL)/pair-qr?token=\(bearer)&format=url")!
var result: String?
let sem = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: url) { data, _, _ in
defer { sem.signal() }
guard let data = data,
let text = String(data: data, encoding: .utf8) else { return }
if let match = text.range(of: #"pi-remote://[^\s]+"#,
options: .regularExpression) {
result = String(text[match])
}
}.resume()
_ = sem.wait(timeout: .now() + 5)
return result
}
/// Delete a session via the sidecar REST API (best-effort, for cleanup).
static func deleteSession(_ id: String) {
var req = URLRequest(url: URL(string: "\(baseURL)/sessions/\(id)")!)
req.httpMethod = "DELETE"
req.setValue("Bearer \(bearer)", forHTTPHeaderField: "Authorization")
let sem = DispatchSemaphore(value: 0)
URLSession.shared.dataTask(with: req) { _, _, _ in sem.signal() }.resume()
_ = sem.wait(timeout: .now() + 5)
}
}

View File

@ -1,21 +0,0 @@
import XCTest
@MainActor
final class LockScreenUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLockScreenAppearance() throws {
let app = XCUIApplication()
app.launchLocked()
// Assert LockView is showing
let lockText = app.staticTexts["Locked"]
XCTAssertTrue(lockText.waitForExistence(timeout: 5), "Lock screen should be visible when forced")
// We can't easily test Face ID success in simulator without manual menu interaction,
// so we just verify the lock screen is presented.
}
}

View File

@ -1,47 +0,0 @@
import XCTest
@MainActor
final class ModifierBarUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testModifierBarButtons() throws {
let app = XCUIApplication()
app.launchPaired()
// List of buttons to verify
// Wait until the terminal view is up and the modifier bar visible.
XCTAssertTrue(app.buttons["Ctrl"].waitForExistence(timeout: 10),
"Modifier bar should appear after pairing")
let buttons = [
"Ctrl", "Esc", "Tab", "", "", "", "", "⇧↵", "📋"
]
for title in buttons {
let btn = app.buttons[title]
XCTAssertTrue(btn.exists, "Modifier button '\(title)' should be present")
}
// Test Ctrl sticky state
let ctrlBtn = app.buttons["Ctrl"]
// In SwiftUI, the visual state (background color) isn't directly accessible via XCUITest
// unless we add accessibilityValue or similar.
// However, we can verify that tapping it doesn't crash and the app remains responsive.
ctrlBtn.tap()
// Tap a few other buttons
app.buttons["Esc"].tap()
app.buttons[""].tap()
app.buttons["📋"].tap()
// Verify PasteSheet appears (navigationTitle is "Paste")
let pasteSheetTitle = app.staticTexts["Paste"]
XCTAssertTrue(pasteSheetTitle.waitForExistence(timeout: 5), "Paste sheet should appear after tapping clipboard button")
// Dismiss PasteSheet
app.buttons["Cancel"].tap()
}
}

View File

@ -1,31 +0,0 @@
import XCTest
@MainActor
final class PairingUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testLaunchUnpairedShowsPairingFlow() throws {
let app = XCUIApplication()
app.launchUnpaired()
// Assert PairingFlowView is showing
let scanButton = app.buttons["Tap to Scan"]
XCTAssertTrue(scanButton.waitForExistence(timeout: 5), "PairingFlowView should be visible on unpaired launch")
}
func testAutoPairingViaLaunchArgument() throws {
guard let pairingURL = TestSidecar.freshPairURL() else {
throw XCTSkip("Could not fetch fresh pair URL - sidecar may be down")
}
let app = XCUIApplication()
app.launchWithPairing(url: pairingURL)
let switcherButton = app.buttons["Switcher"]
XCTAssertTrue(switcherButton.waitForExistence(timeout: 15),
"App should auto-pair and show MainTerminalView")
}
}

View File

@ -1,72 +0,0 @@
import XCTest
@MainActor
final class SessionSwitcherUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testSessionSwitcherFlow() throws {
let app = XCUIApplication()
app.launchPaired()
// 1. Open Switcher
let switcherButton = app.buttons["Switcher"]
XCTAssertTrue(switcherButton.waitForExistence(timeout: 10))
switcherButton.tap()
// 2. Assert Navigation Title
let navTitle = app.staticTexts["Sessions"]
XCTAssertTrue(navTitle.waitForExistence(timeout: 5), "Session switcher sheet should have title 'Sessions'")
// 3. Assert existing sessions
let mainSession = app.staticTexts["main"]
let workSession = app.staticTexts["work"]
let logsSession = app.staticTexts["logs"]
XCTAssertTrue(mainSession.exists, "Session 'main' should be present")
XCTAssertTrue(workSession.exists, "Session 'work' should be present")
XCTAssertTrue(logsSession.exists, "Session 'logs' should be present")
// 4. Select 'main' and verify dismissal
mainSession.tap()
XCTAssertFalse(navTitle.waitForExistence(timeout: 5), "Sheet should dismiss after selecting a session")
// 5. Create new session
switcherButton.tap()
let addButton = app.buttons["New Session"]
XCTAssertTrue(addButton.waitForExistence(timeout: 5))
addButton.tap()
let nameField = app.textFields["Session name"]
XCTAssertTrue(nameField.waitForExistence(timeout: 5))
nameField.tap()
let uniqueName = "uitest-\(Int(Date().timeIntervalSince1970))"
nameField.typeText(uniqueName)
let createButton = app.buttons["Create"]
XCTAssertTrue(createButton.exists)
createButton.tap()
// 6. Verify new session appears (refresh runs async after spawn;
// allow extra time for the sidecar round-trip + list rerender).
let newSession = app.staticTexts[uniqueName]
if !newSession.waitForExistence(timeout: 15) {
// Capture diagnostic info to file before failing
let screenshot = XCUIScreen.main.screenshot()
let att = XCTAttachment(screenshot: screenshot)
att.lifetime = .keepAlways
att.name = "session-switcher-failure"
add(att)
let hierarchy = XCTAttachment(string: app.debugDescription)
hierarchy.lifetime = .keepAlways
hierarchy.name = "hierarchy"
add(hierarchy)
XCTFail("New session '\(uniqueName)' should appear in the list")
}
// Cleanup: delete via sidecar API
TestSidecar.deleteSession(uniqueName)
}
}

View File

@ -1,55 +0,0 @@
import XCTest
@MainActor
final class SettingsUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testSettingsView() throws {
let app = XCUIApplication()
app.launchPaired()
// 1. Open Settings
let settingsButton = app.buttons["Settings"]
XCTAssertTrue(settingsButton.waitForExistence(timeout: 10))
settingsButton.tap()
// 2. Assert Navigation Title
let navTitle = app.staticTexts["Settings"]
XCTAssertTrue(navTitle.waitForExistence(timeout: 5), "Settings sheet should have title 'Settings'")
// 3. Assert section content (SwiftUI renders Section headers as
// uppercase "SECURITY" etc. but visibility is fragile across SDKs.
// Asserting on the section's contents is more robust.)
XCTAssertTrue(app.switches["Require Face ID"].waitForExistence(timeout: 5),
"Security → Face ID toggle should exist")
XCTAssertTrue(app.buttons["Unpair"].exists,
"Danger → Unpair button should exist")
// 4. Toggle Face ID. SwiftUI's Toggle inside a Form is famously
// sticky for XCUI's .tap() a coordinate hit on the switch itself
// is more reliable.
let faceIDToggle = app.switches["Require Face ID"]
XCTAssertTrue(faceIDToggle.exists, "Require Face ID toggle should exist")
let initialState = faceIDToggle.value as? String
faceIDToggle.coordinate(withNormalizedOffset: CGVector(dx: 0.95, dy: 0.5)).tap()
// Give SwiftUI a moment to update + persist into UserDefaults.
let flipped = NSPredicate(format: "value != %@", initialState ?? "")
expectation(for: flipped, evaluatedWith: faceIDToggle, handler: nil)
waitForExpectations(timeout: 3)
// 5. Verify Sidecar Info
// Host:Port is in a LabeledContent. In XCUITest, LabeledContent often appears as two staticTexts.
// We look for the value that matches the pattern.
let hostText = app.staticTexts.matching(NSPredicate(format: "label CONTAINS '10.13.37.2:17373'")).firstMatch
XCTAssertTrue(hostText.exists, "Sidecar host:port should be displayed correctly")
// 6. Dismiss
let doneButton = app.buttons["Done"]
XCTAssertTrue(doneButton.exists)
doneButton.tap()
XCTAssertFalse(navTitle.waitForExistence(timeout: 5), "Settings sheet should dismiss")
}
}

View File

@ -1,23 +0,0 @@
// SmokeUITests.swift
// Minimal smoke test verifies the UI-testing target is wired correctly.
// Real feature coverage lives in dedicated UITest files (see PairingUITests,
// SessionSwitcherUITests, etc.).
import XCTest
@MainActor
final class SmokeUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
/// Launch the app and verify it shows *some* UI within a reasonable
/// timeout. This is just a wiring check feature tests assert specifics.
func testAppLaunchesWithoutCrashing() throws {
let app = XCUIApplication()
app.launch()
// Wait for any window to appear.
XCTAssertTrue(app.wait(for: .runningForeground, timeout: 10))
}
}

View File

@ -1,22 +0,0 @@
import XCTest
@MainActor
final class StatusBarUITests: XCTestCase {
override func setUpWithError() throws {
continueAfterFailure = false
}
func testStatusBarButtonsPresence() throws {
let app = XCUIApplication()
app.launchPaired()
let switcherButton = app.buttons["Switcher"]
let settingsButton = app.buttons["Settings"]
let unpairButton = app.buttons["Unpair"]
XCTAssertTrue(switcherButton.waitForExistence(timeout: 10), "Switcher button should be present")
XCTAssertTrue(settingsButton.waitForExistence(timeout: 5), "Settings button should be present")
XCTAssertTrue(unpairButton.waitForExistence(timeout: 5), "Unpair button should be present")
}
}

View File

@ -7,12 +7,9 @@
objects = { objects = {
/* Begin PBXBuildFile section */ /* Begin PBXBuildFile section */
0572AF0EFE02ABD12CD5C584 /* SmokeUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = EE5E1A0BE69AF1FEF73E373F /* SmokeUITests.swift */; };
05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */; }; 05CD861F694B84577A4B5A27 /* PairingTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */; };
09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */; }; 09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */ = {isa = PBXBuildFile; fileRef = C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */; };
132F60783EC43AE420FA7CD5 /* LockView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 94525E2A804BF64F12806D4E /* LockView.swift */; };
16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */; }; 16095F16FAB72320676A729D /* ResumeCursorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */; };
17F996CE1F5CF3FF12E5C1AB /* LockScreenUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = DF52ADD110456E368EAE217C /* LockScreenUITests.swift */; };
19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */; }; 19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D4E4BB86FBFBD80287048C1 /* MainTerminalView.swift */; };
1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */; }; 1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */ = {isa = PBXBuildFile; fileRef = 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */; };
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B6B497329B98A4508D963B /* FrameCodec.swift */; }; 2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */ = {isa = PBXBuildFile; fileRef = 15B6B497329B98A4508D963B /* FrameCodec.swift */; };
@ -21,15 +18,10 @@
30E07FF586EABBBB8C70AE60 /* piRemoteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D747BC787A24B4E225B142 /* piRemoteApp.swift */; }; 30E07FF586EABBBB8C70AE60 /* piRemoteApp.swift in Sources */ = {isa = PBXBuildFile; fileRef = 73D747BC787A24B4E225B142 /* piRemoteApp.swift */; };
3486C15393498F5306C8F43B /* ScrollbackCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */; }; 3486C15393498F5306C8F43B /* ScrollbackCacheTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */; };
3AC56484E8AC068C10F31324 /* REVIEW_NOTES.md in Resources */ = {isa = PBXBuildFile; fileRef = 6DE4A325EEA53870390B89D9 /* REVIEW_NOTES.md */; }; 3AC56484E8AC068C10F31324 /* REVIEW_NOTES.md in Resources */ = {isa = PBXBuildFile; fileRef = 6DE4A325EEA53870390B89D9 /* REVIEW_NOTES.md */; };
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 */; };
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 */; };
@ -37,14 +29,9 @@
909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ABB7E636E55917562D9A2C /* DeviceTokenRegistrar.swift */; }; 909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 03ABB7E636E55917562D9A2C /* DeviceTokenRegistrar.swift */; };
9855E1E1C856E20B339F2A0A /* NotificationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A9C78194E7644A78FFA23 /* NotificationDelegate.swift */; }; 9855E1E1C856E20B339F2A0A /* NotificationDelegate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 844A9C78194E7644A78FFA23 /* NotificationDelegate.swift */; };
9AC28FD7FD38F250FE477441 /* ScrollbackCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98EF1F714A1A5E8D4C3DA2B /* ScrollbackCache.swift */; }; 9AC28FD7FD38F250FE477441 /* ScrollbackCache.swift in Sources */ = {isa = PBXBuildFile; fileRef = C98EF1F714A1A5E8D4C3DA2B /* ScrollbackCache.swift */; };
9BDEBAD2C295FDD53946FB8C /* ModifierBarUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 1E689590D75F152E90467016 /* ModifierBarUITests.swift */; };
9E4B31AA31D8A900D36918A3 /* FaceIDGate.swift in Sources */ = {isa = PBXBuildFile; fileRef = 87407DA9C464BA9C9E118308 /* FaceIDGate.swift */; };
A1527F94754A9205576C317C /* Helpers.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5C1F7B03B47D7766D72BA3B8 /* Helpers.swift */; };
A1B807C3E8586E99507463B9 /* FrameCodecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */; }; A1B807C3E8586E99507463B9 /* FrameCodecTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = A85D5F5AF59E84DDC3AE168B /* FrameCodecTests.swift */; };
A3144EA79E01CB9D2DD552C8 /* PairingFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38B86E930CB34DEB9E4C144 /* PairingFlowView.swift */; }; A3144EA79E01CB9D2DD552C8 /* PairingFlowView.swift in Sources */ = {isa = PBXBuildFile; fileRef = A38B86E930CB34DEB9E4C144 /* PairingFlowView.swift */; };
A51AABE190B66EE994D8415B /* SettingsView.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5BCDE8061BF3E2DABEA12E7 /* SettingsView.swift */; };
A56F82D6CEC7C6654C02C7BB /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205F823929F91450C58D4CA /* WebSocketClient.swift */; }; A56F82D6CEC7C6654C02C7BB /* WebSocketClient.swift in Sources */ = {isa = PBXBuildFile; fileRef = 5205F823929F91450C58D4CA /* WebSocketClient.swift */; };
AEAB8079223530B0437F3434 /* PairingUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BEEC5B3171A29F91F7D9B67E /* PairingUITests.swift */; };
AF1F7740D9A9F40BA8308052 /* TerminalViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */; }; AF1F7740D9A9F40BA8308052 /* TerminalViewRepresentable.swift in Sources */ = {isa = PBXBuildFile; fileRef = 39536FD31585716EF30C84C6 /* TerminalViewRepresentable.swift */; };
B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12767F24EC6ECFA77B280A8D /* FontStore.swift */; }; B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */ = {isa = PBXBuildFile; fileRef = 12767F24EC6ECFA77B280A8D /* FontStore.swift */; };
B73AD1B4B8830C1DEE8A78AE /* ModifierStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */; }; B73AD1B4B8830C1DEE8A78AE /* ModifierStateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 278215F3FD64C681C55F23A4 /* ModifierStateTests.swift */; };
@ -52,28 +39,16 @@
C1F266B0DC9D7029E5E5B203 /* DeviceTokenRegistrarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */; }; C1F266B0DC9D7029E5E5B203 /* DeviceTokenRegistrarTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */; };
C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B772854E3FADA8998C93DAF5 /* Keychain.swift */; }; C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */ = {isa = PBXBuildFile; fileRef = B772854E3FADA8998C93DAF5 /* Keychain.swift */; };
C823749124F98D46FB993247 /* KeychainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47CA5A1045A264958B360BF /* KeychainTests.swift */; }; C823749124F98D46FB993247 /* KeychainTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = F47CA5A1045A264958B360BF /* KeychainTests.swift */; };
C87E6CCFC4ADC4AC4CAC4C0D /* SessionRow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2C3C69D7879985E77A45DE76 /* SessionRow.swift */; };
D3E1C0562B97260D28FF1C11 /* Pairing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F544C25D53F52291E2FDB6F /* Pairing.swift */; }; D3E1C0562B97260D28FF1C11 /* Pairing.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0F544C25D53F52291E2FDB6F /* Pairing.swift */; };
D3E8D6064F38E4024A6863C9 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */; }; D3E8D6064F38E4024A6863C9 /* TerminalTheme.swift in Sources */ = {isa = PBXBuildFile; fileRef = E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */; };
D77D662C3311D9646BE57596 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6BDDFB0C0D1D6D6FB490BA8D /* Preview Assets.xcassets */; }; D77D662C3311D9646BE57596 /* Preview Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 6BDDFB0C0D1D6D6FB490BA8D /* Preview Assets.xcassets */; };
D8F05ED47CD1A4A8298EDDFB /* StatusBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0C66962D8B5B6869B60F8101 /* StatusBar.swift */; };
E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F553E905D716538D9DA442E7 /* ModifierBar.swift */; }; E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = F553E905D716538D9DA442E7 /* ModifierBar.swift */; };
EAF7C85B3223C503829028D3 /* SettingsUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 96A48B8A34A0647A8BF12B55 /* SettingsUITests.swift */; };
EF47351AD35418E508ED33C9 /* SessionSwitcherUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = ED280CEF6D334529433F20A3 /* SessionSwitcherUITests.swift */; };
F10312EF88278BB719CCF7E7 /* StatusBarUITests.swift in Sources */ = {isa = PBXBuildFile; fileRef = D30D90D7888D84A2EF1B22EA /* StatusBarUITests.swift */; };
F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 188683139B863ED1AC03A1BB /* ContentView.swift */; }; F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 188683139B863ED1AC03A1BB /* ContentView.swift */; };
F8CBA52AE2CC3D8496361D45 /* TerminalFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */; }; F8CBA52AE2CC3D8496361D45 /* TerminalFont.swift in Sources */ = {isa = PBXBuildFile; fileRef = C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */; };
FADABBF0D0229D84832D3B78 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = D095700C52C60FDA2CB38679 /* SwiftTerm */; }; FADABBF0D0229D84832D3B78 /* SwiftTerm in Frameworks */ = {isa = PBXBuildFile; productRef = D095700C52C60FDA2CB38679 /* SwiftTerm */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
/* Begin PBXContainerItemProxy section */ /* Begin PBXContainerItemProxy section */
4BC4CAFF5DFB43FCB5A72089 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy;
containerPortal = B5A2356AA5371FBA25136FA6 /* Project object */;
proxyType = 1;
remoteGlobalIDString = 4910ACCEB67B73CBA3440774;
remoteInfo = piRemote;
};
B301DDFED8092F66145718E3 /* PBXContainerItemProxy */ = { B301DDFED8092F66145718E3 /* PBXContainerItemProxy */ = {
isa = PBXContainerItemProxy; isa = PBXContainerItemProxy;
containerPortal = B5A2356AA5371FBA25136FA6 /* Project object */; containerPortal = B5A2356AA5371FBA25136FA6 /* Project object */;
@ -86,23 +61,18 @@
/* Begin PBXFileReference section */ /* Begin PBXFileReference section */
03ABB7E636E55917562D9A2C /* DeviceTokenRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegistrar.swift; sourceTree = "<group>"; }; 03ABB7E636E55917562D9A2C /* DeviceTokenRegistrar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegistrar.swift; sourceTree = "<group>"; };
0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = "<group>"; }; 0AF0B5FBC3ACEC8EF5C3FF12 /* QRScannerView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = QRScannerView.swift; sourceTree = "<group>"; };
0C66962D8B5B6869B60F8101 /* StatusBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBar.swift; sourceTree = "<group>"; };
0E401DECD467A1D3AB030610 /* piRemote.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = piRemote.entitlements; sourceTree = "<group>"; }; 0E401DECD467A1D3AB030610 /* piRemote.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = piRemote.entitlements; sourceTree = "<group>"; };
0F544C25D53F52291E2FDB6F /* Pairing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pairing.swift; sourceTree = "<group>"; }; 0F544C25D53F52291E2FDB6F /* Pairing.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Pairing.swift; sourceTree = "<group>"; };
12767F24EC6ECFA77B280A8D /* FontStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontStore.swift; sourceTree = "<group>"; }; 12767F24EC6ECFA77B280A8D /* FontStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FontStore.swift; sourceTree = "<group>"; };
15B6B497329B98A4508D963B /* FrameCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameCodec.swift; sourceTree = "<group>"; }; 15B6B497329B98A4508D963B /* FrameCodec.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FrameCodec.swift; sourceTree = "<group>"; };
188683139B863ED1AC03A1BB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; }; 188683139B863ED1AC03A1BB /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = "<group>"; };
1E689590D75F152E90467016 /* ModifierBarUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierBarUITests.swift; sourceTree = "<group>"; };
22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollbackCacheTests.swift; sourceTree = "<group>"; }; 22658EED98A0B3C2183AACDD /* ScrollbackCacheTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollbackCacheTests.swift; sourceTree = "<group>"; };
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>"; };
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>"; };
55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REVIEW_NOTES_2.md; sourceTree = "<group>"; }; 55DAE4BC86AE950146CD7B94 /* REVIEW_NOTES_2.md */ = {isa = PBXFileReference; lastKnownFileType = net.daringfireball.markdown; path = REVIEW_NOTES_2.md; sourceTree = "<group>"; };
5C1F7B03B47D7766D72BA3B8 /* Helpers.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Helpers.swift; sourceTree = "<group>"; };
62DD6C03615573E339057EBF /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; }; 62DD6C03615573E339057EBF /* AppState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppState.swift; sourceTree = "<group>"; };
658CB2FCA96A8913B1753B1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; }; 658CB2FCA96A8913B1753B1C /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist; path = Info.plist; sourceTree = "<group>"; };
67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConnection.swift; sourceTree = "<group>"; }; 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionConnection.swift; sourceTree = "<group>"; };
@ -112,38 +82,24 @@
7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeCursorTests.swift; sourceTree = "<group>"; }; 7607FF3804A2602B1C6A05D4 /* ResumeCursorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeCursorTests.swift; sourceTree = "<group>"; };
83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegistrarTests.swift; sourceTree = "<group>"; }; 83446A0D895B866E880D4F2D /* DeviceTokenRegistrarTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceTokenRegistrarTests.swift; sourceTree = "<group>"; };
844A9C78194E7644A78FFA23 /* NotificationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDelegate.swift; sourceTree = "<group>"; }; 844A9C78194E7644A78FFA23 /* NotificationDelegate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotificationDelegate.swift; sourceTree = "<group>"; };
87407DA9C464BA9C9E118308 /* FaceIDGate.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FaceIDGate.swift; sourceTree = "<group>"; };
94525E2A804BF64F12806D4E /* LockView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockView.swift; sourceTree = "<group>"; };
959878B4816DD2617038A339 /* SessionRegistry.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionRegistry.swift; sourceTree = "<group>"; };
96A48B8A34A0647A8BF12B55 /* SettingsUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsUITests.swift; sourceTree = "<group>"; };
99FA0A0FD737901834AD5705 /* ThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTests.swift; sourceTree = "<group>"; }; 99FA0A0FD737901834AD5705 /* ThemeTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeTests.swift; sourceTree = "<group>"; };
9E41A5D34EADEA9D2925DDF0 /* piRemoteUITests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = piRemoteUITests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
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>"; };
BC48E39D19238178A180B30C /* ThemeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeStore.swift; sourceTree = "<group>"; }; BC48E39D19238178A180B30C /* ThemeStore.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ThemeStore.swift; sourceTree = "<group>"; };
BEEC5B3171A29F91F7D9B67E /* PairingUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingUITests.swift; sourceTree = "<group>"; };
C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalFont.swift; sourceTree = "<group>"; }; C5B05BBDD469F51657ED89B0 /* TerminalFont.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalFont.swift; sourceTree = "<group>"; };
C5BCDE8061BF3E2DABEA12E7 /* SettingsView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SettingsView.swift; sourceTree = "<group>"; };
C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeCursor.swift; sourceTree = "<group>"; }; C7961BE126AFEEE4B7AA6621 /* ResumeCursor.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ResumeCursor.swift; sourceTree = "<group>"; };
C98EF1F714A1A5E8D4C3DA2B /* ScrollbackCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollbackCache.swift; sourceTree = "<group>"; }; C98EF1F714A1A5E8D4C3DA2B /* ScrollbackCache.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ScrollbackCache.swift; sourceTree = "<group>"; };
CD24C7095F23AF63CCFB23F0 /* piRemoteTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = piRemoteTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; CD24C7095F23AF63CCFB23F0 /* piRemoteTests.xctest */ = {isa = PBXFileReference; includeInIndex = 0; lastKnownFileType = wrapper.cfbundle; path = piRemoteTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
D30D90D7888D84A2EF1B22EA /* StatusBarUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = StatusBarUITests.swift; sourceTree = "<group>"; };
D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidecarCredential.swift; sourceTree = "<group>"; }; D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SidecarCredential.swift; sourceTree = "<group>"; };
DD8434E1D87FFE2616683652 /* ModifierState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierState.swift; sourceTree = "<group>"; }; DD8434E1D87FFE2616683652 /* ModifierState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ModifierState.swift; sourceTree = "<group>"; };
DF52ADD110456E368EAE217C /* LockScreenUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LockScreenUITests.swift; sourceTree = "<group>"; };
E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; }; E3A7FB4B9C4D2B63B016E11A /* TerminalTheme.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TerminalTheme.swift; sourceTree = "<group>"; };
E8C77FC9D24BB5F8DA96386C /* SessionSwitcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionSwitcher.swift; sourceTree = "<group>"; };
E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingTests.swift; sourceTree = "<group>"; }; E9BAF4FBE6CC23FDD9B40040 /* PairingTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PairingTests.swift; sourceTree = "<group>"; };
ED280CEF6D334529433F20A3 /* SessionSwitcherUITests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SessionSwitcherUITests.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 */
@ -164,7 +120,6 @@
children = ( children = (
2E2370A3190FDC144C822FF6 /* piRemote.app */, 2E2370A3190FDC144C822FF6 /* piRemote.app */,
CD24C7095F23AF63CCFB23F0 /* piRemoteTests.xctest */, CD24C7095F23AF63CCFB23F0 /* piRemoteTests.xctest */,
9E41A5D34EADEA9D2925DDF0 /* piRemoteUITests.xctest */,
); );
name = Products; name = Products;
sourceTree = "<group>"; sourceTree = "<group>";
@ -173,7 +128,6 @@
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */, 67F95D26CD899B18D07AB0B2 /* SessionConnection.swift */,
959878B4816DD2617038A339 /* SessionRegistry.swift */,
); );
path = Sessions; path = Sessions;
sourceTree = "<group>"; sourceTree = "<group>";
@ -190,15 +144,6 @@
path = Core; path = Core;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
35F24B7F065B257F93810E5B /* Sessions */ = {
isa = PBXGroup;
children = (
2C3C69D7879985E77A45DE76 /* SessionRow.swift */,
E8C77FC9D24BB5F8DA96386C /* SessionSwitcher.swift */,
);
path = Sessions;
sourceTree = "<group>";
};
49209A78102230A37C0FF8D0 /* Terminal */ = { 49209A78102230A37C0FF8D0 /* Terminal */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -252,7 +197,6 @@
children = ( children = (
C8D95B3C16FEE9C9FBE38FDE /* Sources */, C8D95B3C16FEE9C9FBE38FDE /* Sources */,
69990A9885FB5B354E73AB90 /* Tests */, 69990A9885FB5B354E73AB90 /* Tests */,
B259358DD628B98EC61A6736 /* UITests */,
161D89B853288EC766A0767D /* Products */, 161D89B853288EC766A0767D /* Products */,
); );
sourceTree = "<group>"; sourceTree = "<group>";
@ -267,26 +211,9 @@
path = Input; path = Input;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
B259358DD628B98EC61A6736 /* UITests */ = {
isa = PBXGroup;
children = (
A7B0C4212866DDF358CD483C /* BackgroundLifecycleUITests.swift */,
5C1F7B03B47D7766D72BA3B8 /* Helpers.swift */,
DF52ADD110456E368EAE217C /* LockScreenUITests.swift */,
1E689590D75F152E90467016 /* ModifierBarUITests.swift */,
BEEC5B3171A29F91F7D9B67E /* PairingUITests.swift */,
ED280CEF6D334529433F20A3 /* SessionSwitcherUITests.swift */,
96A48B8A34A0647A8BF12B55 /* SettingsUITests.swift */,
EE5E1A0BE69AF1FEF73E373F /* SmokeUITests.swift */,
D30D90D7888D84A2EF1B22EA /* StatusBarUITests.swift */,
);
path = UITests;
sourceTree = "<group>";
};
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 */,
@ -296,20 +223,11 @@
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;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
C7FBB3C467939760D2971070 /* Status */ = {
isa = PBXGroup;
children = (
0C66962D8B5B6869B60F8101 /* StatusBar.swift */,
);
path = Status;
sourceTree = "<group>";
};
C8D95B3C16FEE9C9FBE38FDE /* Sources */ = { C8D95B3C16FEE9C9FBE38FDE /* Sources */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
@ -343,9 +261,6 @@
children = ( children = (
9DF960DFB90BF425282C35D0 /* Input */, 9DF960DFB90BF425282C35D0 /* Input */,
8A477F7D38B42EEB3F70323F /* Pairing */, 8A477F7D38B42EEB3F70323F /* Pairing */,
35F24B7F065B257F93810E5B /* Sessions */,
F681ED5F43C5283558361FAC /* Settings */,
C7FBB3C467939760D2971070 /* Status */,
49209A78102230A37C0FF8D0 /* Terminal */, 49209A78102230A37C0FF8D0 /* Terminal */,
); );
path = UI; path = UI;
@ -368,7 +283,6 @@
ED7AFC5C0EF365C5831C7245 /* Auth */ = { ED7AFC5C0EF365C5831C7245 /* Auth */ = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
87407DA9C464BA9C9E118308 /* FaceIDGate.swift */,
B772854E3FADA8998C93DAF5 /* Keychain.swift */, B772854E3FADA8998C93DAF5 /* Keychain.swift */,
0F544C25D53F52291E2FDB6F /* Pairing.swift */, 0F544C25D53F52291E2FDB6F /* Pairing.swift */,
D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */, D3FCCEE1BAA0983D83FC84DD /* SidecarCredential.swift */,
@ -376,36 +290,9 @@
path = Auth; path = Auth;
sourceTree = "<group>"; sourceTree = "<group>";
}; };
F681ED5F43C5283558361FAC /* Settings */ = {
isa = PBXGroup;
children = (
94525E2A804BF64F12806D4E /* LockView.swift */,
C5BCDE8061BF3E2DABEA12E7 /* SettingsView.swift */,
);
path = Settings;
sourceTree = "<group>";
};
/* End PBXGroup section */ /* End PBXGroup section */
/* Begin PBXNativeTarget section */ /* Begin PBXNativeTarget section */
2B14D5AC9C0B9175642D6460 /* piRemoteUITests */ = {
isa = PBXNativeTarget;
buildConfigurationList = FAF190D5257A3BA4C5F939C3 /* Build configuration list for PBXNativeTarget "piRemoteUITests" */;
buildPhases = (
6588DBB02A562A96AA88EE1A /* Sources */,
);
buildRules = (
);
dependencies = (
94235F03119D91B660C7E290 /* PBXTargetDependency */,
);
name = piRemoteUITests;
packageProductDependencies = (
);
productName = piRemoteUITests;
productReference = 9E41A5D34EADEA9D2925DDF0 /* piRemoteUITests.xctest */;
productType = "com.apple.product-type.bundle.ui-testing";
};
2C3DD20A67B90DDE04FDEE41 /* piRemoteTests */ = { 2C3DD20A67B90DDE04FDEE41 /* piRemoteTests */ = {
isa = PBXNativeTarget; isa = PBXNativeTarget;
buildConfigurationList = C553B125FB09A7C04D602AE2 /* Build configuration list for PBXNativeTarget "piRemoteTests" */; buildConfigurationList = C553B125FB09A7C04D602AE2 /* Build configuration list for PBXNativeTarget "piRemoteTests" */;
@ -455,11 +342,6 @@
BuildIndependentTargetsInParallel = YES; BuildIndependentTargetsInParallel = YES;
LastUpgradeCheck = 1640; LastUpgradeCheck = 1640;
TargetAttributes = { TargetAttributes = {
2B14D5AC9C0B9175642D6460 = {
DevelopmentTeam = KNXX8R3648;
ProvisioningStyle = Automatic;
TestTargetID = 4910ACCEB67B73CBA3440774;
};
2C3DD20A67B90DDE04FDEE41 = { 2C3DD20A67B90DDE04FDEE41 = {
DevelopmentTeam = KNXX8R3648; DevelopmentTeam = KNXX8R3648;
ProvisioningStyle = Automatic; ProvisioningStyle = Automatic;
@ -490,7 +372,6 @@
targets = ( targets = (
4910ACCEB67B73CBA3440774 /* piRemote */, 4910ACCEB67B73CBA3440774 /* piRemote */,
2C3DD20A67B90DDE04FDEE41 /* piRemoteTests */, 2C3DD20A67B90DDE04FDEE41 /* piRemoteTests */,
2B14D5AC9C0B9175642D6460 /* piRemoteUITests */,
); );
}; };
/* End PBXProject section */ /* End PBXProject section */
@ -524,11 +405,9 @@
734F2FECD358816F695D26CD /* AppState.swift in Sources */, 734F2FECD358816F695D26CD /* AppState.swift in Sources */,
F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */, F6C311D17A8DAA4F19464E25 /* ContentView.swift in Sources */,
909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */, 909A26B85FA298A870E407CD /* DeviceTokenRegistrar.swift in Sources */,
9E4B31AA31D8A900D36918A3 /* FaceIDGate.swift in Sources */,
B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */, B3809456CF2E96F1B1B862C2 /* FontStore.swift in Sources */,
2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */, 2AA3AC859917D32C1444FC5B /* FrameCodec.swift in Sources */,
C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */, C776D609DB29E5B4C90881F9 /* Keychain.swift in Sources */,
132F60783EC43AE420FA7CD5 /* LockView.swift in Sources */,
19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */, 19E584DD72E8F6DE3AF4E77F /* MainTerminalView.swift in Sources */,
E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */, E9126D5D059DAD3717FA2398 /* ModifierBar.swift in Sources */,
B8800C5E81FBB0C3CE9C6E7D /* ModifierState.swift in Sources */, B8800C5E81FBB0C3CE9C6E7D /* ModifierState.swift in Sources */,
@ -540,12 +419,7 @@
09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */, 09AC16350B4E83B71B05A9D5 /* ResumeCursor.swift in Sources */,
9AC28FD7FD38F250FE477441 /* ScrollbackCache.swift in Sources */, 9AC28FD7FD38F250FE477441 /* ScrollbackCache.swift in Sources */,
1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */, 1F353AB548615ECD7D241EF7 /* SessionConnection.swift in Sources */,
6A52DBC5C2845ADA05B37A35 /* SessionRegistry.swift in Sources */,
C87E6CCFC4ADC4AC4CAC4C0D /* SessionRow.swift in Sources */,
4334301CFE20E6B66BDE7D19 /* SessionSwitcher.swift in Sources */,
A51AABE190B66EE994D8415B /* SettingsView.swift in Sources */,
2D8C05476A83F5CAB9A55A11 /* SidecarCredential.swift in Sources */, 2D8C05476A83F5CAB9A55A11 /* SidecarCredential.swift in Sources */,
D8F05ED47CD1A4A8298EDDFB /* StatusBar.swift in Sources */,
F8CBA52AE2CC3D8496361D45 /* TerminalFont.swift in Sources */, F8CBA52AE2CC3D8496361D45 /* TerminalFont.swift in Sources */,
D3E8D6064F38E4024A6863C9 /* TerminalTheme.swift in Sources */, D3E8D6064F38E4024A6863C9 /* TerminalTheme.swift in Sources */,
7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */, 7BD37B4A99532FD542D21526 /* TerminalViewController.swift in Sources */,
@ -556,27 +430,10 @@
); );
runOnlyForDeploymentPostprocessing = 0; runOnlyForDeploymentPostprocessing = 0;
}; };
6588DBB02A562A96AA88EE1A /* Sources */ = {
isa = PBXSourcesBuildPhase;
buildActionMask = 2147483647;
files = (
5D6FE414C7ECB0E9146BD388 /* BackgroundLifecycleUITests.swift in Sources */,
A1527F94754A9205576C317C /* Helpers.swift in Sources */,
17F996CE1F5CF3FF12E5C1AB /* LockScreenUITests.swift in Sources */,
9BDEBAD2C295FDD53946FB8C /* ModifierBarUITests.swift in Sources */,
AEAB8079223530B0437F3434 /* PairingUITests.swift in Sources */,
EF47351AD35418E508ED33C9 /* SessionSwitcherUITests.swift in Sources */,
EAF7C85B3223C503829028D3 /* SettingsUITests.swift in Sources */,
0572AF0EFE02ABD12CD5C584 /* SmokeUITests.swift in Sources */,
F10312EF88278BB719CCF7E7 /* StatusBarUITests.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
};
6DDDB771E08071591D668B0A /* Sources */ = { 6DDDB771E08071591D668B0A /* Sources */ = {
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 */,
@ -584,7 +441,6 @@
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;
@ -597,11 +453,6 @@
target = 4910ACCEB67B73CBA3440774 /* piRemote */; target = 4910ACCEB67B73CBA3440774 /* piRemote */;
targetProxy = B301DDFED8092F66145718E3 /* PBXContainerItemProxy */; targetProxy = B301DDFED8092F66145718E3 /* PBXContainerItemProxy */;
}; };
94235F03119D91B660C7E290 /* PBXTargetDependency */ = {
isa = PBXTargetDependency;
target = 4910ACCEB67B73CBA3440774 /* piRemote */;
targetProxy = 4BC4CAFF5DFB43FCB5A72089 /* PBXContainerItemProxy */;
};
/* End PBXTargetDependency section */ /* End PBXTargetDependency section */
/* Begin XCBuildConfiguration section */ /* Begin XCBuildConfiguration section */
@ -802,40 +653,6 @@
}; };
name = Release; name = Release;
}; };
CB33989104A2C99094B6896C /* Debug */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "de.vpsj.pi-remote.uitests";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = piRemote;
};
name = Debug;
};
F837F7618AB4341D74A53209 /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)";
IPHONEOS_DEPLOYMENT_TARGET = 17.0;
LD_RUNPATH_SEARCH_PATHS = (
"$(inherited)",
"@executable_path/Frameworks",
"@loader_path/Frameworks",
);
PRODUCT_BUNDLE_IDENTIFIER = "de.vpsj.pi-remote.uitests";
SDKROOT = iphoneos;
TARGETED_DEVICE_FAMILY = "1,2";
TEST_TARGET_NAME = piRemote;
};
name = Release;
};
/* End XCBuildConfiguration section */ /* End XCBuildConfiguration section */
/* Begin XCConfigurationList section */ /* Begin XCConfigurationList section */
@ -866,15 +683,6 @@
defaultConfigurationIsVisible = 0; defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug; defaultConfigurationName = Debug;
}; };
FAF190D5257A3BA4C5F939C3 /* Build configuration list for PBXNativeTarget "piRemoteUITests" */ = {
isa = XCConfigurationList;
buildConfigurations = (
CB33989104A2C99094B6896C /* Debug */,
F837F7618AB4341D74A53209 /* Release */,
);
defaultConfigurationIsVisible = 0;
defaultConfigurationName = Debug;
};
/* End XCConfigurationList section */ /* End XCConfigurationList section */
/* Begin XCRemoteSwiftPackageReference section */ /* Begin XCRemoteSwiftPackageReference section */

View File

@ -50,17 +50,6 @@
ReferencedContainer = "container:piRemote.xcodeproj"> ReferencedContainer = "container:piRemote.xcodeproj">
</BuildableReference> </BuildableReference>
</TestableReference> </TestableReference>
<TestableReference
skipped = "NO"
parallelizable = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "2B14D5AC9C0B9175642D6460"
BuildableName = "piRemoteUITests.xctest"
BlueprintName = "piRemoteUITests"
ReferencedContainer = "container:piRemote.xcodeproj">
</BuildableReference>
</TestableReference>
</Testables> </Testables>
<CommandLineArguments> <CommandLineArguments>
</CommandLineArguments> </CommandLineArguments>

View File

@ -35,7 +35,6 @@ schemes:
config: Debug config: Debug
targets: targets:
- piRemoteTests - piRemoteTests
- piRemoteUITests
archive: archive:
config: Release config: Release
@ -83,16 +82,3 @@ targets:
- path: Tests/CoreTests - path: Tests/CoreTests
dependencies: dependencies:
- target: piRemote - target: piRemote
piRemoteUITests:
type: bundle.ui-testing
platform: iOS
deploymentTarget: "17.0"
sources:
- path: UITests
settings:
base:
TEST_TARGET_NAME: piRemote
PRODUCT_BUNDLE_IDENTIFIER: de.vpsj.pi-remote.uitests
dependencies:
- target: piRemote