diff --git a/AutoCat.xcodeproj/project.pbxproj b/AutoCat.xcodeproj/project.pbxproj index 4218b8e..d79be82 100644 --- a/AutoCat.xcodeproj/project.pbxproj +++ b/AutoCat.xcodeproj/project.pbxproj @@ -44,8 +44,6 @@ 7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F72CE900440004B740 /* ReportViewModel.swift */; }; 7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; }; 7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78FE2CE91A740004B740 /* Vehicle.swift */; }; - 7A22B6ED2C67FDEA00E60173 /* SwiftLocationMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */; }; - 7A22B6EE2C67FDEA00E60173 /* GeocoderMock.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */; }; 7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; }; 7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; }; 7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF6249FEF690035F39E /* Recorder.swift */; }; @@ -306,8 +304,6 @@ 7A1E78F72CE900440004B740 /* ReportViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportViewModel.swift; sourceTree = ""; }; 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = ""; }; 7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = ""; }; - 7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeocoderMock.swift; sourceTree = ""; }; - 7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationMock.swift; sourceTree = ""; }; 7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = ""; }; 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = ""; }; 7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = ""; }; @@ -723,15 +719,6 @@ path = Data; sourceTree = ""; }; - 7A22B6EC2C67FDEA00E60173 /* Mocks */ = { - isa = PBXGroup; - children = ( - 7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */, - 7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */, - ); - path = Mocks; - sourceTree = ""; - }; 7A3F07A924360D9100E59687 /* Extensions */ = { isa = PBXGroup; children = ( @@ -1044,7 +1031,6 @@ 7AF6D2292677C3950086EA64 /* Extensions */, 7A11474523FF2A9000B424AF /* Models */, 7AF6D20D2677C0C30086EA64 /* Utils */, - 7A22B6EC2C67FDEA00E60173 /* Mocks */, 7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */, 7AF6D1F22677C03B0086EA64 /* Info.plist */, ); @@ -1451,8 +1437,6 @@ buildActionMask = 2147483647; files = ( 7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */, - 7A22B6ED2C67FDEA00E60173 /* SwiftLocationMock.swift in Sources */, - 7A22B6EE2C67FDEA00E60173 /* GeocoderMock.swift in Sources */, 7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */, 7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */, 7A761C08267E8EA20005F28F /* JWT.swift in Sources */, @@ -1777,6 +1761,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1801,6 +1786,7 @@ PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; }; @@ -1827,6 +1813,7 @@ PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat"; @@ -1853,6 +1840,7 @@ PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = NO; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat"; @@ -1883,6 +1871,7 @@ PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MOCKING"; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; @@ -1913,6 +1902,7 @@ PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore; PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)"; SKIP_INSTALL = YES; + SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES; SWIFT_VERSION = 5.0; TARGETED_DEVICE_FAMILY = "1,2"; VERSIONING_SYSTEM = "apple-generic"; diff --git a/AutoCat/AppDelegate.swift b/AutoCat/AppDelegate.swift index c2dba56..08812f0 100644 --- a/AutoCat/AppDelegate.swift +++ b/AutoCat/AppDelegate.swift @@ -83,14 +83,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate { let container = ServiceContainer.shared - container.register(SettingsServiceProtocol.self, instance: SettingsService(defaults: .standard)) + let settingsService = SettingsService(defaults: .standard) + + container.register(SettingsServiceProtocol.self, instance: settingsService) container.register(ApiServiceProtocol.self, instance: ApiService()) - container.register(GeocoderProtocol.self, instance: CLGeocoder()) - container.register(SwiftLocationProtocol.self, instance: Location()) - container.register(LocationServiceProtocol.self, instance: LocationService()) + + let locationService = LocationService( + geocoder: CLGeocoder(), + locationManager: Location(), + settingsService: settingsService + ) + + container.register(LocationServiceProtocol.self, instance: locationService) Task { - container.register(StorageServiceProtocol.self, instance: try await StorageService()) + container.register(StorageServiceProtocol.self, + instance: try await StorageService(settingsService: settingsService)) } } diff --git a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift b/AutoCat/Screens/EventsScreen/EventsCoordinator.swift index b8eb12e..0c72d9b 100644 --- a/AutoCat/Screens/EventsScreen/EventsCoordinator.swift +++ b/AutoCat/Screens/EventsScreen/EventsCoordinator.swift @@ -21,7 +21,13 @@ class EventsCoordinator { func start(vehicle: VehicleDto) async -> VehicleDto { - let viewModel = EventsViewModel(vehicle: vehicle) + let resolver = ServiceContainer.shared + let viewModel = EventsViewModel( + apiService: resolver.resolve(ApiServiceProtocol.self), + storageService: resolver.resolve(StorageServiceProtocol.self), + settingsService: resolver.resolve(SettingsServiceProtocol.self), + vehicle: vehicle + ) viewModel.coordinator = self let controller = CustomHostingController(rootView: EventsScreen(viewModel: viewModel)) navController.pushViewController(controller, animated: true) diff --git a/AutoCat/Screens/EventsScreen/EventsScreen.swift b/AutoCat/Screens/EventsScreen/EventsScreen.swift index fd15fae..d40d888 100644 --- a/AutoCat/Screens/EventsScreen/EventsScreen.swift +++ b/AutoCat/Screens/EventsScreen/EventsScreen.swift @@ -138,5 +138,8 @@ struct EventsScreen: View { } #Preview { - EventsScreen(viewModel: .init(vehicle: .preview)) + EventsScreen(viewModel: .init(apiService: MockApiServiceProtocol(), + storageService: MockStorageServiceProtocol(), + settingsService: MockSettingsServiceProtocol(), + vehicle: .preview)) } diff --git a/AutoCat/Screens/EventsScreen/EventsViewModel.swift b/AutoCat/Screens/EventsScreen/EventsViewModel.swift index d5d7335..e564623 100644 --- a/AutoCat/Screens/EventsScreen/EventsViewModel.swift +++ b/AutoCat/Screens/EventsScreen/EventsViewModel.swift @@ -26,9 +26,9 @@ class EventsViewModel: ACHudContainer { typealias VehicleOperation = () async throws -> VehicleDto - @ObservationIgnored @Service var apiService: ApiServiceProtocol - @ObservationIgnored @Service var storageService: StorageServiceProtocol - @ObservationIgnored @Service var settingsService: SettingsServiceProtocol + let apiService: ApiServiceProtocol + let storageService: StorageServiceProtocol + let settingsService: SettingsServiceProtocol weak var coordinator: EventsCoordinator? @@ -45,8 +45,14 @@ class EventsViewModel: ACHudContainer { UIPasteboard.general.data(forPasteboardType: UTType.vehicleEvent.identifier) != nil } - init(vehicle: VehicleDto) { + init(apiService: ApiServiceProtocol, + storageService: StorageServiceProtocol, + settingsService: SettingsServiceProtocol, + vehicle: VehicleDto) { + self.apiService = apiService + self.storageService = storageService + self.settingsService = settingsService self.vehicle = vehicle updateEvents() diff --git a/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift b/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift index ff76661..eb46f76 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersCoordinator.swift @@ -22,7 +22,10 @@ class FiltersCoordinator: Coordinator { } func start() async throws -> Filter? { - let viewModel = FiltersViewModel(filter: filter) + let viewModel = FiltersViewModel( + apiService: ServiceContainer.shared.resolve(ApiServiceProtocol.self), + filter: filter + ) let controller = CustomHostingController(rootView: FiltersScreen(viewModel: viewModel)) viewController?.pushViewController(controller, animated: true) await controller.waitForDisappear() diff --git a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift index be5d1d0..7140fcb 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersScreen.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersScreen.swift @@ -120,5 +120,8 @@ struct FiltersScreen: View { } #Preview { - FiltersScreen(viewModel: .init(filter: Filter())) + FiltersScreen(viewModel: .init( + apiService: MockApiServiceProtocol(), + filter: Filter() + )) } diff --git a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift index 1be2383..52a57a6 100644 --- a/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift +++ b/AutoCat/Screens/FiltersScreen/FiltersViewModel.swift @@ -13,7 +13,7 @@ import AutoCatCore @Observable class FiltersViewModel { - @ObservationIgnored @Service var api: ApiServiceProtocol + let apiService: ApiServiceProtocol var filter: Filter { didSet { @@ -35,7 +35,9 @@ class FiltersViewModel { @ObservationIgnored var currentBrand: StringOption = .any - init(filter: Filter) { + init(apiService: ApiServiceProtocol, filter: Filter) { + + self.apiService = apiService self.filter = filter } @@ -44,13 +46,13 @@ class FiltersViewModel { return } - brands = [.any] + ((try? await api.getBrands()) ?? []).map { .value($0) } - colors = [.any] + ((try? await api.getColors()) ?? []).map { .value($0) } - years = [.any] + ((try? await api.getYears())?.map(String.init) ?? []).map { .value($0) } + brands = [.any] + ((try? await apiService.getBrands()) ?? []).map { .value($0) } + colors = [.any] + ((try? await apiService.getColors()) ?? []).map { .value($0) } + years = [.any] + ((try? await apiService.getYears())?.map(String.init) ?? []).map { .value($0) } } func loadModels(brand: String) async { - models = [.any] + ((try? await api.getModels(of: brand)) ?? []).map { .value($0) } + models = [.any] + ((try? await apiService.getModels(of: brand)) ?? []).map { .value($0) } filter.model = .any } diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift index 5f5c4fd..94ec031 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerCoordinator.swift @@ -23,7 +23,10 @@ final class LocationPickerCoordinator: Coordinator { func start() async throws -> VehicleEventDto? { - let viewModel = LocationPickerViewModel(event: event) + let viewModel = LocationPickerViewModel( + locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self), + event: event + ) let screen = LocationPickerScreen(viewModel: viewModel) let controller = CustomHostingController(rootView: screen) viewController?.pushViewController(controller, animated: true) diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift index da27f5c..846f410 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerScreen.swift @@ -51,7 +51,10 @@ struct LocationPickerScreen: View { var event = VehicleEventDto(lat: 47.250049, lon: 39.711821, addedBy: nil) event.address = "Ул. Ленина, 123" - let viewModel = LocationPickerViewModel(event: event) + let viewModel = LocationPickerViewModel( + locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self), + event: event + ) return LocationPickerScreen(viewModel: viewModel) } diff --git a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift index 3cb8f93..2642e38 100644 --- a/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift +++ b/AutoCat/Screens/LocationPickerScreen/LocationPickerViewModel.swift @@ -15,14 +15,16 @@ import SwiftUI @Observable final class LocationPickerViewModel { - @ObservationIgnored @Service var locationService: LocationServiceProtocol + let locationService: LocationServiceProtocol var event: VehicleEventDto var position: MapCameraPosition var result: VehicleEventDto? - init(event: VehicleEventDto) { + init(locationService: LocationServiceProtocol, event: VehicleEventDto) { + + self.locationService = locationService self.event = event if event.latitude == 0 && event.longitude == 0 { diff --git a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift index aa7faee..7512ad5 100644 --- a/AutoCat/Screens/NotesScreen/NotesCoordinator.swift +++ b/AutoCat/Screens/NotesScreen/NotesCoordinator.swift @@ -24,7 +24,12 @@ class NotesCoordinator: Coordinator { func start() async throws -> VehicleDto { - let viewModel = NotesViewModel(vehicle: vehicle) + let resolver = ServiceContainer.shared + let viewModel = NotesViewModel( + storageService: resolver.resolve(StorageServiceProtocol.self), + apiService: resolver.resolve(ApiServiceProtocol.self), + vehicle: vehicle + ) let controller = CustomHostingController(rootView: NotesScreen(viewModel: viewModel)) viewController?.pushViewController(controller, animated: true) await controller.waitForDisappear() diff --git a/AutoCat/Screens/NotesScreen/NotesScreen.swift b/AutoCat/Screens/NotesScreen/NotesScreen.swift index 9dd270c..a0118dc 100644 --- a/AutoCat/Screens/NotesScreen/NotesScreen.swift +++ b/AutoCat/Screens/NotesScreen/NotesScreen.swift @@ -95,7 +95,11 @@ struct NotesScreen: View { .init(text: "zxcv", user: "") ] - let vm = NotesViewModel(vehicle: vehicle) + let vm = NotesViewModel( + storageService: MockStorageServiceProtocol(), + apiService: MockApiServiceProtocol(), + vehicle: vehicle + ) return NotesScreen(viewModel: vm) } diff --git a/AutoCat/Screens/NotesScreen/NotesViewModel.swift b/AutoCat/Screens/NotesScreen/NotesViewModel.swift index d308c24..1e4330a 100644 --- a/AutoCat/Screens/NotesScreen/NotesViewModel.swift +++ b/AutoCat/Screens/NotesScreen/NotesViewModel.swift @@ -15,14 +15,18 @@ import UniformTypeIdentifiers @Observable class NotesViewModel: ACHudContainer { - @ObservationIgnored @Service var storageService: StorageServiceProtocol - @ObservationIgnored @Service var apiService: ApiServiceProtocol + let storageService: StorageServiceProtocol + let apiService: ApiServiceProtocol var vehicle: VehicleDto var hud: ACHud? - init(vehicle: VehicleDto) { + init(storageService: StorageServiceProtocol, + apiService: ApiServiceProtocol, + vehicle: VehicleDto) { + self.storageService = storageService + self.apiService = apiService self.vehicle = vehicle } diff --git a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift index 4c22990..687823b 100644 --- a/AutoCat/Screens/ReportScreen/ReportCoordinator.swift +++ b/AutoCat/Screens/ReportScreen/ReportCoordinator.swift @@ -28,7 +28,14 @@ class ReportCoordinator: Coordinator { func start() async throws -> VehicleDto { - let viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent) + let resolver = ServiceContainer.shared + let viewModel = await ReportViewModel( + apiService: resolver.resolve(ApiServiceProtocol.self), + storageService: resolver.resolve(StorageServiceProtocol.self), + settingsService: resolver.resolve(SettingsServiceProtocol.self), + vehicle: vehicle, + isPersistent: isPersistent + ) viewModel.coordinator = self let controller = CustomHostingController(rootView: ReportScreen(viewModel: viewModel)) let newNavController = UINavigationController(rootViewController: controller) diff --git a/AutoCat/Screens/ReportScreen/ReportScreen.swift b/AutoCat/Screens/ReportScreen/ReportScreen.swift index ca9a127..07064ab 100644 --- a/AutoCat/Screens/ReportScreen/ReportScreen.swift +++ b/AutoCat/Screens/ReportScreen/ReportScreen.swift @@ -126,5 +126,11 @@ struct ReportScreen: View { } #Preview { - ReportScreen(viewModel: .init(vehicle: .preview, isPersistent: false)) + ReportScreen(viewModel: .init( + apiService: MockApiServiceProtocol(), + storageService: MockStorageServiceProtocol(), + settingsService: MockSettingsServiceProtocol(), + vehicle: .preview, + isPersistent: false + )) } diff --git a/AutoCat/Screens/ReportScreen/ReportViewModel.swift b/AutoCat/Screens/ReportScreen/ReportViewModel.swift index 7736bdd..97ee4dd 100644 --- a/AutoCat/Screens/ReportScreen/ReportViewModel.swift +++ b/AutoCat/Screens/ReportScreen/ReportViewModel.swift @@ -13,9 +13,9 @@ import SwiftUI @Observable class ReportViewModel: ACHudContainer { - @ObservationIgnored @Service var api: ApiServiceProtocol - @ObservationIgnored @Service var storageService: StorageServiceProtocol - @ObservationIgnored @Service var settings: SettingsServiceProtocol + let apiService: ApiServiceProtocol + let storageService: StorageServiceProtocol + let settingsService: SettingsServiceProtocol var coordinator: ReportCoordinator? @@ -44,7 +44,7 @@ class ReportViewModel: ACHudContainer { } var showDebugInfo: Bool { - settings.showDebugInfo + settingsService.showDebugInfo } var shareLink: URL? { @@ -55,7 +55,15 @@ class ReportViewModel: ACHudContainer { return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt) } - init(vehicle: VehicleDto, isPersistent: Bool) { + init(apiService: ApiServiceProtocol, + storageService: StorageServiceProtocol, + settingsService: SettingsServiceProtocol, + vehicle: VehicleDto, + isPersistent: Bool) { + + self.apiService = apiService + self.storageService = storageService + self.settingsService = settingsService self.vehicle = vehicle self.isPersistent = isPersistent } @@ -84,7 +92,7 @@ class ReportViewModel: ACHudContainer { func checkGB() async { await wrapWithToast { - self.vehicle = try await self.api.checkVehicleGb(by: self.vehicle.getNumber()) + self.vehicle = try await self.apiService.checkVehicleGb(by: self.vehicle.getNumber()) try await self.storageService.updateVehicleIfExists(dto: self.vehicle) } } diff --git a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift index 00527cd..e87eca2 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsCoordinator.swift @@ -23,7 +23,7 @@ class SettingsCoordinator: Coordinator { func start() async throws { - let viewModel = SettingsViewModel() + let viewModel = SettingsViewModel(settingsService: ServiceContainer.shared.resolve(SettingsServiceProtocol.self)) viewModel.coordinator = self let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel)) settingsController = controller diff --git a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift index 6ed0b97..009b670 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsScreen.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsScreen.swift @@ -88,5 +88,5 @@ struct SettingsScreen: View { } #Preview { - SettingsScreen(viewModel: .init()) + SettingsScreen(viewModel: .init(settingsService: MockSettingsServiceProtocol())) } diff --git a/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift index aff4f19..86984c8 100644 --- a/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift +++ b/AutoCat/Screens/SettingsScreen/SettingsViewModel.swift @@ -42,8 +42,9 @@ class SettingsViewModel { return jwt.payload.email } - init() { - self.settingService = try! ServiceContainer.shared.resolve(SettingsServiceProtocol.self) + init(settingsService: SettingsServiceProtocol) { + + self.settingService = settingsService } func signOut() { diff --git a/AutoCatCore/DependencyInjection/ServiceContainer.swift b/AutoCatCore/DependencyInjection/ServiceContainer.swift index 73eb7e3..5b26e74 100644 --- a/AutoCatCore/DependencyInjection/ServiceContainer.swift +++ b/AutoCatCore/DependencyInjection/ServiceContainer.swift @@ -6,6 +6,8 @@ // Copyright © 2024 Selim Mustafaev. All rights reserved. // +import Foundation + public enum DIError: Error { case wrongServiceType(String) @@ -47,18 +49,18 @@ public class ServiceContainer { if let service = cachedService as? Service { return service } else { - throw DIError.wrongServiceType(type) + fatalError("Wrong service type for service: \(type)") } } guard let factory = factories[type] else { - throw DIError.serviceNotFound(type) + fatalError("Service \(type) not found") } if let service = factory() as? Service { return service } else { - throw DIError.wrongServiceType(type) + fatalError("Wrong service type for service: \(type)") } } } diff --git a/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift b/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift index b7ffe26..ca5a175 100644 --- a/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift +++ b/AutoCatCore/DependencyInjection/ServicePropertyWrapper.swift @@ -6,8 +6,8 @@ // Copyright © 2024 Selim Mustafaev. All rights reserved. // -@MainActor @propertyWrapper +@MainActor public struct Service { public var service: Service diff --git a/AutoCatCore/Mocks/GeocoderMock.swift b/AutoCatCore/Mocks/GeocoderMock.swift deleted file mode 100644 index 72c24f7..0000000 --- a/AutoCatCore/Mocks/GeocoderMock.swift +++ /dev/null @@ -1,51 +0,0 @@ -// -// GeocoderMock.swift -// AutoCatCoreTests -// -// Created by Selim Mustafaev on 31.07.2024. -// Copyright © 2024 Selim Mustafaev. All rights reserved. -// - -import CoreLocation -import AutoCatCore -import Intents -import Contacts - -final class GeocoderMock { - - struct Location { - - let latitude: CLLocationDegrees - let longitude: CLLocationDegrees - let address: String? - } - - var locations: [Location] = [] - - func addLocation(latitude: CLLocationDegrees, longitude: CLLocationDegrees, address: String?) { - - locations.append(Location(latitude: latitude, - longitude: longitude, - address: address)) - } -} - -extension GeocoderMock: GeocoderProtocol { - - func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark] { - - let first = locations.first { - $0.latitude == location.coordinate.latitude && $0.longitude == location.coordinate.longitude - } - - guard let first else { - return [] - } - - let placemark = CLPlacemark(location: CLLocation(latitude: first.latitude, longitude: first.longitude), - name: first.address, - postalAddress: nil) - - return [placemark] - } -} diff --git a/AutoCatCore/Mocks/SwiftLocationMock.swift b/AutoCatCore/Mocks/SwiftLocationMock.swift deleted file mode 100644 index 00ba19d..0000000 --- a/AutoCatCore/Mocks/SwiftLocationMock.swift +++ /dev/null @@ -1,45 +0,0 @@ -// -// SwiftLocationMock.swift -// AutoCatCoreTests -// -// Created by Selim Mustafaev on 02.08.2024. -// Copyright © 2024 Selim Mustafaev. All rights reserved. -// - -import AutoCatCore -import CoreLocation -import SwiftLocation - -final class SwiftLocationMock { - - var authorizationStatus: CLAuthorizationStatus = .notDetermined - var requestedStatus: CLAuthorizationStatus = .notDetermined - var location: CLLocation? - - var requestLocationTime: TimeInterval = 0 - var requestLocationCount = 0 -} - -extension SwiftLocationMock: SwiftLocationProtocol { - - func requestPermission(_ permission: LocationPermission) async throws -> CLAuthorizationStatus { - authorizationStatus = requestedStatus - return requestedStatus - } - - func requestLocation(accuracy filters: AccuracyFilters?, - timeout: TimeInterval?) async throws -> Tasks.ContinuousUpdateLocation.StreamEvent { - - requestLocationCount += 1 - - if requestLocationTime > 0 { - try await Task.sleep(nanoseconds: UInt64(requestLocationTime*1_000_000_000)) - } - - if let location { - return .didUpdateLocations([location]) - } else { - return .didUpdateLocations([]) - } - } -} diff --git a/AutoCatCore/Services/LocationService/GeocoderProtocol.swift b/AutoCatCore/Services/LocationService/GeocoderProtocol.swift index a12cf0e..c3ac212 100644 --- a/AutoCatCore/Services/LocationService/GeocoderProtocol.swift +++ b/AutoCatCore/Services/LocationService/GeocoderProtocol.swift @@ -7,7 +7,9 @@ // import CoreLocation +import Mockable +@Mockable public protocol GeocoderProtocol { func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark] diff --git a/AutoCatCore/Services/LocationService/LocationService.swift b/AutoCatCore/Services/LocationService/LocationService.swift index dade295..28e153e 100644 --- a/AutoCatCore/Services/LocationService/LocationService.swift +++ b/AutoCatCore/Services/LocationService/LocationService.swift @@ -12,14 +12,19 @@ import SwiftLocation @MainActor public final class LocationService { - @Service var geocoder: GeocoderProtocol - @Service var locationManager: SwiftLocationProtocol - @Service var settingsService: SettingsServiceProtocol + let geocoder: GeocoderProtocol + let locationManager: SwiftLocationProtocol + let settingsService: SettingsServiceProtocol private var eventTask: Task? - public init() { + public init(geocoder: GeocoderProtocol, + locationManager: SwiftLocationProtocol, + settingsService: SettingsServiceProtocol) { + self.geocoder = geocoder + self.locationManager = locationManager + self.settingsService = settingsService } private func checkPermissions() async throws { diff --git a/AutoCatCore/Services/LocationService/SwiftLocationProtocol.swift b/AutoCatCore/Services/LocationService/SwiftLocationProtocol.swift index 3e588a2..d0b081c 100644 --- a/AutoCatCore/Services/LocationService/SwiftLocationProtocol.swift +++ b/AutoCatCore/Services/LocationService/SwiftLocationProtocol.swift @@ -8,7 +8,9 @@ import SwiftLocation import CoreLocation +import Mockable +@Mockable public protocol SwiftLocationProtocol { var authorizationStatus: CLAuthorizationStatus { get } diff --git a/AutoCatCore/Services/StorageService/StorageService.swift b/AutoCatCore/Services/StorageService/StorageService.swift index e05f454..c9a0adb 100644 --- a/AutoCatCore/Services/StorageService/StorageService.swift +++ b/AutoCatCore/Services/StorageService/StorageService.swift @@ -26,12 +26,14 @@ public enum StorageError: LocalizedError { public actor StorageService: StorageServiceProtocol { - @Service var settingsService: SettingsServiceProtocol + let settingsService: SettingsServiceProtocol var realm: Realm! - public init(config: Realm.Configuration = .defaultConfiguration) async throws { + public init(settingsService: SettingsServiceProtocol, + config: Realm.Configuration = .defaultConfiguration) async throws { + self.settingsService = settingsService realm = try await Realm(configuration: config, actor: self) } diff --git a/AutoCatCoreTests/LocationServiceTests.swift b/AutoCatCoreTests/LocationServiceTests.swift index 7b415f4..d937d6a 100644 --- a/AutoCatCoreTests/LocationServiceTests.swift +++ b/AutoCatCoreTests/LocationServiceTests.swift @@ -9,6 +9,7 @@ import Testing import CoreLocation import Mockable +import Intents @testable import AutoCatCore @MainActor @@ -18,18 +19,20 @@ struct LocationServiceTests { let longitude: CLLocationDegrees = 10 let address = "Test Address" - let geocoder = GeocoderMock() - let locationManager = SwiftLocationMock() + let location: CLLocation + + let geocoderMock = MockGeocoderProtocol() + let locationManagerMock = MockSwiftLocationProtocol() let settingsServiceMock = MockSettingsServiceProtocol() let locationService: LocationService init() { - ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder) - ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: locationManager) - ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock) + self.location = CLLocation(latitude: latitude, longitude: longitude) - self.locationService = LocationService() + self.locationService = LocationService(geocoder: geocoderMock, + locationManager: locationManagerMock, + settingsService: settingsServiceMock) given(settingsServiceMock) .user.willReturn(User()) @@ -38,42 +41,64 @@ struct LocationServiceTests { @Test func getValidAddress() async throws { - geocoder.addLocation(latitude: latitude, - longitude: longitude, - address: address) + let placemark = CLPlacemark(location: location, name: address, postalAddress: nil) + + given(geocoderMock) + .reverseGeocodeLocation(.any) + .willReturn([placemark]) let result = try await locationService.getAddressForLocation(latitude: latitude, longitude: longitude) + verify(geocoderMock) + .reverseGeocodeLocation(.any) + .called(.once) + #expect(result == address) } @Test func getNilAddress() async throws { - geocoder.addLocation(latitude: latitude, - longitude: longitude, - address: nil) + let placemark = CLPlacemark(location: location, name: nil, postalAddress: nil) + + given(geocoderMock) + .reverseGeocodeLocation(.any) + .willReturn([placemark]) await #expect(throws: LocationError.reverseGeocode) { _ = try await locationService.getAddressForLocation(latitude: latitude, longitude: longitude) } + + verify(geocoderMock) + .reverseGeocodeLocation(.any) + .called(.once) } @Test func addressNotFound() async throws { + given(geocoderMock) + .reverseGeocodeLocation(.any) + .willReturn([]) + await #expect(throws: LocationError.reverseGeocode) { _ = try await locationService.getAddressForLocation(latitude: latitude, longitude: longitude) } + + verify(geocoderMock) + .reverseGeocodeLocation(.any) + .called(.once) } @Test("Get location: denied") func getLocationDenied() async throws { - locationManager.authorizationStatus = .denied + given(locationManagerMock) + .authorizationStatus + .willReturn(.denied) await #expect(throws: CLError(.denied)) { _ = try await locationService.requestCurrentLocation() @@ -83,8 +108,21 @@ struct LocationServiceTests { @Test("Get location: not determined -> denied") func getLocationNotDeterminedDenied() async throws { - locationManager.authorizationStatus = .notDetermined - locationManager.requestedStatus = .denied + given(locationManagerMock) + .authorizationStatus + .willReturn(.notDetermined) + + given(locationManagerMock) + .requestPermission(.value(.always)) + .willReturn(.denied) + + when(locationManagerMock) + .requestPermission(.value(.always)) + .perform { + given(locationManagerMock) + .authorizationStatus + .willReturn(.denied) + } await #expect(throws: CLError(.denied)) { _ = try await locationService.requestCurrentLocation() @@ -94,9 +132,25 @@ struct LocationServiceTests { @Test("Get location: not determined -> allow") func getLocationNotDeterminedAllow() async throws { - locationManager.authorizationStatus = .notDetermined - locationManager.requestedStatus = .authorizedWhenInUse - locationManager.location = CLLocation(latitude: latitude, longitude: longitude) + given(locationManagerMock) + .authorizationStatus + .willReturn(.notDetermined) + + given(locationManagerMock) + .requestPermission(.value(.always)) + .willReturn(.authorizedWhenInUse) + + when(locationManagerMock) + .requestPermission(.value(.always)) + .perform { + given(locationManagerMock) + .authorizationStatus + .willReturn(.authorizedWhenInUse) + } + + given(locationManagerMock) + .requestLocation(accuracy: .any, timeout: .any) + .willReturn(.didUpdateLocations([location])) let event = try await locationService.requestCurrentLocation() @@ -107,8 +161,13 @@ struct LocationServiceTests { @Test("Get location: normal") func getLocationNormal() async throws { - locationManager.authorizationStatus = .authorizedWhenInUse - locationManager.location = CLLocation(latitude: latitude, longitude: longitude) + given(locationManagerMock) + .authorizationStatus + .willReturn(.authorizedWhenInUse) + + given(locationManagerMock) + .requestLocation(accuracy: .any, timeout: .any) + .willReturn(.didUpdateLocations([location])) let event = try await locationService.requestCurrentLocation() @@ -119,7 +178,13 @@ struct LocationServiceTests { @Test("Get location: no location") func getLocationNone() async throws { - locationManager.authorizationStatus = .authorizedWhenInUse + given(locationManagerMock) + .authorizationStatus + .willReturn(.authorizedWhenInUse) + + given(locationManagerMock) + .requestLocation(accuracy: .any, timeout: .any) + .willReturn(.didUpdateLocations([])) await #expect(throws: LocationError.generic) { _ = try await locationService.requestCurrentLocation() @@ -129,19 +194,25 @@ struct LocationServiceTests { @Test("Get location: parallel requests") func getLocationParallel() async throws { - locationManager.authorizationStatus = .authorizedWhenInUse - locationManager.location = CLLocation(latitude: latitude, longitude: longitude) - locationManager.requestLocationTime = 1 + given(locationManagerMock) + .authorizationStatus + .willReturn(.authorizedWhenInUse) + + given(locationManagerMock) + .requestLocation(accuracy: .any, timeout: .any) + .willReturn(.didUpdateLocations([location])) async let task1 = locationService.requestCurrentLocation() async let task2 = locationService.requestCurrentLocation() - try await Task.sleep(nanoseconds: 1_500_000_000) + try await Task.sleep(nanoseconds: 500_000_000) async let task3 = locationService.requestCurrentLocation() let (event1, event2, event3) = try await (task1, task2, task3) - #expect(locationManager.requestLocationCount == 2) + verify(locationManagerMock) + .requestLocation(accuracy: .any, timeout: .any) + .called(.exactly(2)) #expect(event1.latitude == latitude) #expect(event1.longitude == longitude) diff --git a/AutoCatCoreTests/Storage/StorageServiceTests.swift b/AutoCatCoreTests/Storage/StorageServiceTests.swift index fe115af..0217b83 100644 --- a/AutoCatCoreTests/Storage/StorageServiceTests.swift +++ b/AutoCatCoreTests/Storage/StorageServiceTests.swift @@ -30,9 +30,7 @@ struct StorageServiceTests { config.inMemoryIdentifier = UUID().uuidString settingsServiceMock = MockSettingsServiceProtocol() - await ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock) - - self.storageService = try await StorageService(config: config) + self.storageService = try await StorageService(settingsService: settingsServiceMock, config: config) try addTestVehicle(config: config) diff --git a/AutoCatTests/EventsTests.swift b/AutoCatTests/EventsTests.swift index 3d65178..ee60c27 100644 --- a/AutoCatTests/EventsTests.swift +++ b/AutoCatTests/EventsTests.swift @@ -29,11 +29,10 @@ struct EventsTests { storageServiceMock = MockStorageServiceProtocol() apiServiceMock = MockApiServiceProtocol() - ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock) - ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock) - ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock) - - viewModel = EventsViewModel(vehicle: VehicleDto()) + viewModel = EventsViewModel(apiService: apiServiceMock, + storageService: storageServiceMock, + settingsService: settingsServiceMock, + vehicle: VehicleDto()) given(settingsServiceMock) .user.willReturn(User()) @@ -56,7 +55,7 @@ struct EventsTests { .add(event: .value(.valid), to: .value(VehicleDto.validNumber)) .willReturn(unrecognizedVehicleWithEvent) - viewModel = EventsViewModel(vehicle: isUnrecognized ? .unrecognized : .normal) + viewModel.vehicle = isUnrecognized ? .unrecognized : .normal await viewModel.addEvent(.valid) verify(apiServiceMock) @@ -96,7 +95,7 @@ struct EventsTests { .remove(event: .value(VehicleEventDto.validId), from: .value(VehicleDto.validNumber)) .willReturn(.unrecognized) - viewModel = EventsViewModel(vehicle: vehicleWithEvent) + viewModel.vehicle = vehicleWithEvent await viewModel.deleteEvent(VehicleEventDto.valid.viewModel) verify(apiServiceMock) diff --git a/AutoCatTests/FiltersTests.swift b/AutoCatTests/FiltersTests.swift index f3752e4..f349960 100644 --- a/AutoCatTests/FiltersTests.swift +++ b/AutoCatTests/FiltersTests.swift @@ -26,9 +26,7 @@ struct FiltersTests { let testYear = 2222 init() { - ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock) - ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock) - viewModel = FiltersViewModel(filter: Filter()) + viewModel = FiltersViewModel(apiService: apiServiceMock, filter: Filter()) } @Test("Main filters data loaded") diff --git a/AutoCatTests/LocationPickerTests.swift b/AutoCatTests/LocationPickerTests.swift index a9a9a9c..5f1e0a3 100644 --- a/AutoCatTests/LocationPickerTests.swift +++ b/AutoCatTests/LocationPickerTests.swift @@ -8,6 +8,8 @@ import Testing import CoreLocation +import Mockable +import Intents @testable import AutoCat @testable import AutoCatCore @@ -19,19 +21,24 @@ struct LocationPickerTests { let longitude: CLLocationDegrees = 10 let address = "Test Address" - let geocoder = GeocoderMock() + let geocoderMock = MockGeocoderProtocol() - init() { + func makeViewModel(event: VehicleEventDto) -> LocationPickerViewModel { - ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder) - ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: SwiftLocationMock()) - ServiceContainer.shared.register(LocationServiceProtocol.self, instance: LocationService()) + let locationService = LocationService( + geocoder: geocoderMock, + locationManager: MockSwiftLocationProtocol(), + settingsService: MockSettingsServiceProtocol() + ) + + return LocationPickerViewModel(locationService: locationService, + event: event) } @Test("Set initial location (user)") func setInitialLocationUser() async throws { - let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) + let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) #expect(viewModel.position == .userLocation(fallback: .automatic)) } @@ -39,7 +46,7 @@ struct LocationPickerTests { @Test("Set initial location (custom)") func setInitialLocationCustom() async throws { - let viewModel = LocationPickerViewModel(event: .init(lat: latitude, lon: longitude, addedBy: nil)) + let viewModel = makeViewModel(event: .init(lat: latitude, lon: longitude, addedBy: nil)) #expect(viewModel.position.region?.center.latitude == latitude) #expect(viewModel.position.region?.center.longitude == longitude) @@ -48,11 +55,14 @@ struct LocationPickerTests { @Test("Update event") func updateEvent() async throws { - let viewModel = LocationPickerViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) + let viewModel = makeViewModel(event: .init(lat: 0, lon: 0, addedBy: nil)) - geocoder.addLocation(latitude: latitude, - longitude: longitude, - address: address) + let location = CLLocation(latitude: latitude, longitude: longitude) + let placemark = CLPlacemark(location: location, name: address, postalAddress: nil) + + given(geocoderMock) + .reverseGeocodeLocation(.any) + .willReturn([placemark]) await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude)) diff --git a/AutoCatTests/NotesTests.swift b/AutoCatTests/NotesTests.swift index 36694c3..b0a5669 100644 --- a/AutoCatTests/NotesTests.swift +++ b/AutoCatTests/NotesTests.swift @@ -29,9 +29,11 @@ final class NotesTests { storageServiceMock = MockStorageServiceProtocol() apiServiceMock = MockApiServiceProtocol() - ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock) - ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock) - viewModel = NotesViewModel(vehicle: VehicleDto()) + viewModel = NotesViewModel( + storageService: storageServiceMock, + apiService: apiServiceMock, + vehicle: VehicleDto() + ) } @Test("Add note (normal vehicle)") diff --git a/AutoCatTests/ReportTests.swift b/AutoCatTests/ReportTests.swift index 79b4644..cc99036 100644 --- a/AutoCatTests/ReportTests.swift +++ b/AutoCatTests/ReportTests.swift @@ -29,10 +29,23 @@ class ReportTests { settingsServiceMock = MockSettingsServiceProtocol() apiServiceMock = MockApiServiceProtocol() - ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock) - ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock) - ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock) - viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: true) + viewModel = ReportViewModel( + apiService: apiServiceMock, + storageService: storageServiceMock, + settingsService: settingsServiceMock, + vehicle: VehicleDto(), + isPersistent: true + ) + } + + func makeViewModel(vehicle: VehicleDto, isPersistent: Bool) -> ReportViewModel { + ReportViewModel( + apiService: apiServiceMock, + storageService: storageServiceMock, + settingsService: settingsServiceMock, + vehicle: vehicle, + isPersistent: isPersistent + ) } @Test("Load vehicle detail") @@ -41,7 +54,7 @@ class ReportTests { let incompleteVehicleModel = VehicleDto(number: existingVehicleNumber) let fullVehicleModel = VehicleDto(number: existingVehicleNumber, color: testColor) - viewModel = ReportViewModel(vehicle: incompleteVehicleModel, isPersistent: true) + viewModel.vehicle = incompleteVehicleModel #expect(viewModel.vehicle.color == nil) @@ -62,8 +75,6 @@ class ReportTests { @Test("Load vehicle error") func loadVehicleError() async throws { - viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: true) - given(storageServiceMock) .loadVehicle(number: .any) .willThrow(StorageError.vehicleNotFound) @@ -80,8 +91,6 @@ class ReportTests { @Test("Show debug info", arguments: [true, false]) func showDebugInfo(value: Bool) async throws { - viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: false) - given(settingsServiceMock) .showDebugInfo.willReturn(value) @@ -94,7 +103,7 @@ class ReportTests { let vehicle = VehicleDto(number: existingVehicleNumber) let updatedVehicle = VehicleDto(number: existingVehicleNumber, color: testColor) - viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent) + viewModel = makeViewModel(vehicle: vehicle, isPersistent: isPersistent) given(apiServiceMock) .checkVehicleGb(by: .value(existingVehicleNumber)) @@ -104,6 +113,11 @@ class ReportTests { .updateVehicleIfExists(dto: .value(updatedVehicle)) .willReturn() + given(storageServiceMock) + .loadVehicle(number: .value(existingVehicleNumber)) + .willReturn(vehicle) + + await viewModel.onAppear() await viewModel.checkGB() verify(apiServiceMock) @@ -114,6 +128,10 @@ class ReportTests { .updateVehicleIfExists(dto: .value(updatedVehicle)) .called(.once) + verify(storageServiceMock) + .loadVehicle(number: .value(existingVehicleNumber)) + .called(isPersistent ? .once : .never) + #expect(viewModel.vehicle.color == testColor) #expect(viewModel.hud == nil) } @@ -122,18 +140,27 @@ class ReportTests { func checkGbError(isPersistent: Bool) async throws { let vehicle = VehicleDto(number: existingVehicleNumber) - viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent) + viewModel = makeViewModel(vehicle: vehicle, isPersistent: isPersistent) given(apiServiceMock) .checkVehicleGb(by: .value(existingVehicleNumber)) .willThrow(TestError.generic) + given(storageServiceMock) + .loadVehicle(number: .value(existingVehicleNumber)) + .willReturn(vehicle) + + await viewModel.onAppear() await viewModel.checkGB() verify(apiServiceMock) .checkVehicleGb(by: .value(existingVehicleNumber)) .called(.once) + verify(storageServiceMock) + .loadVehicle(number: .value(existingVehicleNumber)) + .called(isPersistent ? .once : .never) + #expect(viewModel.hud == .error(TestError.generic)) } }