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:
194
PlantGuideTests/Mocks/MockImageStorage.swift
Normal file
194
PlantGuideTests/Mocks/MockImageStorage.swift
Normal file
@@ -0,0 +1,194 @@
|
||||
//
|
||||
// MockImageStorage.swift
|
||||
// PlantGuideTests
|
||||
//
|
||||
// Mock implementation of ImageStorageProtocol for unit testing.
|
||||
// Provides configurable behavior and call tracking for verification.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import UIKit
|
||||
@testable import PlantGuide
|
||||
|
||||
// MARK: - MockImageStorage
|
||||
|
||||
/// Mock implementation of ImageStorageProtocol for testing
|
||||
final actor MockImageStorage: ImageStorageProtocol {
|
||||
|
||||
// MARK: - Storage
|
||||
|
||||
private var storedImages: [String: UIImage] = [:]
|
||||
private var plantImages: [UUID: [String]] = [:]
|
||||
|
||||
// MARK: - Call Tracking
|
||||
|
||||
private(set) var saveCallCount = 0
|
||||
private(set) var loadCallCount = 0
|
||||
private(set) var deleteCallCount = 0
|
||||
private(set) var deleteAllCallCount = 0
|
||||
|
||||
// MARK: - Error Configuration
|
||||
|
||||
var shouldThrowOnSave = false
|
||||
var shouldThrowOnLoad = false
|
||||
var shouldThrowOnDelete = false
|
||||
var shouldThrowOnDeleteAll = false
|
||||
|
||||
var errorToThrow: Error = ImageStorageError.writeFailed(
|
||||
NSError(domain: "MockError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Mock storage error"])
|
||||
)
|
||||
|
||||
// MARK: - Captured Values
|
||||
|
||||
private(set) var lastSavedImage: UIImage?
|
||||
private(set) var lastSavedPlantID: UUID?
|
||||
private(set) var lastLoadedPath: String?
|
||||
private(set) var lastDeletedPath: String?
|
||||
private(set) var lastDeletedAllPlantID: UUID?
|
||||
|
||||
// MARK: - Generated Path Control
|
||||
|
||||
var pathToReturn: String?
|
||||
|
||||
// MARK: - ImageStorageProtocol
|
||||
|
||||
func save(_ image: UIImage, for plantID: UUID) async throws -> String {
|
||||
saveCallCount += 1
|
||||
lastSavedImage = image
|
||||
lastSavedPlantID = plantID
|
||||
|
||||
if shouldThrowOnSave {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
// Generate or use provided path
|
||||
let path = pathToReturn ?? "\(plantID.uuidString)/\(UUID().uuidString).jpg"
|
||||
|
||||
// Store the image
|
||||
storedImages[path] = image
|
||||
|
||||
// Track images per plant
|
||||
var paths = plantImages[plantID] ?? []
|
||||
paths.append(path)
|
||||
plantImages[plantID] = paths
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
func load(path: String) async -> UIImage? {
|
||||
loadCallCount += 1
|
||||
lastLoadedPath = path
|
||||
return storedImages[path]
|
||||
}
|
||||
|
||||
func delete(path: String) async throws {
|
||||
deleteCallCount += 1
|
||||
lastDeletedPath = path
|
||||
|
||||
if shouldThrowOnDelete {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
guard storedImages[path] != nil else {
|
||||
throw ImageStorageError.fileNotFound
|
||||
}
|
||||
|
||||
storedImages.removeValue(forKey: path)
|
||||
|
||||
// Remove from plant tracking
|
||||
for (plantID, var paths) in plantImages {
|
||||
if let index = paths.firstIndex(of: path) {
|
||||
paths.remove(at: index)
|
||||
plantImages[plantID] = paths
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func deleteAll(for plantID: UUID) async throws {
|
||||
deleteAllCallCount += 1
|
||||
lastDeletedAllPlantID = plantID
|
||||
|
||||
if shouldThrowOnDeleteAll {
|
||||
throw errorToThrow
|
||||
}
|
||||
|
||||
// Remove all images for this plant
|
||||
if let paths = plantImages[plantID] {
|
||||
for path in paths {
|
||||
storedImages.removeValue(forKey: path)
|
||||
}
|
||||
plantImages.removeValue(forKey: plantID)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helper Methods
|
||||
|
||||
/// Resets all state for clean test setup
|
||||
func reset() {
|
||||
storedImages = [:]
|
||||
plantImages = [:]
|
||||
|
||||
saveCallCount = 0
|
||||
loadCallCount = 0
|
||||
deleteCallCount = 0
|
||||
deleteAllCallCount = 0
|
||||
|
||||
shouldThrowOnSave = false
|
||||
shouldThrowOnLoad = false
|
||||
shouldThrowOnDelete = false
|
||||
shouldThrowOnDeleteAll = false
|
||||
|
||||
lastSavedImage = nil
|
||||
lastSavedPlantID = nil
|
||||
lastLoadedPath = nil
|
||||
lastDeletedPath = nil
|
||||
lastDeletedAllPlantID = nil
|
||||
|
||||
pathToReturn = nil
|
||||
}
|
||||
|
||||
/// Adds an image directly to storage (bypasses save method)
|
||||
func addImage(_ image: UIImage, at path: String, for plantID: UUID) {
|
||||
storedImages[path] = image
|
||||
var paths = plantImages[plantID] ?? []
|
||||
paths.append(path)
|
||||
plantImages[plantID] = paths
|
||||
}
|
||||
|
||||
/// Gets stored image count
|
||||
var imageCount: Int {
|
||||
storedImages.count
|
||||
}
|
||||
|
||||
/// Gets image count for a specific plant
|
||||
func imageCount(for plantID: UUID) -> Int {
|
||||
plantImages[plantID]?.count ?? 0
|
||||
}
|
||||
|
||||
/// Gets all paths for a plant
|
||||
func paths(for plantID: UUID) -> [String] {
|
||||
plantImages[plantID] ?? []
|
||||
}
|
||||
|
||||
/// Checks if an image exists at a path
|
||||
func imageExists(at path: String) -> Bool {
|
||||
storedImages[path] != nil
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Test Image Creation Helper
|
||||
|
||||
extension MockImageStorage {
|
||||
/// Creates a simple test image for use in tests
|
||||
static func createTestImage(
|
||||
color: UIColor = .red,
|
||||
size: CGSize = CGSize(width: 100, height: 100)
|
||||
) -> UIImage {
|
||||
let renderer = UIGraphicsImageRenderer(size: size)
|
||||
return renderer.image { context in
|
||||
color.setFill()
|
||||
context.fill(CGRect(origin: .zero, size: size))
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user