Adding tests for search screen
This commit is contained in:
parent
071a6f800d
commit
0c4b14debf
@ -26,7 +26,8 @@
|
|||||||
buildConfiguration = "Debug"
|
buildConfiguration = "Debug"
|
||||||
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
selectedDebuggerIdentifier = "Xcode.DebuggerFoundation.Debugger.LLDB"
|
||||||
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
selectedLauncherIdentifier = "Xcode.DebuggerFoundation.Launcher.LLDB"
|
||||||
shouldUseLaunchSchemeArgsEnv = "YES">
|
shouldUseLaunchSchemeArgsEnv = "YES"
|
||||||
|
codeCoverageEnabled = "YES">
|
||||||
<Testables>
|
<Testables>
|
||||||
<TestableReference
|
<TestableReference
|
||||||
skipped = "NO">
|
skipped = "NO">
|
||||||
|
|||||||
@ -20,7 +20,6 @@ final class SearchCoordinator {
|
|||||||
let resolver = ServiceContainer.shared
|
let resolver = ServiceContainer.shared
|
||||||
let viewModel = SearchViewModel(
|
let viewModel = SearchViewModel(
|
||||||
apiService: resolver.resolve(ApiServiceProtocol.self),
|
apiService: resolver.resolve(ApiServiceProtocol.self),
|
||||||
storageService: resolver.resolve(StorageServiceProtocol.self),
|
|
||||||
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
|
vehicleService: resolver.resolve(VehicleServiceProtocol.self)
|
||||||
)
|
)
|
||||||
viewModel.coordinator = self
|
viewModel.coordinator = self
|
||||||
|
|||||||
@ -15,7 +15,6 @@ import Combine
|
|||||||
final class SearchViewModel: ACHudContainer {
|
final class SearchViewModel: ACHudContainer {
|
||||||
|
|
||||||
let apiService: ApiServiceProtocol
|
let apiService: ApiServiceProtocol
|
||||||
let storageService: StorageServiceProtocol
|
|
||||||
let vehicleService: VehicleServiceProtocol
|
let vehicleService: VehicleServiceProtocol
|
||||||
var coordinator: SearchCoordinator?
|
var coordinator: SearchCoordinator?
|
||||||
|
|
||||||
@ -35,8 +34,10 @@ final class SearchViewModel: ACHudContainer {
|
|||||||
if searchText != oldValue {
|
if searchText != oldValue {
|
||||||
filter.searchString = searchText
|
filter.searchString = searchText
|
||||||
searchTask = Task { [filter] in
|
searchTask = Task { [filter] in
|
||||||
try? await Task.sleep(for: .milliseconds(500))
|
do {
|
||||||
await reloadData(with: filter)
|
try await Task.sleep(for: .milliseconds(500))
|
||||||
|
await reloadData(with: filter)
|
||||||
|
} catch {}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,11 +52,9 @@ final class SearchViewModel: ACHudContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
init(apiService: ApiServiceProtocol,
|
init(apiService: ApiServiceProtocol,
|
||||||
storageService: StorageServiceProtocol,
|
|
||||||
vehicleService: VehicleServiceProtocol) {
|
vehicleService: VehicleServiceProtocol) {
|
||||||
|
|
||||||
self.apiService = apiService
|
self.apiService = apiService
|
||||||
self.storageService = storageService
|
|
||||||
self.vehicleService = vehicleService
|
self.vehicleService = vehicleService
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -80,7 +79,6 @@ final class SearchViewModel: ACHudContainer {
|
|||||||
|
|
||||||
func loadSearchResults(filter: Filter) async throws {
|
func loadSearchResults(filter: Filter) async throws {
|
||||||
|
|
||||||
let query = filter.searchString
|
|
||||||
let response = try await apiService.getVehicles(with: filter, pageToken: pageToken, pageSize: 20)
|
let response = try await apiService.getVehicles(with: filter, pageToken: pageToken, pageSize: 20)
|
||||||
|
|
||||||
if response.items.isEmpty {
|
if response.items.isEmpty {
|
||||||
@ -172,18 +170,22 @@ final class SearchViewModel: ACHudContainer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func updateVehicle(_ vehicle: VehicleDto) async {
|
func updateVehicle(_ vehicle: VehicleDto) async {
|
||||||
await wrapWithToast { [weak self] in
|
do {
|
||||||
guard let self else {
|
hud = .progress
|
||||||
|
let (updatedVehicle, errors) = try await vehicleService.updateSearch(number: vehicle.number)
|
||||||
|
|
||||||
|
if !errors.isEmpty {
|
||||||
|
showErrors(errors)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
let updatedVehicle = try await apiService.checkVehicle(by: vehicle.getNumber(), notes: [], events: [], force: true)
|
|
||||||
try await storageService.updateVehicle(dto: updatedVehicle, policy: .ifExists)
|
|
||||||
|
|
||||||
if let index = vehicles.firstIndex(where: { $0.number == updatedVehicle.number }) {
|
if let index = vehicles.firstIndex(where: { $0.number == updatedVehicle.number }) {
|
||||||
vehicles[index] = updatedVehicle
|
vehicles[index] = updatedVehicle
|
||||||
vehicleSections = vehicles.groupedByDate(type: .updatedDate)
|
vehicleSections = vehicles.groupedByDate(type: .updatedDate)
|
||||||
}
|
}
|
||||||
|
hud = nil
|
||||||
|
} catch {
|
||||||
|
hud = .error(error)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,13 +1,13 @@
|
|||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
public final class PagedResponse<T>: Decodable, Sendable where T: Decodable & Sendable {
|
public struct PagedResponse<T>: Decodable, Sendable where T: Decodable & Sendable {
|
||||||
public let count: Int?
|
public var count: Int?
|
||||||
public let pageToken: String?
|
public var pageToken: String?
|
||||||
public let items: [T]
|
public var items: [T]
|
||||||
|
|
||||||
public init() {
|
public init(items: [T]) {
|
||||||
self.items = []
|
self.count = items.count
|
||||||
self.count = nil
|
|
||||||
self.pageToken = nil
|
self.pageToken = nil
|
||||||
|
self.items = items
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -21,7 +21,7 @@ struct HistoryTests {
|
|||||||
|
|
||||||
var viewModel: HistoryViewModel
|
var viewModel: HistoryViewModel
|
||||||
|
|
||||||
init() async {
|
init() {
|
||||||
|
|
||||||
viewModel = HistoryViewModel(
|
viewModel = HistoryViewModel(
|
||||||
apiService: apiServiceMock,
|
apiService: apiServiceMock,
|
||||||
|
|||||||
241
AutoCatTests/SearchTests.swift
Normal file
241
AutoCatTests/SearchTests.swift
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
//
|
||||||
|
// SearchTests.swift
|
||||||
|
// AutoCatTests
|
||||||
|
//
|
||||||
|
// Created by Selim Mustafaev on 22.02.2025.
|
||||||
|
// Copyright © 2025 Selim Mustafaev. All rights reserved.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Testing
|
||||||
|
import Mockable
|
||||||
|
import AutoCatCore
|
||||||
|
@testable import AutoCat
|
||||||
|
|
||||||
|
@MainActor
|
||||||
|
struct SearchTests {
|
||||||
|
|
||||||
|
let apiServiceMock = MockApiServiceProtocol()
|
||||||
|
let vehicleServiceMock = MockVehicleServiceProtocol()
|
||||||
|
|
||||||
|
var viewModel: SearchViewModel
|
||||||
|
|
||||||
|
init() {
|
||||||
|
|
||||||
|
viewModel = SearchViewModel(
|
||||||
|
apiService: apiServiceMock,
|
||||||
|
vehicleService: vehicleServiceMock
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Initial load")
|
||||||
|
func initialLoad() async {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles == [.normal])
|
||||||
|
#expect(viewModel.vehicleSections.count == 1)
|
||||||
|
#expect(viewModel.searchText == "")
|
||||||
|
#expect(viewModel.hud == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Initial load error")
|
||||||
|
func initialLoadError() async {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .value(nil), pageSize: .any)
|
||||||
|
.willThrow(TestError.generic)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.isEmpty)
|
||||||
|
#expect(viewModel.hud == .error(TestError.generic))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Search")
|
||||||
|
func search() async throws {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
|
||||||
|
viewModel.searchText = "test1"
|
||||||
|
viewModel.searchText = "test2"
|
||||||
|
try await Task.sleep(for: .milliseconds(600))
|
||||||
|
viewModel.searchText = "test3"
|
||||||
|
try await Task.sleep(for: .milliseconds(600))
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.exactly(3))
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles == [.normal])
|
||||||
|
#expect(viewModel.vehicleSections.count == 1)
|
||||||
|
#expect(viewModel.filter.searchString == "test3")
|
||||||
|
#expect(viewModel.hud == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Search error")
|
||||||
|
func searchError() async throws {
|
||||||
|
|
||||||
|
var filterWithString = Filter()
|
||||||
|
filterWithString.searchString = "test"
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willThrow(TestError.generic)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
viewModel.searchText = "test"
|
||||||
|
try await Task.sleep(for: .milliseconds(600))
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.isEmpty)
|
||||||
|
#expect(viewModel.hud == .error(TestError.generic))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Pagination")
|
||||||
|
func pagination() async {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: []))
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
await viewModel.loadMoreData()
|
||||||
|
await viewModel.loadMoreData()
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.exactly(3))
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.count == 2)
|
||||||
|
#expect(viewModel.hasMoreData == false)
|
||||||
|
#expect(viewModel.hud == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Pagination error")
|
||||||
|
func paginationError() async {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willThrow(TestError.generic)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
await viewModel.loadMoreData()
|
||||||
|
await viewModel.loadMoreData()
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.exactly(3))
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.count == 2)
|
||||||
|
#expect(viewModel.hasMoreData == false)
|
||||||
|
#expect(viewModel.hud == .error(TestError.generic))
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Update vehicle")
|
||||||
|
func updateVehicle() async {
|
||||||
|
|
||||||
|
let updatedVehicle: VehicleDto = .normal
|
||||||
|
.addEvent(.valid)
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
|
||||||
|
given(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .value(VehicleDto.normal.number))
|
||||||
|
.willReturn((updatedVehicle, []))
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
await viewModel.updateVehicle(.normal)
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
verify(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.first?.events.isEmpty == false)
|
||||||
|
#expect(viewModel.hud == nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Update vehicle error")
|
||||||
|
func updateVehicleError() async {
|
||||||
|
|
||||||
|
let updatedVehicle: VehicleDto = .normal
|
||||||
|
.addEvent(.valid)
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
|
||||||
|
given(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .value(VehicleDto.normal.number))
|
||||||
|
.willReturn((updatedVehicle, [TestError.generic]))
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
await viewModel.updateVehicle(.normal)
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
verify(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.first?.events.isEmpty == true)
|
||||||
|
|
||||||
|
if case .message(_, _) = viewModel.hud {} else {
|
||||||
|
Issue.record("hud must be in .message state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test("Update vehicle error 2")
|
||||||
|
func updateVehicleError2() async {
|
||||||
|
|
||||||
|
given(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.willReturn(PagedResponse(items: [VehicleDto.normal]))
|
||||||
|
|
||||||
|
given(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .value(VehicleDto.normal.number))
|
||||||
|
.willThrow(TestError.generic)
|
||||||
|
|
||||||
|
await viewModel.onAppear()
|
||||||
|
await viewModel.updateVehicle(.normal)
|
||||||
|
|
||||||
|
verify(apiServiceMock)
|
||||||
|
.getVehicles(with: .any, pageToken: .any, pageSize: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
verify(vehicleServiceMock)
|
||||||
|
.updateSearch(number: .any)
|
||||||
|
.called(.once)
|
||||||
|
|
||||||
|
#expect(viewModel.vehicles.first?.events.isEmpty == true)
|
||||||
|
#expect(viewModel.hud == .error(TestError.generic))
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue
Block a user