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 */; };
|
7A1E78F82CE900440004B740 /* ReportViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F72CE900440004B740 /* ReportViewModel.swift */; };
|
||||||
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; };
|
7A1E78FA2CE9005C0004B740 /* ReportCoordinator.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78F92CE9005C0004B740 /* ReportCoordinator.swift */; };
|
||||||
7A1E78FF2CE91A740004B740 /* Vehicle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A1E78FE2CE91A740004B740 /* Vehicle.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 */; };
|
7A27ADF3249F8B650035F39E /* RecordsController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF2249F8B650035F39E /* RecordsController.swift */; };
|
||||||
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
|
7A27ADF5249FD2F90035F39E /* FileManagerExt.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF4249FD2F90035F39E /* FileManagerExt.swift */; };
|
||||||
7A27ADF7249FEF690035F39E /* Recorder.swift in Sources */ = {isa = PBXBuildFile; fileRef = 7A27ADF6249FEF690035F39E /* Recorder.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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
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>"; };
|
7A27ADF6249FEF690035F39E /* Recorder.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Recorder.swift; sourceTree = "<group>"; };
|
||||||
@ -723,15 +719,6 @@
|
|||||||
path = Data;
|
path = Data;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
7A22B6EC2C67FDEA00E60173 /* Mocks */ = {
|
|
||||||
isa = PBXGroup;
|
|
||||||
children = (
|
|
||||||
7A22B6EA2C67FDEA00E60173 /* GeocoderMock.swift */,
|
|
||||||
7A22B6EB2C67FDEA00E60173 /* SwiftLocationMock.swift */,
|
|
||||||
);
|
|
||||||
path = Mocks;
|
|
||||||
sourceTree = "<group>";
|
|
||||||
};
|
|
||||||
7A3F07A924360D9100E59687 /* Extensions */ = {
|
7A3F07A924360D9100E59687 /* Extensions */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@ -1044,7 +1031,6 @@
|
|||||||
7AF6D2292677C3950086EA64 /* Extensions */,
|
7AF6D2292677C3950086EA64 /* Extensions */,
|
||||||
7A11474523FF2A9000B424AF /* Models */,
|
7A11474523FF2A9000B424AF /* Models */,
|
||||||
7AF6D20D2677C0C30086EA64 /* Utils */,
|
7AF6D20D2677C0C30086EA64 /* Utils */,
|
||||||
7A22B6EC2C67FDEA00E60173 /* Mocks */,
|
|
||||||
7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */,
|
7AF6D1F12677C03B0086EA64 /* AutoCatCore.h */,
|
||||||
7AF6D1F22677C03B0086EA64 /* Info.plist */,
|
7AF6D1F22677C03B0086EA64 /* Info.plist */,
|
||||||
);
|
);
|
||||||
@ -1451,8 +1437,6 @@
|
|||||||
buildActionMask = 2147483647;
|
buildActionMask = 2147483647;
|
||||||
files = (
|
files = (
|
||||||
7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */,
|
7A5D84C22C1AE5C900C2209B /* VehicleModel.swift in Sources */,
|
||||||
7A22B6ED2C67FDEA00E60173 /* SwiftLocationMock.swift in Sources */,
|
|
||||||
7A22B6EE2C67FDEA00E60173 /* GeocoderMock.swift in Sources */,
|
|
||||||
7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */,
|
7A5D84B92C1AD3C200C2209B /* DtoConvertible.swift in Sources */,
|
||||||
7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */,
|
7AF6D2182677C1680086EA64 /* VehicleAd.swift in Sources */,
|
||||||
7A761C08267E8EA20005F28F /* JWT.swift in Sources */,
|
7A761C08267E8EA20005F28F /* JWT.swift in Sources */,
|
||||||
@ -1777,6 +1761,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
@ -1801,6 +1786,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests;
|
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCoreTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
};
|
};
|
||||||
@ -1827,6 +1813,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat";
|
||||||
@ -1853,6 +1840,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests;
|
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatTests;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME)";
|
PRODUCT_NAME = "$(TARGET_NAME)";
|
||||||
SWIFT_EMIT_LOC_STRINGS = NO;
|
SWIFT_EMIT_LOC_STRINGS = NO;
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat";
|
TEST_HOST = "$(BUILT_PRODUCTS_DIR)/AutoCat.app/$(BUNDLE_EXECUTABLE_FOLDER_PATH)/AutoCat";
|
||||||
@ -1883,6 +1871,7 @@
|
|||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MOCKING";
|
SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG MOCKING";
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
@ -1913,6 +1902,7 @@
|
|||||||
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore;
|
PRODUCT_BUNDLE_IDENTIFIER = pro.aliencat.AutoCatCore;
|
||||||
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
PRODUCT_NAME = "$(TARGET_NAME:c99extidentifier)";
|
||||||
SKIP_INSTALL = YES;
|
SKIP_INSTALL = YES;
|
||||||
|
SWIFT_UPCOMING_FEATURE_DISABLE_OUTWARD_ACTOR_ISOLATION = YES;
|
||||||
SWIFT_VERSION = 5.0;
|
SWIFT_VERSION = 5.0;
|
||||||
TARGETED_DEVICE_FAMILY = "1,2";
|
TARGETED_DEVICE_FAMILY = "1,2";
|
||||||
VERSIONING_SYSTEM = "apple-generic";
|
VERSIONING_SYSTEM = "apple-generic";
|
||||||
|
|||||||
@ -83,14 +83,22 @@ class AppDelegate: UIResponder, UIApplicationDelegate {
|
|||||||
|
|
||||||
let container = ServiceContainer.shared
|
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(ApiServiceProtocol.self, instance: ApiService())
|
||||||
container.register(GeocoderProtocol.self, instance: CLGeocoder())
|
|
||||||
container.register(SwiftLocationProtocol.self, instance: Location())
|
let locationService = LocationService(
|
||||||
container.register(LocationServiceProtocol.self, instance: LocationService())
|
geocoder: CLGeocoder(),
|
||||||
|
locationManager: Location(),
|
||||||
|
settingsService: settingsService
|
||||||
|
)
|
||||||
|
|
||||||
|
container.register(LocationServiceProtocol.self, instance: locationService)
|
||||||
|
|
||||||
Task {
|
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 {
|
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
|
viewModel.coordinator = self
|
||||||
let controller = CustomHostingController(rootView: EventsScreen(viewModel: viewModel))
|
let controller = CustomHostingController(rootView: EventsScreen(viewModel: viewModel))
|
||||||
navController.pushViewController(controller, animated: true)
|
navController.pushViewController(controller, animated: true)
|
||||||
|
|||||||
@ -138,5 +138,8 @@ struct EventsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#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
|
typealias VehicleOperation = () async throws -> VehicleDto
|
||||||
|
|
||||||
@ObservationIgnored @Service var apiService: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
@ObservationIgnored @Service var storageService: StorageServiceProtocol
|
let storageService: StorageServiceProtocol
|
||||||
@ObservationIgnored @Service var settingsService: SettingsServiceProtocol
|
let settingsService: SettingsServiceProtocol
|
||||||
|
|
||||||
weak var coordinator: EventsCoordinator?
|
weak var coordinator: EventsCoordinator?
|
||||||
|
|
||||||
@ -45,8 +45,14 @@ class EventsViewModel: ACHudContainer {
|
|||||||
UIPasteboard.general.data(forPasteboardType: UTType.vehicleEvent.identifier) != nil
|
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
|
self.vehicle = vehicle
|
||||||
|
|
||||||
updateEvents()
|
updateEvents()
|
||||||
|
|||||||
@ -22,7 +22,10 @@ class FiltersCoordinator: Coordinator {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func start() async throws -> Filter? {
|
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))
|
let controller = CustomHostingController(rootView: FiltersScreen(viewModel: viewModel))
|
||||||
viewController?.pushViewController(controller, animated: true)
|
viewController?.pushViewController(controller, animated: true)
|
||||||
await controller.waitForDisappear()
|
await controller.waitForDisappear()
|
||||||
|
|||||||
@ -120,5 +120,8 @@ struct FiltersScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
FiltersScreen(viewModel: .init(filter: Filter()))
|
FiltersScreen(viewModel: .init(
|
||||||
|
apiService: MockApiServiceProtocol(),
|
||||||
|
filter: Filter()
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,7 +13,7 @@ import AutoCatCore
|
|||||||
@Observable
|
@Observable
|
||||||
class FiltersViewModel {
|
class FiltersViewModel {
|
||||||
|
|
||||||
@ObservationIgnored @Service var api: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
|
|
||||||
var filter: Filter {
|
var filter: Filter {
|
||||||
didSet {
|
didSet {
|
||||||
@ -35,7 +35,9 @@ class FiltersViewModel {
|
|||||||
|
|
||||||
@ObservationIgnored var currentBrand: StringOption = .any
|
@ObservationIgnored var currentBrand: StringOption = .any
|
||||||
|
|
||||||
init(filter: Filter) {
|
init(apiService: ApiServiceProtocol, filter: Filter) {
|
||||||
|
|
||||||
|
self.apiService = apiService
|
||||||
self.filter = filter
|
self.filter = filter
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -44,13 +46,13 @@ class FiltersViewModel {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
brands = [.any] + ((try? await api.getBrands()) ?? []).map { .value($0) }
|
brands = [.any] + ((try? await apiService.getBrands()) ?? []).map { .value($0) }
|
||||||
colors = [.any] + ((try? await api.getColors()) ?? []).map { .value($0) }
|
colors = [.any] + ((try? await apiService.getColors()) ?? []).map { .value($0) }
|
||||||
years = [.any] + ((try? await api.getYears())?.map(String.init) ?? []).map { .value($0) }
|
years = [.any] + ((try? await apiService.getYears())?.map(String.init) ?? []).map { .value($0) }
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadModels(brand: String) async {
|
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
|
filter.model = .any
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -23,7 +23,10 @@ final class LocationPickerCoordinator: Coordinator {
|
|||||||
|
|
||||||
func start() async throws -> VehicleEventDto? {
|
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 screen = LocationPickerScreen(viewModel: viewModel)
|
||||||
let controller = CustomHostingController(rootView: screen)
|
let controller = CustomHostingController(rootView: screen)
|
||||||
viewController?.pushViewController(controller, animated: true)
|
viewController?.pushViewController(controller, animated: true)
|
||||||
|
|||||||
@ -51,7 +51,10 @@ struct LocationPickerScreen: View {
|
|||||||
var event = VehicleEventDto(lat: 47.250049, lon: 39.711821, addedBy: nil)
|
var event = VehicleEventDto(lat: 47.250049, lon: 39.711821, addedBy: nil)
|
||||||
event.address = "Ул. Ленина, 123"
|
event.address = "Ул. Ленина, 123"
|
||||||
|
|
||||||
let viewModel = LocationPickerViewModel(event: event)
|
let viewModel = LocationPickerViewModel(
|
||||||
|
locationService: ServiceContainer.shared.resolve(LocationServiceProtocol.self),
|
||||||
|
event: event
|
||||||
|
)
|
||||||
|
|
||||||
return LocationPickerScreen(viewModel: viewModel)
|
return LocationPickerScreen(viewModel: viewModel)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,14 +15,16 @@ import SwiftUI
|
|||||||
@Observable
|
@Observable
|
||||||
final class LocationPickerViewModel {
|
final class LocationPickerViewModel {
|
||||||
|
|
||||||
@ObservationIgnored @Service var locationService: LocationServiceProtocol
|
let locationService: LocationServiceProtocol
|
||||||
|
|
||||||
var event: VehicleEventDto
|
var event: VehicleEventDto
|
||||||
var position: MapCameraPosition
|
var position: MapCameraPosition
|
||||||
|
|
||||||
var result: VehicleEventDto?
|
var result: VehicleEventDto?
|
||||||
|
|
||||||
init(event: VehicleEventDto) {
|
init(locationService: LocationServiceProtocol, event: VehicleEventDto) {
|
||||||
|
|
||||||
|
self.locationService = locationService
|
||||||
self.event = event
|
self.event = event
|
||||||
|
|
||||||
if event.latitude == 0 && event.longitude == 0 {
|
if event.latitude == 0 && event.longitude == 0 {
|
||||||
|
|||||||
@ -24,7 +24,12 @@ class NotesCoordinator: Coordinator {
|
|||||||
|
|
||||||
func start() async throws -> VehicleDto {
|
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))
|
let controller = CustomHostingController(rootView: NotesScreen(viewModel: viewModel))
|
||||||
viewController?.pushViewController(controller, animated: true)
|
viewController?.pushViewController(controller, animated: true)
|
||||||
await controller.waitForDisappear()
|
await controller.waitForDisappear()
|
||||||
|
|||||||
@ -95,7 +95,11 @@ struct NotesScreen: View {
|
|||||||
.init(text: "zxcv", user: "")
|
.init(text: "zxcv", user: "")
|
||||||
]
|
]
|
||||||
|
|
||||||
let vm = NotesViewModel(vehicle: vehicle)
|
let vm = NotesViewModel(
|
||||||
|
storageService: MockStorageServiceProtocol(),
|
||||||
|
apiService: MockApiServiceProtocol(),
|
||||||
|
vehicle: vehicle
|
||||||
|
)
|
||||||
|
|
||||||
return NotesScreen(viewModel: vm)
|
return NotesScreen(viewModel: vm)
|
||||||
}
|
}
|
||||||
|
|||||||
@ -15,14 +15,18 @@ import UniformTypeIdentifiers
|
|||||||
@Observable
|
@Observable
|
||||||
class NotesViewModel: ACHudContainer {
|
class NotesViewModel: ACHudContainer {
|
||||||
|
|
||||||
@ObservationIgnored @Service var storageService: StorageServiceProtocol
|
let storageService: StorageServiceProtocol
|
||||||
@ObservationIgnored @Service var apiService: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
|
|
||||||
var vehicle: VehicleDto
|
var vehicle: VehicleDto
|
||||||
var hud: ACHud?
|
var hud: ACHud?
|
||||||
|
|
||||||
init(vehicle: VehicleDto) {
|
init(storageService: StorageServiceProtocol,
|
||||||
|
apiService: ApiServiceProtocol,
|
||||||
|
vehicle: VehicleDto) {
|
||||||
|
|
||||||
|
self.storageService = storageService
|
||||||
|
self.apiService = apiService
|
||||||
self.vehicle = vehicle
|
self.vehicle = vehicle
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -28,7 +28,14 @@ class ReportCoordinator: Coordinator {
|
|||||||
|
|
||||||
func start() async throws -> VehicleDto {
|
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
|
viewModel.coordinator = self
|
||||||
let controller = CustomHostingController(rootView: ReportScreen(viewModel: viewModel))
|
let controller = CustomHostingController(rootView: ReportScreen(viewModel: viewModel))
|
||||||
let newNavController = UINavigationController(rootViewController: controller)
|
let newNavController = UINavigationController(rootViewController: controller)
|
||||||
|
|||||||
@ -126,5 +126,11 @@ struct ReportScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#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
|
@Observable
|
||||||
class ReportViewModel: ACHudContainer {
|
class ReportViewModel: ACHudContainer {
|
||||||
|
|
||||||
@ObservationIgnored @Service var api: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
@ObservationIgnored @Service var storageService: StorageServiceProtocol
|
let storageService: StorageServiceProtocol
|
||||||
@ObservationIgnored @Service var settings: SettingsServiceProtocol
|
let settingsService: SettingsServiceProtocol
|
||||||
|
|
||||||
var coordinator: ReportCoordinator?
|
var coordinator: ReportCoordinator?
|
||||||
|
|
||||||
@ -44,7 +44,7 @@ class ReportViewModel: ACHudContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var showDebugInfo: Bool {
|
var showDebugInfo: Bool {
|
||||||
settings.showDebugInfo
|
settingsService.showDebugInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
var shareLink: URL? {
|
var shareLink: URL? {
|
||||||
@ -55,7 +55,15 @@ class ReportViewModel: ACHudContainer {
|
|||||||
return URL(string: Constants.reportLinkBaseURL + "?token=" + jwt)
|
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.vehicle = vehicle
|
||||||
self.isPersistent = isPersistent
|
self.isPersistent = isPersistent
|
||||||
}
|
}
|
||||||
@ -84,7 +92,7 @@ class ReportViewModel: ACHudContainer {
|
|||||||
|
|
||||||
func checkGB() async {
|
func checkGB() async {
|
||||||
await wrapWithToast {
|
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)
|
try await self.storageService.updateVehicleIfExists(dto: self.vehicle)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,7 @@ class SettingsCoordinator: Coordinator {
|
|||||||
|
|
||||||
func start() async throws {
|
func start() async throws {
|
||||||
|
|
||||||
let viewModel = SettingsViewModel()
|
let viewModel = SettingsViewModel(settingsService: ServiceContainer.shared.resolve(SettingsServiceProtocol.self))
|
||||||
viewModel.coordinator = self
|
viewModel.coordinator = self
|
||||||
let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel))
|
let controller = UIHostingController(rootView: SettingsScreen(viewModel: viewModel))
|
||||||
settingsController = controller
|
settingsController = controller
|
||||||
|
|||||||
@ -88,5 +88,5 @@ struct SettingsScreen: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
SettingsScreen(viewModel: .init())
|
SettingsScreen(viewModel: .init(settingsService: MockSettingsServiceProtocol()))
|
||||||
}
|
}
|
||||||
|
|||||||
@ -42,8 +42,9 @@ class SettingsViewModel {
|
|||||||
return jwt.payload.email
|
return jwt.payload.email
|
||||||
}
|
}
|
||||||
|
|
||||||
init() {
|
init(settingsService: SettingsServiceProtocol) {
|
||||||
self.settingService = try! ServiceContainer.shared.resolve(SettingsServiceProtocol.self)
|
|
||||||
|
self.settingService = settingsService
|
||||||
}
|
}
|
||||||
|
|
||||||
func signOut() {
|
func signOut() {
|
||||||
|
|||||||
@ -6,6 +6,8 @@
|
|||||||
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
public enum DIError: Error {
|
public enum DIError: Error {
|
||||||
|
|
||||||
case wrongServiceType(String)
|
case wrongServiceType(String)
|
||||||
@ -47,18 +49,18 @@ public class ServiceContainer {
|
|||||||
if let service = cachedService as? Service {
|
if let service = cachedService as? Service {
|
||||||
return service
|
return service
|
||||||
} else {
|
} else {
|
||||||
throw DIError.wrongServiceType(type)
|
fatalError("Wrong service type for service: \(type)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
guard let factory = factories[type] else {
|
guard let factory = factories[type] else {
|
||||||
throw DIError.serviceNotFound(type)
|
fatalError("Service \(type) not found")
|
||||||
}
|
}
|
||||||
|
|
||||||
if let service = factory() as? Service {
|
if let service = factory() as? Service {
|
||||||
return service
|
return service
|
||||||
} else {
|
} else {
|
||||||
throw DIError.wrongServiceType(type)
|
fatalError("Wrong service type for service: \(type)")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -6,8 +6,8 @@
|
|||||||
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
// Copyright © 2024 Selim Mustafaev. All rights reserved.
|
||||||
//
|
//
|
||||||
|
|
||||||
@MainActor
|
|
||||||
@propertyWrapper
|
@propertyWrapper
|
||||||
|
@MainActor
|
||||||
public struct Service<Service> {
|
public struct Service<Service> {
|
||||||
|
|
||||||
public var 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 CoreLocation
|
||||||
|
import Mockable
|
||||||
|
|
||||||
|
@Mockable
|
||||||
public protocol GeocoderProtocol {
|
public protocol GeocoderProtocol {
|
||||||
|
|
||||||
func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark]
|
func reverseGeocodeLocation(_ location: CLLocation) async throws -> [CLPlacemark]
|
||||||
|
|||||||
@ -12,14 +12,19 @@ import SwiftLocation
|
|||||||
@MainActor
|
@MainActor
|
||||||
public final class LocationService {
|
public final class LocationService {
|
||||||
|
|
||||||
@Service var geocoder: GeocoderProtocol
|
let geocoder: GeocoderProtocol
|
||||||
@Service var locationManager: SwiftLocationProtocol
|
let locationManager: SwiftLocationProtocol
|
||||||
@Service var settingsService: SettingsServiceProtocol
|
let settingsService: SettingsServiceProtocol
|
||||||
|
|
||||||
private var eventTask: Task<VehicleEventDto,Error>?
|
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 {
|
private func checkPermissions() async throws {
|
||||||
|
|||||||
@ -8,7 +8,9 @@
|
|||||||
|
|
||||||
import SwiftLocation
|
import SwiftLocation
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import Mockable
|
||||||
|
|
||||||
|
@Mockable
|
||||||
public protocol SwiftLocationProtocol {
|
public protocol SwiftLocationProtocol {
|
||||||
|
|
||||||
var authorizationStatus: CLAuthorizationStatus { get }
|
var authorizationStatus: CLAuthorizationStatus { get }
|
||||||
|
|||||||
@ -26,12 +26,14 @@ public enum StorageError: LocalizedError {
|
|||||||
|
|
||||||
public actor StorageService: StorageServiceProtocol {
|
public actor StorageService: StorageServiceProtocol {
|
||||||
|
|
||||||
@Service var settingsService: SettingsServiceProtocol
|
let settingsService: SettingsServiceProtocol
|
||||||
|
|
||||||
var realm: Realm!
|
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)
|
realm = try await Realm(configuration: config, actor: self)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -9,6 +9,7 @@
|
|||||||
import Testing
|
import Testing
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
import Mockable
|
import Mockable
|
||||||
|
import Intents
|
||||||
@testable import AutoCatCore
|
@testable import AutoCatCore
|
||||||
|
|
||||||
@MainActor
|
@MainActor
|
||||||
@ -18,18 +19,20 @@ struct LocationServiceTests {
|
|||||||
let longitude: CLLocationDegrees = 10
|
let longitude: CLLocationDegrees = 10
|
||||||
let address = "Test Address"
|
let address = "Test Address"
|
||||||
|
|
||||||
let geocoder = GeocoderMock()
|
let location: CLLocation
|
||||||
let locationManager = SwiftLocationMock()
|
|
||||||
|
let geocoderMock = MockGeocoderProtocol()
|
||||||
|
let locationManagerMock = MockSwiftLocationProtocol()
|
||||||
let settingsServiceMock = MockSettingsServiceProtocol()
|
let settingsServiceMock = MockSettingsServiceProtocol()
|
||||||
let locationService: LocationService
|
let locationService: LocationService
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
|
|
||||||
ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder)
|
self.location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: locationManager)
|
|
||||||
ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock)
|
|
||||||
|
|
||||||
self.locationService = LocationService()
|
self.locationService = LocationService(geocoder: geocoderMock,
|
||||||
|
locationManager: locationManagerMock,
|
||||||
|
settingsService: settingsServiceMock)
|
||||||
|
|
||||||
given(settingsServiceMock)
|
given(settingsServiceMock)
|
||||||
.user.willReturn(User())
|
.user.willReturn(User())
|
||||||
@ -38,42 +41,64 @@ struct LocationServiceTests {
|
|||||||
@Test
|
@Test
|
||||||
func getValidAddress() async throws {
|
func getValidAddress() async throws {
|
||||||
|
|
||||||
geocoder.addLocation(latitude: latitude,
|
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil)
|
||||||
longitude: longitude,
|
|
||||||
address: address)
|
given(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.willReturn([placemark])
|
||||||
|
|
||||||
let result = try await locationService.getAddressForLocation(latitude: latitude,
|
let result = try await locationService.getAddressForLocation(latitude: latitude,
|
||||||
longitude: longitude)
|
longitude: longitude)
|
||||||
|
|
||||||
|
verify(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
#expect(result == address)
|
#expect(result == address)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func getNilAddress() async throws {
|
func getNilAddress() async throws {
|
||||||
|
|
||||||
geocoder.addLocation(latitude: latitude,
|
let placemark = CLPlacemark(location: location, name: nil, postalAddress: nil)
|
||||||
longitude: longitude,
|
|
||||||
address: nil)
|
given(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.willReturn([placemark])
|
||||||
|
|
||||||
await #expect(throws: LocationError.reverseGeocode) {
|
await #expect(throws: LocationError.reverseGeocode) {
|
||||||
_ = try await locationService.getAddressForLocation(latitude: latitude,
|
_ = try await locationService.getAddressForLocation(latitude: latitude,
|
||||||
longitude: longitude)
|
longitude: longitude)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.called(.once)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
func addressNotFound() async throws {
|
func addressNotFound() async throws {
|
||||||
|
|
||||||
|
given(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.willReturn([])
|
||||||
|
|
||||||
await #expect(throws: LocationError.reverseGeocode) {
|
await #expect(throws: LocationError.reverseGeocode) {
|
||||||
_ = try await locationService.getAddressForLocation(latitude: latitude,
|
_ = try await locationService.getAddressForLocation(latitude: latitude,
|
||||||
longitude: longitude)
|
longitude: longitude)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
verify(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.called(.once)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Get location: denied")
|
@Test("Get location: denied")
|
||||||
func getLocationDenied() async throws {
|
func getLocationDenied() async throws {
|
||||||
|
|
||||||
locationManager.authorizationStatus = .denied
|
given(locationManagerMock)
|
||||||
|
.authorizationStatus
|
||||||
|
.willReturn(.denied)
|
||||||
|
|
||||||
await #expect(throws: CLError(.denied)) {
|
await #expect(throws: CLError(.denied)) {
|
||||||
_ = try await locationService.requestCurrentLocation()
|
_ = try await locationService.requestCurrentLocation()
|
||||||
@ -83,8 +108,21 @@ struct LocationServiceTests {
|
|||||||
@Test("Get location: not determined -> denied")
|
@Test("Get location: not determined -> denied")
|
||||||
func getLocationNotDeterminedDenied() async throws {
|
func getLocationNotDeterminedDenied() async throws {
|
||||||
|
|
||||||
locationManager.authorizationStatus = .notDetermined
|
given(locationManagerMock)
|
||||||
locationManager.requestedStatus = .denied
|
.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)) {
|
await #expect(throws: CLError(.denied)) {
|
||||||
_ = try await locationService.requestCurrentLocation()
|
_ = try await locationService.requestCurrentLocation()
|
||||||
@ -94,9 +132,25 @@ struct LocationServiceTests {
|
|||||||
@Test("Get location: not determined -> allow")
|
@Test("Get location: not determined -> allow")
|
||||||
func getLocationNotDeterminedAllow() async throws {
|
func getLocationNotDeterminedAllow() async throws {
|
||||||
|
|
||||||
locationManager.authorizationStatus = .notDetermined
|
given(locationManagerMock)
|
||||||
locationManager.requestedStatus = .authorizedWhenInUse
|
.authorizationStatus
|
||||||
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
|
.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()
|
let event = try await locationService.requestCurrentLocation()
|
||||||
|
|
||||||
@ -107,8 +161,13 @@ struct LocationServiceTests {
|
|||||||
@Test("Get location: normal")
|
@Test("Get location: normal")
|
||||||
func getLocationNormal() async throws {
|
func getLocationNormal() async throws {
|
||||||
|
|
||||||
locationManager.authorizationStatus = .authorizedWhenInUse
|
given(locationManagerMock)
|
||||||
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
|
.authorizationStatus
|
||||||
|
.willReturn(.authorizedWhenInUse)
|
||||||
|
|
||||||
|
given(locationManagerMock)
|
||||||
|
.requestLocation(accuracy: .any, timeout: .any)
|
||||||
|
.willReturn(.didUpdateLocations([location]))
|
||||||
|
|
||||||
let event = try await locationService.requestCurrentLocation()
|
let event = try await locationService.requestCurrentLocation()
|
||||||
|
|
||||||
@ -119,7 +178,13 @@ struct LocationServiceTests {
|
|||||||
@Test("Get location: no location")
|
@Test("Get location: no location")
|
||||||
func getLocationNone() async throws {
|
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) {
|
await #expect(throws: LocationError.generic) {
|
||||||
_ = try await locationService.requestCurrentLocation()
|
_ = try await locationService.requestCurrentLocation()
|
||||||
@ -129,19 +194,25 @@ struct LocationServiceTests {
|
|||||||
@Test("Get location: parallel requests")
|
@Test("Get location: parallel requests")
|
||||||
func getLocationParallel() async throws {
|
func getLocationParallel() async throws {
|
||||||
|
|
||||||
locationManager.authorizationStatus = .authorizedWhenInUse
|
given(locationManagerMock)
|
||||||
locationManager.location = CLLocation(latitude: latitude, longitude: longitude)
|
.authorizationStatus
|
||||||
locationManager.requestLocationTime = 1
|
.willReturn(.authorizedWhenInUse)
|
||||||
|
|
||||||
|
given(locationManagerMock)
|
||||||
|
.requestLocation(accuracy: .any, timeout: .any)
|
||||||
|
.willReturn(.didUpdateLocations([location]))
|
||||||
|
|
||||||
async let task1 = locationService.requestCurrentLocation()
|
async let task1 = locationService.requestCurrentLocation()
|
||||||
async let task2 = 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()
|
async let task3 = locationService.requestCurrentLocation()
|
||||||
|
|
||||||
let (event1, event2, event3) = try await (task1, task2, task3)
|
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.latitude == latitude)
|
||||||
#expect(event1.longitude == longitude)
|
#expect(event1.longitude == longitude)
|
||||||
|
|||||||
@ -30,9 +30,7 @@ struct StorageServiceTests {
|
|||||||
config.inMemoryIdentifier = UUID().uuidString
|
config.inMemoryIdentifier = UUID().uuidString
|
||||||
|
|
||||||
settingsServiceMock = MockSettingsServiceProtocol()
|
settingsServiceMock = MockSettingsServiceProtocol()
|
||||||
await ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock)
|
self.storageService = try await StorageService(settingsService: settingsServiceMock, config: config)
|
||||||
|
|
||||||
self.storageService = try await StorageService(config: config)
|
|
||||||
|
|
||||||
try addTestVehicle(config: config)
|
try addTestVehicle(config: config)
|
||||||
|
|
||||||
|
|||||||
@ -29,11 +29,10 @@ struct EventsTests {
|
|||||||
storageServiceMock = MockStorageServiceProtocol()
|
storageServiceMock = MockStorageServiceProtocol()
|
||||||
apiServiceMock = MockApiServiceProtocol()
|
apiServiceMock = MockApiServiceProtocol()
|
||||||
|
|
||||||
ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock)
|
viewModel = EventsViewModel(apiService: apiServiceMock,
|
||||||
ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock)
|
storageService: storageServiceMock,
|
||||||
ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock)
|
settingsService: settingsServiceMock,
|
||||||
|
vehicle: VehicleDto())
|
||||||
viewModel = EventsViewModel(vehicle: VehicleDto())
|
|
||||||
|
|
||||||
given(settingsServiceMock)
|
given(settingsServiceMock)
|
||||||
.user.willReturn(User())
|
.user.willReturn(User())
|
||||||
@ -56,7 +55,7 @@ struct EventsTests {
|
|||||||
.add(event: .value(.valid), to: .value(VehicleDto.validNumber))
|
.add(event: .value(.valid), to: .value(VehicleDto.validNumber))
|
||||||
.willReturn(unrecognizedVehicleWithEvent)
|
.willReturn(unrecognizedVehicleWithEvent)
|
||||||
|
|
||||||
viewModel = EventsViewModel(vehicle: isUnrecognized ? .unrecognized : .normal)
|
viewModel.vehicle = isUnrecognized ? .unrecognized : .normal
|
||||||
await viewModel.addEvent(.valid)
|
await viewModel.addEvent(.valid)
|
||||||
|
|
||||||
verify(apiServiceMock)
|
verify(apiServiceMock)
|
||||||
@ -96,7 +95,7 @@ struct EventsTests {
|
|||||||
.remove(event: .value(VehicleEventDto.validId), from: .value(VehicleDto.validNumber))
|
.remove(event: .value(VehicleEventDto.validId), from: .value(VehicleDto.validNumber))
|
||||||
.willReturn(.unrecognized)
|
.willReturn(.unrecognized)
|
||||||
|
|
||||||
viewModel = EventsViewModel(vehicle: vehicleWithEvent)
|
viewModel.vehicle = vehicleWithEvent
|
||||||
await viewModel.deleteEvent(VehicleEventDto.valid.viewModel)
|
await viewModel.deleteEvent(VehicleEventDto.valid.viewModel)
|
||||||
|
|
||||||
verify(apiServiceMock)
|
verify(apiServiceMock)
|
||||||
|
|||||||
@ -26,9 +26,7 @@ struct FiltersTests {
|
|||||||
let testYear = 2222
|
let testYear = 2222
|
||||||
|
|
||||||
init() {
|
init() {
|
||||||
ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock)
|
viewModel = FiltersViewModel(apiService: apiServiceMock, filter: Filter())
|
||||||
ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock)
|
|
||||||
viewModel = FiltersViewModel(filter: Filter())
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Main filters data loaded")
|
@Test("Main filters data loaded")
|
||||||
|
|||||||
@ -8,6 +8,8 @@
|
|||||||
|
|
||||||
import Testing
|
import Testing
|
||||||
import CoreLocation
|
import CoreLocation
|
||||||
|
import Mockable
|
||||||
|
import Intents
|
||||||
|
|
||||||
@testable import AutoCat
|
@testable import AutoCat
|
||||||
@testable import AutoCatCore
|
@testable import AutoCatCore
|
||||||
@ -19,19 +21,24 @@ struct LocationPickerTests {
|
|||||||
let longitude: CLLocationDegrees = 10
|
let longitude: CLLocationDegrees = 10
|
||||||
let address = "Test Address"
|
let address = "Test Address"
|
||||||
|
|
||||||
let geocoder = GeocoderMock()
|
let geocoderMock = MockGeocoderProtocol()
|
||||||
|
|
||||||
init() {
|
func makeViewModel(event: VehicleEventDto) -> LocationPickerViewModel {
|
||||||
|
|
||||||
ServiceContainer.shared.register(GeocoderProtocol.self, instance: geocoder)
|
let locationService = LocationService(
|
||||||
ServiceContainer.shared.register(SwiftLocationProtocol.self, instance: SwiftLocationMock())
|
geocoder: geocoderMock,
|
||||||
ServiceContainer.shared.register(LocationServiceProtocol.self, instance: LocationService())
|
locationManager: MockSwiftLocationProtocol(),
|
||||||
|
settingsService: MockSettingsServiceProtocol()
|
||||||
|
)
|
||||||
|
|
||||||
|
return LocationPickerViewModel(locationService: locationService,
|
||||||
|
event: event)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Set initial location (user)")
|
@Test("Set initial location (user)")
|
||||||
func setInitialLocationUser() async throws {
|
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))
|
#expect(viewModel.position == .userLocation(fallback: .automatic))
|
||||||
}
|
}
|
||||||
@ -39,7 +46,7 @@ struct LocationPickerTests {
|
|||||||
@Test("Set initial location (custom)")
|
@Test("Set initial location (custom)")
|
||||||
func setInitialLocationCustom() async throws {
|
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.latitude == latitude)
|
||||||
#expect(viewModel.position.region?.center.longitude == longitude)
|
#expect(viewModel.position.region?.center.longitude == longitude)
|
||||||
@ -48,11 +55,14 @@ struct LocationPickerTests {
|
|||||||
@Test("Update event")
|
@Test("Update event")
|
||||||
func updateEvent() async throws {
|
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,
|
let location = CLLocation(latitude: latitude, longitude: longitude)
|
||||||
longitude: longitude,
|
let placemark = CLPlacemark(location: location, name: address, postalAddress: nil)
|
||||||
address: address)
|
|
||||||
|
given(geocoderMock)
|
||||||
|
.reverseGeocodeLocation(.any)
|
||||||
|
.willReturn([placemark])
|
||||||
|
|
||||||
await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude))
|
await viewModel.updateEvent(center: .init(latitude: latitude, longitude: longitude))
|
||||||
|
|
||||||
|
|||||||
@ -29,9 +29,11 @@ final class NotesTests {
|
|||||||
storageServiceMock = MockStorageServiceProtocol()
|
storageServiceMock = MockStorageServiceProtocol()
|
||||||
apiServiceMock = MockApiServiceProtocol()
|
apiServiceMock = MockApiServiceProtocol()
|
||||||
|
|
||||||
ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock)
|
viewModel = NotesViewModel(
|
||||||
ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock)
|
storageService: storageServiceMock,
|
||||||
viewModel = NotesViewModel(vehicle: VehicleDto())
|
apiService: apiServiceMock,
|
||||||
|
vehicle: VehicleDto()
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test("Add note (normal vehicle)")
|
@Test("Add note (normal vehicle)")
|
||||||
|
|||||||
@ -29,10 +29,23 @@ class ReportTests {
|
|||||||
settingsServiceMock = MockSettingsServiceProtocol()
|
settingsServiceMock = MockSettingsServiceProtocol()
|
||||||
apiServiceMock = MockApiServiceProtocol()
|
apiServiceMock = MockApiServiceProtocol()
|
||||||
|
|
||||||
ServiceContainer.shared.register(StorageServiceProtocol.self, instance: storageServiceMock)
|
viewModel = ReportViewModel(
|
||||||
ServiceContainer.shared.register(SettingsServiceProtocol.self, instance: settingsServiceMock)
|
apiService: apiServiceMock,
|
||||||
ServiceContainer.shared.register(ApiServiceProtocol.self, instance: apiServiceMock)
|
storageService: storageServiceMock,
|
||||||
viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: true)
|
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")
|
@Test("Load vehicle detail")
|
||||||
@ -41,7 +54,7 @@ class ReportTests {
|
|||||||
let incompleteVehicleModel = VehicleDto(number: existingVehicleNumber)
|
let incompleteVehicleModel = VehicleDto(number: existingVehicleNumber)
|
||||||
let fullVehicleModel = VehicleDto(number: existingVehicleNumber, color: testColor)
|
let fullVehicleModel = VehicleDto(number: existingVehicleNumber, color: testColor)
|
||||||
|
|
||||||
viewModel = ReportViewModel(vehicle: incompleteVehicleModel, isPersistent: true)
|
viewModel.vehicle = incompleteVehicleModel
|
||||||
|
|
||||||
#expect(viewModel.vehicle.color == nil)
|
#expect(viewModel.vehicle.color == nil)
|
||||||
|
|
||||||
@ -62,8 +75,6 @@ class ReportTests {
|
|||||||
@Test("Load vehicle error")
|
@Test("Load vehicle error")
|
||||||
func loadVehicleError() async throws {
|
func loadVehicleError() async throws {
|
||||||
|
|
||||||
viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: true)
|
|
||||||
|
|
||||||
given(storageServiceMock)
|
given(storageServiceMock)
|
||||||
.loadVehicle(number: .any)
|
.loadVehicle(number: .any)
|
||||||
.willThrow(StorageError.vehicleNotFound)
|
.willThrow(StorageError.vehicleNotFound)
|
||||||
@ -80,8 +91,6 @@ class ReportTests {
|
|||||||
@Test("Show debug info", arguments: [true, false])
|
@Test("Show debug info", arguments: [true, false])
|
||||||
func showDebugInfo(value: Bool) async throws {
|
func showDebugInfo(value: Bool) async throws {
|
||||||
|
|
||||||
viewModel = ReportViewModel(vehicle: VehicleDto(), isPersistent: false)
|
|
||||||
|
|
||||||
given(settingsServiceMock)
|
given(settingsServiceMock)
|
||||||
.showDebugInfo.willReturn(value)
|
.showDebugInfo.willReturn(value)
|
||||||
|
|
||||||
@ -94,7 +103,7 @@ class ReportTests {
|
|||||||
let vehicle = VehicleDto(number: existingVehicleNumber)
|
let vehicle = VehicleDto(number: existingVehicleNumber)
|
||||||
let updatedVehicle = VehicleDto(number: existingVehicleNumber, color: testColor)
|
let updatedVehicle = VehicleDto(number: existingVehicleNumber, color: testColor)
|
||||||
|
|
||||||
viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent)
|
viewModel = makeViewModel(vehicle: vehicle, isPersistent: isPersistent)
|
||||||
|
|
||||||
given(apiServiceMock)
|
given(apiServiceMock)
|
||||||
.checkVehicleGb(by: .value(existingVehicleNumber))
|
.checkVehicleGb(by: .value(existingVehicleNumber))
|
||||||
@ -104,6 +113,11 @@ class ReportTests {
|
|||||||
.updateVehicleIfExists(dto: .value(updatedVehicle))
|
.updateVehicleIfExists(dto: .value(updatedVehicle))
|
||||||
.willReturn()
|
.willReturn()
|
||||||
|
|
||||||
|
given(storageServiceMock)
|
||||||
|
.loadVehicle(number: .value(existingVehicleNumber))
|
||||||
|
.willReturn(vehicle)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
await viewModel.checkGB()
|
await viewModel.checkGB()
|
||||||
|
|
||||||
verify(apiServiceMock)
|
verify(apiServiceMock)
|
||||||
@ -114,6 +128,10 @@ class ReportTests {
|
|||||||
.updateVehicleIfExists(dto: .value(updatedVehicle))
|
.updateVehicleIfExists(dto: .value(updatedVehicle))
|
||||||
.called(.once)
|
.called(.once)
|
||||||
|
|
||||||
|
verify(storageServiceMock)
|
||||||
|
.loadVehicle(number: .value(existingVehicleNumber))
|
||||||
|
.called(isPersistent ? .once : .never)
|
||||||
|
|
||||||
#expect(viewModel.vehicle.color == testColor)
|
#expect(viewModel.vehicle.color == testColor)
|
||||||
#expect(viewModel.hud == nil)
|
#expect(viewModel.hud == nil)
|
||||||
}
|
}
|
||||||
@ -122,18 +140,27 @@ class ReportTests {
|
|||||||
func checkGbError(isPersistent: Bool) async throws {
|
func checkGbError(isPersistent: Bool) async throws {
|
||||||
|
|
||||||
let vehicle = VehicleDto(number: existingVehicleNumber)
|
let vehicle = VehicleDto(number: existingVehicleNumber)
|
||||||
viewModel = ReportViewModel(vehicle: vehicle, isPersistent: isPersistent)
|
viewModel = makeViewModel(vehicle: vehicle, isPersistent: isPersistent)
|
||||||
|
|
||||||
given(apiServiceMock)
|
given(apiServiceMock)
|
||||||
.checkVehicleGb(by: .value(existingVehicleNumber))
|
.checkVehicleGb(by: .value(existingVehicleNumber))
|
||||||
.willThrow(TestError.generic)
|
.willThrow(TestError.generic)
|
||||||
|
|
||||||
|
given(storageServiceMock)
|
||||||
|
.loadVehicle(number: .value(existingVehicleNumber))
|
||||||
|
.willReturn(vehicle)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
await viewModel.checkGB()
|
await viewModel.checkGB()
|
||||||
|
|
||||||
verify(apiServiceMock)
|
verify(apiServiceMock)
|
||||||
.checkVehicleGb(by: .value(existingVehicleNumber))
|
.checkVehicleGb(by: .value(existingVehicleNumber))
|
||||||
.called(.once)
|
.called(.once)
|
||||||
|
|
||||||
|
verify(storageServiceMock)
|
||||||
|
.loadVehicle(number: .value(existingVehicleNumber))
|
||||||
|
.called(isPersistent ? .once : .never)
|
||||||
|
|
||||||
#expect(viewModel.hud == .error(TestError.generic))
|
#expect(viewModel.hud == .error(TestError.generic))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user