field_ios

In-the-field app for Radroots on iOS
git clone https://radroots.dev/git/field_ios.git
Log | Files | Refs | LICENSE

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