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:
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user