Phases 1-4: full VNC client implementation

- SessionController wraps RoyalVNCKit.VNCConnection via nonisolated delegate
  adapter that bridges callbacks to @MainActor; Keychain-resolved passwords;
  reconnect with jittered exponential backoff; NWPathMonitor adaptive-quality
  hook; framebuffer rendered to CALayer.contents from didUpdateFramebuffer.
- Touch + trackpad input modes with floating soft cursor overlay; hardware
  keyboard via pressesBegan/Ended → X11 keysyms; UIPointerInteraction with
  hidden cursor for indirect pointers; pinch-to-zoom; Apple Pencil as direct
  touch; two-finger pan / indirect scroll wheel events.
- Bidirectional clipboard sync (per-connection opt-in); multi-monitor screen
  picker with input remapping; screenshot capture → share sheet; on-disconnect
  reconnect/close prompt; view-only and curtain-mode persisted.
- iPad multi-window via WindowGroup(for: UUID.self) + context-menu open;
  CloudKit-backed ModelContainer with local fallback; PrivacyInfo.xcprivacy.

10 VNCCore tests + 4 VNCUI tests pass; iPhone and iPad simulator builds clean.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
Trey T
2026-04-16 20:07:54 -05:00
parent 102c3484e9
commit 1c01b3573f
28 changed files with 2359 additions and 158 deletions

View File

@@ -1,6 +1,7 @@
import Testing
@testable import VNCCore
import Foundation
import CoreGraphics
@Suite struct SessionStateTests {
@Test func idleEqualsIdle() {
@@ -17,3 +18,74 @@ import Foundation
#expect(DisconnectReason.userRequested != .authenticationFailed)
}
}
@Suite struct ReconnectPolicyTests {
@Test func userRequestedNeverReconnects() {
let policy = ReconnectPolicy.default
#expect(!policy.shouldReconnect(for: .userRequested))
#expect(!policy.shouldReconnect(for: .authenticationFailed))
}
@Test func networkFailuresReconnect() {
let policy = ReconnectPolicy.default
#expect(policy.shouldReconnect(for: .networkError("oops")))
#expect(policy.shouldReconnect(for: .remoteClosed))
}
@Test func policyDelaysGrowAndCap() {
let policy = ReconnectPolicy(maxAttempts: 5,
baseDelaySeconds: 1,
maxDelaySeconds: 8,
jitterFraction: 0)
#expect(policy.delay(for: 1) == 1)
#expect(policy.delay(for: 2) == 2)
#expect(policy.delay(for: 3) == 4)
#expect(policy.delay(for: 4) == 8)
#expect(policy.delay(for: 5) == 8)
#expect(policy.delay(for: 6) == nil)
}
@Test func zeroAttemptsDisablesReconnect() {
let policy = ReconnectPolicy.none
#expect(!policy.shouldReconnect(for: .networkError("x")))
}
}
@Suite struct RemoteScreenTests {
@Test func screensAreHashable() {
let a = RemoteScreen(id: 1, frame: CGRect(x: 0, y: 0, width: 1920, height: 1080))
let b = RemoteScreen(id: 1, frame: CGRect(x: 0, y: 0, width: 1920, height: 1080))
let c = RemoteScreen(id: 2, frame: CGRect(x: 0, y: 0, width: 1920, height: 1080))
#expect(a == b)
#expect(a != c)
#expect(Set([a, b, c]).count == 2)
}
}
@Suite struct PasswordProviderTests {
private final class StubKeychain: KeychainServicing, @unchecked Sendable {
var stored: [String: String] = [:]
func storePassword(_ password: String, account: String) throws {
stored[account] = password
}
func loadPassword(account: String) throws -> String? {
stored[account]
}
func deletePassword(account: String) throws {
stored[account] = nil
}
}
@Test func keychainBackedProviderReturnsStored() {
let keychain = StubKeychain()
try? keychain.storePassword("hunter2", account: "abc")
let provider = DefaultPasswordProvider(keychain: keychain)
#expect(provider.password(for: "abc") == "hunter2")
#expect(provider.password(for: "missing") == nil)
}
@Test func staticProviderAlwaysReturnsSame() {
let provider = StaticPasswordProvider(password: "fixed")
#expect(provider.password(for: "anything") == "fixed")
}
}