FieldUITestSupport.swift (9583B)
1 import Foundation 2 import RadrootsKit 3 4 #if DEBUG 5 enum FieldUITestHarness { 6 static var isRequested: Bool { 7 let environment = ProcessInfo.processInfo.environment 8 let arguments = ProcessInfo.processInfo.arguments 9 return environment["RADROOTS_FIELD_IOS_UI_TEST"] == "true" || 10 arguments.contains("--radroots-field-ios-ui-test") 11 } 12 13 static func bool(_ key: String, default defaultValue: Bool) -> Bool { 14 guard let raw = ProcessInfo.processInfo.environment[key] else { 15 return defaultValue 16 } 17 switch raw.trimmingCharacters(in: .whitespacesAndNewlines).lowercased() { 18 case "1", "true", "yes": 19 return true 20 case "0", "false", "no": 21 return false 22 default: 23 return defaultValue 24 } 25 } 26 27 static func string(_ key: String) -> String? { 28 ProcessInfo.processInfo.environment[key]?.trimmingCharacters(in: .whitespacesAndNewlines) 29 } 30 } 31 32 actor FieldUITestBackgroundTaskScheduler: RadrootsBackgroundTaskScheduler { 33 private var pendingTaskSnapshots: [RadrootsBackgroundTaskIdentifier: RadrootsBackgroundTaskSnapshot] 34 private var submittedRequestsValue: [RadrootsBackgroundTaskRequest] 35 private var cancelAllCountValue: Int 36 private let submittedAt: Date 37 38 init(submittedAt: Date = Date(timeIntervalSince1970: 0)) { 39 self.pendingTaskSnapshots = [:] 40 self.submittedRequestsValue = [] 41 self.cancelAllCountValue = 0 42 self.submittedAt = submittedAt 43 } 44 45 func submit(_ request: RadrootsBackgroundTaskRequest) async throws -> RadrootsBackgroundTaskSnapshot { 46 submittedRequestsValue.append(request) 47 let snapshot = try RadrootsBackgroundTaskSnapshot(request: request, submittedAt: submittedAt) 48 pendingTaskSnapshots[request.identifier] = snapshot 49 return snapshot 50 } 51 52 func cancel(_ identifier: RadrootsBackgroundTaskIdentifier) async throws { 53 pendingTaskSnapshots.removeValue(forKey: identifier) 54 } 55 56 func cancelAll() async throws { 57 cancelAllCountValue += 1 58 pendingTaskSnapshots.removeAll() 59 } 60 61 func pendingTasks() async throws -> [RadrootsBackgroundTaskSnapshot] { 62 pendingTaskSnapshots.values.sorted { lhs, rhs in 63 lhs.identifier < rhs.identifier 64 } 65 } 66 67 var submittedRequestCount: Int { 68 submittedRequestsValue.count 69 } 70 71 var cancelAllCount: Int { 72 cancelAllCountValue 73 } 74 } 75 76 actor FieldUITestBackgroundTransferStore: RadrootsBackgroundTransferStore { 77 private var snapshotsByIdentifier: [RadrootsBackgroundTransferIdentifier: RadrootsBackgroundTransferSnapshot] = [:] 78 79 func loadSnapshots() async throws -> [RadrootsBackgroundTransferSnapshot] { 80 snapshotsByIdentifier.values.sorted { left, right in 81 left.identifier < right.identifier 82 } 83 } 84 85 func saveSnapshot(_ snapshot: RadrootsBackgroundTransferSnapshot) async throws { 86 snapshotsByIdentifier[snapshot.identifier] = snapshot 87 } 88 89 func removeSnapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws { 90 snapshotsByIdentifier.removeValue(forKey: identifier) 91 } 92 93 func removeAllSnapshots() async throws { 94 snapshotsByIdentifier.removeAll() 95 } 96 } 97 98 actor FieldUITestBackgroundTransfer: RadrootsBackgroundTransfer { 99 private let store: any RadrootsBackgroundTransferStore 100 private let updatedAt: Date 101 102 init( 103 store: any RadrootsBackgroundTransferStore = FieldUITestBackgroundTransferStore(), 104 updatedAt: Date = Date(timeIntervalSince1970: 0) 105 ) { 106 self.store = store 107 self.updatedAt = updatedAt 108 } 109 110 func enqueue(_ request: RadrootsBackgroundTransferRequest) async throws -> RadrootsBackgroundTransferHandle { 111 let snapshot = try RadrootsBackgroundTransferSnapshot( 112 request: request, 113 state: .queued, 114 updatedAt: updatedAt 115 ) 116 try await store.saveSnapshot(snapshot) 117 return RadrootsBackgroundTransferHandle(request: request) 118 } 119 120 func cancel(_ identifier: RadrootsBackgroundTransferIdentifier) async throws { 121 if let existing = try await store.loadSnapshots().first(where: { $0.identifier == identifier }) { 122 let snapshot = try RadrootsBackgroundTransferSnapshot( 123 request: existing.request, 124 state: .cancelled, 125 progress: existing.progress, 126 updatedAt: updatedAt 127 ) 128 try await store.saveSnapshot(snapshot) 129 } 130 } 131 132 func snapshot(for identifier: RadrootsBackgroundTransferIdentifier) async throws -> RadrootsBackgroundTransferSnapshot? { 133 try await store.loadSnapshots().first { $0.identifier == identifier } 134 } 135 136 func snapshots() async throws -> [RadrootsBackgroundTransferSnapshot] { 137 try await store.loadSnapshots() 138 } 139 140 func handleEventsForBackgroundURLSession( 141 identifier: String, 142 completionHandler: @escaping @Sendable () -> Void 143 ) async { 144 completionHandler() 145 } 146 } 147 148 actor FieldUITestMediaPicker: RadrootsMediaPicker { 149 private let support: RadrootsMediaPickerSupport 150 private let importOutcome: Result<RadrootsMediaImportResult, RadrootsCaptureIntakeError> 151 private let captureOutcome: Result<RadrootsMediaCaptureResult, RadrootsCaptureIntakeError> 152 153 init( 154 support: RadrootsMediaPickerSupport, 155 importOutcome: Result<RadrootsMediaImportResult, RadrootsCaptureIntakeError>, 156 captureOutcome: Result<RadrootsMediaCaptureResult, RadrootsCaptureIntakeError> 157 ) { 158 self.support = support 159 self.importOutcome = importOutcome 160 self.captureOutcome = captureOutcome 161 } 162 163 func currentSupport() async throws -> RadrootsMediaPickerSupport { 164 support 165 } 166 167 func importMedia(_ request: RadrootsMediaImportRequest) async throws -> RadrootsMediaImportResult { 168 switch importOutcome { 169 case .success(let result): 170 return result 171 case .failure(let error): 172 throw error 173 } 174 } 175 176 func captureMedia(_ request: RadrootsMediaCaptureRequest) async throws -> RadrootsMediaCaptureResult { 177 switch captureOutcome { 178 case .success(let result): 179 return result 180 case .failure(let error): 181 throw error 182 } 183 } 184 } 185 186 actor FieldUITestDocumentScanner: RadrootsDocumentScanner { 187 private let support: RadrootsDocumentScannerSupport 188 private let scanOutcome: Result<RadrootsScannedDocument, RadrootsCaptureIntakeError> 189 190 init( 191 support: RadrootsDocumentScannerSupport, 192 scanOutcome: Result<RadrootsScannedDocument, RadrootsCaptureIntakeError> 193 ) { 194 self.support = support 195 self.scanOutcome = scanOutcome 196 } 197 198 func currentSupport() async throws -> RadrootsDocumentScannerSupport { 199 support 200 } 201 202 func scanDocument(_ request: RadrootsDocumentScanRequest) async throws -> RadrootsScannedDocument { 203 switch scanOutcome { 204 case .success(let result): 205 return result 206 case .failure(let error): 207 throw error 208 } 209 } 210 } 211 212 actor FieldUITestExternalActions: RadrootsExternalActions { 213 private let defaultCanOpen: Bool 214 private let openOutcome: Result<Void, RadrootsExternalActionError> 215 216 init( 217 defaultCanOpen: Bool = true, 218 openOutcome: Result<Void, RadrootsExternalActionError> = .success(()) 219 ) { 220 self.defaultCanOpen = defaultCanOpen 221 self.openOutcome = openOutcome 222 } 223 224 func canOpen(_ destination: RadrootsExternalActionDestination) async -> RadrootsExternalActionCapability { 225 RadrootsExternalActionCapability(destination: destination, canOpen: defaultCanOpen) 226 } 227 228 func open(_ request: RadrootsExternalActionRequest) async throws { 229 switch openOutcome { 230 case .success: 231 return 232 case .failure(let error): 233 throw error 234 } 235 } 236 } 237 238 actor FieldUITestUserPresence: RadrootsUserPresence { 239 private let statusValue: RadrootsUserPresenceStatus 240 private let outcomes: [Result<Bool, RadrootsUserPresenceError>] 241 private var requestCount: Int 242 243 init( 244 status: RadrootsUserPresenceStatus = RadrootsUserPresenceStatus( 245 support: .biometricsOrDeviceCredential, 246 biometryKind: .faceID, 247 canEvaluateDeviceCredential: true, 248 canEvaluateBiometrics: true 249 ), 250 outcomes: [Result<Bool, RadrootsUserPresenceError>] = [.success(true)] 251 ) { 252 self.statusValue = status 253 self.outcomes = outcomes.isEmpty ? [.success(true)] : outcomes 254 self.requestCount = 0 255 } 256 257 func currentStatus() async throws -> RadrootsUserPresenceStatus { 258 statusValue 259 } 260 261 func verify(_ request: RadrootsUserPresenceRequest) async throws -> RadrootsUserPresenceResult { 262 let outcome = outcomes[min(requestCount, outcomes.count - 1)] 263 requestCount += 1 264 switch outcome { 265 case .success(let verified): 266 return RadrootsUserPresenceResult(policy: request.policy, verified: verified) 267 case .failure(let error): 268 throw error 269 } 270 } 271 } 272 273 actor FieldUITestRecordingTelemetry: RadrootsTelemetry { 274 private var recordedEventsValue: [RadrootsTelemetryEvent] = [] 275 276 func record(_ event: RadrootsTelemetryEvent) async { 277 recordedEventsValue.append(event) 278 } 279 280 var recordedEvents: [RadrootsTelemetryEvent] { 281 recordedEventsValue 282 } 283 } 284 #endif