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:
Trey t
2026-01-23 12:18:01 -06:00
parent d3ab29eb84
commit 136dfbae33
187 changed files with 69001 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
//
// MockCareScheduleRepository.swift
// PlantGuideTests
//
// Mock implementation of CareScheduleRepositoryProtocol for unit testing.
// Provides configurable behavior and call tracking for verification.
//
import Foundation
@testable import PlantGuide
// MARK: - MockCareScheduleRepository
/// Mock implementation of CareScheduleRepositoryProtocol for testing
final class MockCareScheduleRepository: CareScheduleRepositoryProtocol, @unchecked Sendable {
// MARK: - Storage
var schedules: [UUID: PlantCareSchedule] = [:]
// MARK: - Call Tracking
var saveCallCount = 0
var fetchForPlantCallCount = 0
var fetchAllCallCount = 0
var fetchAllTasksCallCount = 0
var updateTaskCallCount = 0
var deleteCallCount = 0
// MARK: - Error Configuration
var shouldThrowOnSave = false
var shouldThrowOnFetch = false
var shouldThrowOnFetchAll = false
var shouldThrowOnFetchAllTasks = false
var shouldThrowOnUpdateTask = false
var shouldThrowOnDelete = false
var errorToThrow: Error = NSError(
domain: "MockError",
code: -1,
userInfo: [NSLocalizedDescriptionKey: "Mock care schedule repository error"]
)
// MARK: - Captured Values
var lastSavedSchedule: PlantCareSchedule?
var lastFetchedPlantID: UUID?
var lastUpdatedTask: CareTask?
var lastDeletedPlantID: UUID?
// MARK: - CareScheduleRepositoryProtocol
func save(_ schedule: PlantCareSchedule) async throws {
saveCallCount += 1
lastSavedSchedule = schedule
if shouldThrowOnSave {
throw errorToThrow
}
schedules[schedule.plantID] = schedule
}
func fetch(for plantID: UUID) async throws -> PlantCareSchedule? {
fetchForPlantCallCount += 1
lastFetchedPlantID = plantID
if shouldThrowOnFetch {
throw errorToThrow
}
return schedules[plantID]
}
func fetchAll() async throws -> [PlantCareSchedule] {
fetchAllCallCount += 1
if shouldThrowOnFetchAll {
throw errorToThrow
}
return Array(schedules.values)
}
func fetchAllTasks() async throws -> [CareTask] {
fetchAllTasksCallCount += 1
if shouldThrowOnFetchAllTasks {
throw errorToThrow
}
return schedules.values.flatMap { $0.tasks }
}
func updateTask(_ task: CareTask) async throws {
updateTaskCallCount += 1
lastUpdatedTask = task
if shouldThrowOnUpdateTask {
throw errorToThrow
}
// Find and update the task in the appropriate schedule
for (plantID, var schedule) in schedules {
if let index = schedule.tasks.firstIndex(where: { $0.id == task.id }) {
schedule.tasks[index] = task
schedules[plantID] = schedule
break
}
}
}
func delete(for plantID: UUID) async throws {
deleteCallCount += 1
lastDeletedPlantID = plantID
if shouldThrowOnDelete {
throw errorToThrow
}
schedules.removeValue(forKey: plantID)
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
func reset() {
schedules = [:]
saveCallCount = 0
fetchForPlantCallCount = 0
fetchAllCallCount = 0
fetchAllTasksCallCount = 0
updateTaskCallCount = 0
deleteCallCount = 0
shouldThrowOnSave = false
shouldThrowOnFetch = false
shouldThrowOnFetchAll = false
shouldThrowOnFetchAllTasks = false
shouldThrowOnUpdateTask = false
shouldThrowOnDelete = false
lastSavedSchedule = nil
lastFetchedPlantID = nil
lastUpdatedTask = nil
lastDeletedPlantID = nil
}
/// Adds a schedule directly to storage (bypasses save method)
func addSchedule(_ schedule: PlantCareSchedule) {
schedules[schedule.plantID] = schedule
}
/// Adds multiple schedules directly to storage
func addSchedules(_ schedulesToAdd: [PlantCareSchedule]) {
for schedule in schedulesToAdd {
schedules[schedule.plantID] = schedule
}
}
/// Gets all tasks for a specific plant
func getTasks(for plantID: UUID) -> [CareTask] {
schedules[plantID]?.tasks ?? []
}
/// Gets overdue tasks across all schedules
func getOverdueTasks() -> [CareTask] {
schedules.values.flatMap { $0.overdueTasks }
}
/// Gets pending tasks across all schedules
func getPendingTasks() -> [CareTask] {
schedules.values.flatMap { $0.pendingTasks }
}
}