- 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>
509 lines
18 KiB
Swift
509 lines
18 KiB
Swift
//
|
|
// CreateCareScheduleUseCaseTests.swift
|
|
// PlantGuideTests
|
|
//
|
|
// Unit tests for CreateCareScheduleUseCase - the use case for creating plant
|
|
// care schedules based on care requirements and user preferences.
|
|
//
|
|
|
|
import XCTest
|
|
@testable import PlantGuide
|
|
|
|
// MARK: - CreateCareScheduleUseCaseTests
|
|
|
|
final class CreateCareScheduleUseCaseTests: XCTestCase {
|
|
|
|
// MARK: - Properties
|
|
|
|
private var sut: CreateCareScheduleUseCase!
|
|
|
|
// MARK: - Test Lifecycle
|
|
|
|
override func setUp() {
|
|
super.setUp()
|
|
sut = CreateCareScheduleUseCase()
|
|
}
|
|
|
|
override func tearDown() {
|
|
sut = nil
|
|
super.tearDown()
|
|
}
|
|
|
|
// MARK: - Test Helpers
|
|
|
|
private func createBasicCareInfo(
|
|
wateringFrequency: WateringFrequency = .weekly,
|
|
fertilizerSchedule: FertilizerSchedule? = nil
|
|
) -> PlantCareInfo {
|
|
PlantCareInfo(
|
|
scientificName: "Monstera deliciosa",
|
|
commonName: "Swiss Cheese Plant",
|
|
lightRequirement: .partialShade,
|
|
wateringSchedule: WateringSchedule(frequency: wateringFrequency, amount: .moderate),
|
|
temperatureRange: TemperatureRange(minimumCelsius: 18, maximumCelsius: 27),
|
|
fertilizerSchedule: fertilizerSchedule
|
|
)
|
|
}
|
|
|
|
// MARK: - execute() Basic Schedule Creation Tests
|
|
|
|
func testExecute_WhenCalled_ReturnsScheduleWithCorrectPlantID() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo()
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.plantID, plant.id)
|
|
}
|
|
|
|
func testExecute_WhenCalled_ReturnsScheduleWithCorrectLightRequirement() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = PlantCareInfo(
|
|
scientificName: "Test Plant",
|
|
commonName: nil,
|
|
lightRequirement: .fullSun,
|
|
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperatureRange: TemperatureRange(minimumCelsius: 15, maximumCelsius: 30)
|
|
)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.lightRequirement, .fullSun)
|
|
}
|
|
|
|
func testExecute_WhenCalled_ReturnsScheduleWithCorrectTemperatureRange() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = PlantCareInfo(
|
|
scientificName: "Test Plant",
|
|
commonName: nil,
|
|
lightRequirement: .partialShade,
|
|
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .moderate),
|
|
temperatureRange: TemperatureRange(minimumCelsius: 10, maximumCelsius: 25)
|
|
)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.temperatureRange, 10...25)
|
|
}
|
|
|
|
// MARK: - Watering Task Generation Tests
|
|
|
|
func testExecute_WithWeeklyWatering_GeneratesWateringTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
XCTAssertFalse(wateringTasks.isEmpty)
|
|
// With 30 days and weekly watering (7-day interval), expect at least 4 tasks
|
|
XCTAssertGreaterThanOrEqual(wateringTasks.count, 4)
|
|
}
|
|
|
|
func testExecute_WithDailyWatering_GeneratesMoreFrequentTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .daily)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
// With 30 days and daily watering, expect 30 tasks
|
|
XCTAssertGreaterThanOrEqual(wateringTasks.count, 30)
|
|
}
|
|
|
|
func testExecute_WithBiweeklyWatering_GeneratesLessFrequentTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .biweekly)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
// With 30 days and biweekly watering (14-day interval), expect 2 tasks
|
|
XCTAssertGreaterThanOrEqual(wateringTasks.count, 2)
|
|
XCTAssertLessThanOrEqual(wateringTasks.count, 3)
|
|
}
|
|
|
|
func testExecute_WateringTasks_HaveCorrectNotes() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = PlantCareInfo(
|
|
scientificName: "Test Plant",
|
|
commonName: nil,
|
|
lightRequirement: .partialShade,
|
|
wateringSchedule: WateringSchedule(frequency: .weekly, amount: .thorough),
|
|
temperatureRange: TemperatureRange(minimumCelsius: 18, maximumCelsius: 27)
|
|
)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
XCTAssertTrue(wateringTasks.allSatisfy { $0.notes?.contains("thorough") ?? false })
|
|
}
|
|
|
|
func testExecute_WateringTasks_HaveCorrectPlantID() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo()
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertTrue(result.tasks.allSatisfy { $0.plantID == plant.id })
|
|
}
|
|
|
|
// MARK: - Fertilizer Task Generation Tests
|
|
|
|
func testExecute_WithFertilizerSchedule_GeneratesFertilizingTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .balanced)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
XCTAssertFalse(fertilizerTasks.isEmpty)
|
|
}
|
|
|
|
func testExecute_WithoutFertilizerSchedule_DoesNotGenerateFertilizingTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: nil)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
XCTAssertTrue(fertilizerTasks.isEmpty)
|
|
}
|
|
|
|
func testExecute_WithWeeklyFertilizer_GeneratesWeeklyFertilizerTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .organic)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
// With 30 days and weekly fertilizer (7-day interval), expect at least 4 tasks
|
|
XCTAssertGreaterThanOrEqual(fertilizerTasks.count, 4)
|
|
}
|
|
|
|
func testExecute_WithQuarterlyFertilizer_GeneratesSingleFertilizerTask() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .quarterly, type: .balanced)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
// With 30 days and quarterly fertilizer (90-day interval), expect 1 task
|
|
XCTAssertEqual(fertilizerTasks.count, 1)
|
|
}
|
|
|
|
func testExecute_FertilizerTasks_HaveCorrectNotes() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .highNitrogen)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
XCTAssertTrue(fertilizerTasks.allSatisfy { $0.notes?.contains("highNitrogen") ?? false })
|
|
}
|
|
|
|
// MARK: - User Preferences Tests
|
|
|
|
func testExecute_WithPreferredWateringHour_UsesPreferredTime() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
|
|
let preferences = CarePreferences(preferredWateringHour: 18, preferredWateringMinute: 30)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
for task in wateringTasks {
|
|
let hour = calendar.component(.hour, from: task.scheduledDate)
|
|
let minute = calendar.component(.minute, from: task.scheduledDate)
|
|
XCTAssertEqual(hour, 18)
|
|
XCTAssertEqual(minute, 30)
|
|
}
|
|
}
|
|
|
|
func testExecute_WithoutPreferences_UsesDefaultTime() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .weekly)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
for task in wateringTasks {
|
|
let hour = calendar.component(.hour, from: task.scheduledDate)
|
|
XCTAssertEqual(hour, 8) // Default is 8 AM
|
|
}
|
|
}
|
|
|
|
func testExecute_WithPreferences_AppliesTimeToFertilizerTasks() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
let preferences = CarePreferences(preferredWateringHour: 9, preferredWateringMinute: 15)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
for task in fertilizerTasks {
|
|
let hour = calendar.component(.hour, from: task.scheduledDate)
|
|
let minute = calendar.component(.minute, from: task.scheduledDate)
|
|
XCTAssertEqual(hour, 9)
|
|
XCTAssertEqual(minute, 15)
|
|
}
|
|
}
|
|
|
|
// MARK: - Task Scheduling Tests
|
|
|
|
func testExecute_TasksStartFromTomorrow() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo()
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
let today = calendar.startOfDay(for: Date())
|
|
let tomorrow = calendar.date(byAdding: .day, value: 1, to: today)!
|
|
|
|
for task in result.tasks {
|
|
let taskDay = calendar.startOfDay(for: task.scheduledDate)
|
|
XCTAssertGreaterThanOrEqual(taskDay, tomorrow)
|
|
}
|
|
}
|
|
|
|
func testExecute_TasksAreSortedByDate() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
|
|
let careInfo = createBasicCareInfo(
|
|
wateringFrequency: .twiceWeekly,
|
|
fertilizerSchedule: fertilizerSchedule
|
|
)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
for index in 0..<(result.tasks.count - 1) {
|
|
XCTAssertLessThanOrEqual(
|
|
result.tasks[index].scheduledDate,
|
|
result.tasks[index + 1].scheduledDate
|
|
)
|
|
}
|
|
}
|
|
|
|
func testExecute_TasksHaveUniqueIDs() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .weekly, type: .balanced)
|
|
let careInfo = createBasicCareInfo(
|
|
wateringFrequency: .daily,
|
|
fertilizerSchedule: fertilizerSchedule
|
|
)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let taskIDs = result.tasks.map { $0.id }
|
|
let uniqueIDs = Set(taskIDs)
|
|
XCTAssertEqual(taskIDs.count, uniqueIDs.count, "All task IDs should be unique")
|
|
}
|
|
|
|
// MARK: - Schedule Metadata Tests
|
|
|
|
func testExecute_WateringScheduleString_MatchesFrequency() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: .biweekly)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.wateringSchedule, "biweekly")
|
|
}
|
|
|
|
func testExecute_FertilizerScheduleString_WhenNoFertilizer_ReturnsNotRequired() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: nil)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.fertilizerSchedule, "Not required")
|
|
}
|
|
|
|
func testExecute_FertilizerScheduleString_WithFertilizer_ReturnsFrequency() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: .monthly, type: .organic)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
XCTAssertEqual(result.fertilizerSchedule, "monthly")
|
|
}
|
|
|
|
// MARK: - Protocol Conformance Tests
|
|
|
|
func testCreateCareScheduleUseCase_ConformsToProtocol() {
|
|
XCTAssertTrue(sut is CreateCareScheduleUseCaseProtocol)
|
|
}
|
|
|
|
// MARK: - Edge Cases
|
|
|
|
func testExecute_WithAllFertilizerFrequencies_GeneratesCorrectTaskCounts() async throws {
|
|
let frequencies: [(FertilizerFrequency, Int)] = [
|
|
(.weekly, 4), // 30 / 7 = at least 4
|
|
(.biweekly, 2), // 30 / 14 = 2
|
|
(.monthly, 1), // 30 / 30 = 1
|
|
(.quarterly, 1), // 30 / 90 = 1 (minimum 1)
|
|
(.biannually, 1) // 30 / 182 = 1 (minimum 1)
|
|
]
|
|
|
|
for (frequency, expectedMinCount) in frequencies {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let fertilizerSchedule = FertilizerSchedule(frequency: frequency, type: .balanced)
|
|
let careInfo = createBasicCareInfo(fertilizerSchedule: fertilizerSchedule)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let fertilizerTasks = result.tasks.filter { $0.type == .fertilizing }
|
|
XCTAssertGreaterThanOrEqual(
|
|
fertilizerTasks.count,
|
|
expectedMinCount,
|
|
"Expected at least \(expectedMinCount) tasks for \(frequency.rawValue) frequency"
|
|
)
|
|
}
|
|
}
|
|
|
|
func testExecute_WithAllWateringFrequencies_GeneratesCorrectTaskCounts() async throws {
|
|
let frequencies: [(WateringFrequency, Int)] = [
|
|
(.daily, 30), // 30 / 1 = 30
|
|
(.everyOtherDay, 15), // 30 / 2 = 15
|
|
(.twiceWeekly, 10), // 30 / 3 = 10
|
|
(.weekly, 4), // 30 / 7 = 4
|
|
(.biweekly, 2), // 30 / 14 = 2
|
|
(.monthly, 1) // 30 / 30 = 1
|
|
]
|
|
|
|
for (frequency, expectedMinCount) in frequencies {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo(wateringFrequency: frequency)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: nil)
|
|
|
|
// Then
|
|
let wateringTasks = result.tasks.filter { $0.type == .watering }
|
|
XCTAssertGreaterThanOrEqual(
|
|
wateringTasks.count,
|
|
expectedMinCount,
|
|
"Expected at least \(expectedMinCount) tasks for \(frequency.rawValue) frequency"
|
|
)
|
|
}
|
|
}
|
|
|
|
func testExecute_WithMidnightPreferredTime_GeneratesTasksAtMidnight() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo()
|
|
let preferences = CarePreferences(preferredWateringHour: 0, preferredWateringMinute: 0)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
for task in result.tasks {
|
|
let hour = calendar.component(.hour, from: task.scheduledDate)
|
|
let minute = calendar.component(.minute, from: task.scheduledDate)
|
|
XCTAssertEqual(hour, 0)
|
|
XCTAssertEqual(minute, 0)
|
|
}
|
|
}
|
|
|
|
func testExecute_WithLateNightPreferredTime_GeneratesTasksAtLateNight() async throws {
|
|
// Given
|
|
let plant = Plant.mock()
|
|
let careInfo = createBasicCareInfo()
|
|
let preferences = CarePreferences(preferredWateringHour: 23, preferredWateringMinute: 59)
|
|
|
|
// When
|
|
let result = try await sut.execute(for: plant, careInfo: careInfo, preferences: preferences)
|
|
|
|
// Then
|
|
let calendar = Calendar.current
|
|
for task in result.tasks {
|
|
let hour = calendar.component(.hour, from: task.scheduledDate)
|
|
let minute = calendar.component(.minute, from: task.scheduledDate)
|
|
XCTAssertEqual(hour, 23)
|
|
XCTAssertEqual(minute, 59)
|
|
}
|
|
}
|
|
}
|