Add PlantGuide iOS app with plant identification and care management
- Implement camera capture and plant identification workflow - Add Core Data persistence for plants, care schedules, and cached API data - Create collection view with grid/list layouts and filtering - Build plant detail views with care information display - Integrate Trefle botanical API for plant care data - Add local image storage for captured plant photos - Implement dependency injection container for testability - Include accessibility support throughout the app Bug fixes in this commit: - Fix Trefle API decoding by removing duplicate CodingKeys - Fix LocalCachedImage to load from correct PlantImages directory - Set dateAdded when saving plants for proper collection sorting Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
275
PlantGuideTests/Mocks/MockPlantCollectionRepository.swift
Normal file
275
PlantGuideTests/Mocks/MockPlantCollectionRepository.swift
Normal file
@@ -0,0 +1,275 @@
|
||||
//
|
||||
// MockPlantCollectionRepository.swift
|
||||
// PlantGuideTests
|
||||
//
|
||||
// Mock implementation of PlantCollectionRepositoryProtocol for unit testing.
|
||||
// Provides configurable behavior and call tracking for verification.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
@testable import PlantGuide
|
||||
|
||||
// MARK: - MockPlantCollectionRepository
|
||||
|
||||
/// Mock implementation of PlantCollectionRepositoryProtocol for testing
|
||||
final class MockPlantCollectionRepository: PlantCollectionRepositoryProtocol, @unchecked Sendable {
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
var plants: [UUID: Plant] = [:]
|
||||
|
||||
// MARK: - Call Tracking
|
||||
|
||||
var saveCallCount = 0
|
||||
var fetchByIdCallCount = 0
|
||||
var fetchAllCallCount = 0
|
||||
var deleteCallCount = 0
|
||||
var existsCallCount = 0
|
||||
var updatePlantCallCount = 0
|
||||
var searchCallCount = 0
|
||||
var filterCallCount = 0
|
||||
var getFavoritesCallCount = 0
|
||||
var setFavoriteCallCount = 0
|
||||
var getStatisticsCallCount = 0
|
||||
|
||||
// MARK: - Error Configuration
|
||||
|
||||
var shouldThrowOnSave = false
|
||||
var shouldThrowOnFetch = false
|
||||
var shouldThrowOnDelete = false
|
||||
var shouldThrowOnExists = false
|
||||
var shouldThrowOnUpdate = false
|
||||
var shouldThrowOnSearch = false
|
||||
var shouldThrowOnFilter = false
|
||||
var shouldThrowOnGetFavorites = false
|
||||
var shouldThrowOnSetFavorite = false
|
||||
var shouldThrowOnGetStatistics = false
|
||||
|
||||
var errorToThrow: Error = NSError(
|
||||
domain: "MockError",
|
||||
code: -1,
|
||||
userInfo: [NSLocalizedDescriptionKey: "Mock repository error"]
|
||||
)
|
||||
|
||||
// MARK: - Captured Values
|
||||
|
||||
var lastSavedPlant: Plant?
|
||||
var lastDeletedPlantID: UUID?
|
||||
var lastUpdatedPlant: Plant?
|
||||
var lastSearchQuery: String?
|
||||
var lastFilter: PlantFilter?
|
||||
var lastSetFavoritePlantID: UUID?
|
||||
var lastSetFavoriteValue: Bool?
|
||||
|
||||
// MARK: - Statistics Configuration
|
||||
|
||||
var statisticsToReturn: CollectionStatistics?
|
||||
|
||||
// MARK: - PlantRepositoryProtocol
|
||||
|
||||
func save(_ plant: Plant) async throws {
|
||||
saveCallCount += 1
|
||||
lastSavedPlant = plant
|
||||
if shouldThrowOnSave {
|
||||
throw errorToThrow
|
||||
}
|
||||
plants[plant.id] = plant
|
||||
}
|
||||
|
||||
func fetch(id: UUID) async throws -> Plant? {
|
||||
fetchByIdCallCount += 1
|
||||
if shouldThrowOnFetch {
|
||||
throw errorToThrow
|
||||
}
|
||||
return plants[id]
|
||||
}
|
||||
|
||||
func fetchAll() async throws -> [Plant] {
|
||||
fetchAllCallCount += 1
|
||||
if shouldThrowOnFetch {
|
||||
throw errorToThrow
|
||||
}
|
||||
return Array(plants.values)
|
||||
}
|
||||
|
||||
func delete(id: UUID) async throws {
|
||||
deleteCallCount += 1
|
||||
lastDeletedPlantID = id
|
||||
if shouldThrowOnDelete {
|
||||
throw errorToThrow
|
||||
}
|
||||
plants.removeValue(forKey: id)
|
||||
}
|
||||
|
||||
// MARK: - PlantCollectionRepositoryProtocol Extensions
|
||||
|
||||
func exists(id: UUID) async throws -> Bool {
|
||||
existsCallCount += 1
|
||||
if shouldThrowOnExists {
|
||||
throw errorToThrow
|
||||
}
|
||||
return plants[id] != nil
|
||||
}
|
||||
|
||||
func updatePlant(_ plant: Plant) async throws {
|
||||
updatePlantCallCount += 1
|
||||
lastUpdatedPlant = plant
|
||||
if shouldThrowOnUpdate {
|
||||
throw errorToThrow
|
||||
}
|
||||
plants[plant.id] = plant
|
||||
}
|
||||
|
||||
func searchPlants(query: String) async throws -> [Plant] {
|
||||
searchCallCount += 1
|
||||
lastSearchQuery = query
|
||||
if shouldThrowOnSearch {
|
||||
throw errorToThrow
|
||||
}
|
||||
let lowercaseQuery = query.lowercased()
|
||||
return plants.values.filter { plant in
|
||||
plant.scientificName.lowercased().contains(lowercaseQuery) ||
|
||||
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) } ||
|
||||
(plant.notes?.lowercased().contains(lowercaseQuery) ?? false)
|
||||
}
|
||||
}
|
||||
|
||||
func filterPlants(by filter: PlantFilter) async throws -> [Plant] {
|
||||
filterCallCount += 1
|
||||
lastFilter = filter
|
||||
if shouldThrowOnFilter {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
var result = Array(plants.values)
|
||||
|
||||
// Apply search query
|
||||
if let query = filter.searchQuery, !query.isEmpty {
|
||||
let lowercaseQuery = query.lowercased()
|
||||
result = result.filter { plant in
|
||||
plant.scientificName.lowercased().contains(lowercaseQuery) ||
|
||||
plant.commonNames.contains { $0.lowercased().contains(lowercaseQuery) }
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by favorites
|
||||
if let isFavorite = filter.isFavorite {
|
||||
result = result.filter { $0.isFavorite == isFavorite }
|
||||
}
|
||||
|
||||
// Filter by families
|
||||
if let families = filter.families, !families.isEmpty {
|
||||
result = result.filter { families.contains($0.family) }
|
||||
}
|
||||
|
||||
// Filter by identification source
|
||||
if let source = filter.identificationSource {
|
||||
result = result.filter { $0.identificationSource == source }
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
func getFavorites() async throws -> [Plant] {
|
||||
getFavoritesCallCount += 1
|
||||
if shouldThrowOnGetFavorites {
|
||||
throw errorToThrow
|
||||
}
|
||||
return plants.values.filter { $0.isFavorite }
|
||||
.sorted { $0.dateIdentified > $1.dateIdentified }
|
||||
}
|
||||
|
||||
func setFavorite(plantID: UUID, isFavorite: Bool) async throws {
|
||||
setFavoriteCallCount += 1
|
||||
lastSetFavoritePlantID = plantID
|
||||
lastSetFavoriteValue = isFavorite
|
||||
if shouldThrowOnSetFavorite {
|
||||
throw errorToThrow
|
||||
}
|
||||
if var plant = plants[plantID] {
|
||||
plant.isFavorite = isFavorite
|
||||
plants[plantID] = plant
|
||||
}
|
||||
}
|
||||
|
||||
func getCollectionStatistics() async throws -> CollectionStatistics {
|
||||
getStatisticsCallCount += 1
|
||||
if shouldThrowOnGetStatistics {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
if let statistics = statisticsToReturn {
|
||||
return statistics
|
||||
}
|
||||
|
||||
// Calculate statistics from current plants
|
||||
var familyDistribution: [String: Int] = [:]
|
||||
var sourceBreakdown: [IdentificationSource: Int] = [:]
|
||||
|
||||
for plant in plants.values {
|
||||
familyDistribution[plant.family, default: 0] += 1
|
||||
sourceBreakdown[plant.identificationSource, default: 0] += 1
|
||||
}
|
||||
|
||||
return CollectionStatistics(
|
||||
totalPlants: plants.count,
|
||||
favoriteCount: plants.values.filter { $0.isFavorite }.count,
|
||||
familyDistribution: familyDistribution,
|
||||
identificationSourceBreakdown: sourceBreakdown,
|
||||
plantsAddedThisMonth: 0,
|
||||
upcomingTasksCount: 0,
|
||||
overdueTasksCount: 0
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Resets all state for clean test setup
|
||||
func reset() {
|
||||
plants = [:]
|
||||
|
||||
saveCallCount = 0
|
||||
fetchByIdCallCount = 0
|
||||
fetchAllCallCount = 0
|
||||
deleteCallCount = 0
|
||||
existsCallCount = 0
|
||||
updatePlantCallCount = 0
|
||||
searchCallCount = 0
|
||||
filterCallCount = 0
|
||||
getFavoritesCallCount = 0
|
||||
setFavoriteCallCount = 0
|
||||
getStatisticsCallCount = 0
|
||||
|
||||
shouldThrowOnSave = false
|
||||
shouldThrowOnFetch = false
|
||||
shouldThrowOnDelete = false
|
||||
shouldThrowOnExists = false
|
||||
shouldThrowOnUpdate = false
|
||||
shouldThrowOnSearch = false
|
||||
shouldThrowOnFilter = false
|
||||
shouldThrowOnGetFavorites = false
|
||||
shouldThrowOnSetFavorite = false
|
||||
shouldThrowOnGetStatistics = false
|
||||
|
||||
lastSavedPlant = nil
|
||||
lastDeletedPlantID = nil
|
||||
lastUpdatedPlant = nil
|
||||
lastSearchQuery = nil
|
||||
lastFilter = nil
|
||||
lastSetFavoritePlantID = nil
|
||||
lastSetFavoriteValue = nil
|
||||
statisticsToReturn = nil
|
||||
}
|
||||
|
||||
/// Adds a plant directly to storage (bypasses save method)
|
||||
func addPlant(_ plant: Plant) {
|
||||
plants[plant.id] = plant
|
||||
}
|
||||
|
||||
/// Adds multiple plants directly to storage
|
||||
func addPlants(_ plantsToAdd: [Plant]) {
|
||||
for plant in plantsToAdd {
|
||||
plants[plant.id] = plant
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user