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,245 @@
//
// MockPlantClassificationService.swift
// PlantGuideTests
//
// Mock implementations for ML-related services for unit testing.
// Provides configurable behavior and call tracking for verification.
//
import Foundation
import CoreGraphics
import UIKit
@testable import PlantGuide
// MARK: - MockPlantClassificationService
/// Mock implementation of PlantClassificationServiceProtocol for testing
final actor MockPlantClassificationService: PlantClassificationServiceProtocol {
// MARK: - Call Tracking
private(set) var classifyCallCount = 0
// MARK: - Error Configuration
var shouldThrowOnClassify = false
var errorToThrow: Error = PlantClassificationError.modelLoadFailed
// MARK: - Return Value Configuration
var predictionsToReturn: [PlantPrediction] = []
// MARK: - Captured Values
private(set) var lastClassifiedImage: CGImage?
// MARK: - PlantClassificationServiceProtocol
func classify(image: CGImage) async throws -> [PlantPrediction] {
classifyCallCount += 1
lastClassifiedImage = image
if shouldThrowOnClassify {
throw errorToThrow
}
// Return configured predictions or empty array
return predictionsToReturn
}
// MARK: - Helper Methods
/// Resets all state for clean test setup
func reset() {
classifyCallCount = 0
shouldThrowOnClassify = false
errorToThrow = PlantClassificationError.modelLoadFailed
predictionsToReturn = []
lastClassifiedImage = nil
}
/// Configures the mock to return predictions for common test plants
func configureMockPredictions(_ predictions: [PlantPrediction]) {
predictionsToReturn = predictions
}
/// Creates a default set of mock predictions
func configureDefaultPredictions() {
predictionsToReturn = [
PlantPrediction(
speciesIndex: 0,
confidence: 0.92,
scientificName: "Monstera deliciosa",
commonNames: ["Swiss Cheese Plant", "Monstera"]
),
PlantPrediction(
speciesIndex: 1,
confidence: 0.75,
scientificName: "Philodendron bipinnatifidum",
commonNames: ["Split Leaf Philodendron"]
),
PlantPrediction(
speciesIndex: 2,
confidence: 0.45,
scientificName: "Epipremnum aureum",
commonNames: ["Pothos", "Devil's Ivy"]
)
]
}
/// Configures low confidence predictions for testing fallback behavior
func configureLowConfidencePredictions() {
predictionsToReturn = [
PlantPrediction(
speciesIndex: 0,
confidence: 0.35,
scientificName: "Unknown plant",
commonNames: ["Unidentified"]
)
]
}
}
// MARK: - MockImagePreprocessor
/// Mock implementation of ImagePreprocessorProtocol for testing
struct MockImagePreprocessor: ImagePreprocessorProtocol, Sendable {
// MARK: - Configuration
var shouldThrow = false
var errorToThrow: Error = ImagePreprocessorError.cgImageCreationFailed
// MARK: - Return Value Configuration
var imageToReturn: CGImage?
// MARK: - ImagePreprocessorProtocol
func preprocess(_ image: UIImage) async throws -> CGImage {
if shouldThrow {
throw errorToThrow
}
// Return configured image or create one from the input
if let configuredImage = imageToReturn {
return configuredImage
}
guard let cgImage = image.cgImage else {
throw ImagePreprocessorError.cgImageCreationFailed
}
return cgImage
}
}
// MARK: - MockIdentifyPlantUseCase
/// Mock implementation of IdentifyPlantUseCaseProtocol for testing
struct MockIdentifyPlantUseCase: IdentifyPlantUseCaseProtocol, Sendable {
// MARK: - Configuration
var shouldThrow = false
var errorToThrow: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound
// MARK: - Return Value Configuration
var predictionsToReturn: [ViewPlantPrediction] = []
// MARK: - IdentifyPlantUseCaseProtocol
func execute(image: UIImage) async throws -> [ViewPlantPrediction] {
if shouldThrow {
throw errorToThrow
}
return predictionsToReturn
}
// MARK: - Factory Methods
/// Creates a mock that returns high-confidence predictions
static func withHighConfidencePredictions() -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
speciesName: "Monstera deliciosa",
commonName: "Swiss Cheese Plant",
confidence: 0.95
)
]
return mock
}
/// Creates a mock that returns low-confidence predictions
static func withLowConfidencePredictions() -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
speciesName: "Unknown",
commonName: nil,
confidence: 0.35
)
]
return mock
}
/// Creates a mock that throws an error
static func withError(_ error: Error = IdentifyPlantOnDeviceUseCaseError.noMatchesFound) -> MockIdentifyPlantUseCase {
var mock = MockIdentifyPlantUseCase()
mock.shouldThrow = true
mock.errorToThrow = error
return mock
}
}
// MARK: - MockIdentifyPlantOnlineUseCase
/// Mock implementation of IdentifyPlantOnlineUseCaseProtocol for testing
struct MockIdentifyPlantOnlineUseCase: IdentifyPlantOnlineUseCaseProtocol, Sendable {
// MARK: - Configuration
var shouldThrow = false
var errorToThrow: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound
// MARK: - Return Value Configuration
var predictionsToReturn: [ViewPlantPrediction] = []
// MARK: - IdentifyPlantOnlineUseCaseProtocol
func execute(image: UIImage) async throws -> [ViewPlantPrediction] {
if shouldThrow {
throw errorToThrow
}
return predictionsToReturn
}
// MARK: - Factory Methods
/// Creates a mock that returns API predictions
static func withPredictions() -> MockIdentifyPlantOnlineUseCase {
var mock = MockIdentifyPlantOnlineUseCase()
mock.predictionsToReturn = [
ViewPlantPrediction(
id: UUID(),
speciesName: "Monstera deliciosa",
commonName: "Swiss Cheese Plant",
confidence: 0.98
)
]
return mock
}
/// Creates a mock that throws an error
static func withError(_ error: Error = IdentifyPlantOnlineUseCaseError.noMatchesFound) -> MockIdentifyPlantOnlineUseCase {
var mock = MockIdentifyPlantOnlineUseCase()
mock.shouldThrow = true
mock.errorToThrow = error
return mock
}
}