- 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>
195 lines
5.0 KiB
Swift
195 lines
5.0 KiB
Swift
//
|
|
// 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))
|
|
}
|
|
}
|
|
}
|