fix: issue #17 - upcoming tasks
Automated fix by Tony CI v3. Refs #17 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -469,13 +469,15 @@ extension CoreDataStack {
|
|||||||
func resetForTesting() throws {
|
func resetForTesting() throws {
|
||||||
let context = viewContext()
|
let context = viewContext()
|
||||||
|
|
||||||
// Delete all entities
|
// Delete all entities individually (NSBatchDeleteRequest is unsupported on in-memory stores)
|
||||||
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
|
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
|
||||||
|
|
||||||
for entityName in entityNames {
|
for entityName in entityNames {
|
||||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
|
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
|
||||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
let objects = try context.fetch(fetchRequest)
|
||||||
try context.execute(deleteRequest)
|
for object in objects {
|
||||||
|
context.delete(object)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try save(context: context)
|
try save(context: context)
|
||||||
|
|||||||
@@ -84,15 +84,15 @@ final class FilterPreferencesStorage: FilterPreferencesStorageProtocol, @uncheck
|
|||||||
// Save sortAscending
|
// Save sortAscending
|
||||||
userDefaults.set(filter.sortAscending, forKey: Keys.filterSortAscending)
|
userDefaults.set(filter.sortAscending, forKey: Keys.filterSortAscending)
|
||||||
|
|
||||||
// Save families (as array of strings)
|
// Save families (as array of strings) — empty set treated as nil (no filter)
|
||||||
if let families = filter.families {
|
if let families = filter.families, !families.isEmpty {
|
||||||
userDefaults.set(Array(families), forKey: Keys.filterFamilies)
|
userDefaults.set(Array(families), forKey: Keys.filterFamilies)
|
||||||
} else {
|
} else {
|
||||||
userDefaults.removeObject(forKey: Keys.filterFamilies)
|
userDefaults.removeObject(forKey: Keys.filterFamilies)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save light requirements (as array of raw values)
|
// Save light requirements (as array of raw values) — empty set treated as nil
|
||||||
if let lightRequirements = filter.lightRequirements {
|
if let lightRequirements = filter.lightRequirements, !lightRequirements.isEmpty {
|
||||||
let rawValues = lightRequirements.map { $0.rawValue }
|
let rawValues = lightRequirements.map { $0.rawValue }
|
||||||
userDefaults.set(rawValues, forKey: Keys.filterLightRequirements)
|
userDefaults.set(rawValues, forKey: Keys.filterLightRequirements)
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -106,6 +106,9 @@ final class CollectionViewModel {
|
|||||||
/// Task for debounced search
|
/// Task for debounced search
|
||||||
private var searchTask: Task<Void, Never>?
|
private var searchTask: Task<Void, Never>?
|
||||||
|
|
||||||
|
/// Active load task to prevent duplicate concurrent loads
|
||||||
|
private var currentLoadTask: Task<Void, Never>?
|
||||||
|
|
||||||
/// Search debounce interval in nanoseconds (300ms)
|
/// Search debounce interval in nanoseconds (300ms)
|
||||||
private let searchDebounceNanoseconds: UInt64 = 300_000_000
|
private let searchDebounceNanoseconds: UInt64 = 300_000_000
|
||||||
|
|
||||||
@@ -156,21 +159,28 @@ final class CollectionViewModel {
|
|||||||
/// This method fetches all plants based on the current filter and updates
|
/// This method fetches all plants based on the current filter and updates
|
||||||
/// the view model's state accordingly. Shows loading state during fetch.
|
/// the view model's state accordingly. Shows loading state during fetch.
|
||||||
func loadPlants() async {
|
func loadPlants() async {
|
||||||
guard !isLoading else { return }
|
if let currentLoadTask {
|
||||||
|
await currentLoadTask.value
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
isLoading = true
|
isLoading = true
|
||||||
error = nil
|
error = nil
|
||||||
|
|
||||||
do {
|
let task = Task { @MainActor in
|
||||||
let fetchedPlants = try await fetchCollectionUseCase.execute(filter: currentFilter)
|
do {
|
||||||
allPlants = fetchedPlants
|
let fetchedPlants = try await self.fetchCollectionUseCase.execute(filter: self.currentFilter)
|
||||||
applySearchFilter()
|
self.allPlants = fetchedPlants
|
||||||
} catch {
|
self.applySearchFilter()
|
||||||
self.error = error.localizedDescription
|
} catch {
|
||||||
plants = []
|
self.error = error.localizedDescription
|
||||||
allPlants = []
|
self.plants = []
|
||||||
|
self.allPlants = []
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
currentLoadTask = task
|
||||||
|
await task.value
|
||||||
|
currentLoadTask = nil
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -148,8 +148,8 @@ final class CareScheduleViewModel {
|
|||||||
do {
|
do {
|
||||||
try await careScheduleRepository.updateTask(completedTask)
|
try await careScheduleRepository.updateTask(completedTask)
|
||||||
} catch {
|
} catch {
|
||||||
// Log error - in a production app, you might want to show an alert
|
// Revert in-memory state so UI stays consistent with Core Data
|
||||||
print("Failed to persist task completion: \(error)")
|
allTasks[index] = task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -133,13 +133,14 @@ final class PlantDetailViewModel {
|
|||||||
forceRefresh: forceRefresh
|
forceRefresh: forceRefresh
|
||||||
)
|
)
|
||||||
careInfo = info
|
careInfo = info
|
||||||
|
|
||||||
// Check for existing schedule
|
|
||||||
await loadExistingSchedule()
|
|
||||||
} catch {
|
} catch {
|
||||||
self.error = error
|
self.error = error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always load the existing schedule from Core Data, even if care info
|
||||||
|
// fetch failed. This ensures task completion states are always current.
|
||||||
|
await loadExistingSchedule()
|
||||||
|
|
||||||
isLoading = false
|
isLoading = false
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,7 +299,10 @@ final class PlantDetailViewModel {
|
|||||||
do {
|
do {
|
||||||
try await careScheduleRepository.updateTask(completedTask)
|
try await careScheduleRepository.updateTask(completedTask)
|
||||||
} catch {
|
} catch {
|
||||||
print("Failed to persist task completion: \(error)")
|
// Revert in-memory state so UI stays consistent with Core Data
|
||||||
|
schedule.tasks[index] = task
|
||||||
|
careSchedule = schedule
|
||||||
|
self.error = error
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -216,8 +216,8 @@ final class TodayViewModel {
|
|||||||
do {
|
do {
|
||||||
try await careScheduleRepository.updateTask(completedTask)
|
try await careScheduleRepository.updateTask(completedTask)
|
||||||
} catch {
|
} catch {
|
||||||
// Log error - in a production app, you might want to show an alert
|
// Revert in-memory state so UI stays consistent with Core Data
|
||||||
print("Failed to persist task completion: \(error)")
|
allTasks[index] = task
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -86,9 +86,11 @@ final class MockCoreDataStack: CoreDataStackProtocol, @unchecked Sendable {
|
|||||||
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
|
let entityNames = persistentContainer.managedObjectModel.entities.compactMap { $0.name }
|
||||||
|
|
||||||
for entityName in entityNames {
|
for entityName in entityNames {
|
||||||
let fetchRequest = NSFetchRequest<NSFetchRequestResult>(entityName: entityName)
|
let fetchRequest = NSFetchRequest<NSManagedObject>(entityName: entityName)
|
||||||
let deleteRequest = NSBatchDeleteRequest(fetchRequest: fetchRequest)
|
let objects = try context.fetch(fetchRequest)
|
||||||
try context.execute(deleteRequest)
|
for object in objects {
|
||||||
|
context.delete(object)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
try save(context: context)
|
try save(context: context)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ struct MockImagePreprocessor: ImagePreprocessorProtocol, Sendable {
|
|||||||
// MARK: - MockIdentifyPlantUseCase
|
// MARK: - MockIdentifyPlantUseCase
|
||||||
|
|
||||||
/// Mock implementation of IdentifyPlantUseCaseProtocol for testing
|
/// Mock implementation of IdentifyPlantUseCaseProtocol for testing
|
||||||
struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
final class MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, @unchecked Sendable {
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
|||||||
|
|
||||||
/// Creates a mock that returns high-confidence predictions
|
/// Creates a mock that returns high-confidence predictions
|
||||||
static func withHighConfidencePredictions() -> MockIdentifyPlantUseCase {
|
static func withHighConfidencePredictions() -> MockIdentifyPlantUseCase {
|
||||||
var mock = MockIdentifyPlantUseCase()
|
let mock = MockIdentifyPlantUseCase()
|
||||||
mock.predictionsToReturn = [
|
mock.predictionsToReturn = [
|
||||||
ViewPlantPrediction(
|
ViewPlantPrediction(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
@@ -181,7 +181,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
|||||||
|
|
||||||
/// Creates a mock that returns low-confidence predictions
|
/// Creates a mock that returns low-confidence predictions
|
||||||
static func withLowConfidencePredictions() -> MockIdentifyPlantUseCase {
|
static func withLowConfidencePredictions() -> MockIdentifyPlantUseCase {
|
||||||
var mock = MockIdentifyPlantUseCase()
|
let mock = MockIdentifyPlantUseCase()
|
||||||
mock.predictionsToReturn = [
|
mock.predictionsToReturn = [
|
||||||
ViewPlantPrediction(
|
ViewPlantPrediction(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
@@ -195,7 +195,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
|||||||
|
|
||||||
/// Creates a mock that throws an error
|
/// Creates a mock that throws an error
|
||||||
static func withError(_ error: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound) -> MockIdentifyPlantUseCase {
|
static func withError(_ error: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound) -> MockIdentifyPlantUseCase {
|
||||||
var mock = MockIdentifyPlantUseCase()
|
let mock = MockIdentifyPlantUseCase()
|
||||||
mock.shouldThrow = true
|
mock.shouldThrow = true
|
||||||
mock.errorToThrow = error
|
mock.errorToThrow = error
|
||||||
return mock
|
return mock
|
||||||
@@ -205,7 +205,7 @@ struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
|
|||||||
// MARK: - MockIdentifyPlantOnlineUseCase
|
// MARK: - MockIdentifyPlantOnlineUseCase
|
||||||
|
|
||||||
/// Mock implementation of IdentifyPlantOnlineUseCaseProtocol for testing
|
/// Mock implementation of IdentifyPlantOnlineUseCaseProtocol for testing
|
||||||
struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Sendable {
|
final class MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, @unchecked Sendable {
|
||||||
|
|
||||||
// MARK: - Configuration
|
// MARK: - Configuration
|
||||||
|
|
||||||
@@ -229,7 +229,7 @@ struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Senda
|
|||||||
|
|
||||||
/// Creates a mock that returns API predictions
|
/// Creates a mock that returns API predictions
|
||||||
static func withPredictions() -> MockIdentifyPlantOnlineUseCase {
|
static func withPredictions() -> MockIdentifyPlantOnlineUseCase {
|
||||||
var mock = MockIdentifyPlantOnlineUseCase()
|
let mock = MockIdentifyPlantOnlineUseCase()
|
||||||
mock.predictionsToReturn = [
|
mock.predictionsToReturn = [
|
||||||
ViewPlantPrediction(
|
ViewPlantPrediction(
|
||||||
id: UUID(),
|
id: UUID(),
|
||||||
@@ -243,7 +243,7 @@ struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Senda
|
|||||||
|
|
||||||
/// Creates a mock that throws an error
|
/// Creates a mock that throws an error
|
||||||
static func withError(_ error: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound) -> MockIdentifyPlantOnlineUseCase {
|
static func withError(_ error: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound) -> MockIdentifyPlantOnlineUseCase {
|
||||||
var mock = MockIdentifyPlantOnlineUseCase()
|
let mock = MockIdentifyPlantOnlineUseCase()
|
||||||
mock.shouldThrow = true
|
mock.shouldThrow = true
|
||||||
mock.errorToThrow = error
|
mock.errorToThrow = error
|
||||||
return mock
|
return mock
|
||||||
|
|||||||
Reference in New Issue
Block a user