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