Going back from @Service property wrapper to init injection (for parallel testing support, and better testing overall)
This commit is contained in:
parent
96ebf45dcc
commit
9f08dfb358
@ -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 = "<group>"; };
|
||||
7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReportCoordinator.swift; sourceTree = "<group>"; };
|
||||
7A1E78FE2CE91A740004B740 /* Vehicle.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Vehicle.swift; sourceTree = "<group>"; };
|
||||
7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GeocoderMock.swift; sourceTree = "<group>"; };
|
||||
7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SwiftLocationMock.swift; sourceTree = "<group>"; };
|
||||
7A27ADF2249F8B650035F39E /* RecordsController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecordsController.swift; sourceTree = "<group>"; };
|
||||
7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileManagerExt.swift; sourceTree = "<group>"; };
|
||||
7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = "<group>"; };
|
||||
@ -723,15 +719,6 @@
|
||||
path = Data;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
7A22B6EC2C67FDEA00E60173 /* Mocks */ = {
|
||||
isa = PBXGroup;
|
||||
children = (
|
||||
7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */,
|
||||
7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */,
|
||||
);
|
||||
path = Mocks;
|
||||
sourceTree = "<group>";
|
||||
};
|
||||
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";
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -138,5 +138,8 @@ struct EventsScreen: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
EventsScreen(viewModel: .init(vehicle: .preview))
|
||||
EventsScreen(viewModel: .init(apiService: MockApiServiceProtocol(),
|
||||
storageService: MockStorageServiceProtocol(),
|
||||
settingsService: MockSettingsServiceProtocol(),
|
||||
vehicle: .preview))
|
||||
}
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -120,5 +120,8 @@ struct FiltersScreen: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
FiltersScreen(viewModel: .init(filter: Filter()))
|
||||
FiltersScreen(viewModel: .init(
|
||||
apiService: MockApiServiceProtocol(),
|
||||
filter: Filter()
|
||||
))
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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 {
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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
|
||||
))
|
||||
}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
|
||||
@ -88,5 +88,5 @@ struct SettingsScreen: View {
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SettingsScreen(viewModel: .init())
|
||||
SettingsScreen(viewModel: .init(settingsService: MockSettingsServiceProtocol()))
|
||||
}
|
||||
|
||||
@ -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() {
|
||||
|
||||
@ -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)")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -6,8 +6,8 @@
|
||||
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||
//
|
||||
|
||||
@MainActor
|
||||
@propertyWrapper
|
||||
@MainActor
|
||||
public struct Service<Service> {
|
||||
|
||||
public var service: Service
|
||||
|
||||
@ -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]
|
||||
}
|
||||
}
|
||||
@ -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([])
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -7,7 +7,9 @@
|
||||
//
|
||||
|
||||
import CoreLocation
|
||||
import Mockable
|
||||
|
||||
@Mockable
|
||||
public protocol GeocoderProtocol {
|
||||
|
||||
func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark]
|
||||
|
||||
@ -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<VehicleEventDto,Error>?
|
||||
|
||||
public init() {
|
||||
public init(geocoder: GeocoderProtocol,
|
||||
locationManager: SwiftLocationProtocol,
|
||||
settingsService: SettingsServiceProtocol) {
|
||||
|
||||
self.geocoder = geocoder
|
||||
self.locationManager = locationManager
|
||||
self.settingsService = settingsService
|
||||
}
|
||||
|
||||
private func checkPermissions() async throws {
|
||||
|
||||
@ -8,7 +8,9 @@
|
||||
|
||||
import SwiftLocation
|
||||
import CoreLocation
|
||||
import Mockable
|
||||
|
||||
@Mockable
|
||||
public protocol SwiftLocationProtocol {
|
||||
|
||||
var authorizationStatus: CLAuthorizationStatus { get }
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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)
|
||||
|
||||
|
||||
@ -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)
|
||||
|
||||
@ -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")
|
||||
|
||||
@ -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))
|
||||
|
||||
|
||||
@ -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)")
|
||||
|
||||
@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user