Files
PlantGuide/PlantGuideTests/CreateCareScheduleUseCaseTests.swift
Trey t f41c77876a Add batch actions for multi-task completion (Phase 7)
Implement batch task completion feature allowing users to select and
complete multiple care tasks at once. Adds edit mode to Today View with
selection checkmarks, floating BatchActionBar, and confirmation dialog
for completing more than 3 tasks.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-23 15:19:21 -06:00

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") })
}
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") })
}
// 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)
}
}
}