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:
622
PlantGuideTests/CollectionViewModelTests.swift
Normal file
622
PlantGuideTests/CollectionViewModelTests.swift
Normal file
@@ -0,0 +1,622 @@
|
||||
//
|
||||
// CollectionViewModelTests.swift
|
||||
// PlantGuideTests
|
||||
//
|
||||
// Unit tests for CollectionViewModel - the view model managing plant collection display,
|
||||
// filtering, search, and user interactions like favoriting and deleting plants.
|
||||
//
|
||||
|
||||
import XCTest
|
||||
@testable import PlantGuide
|
||||
|
||||
// MARK: - Mock Protocols
|
||||
|
||||
/// Mock implementation of FetchCollectionUseCaseProtocol for testing
|
||||
final class MockFetchCollectionUseCase: FetchCollectionUseCaseProtocol, @unchecked Sendable {
|
||||
|
||||
var plantsToReturn: [Plant] = []
|
||||
var statisticsToReturn: CollectionStatistics?
|
||||
var errorToThrow: Error?
|
||||
var executeCallCount = 0
|
||||
var executeWithFilterCallCount = 0
|
||||
var fetchStatisticsCallCount = 0
|
||||
var lastFilter: PlantFilter?
|
||||
|
||||
func execute() async throws -> [Plant] {
|
||||
executeCallCount += 1
|
||||
if let error = errorToThrow {
|
||||
throw error
|
||||
}
|
||||
return plantsToReturn
|
||||
}
|
||||
|
||||
func execute(filter: PlantFilter) async throws -> [Plant] {
|
||||
executeWithFilterCallCount += 1
|
||||
lastFilter = filter
|
||||
if let error = errorToThrow {
|
||||
throw error
|
||||
}
|
||||
return plantsToReturn
|
||||
}
|
||||
|
||||
func fetchStatistics() async throws -> CollectionStatistics {
|
||||
fetchStatisticsCallCount += 1
|
||||
if let error = errorToThrow {
|
||||
throw error
|
||||
}
|
||||
return statisticsToReturn ?? CollectionStatistics(
|
||||
totalPlants: plantsToReturn.count,
|
||||
favoriteCount: plantsToReturn.filter { $0.isFavorite }.count,
|
||||
familyDistribution: [:],
|
||||
identificationSourceBreakdown: [:],
|
||||
plantsAddedThisMonth: 0,
|
||||
upcomingTasksCount: 0,
|
||||
overdueTasksCount: 0
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock implementation of ToggleFavoriteUseCaseProtocol for testing
|
||||
final class MockToggleFavoriteUseCase: ToggleFavoriteUseCaseProtocol, @unchecked Sendable {
|
||||
|
||||
var resultToReturn: Bool = true
|
||||
var errorToThrow: Error?
|
||||
var executeCallCount = 0
|
||||
var lastPlantID: UUID?
|
||||
|
||||
func execute(plantID: UUID) async throws -> Bool {
|
||||
executeCallCount += 1
|
||||
lastPlantID = plantID
|
||||
if let error = errorToThrow {
|
||||
throw error
|
||||
}
|
||||
return resultToReturn
|
||||
}
|
||||
}
|
||||
|
||||
/// Mock implementation of DeletePlantUseCaseProtocol for testing
|
||||
final class MockDeletePlantUseCase: DeletePlantUseCaseProtocol, @unchecked Sendable {
|
||||
|
||||
var errorToThrow: Error?
|
||||
var executeCallCount = 0
|
||||
var lastPlantID: UUID?
|
||||
|
||||
func execute(plantID: UUID) async throws {
|
||||
executeCallCount += 1
|
||||
lastPlantID = plantID
|
||||
if let error = errorToThrow {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - CollectionViewModelTests
|
||||
|
||||
@MainActor
|
||||
final class CollectionViewModelTests: XCTestCase {
|
||||
|
||||
// MARK: - Properties
|
||||
|
||||
private var sut: CollectionViewModel!
|
||||
private var mockFetchUseCase: MockFetchCollectionUseCase!
|
||||
private var mockToggleFavoriteUseCase: MockToggleFavoriteUseCase!
|
||||
private var mockDeleteUseCase: MockDeletePlantUseCase!
|
||||
|
||||
// MARK: - Test Lifecycle
|
||||
|
||||
override func setUp() {
|
||||
super.setUp()
|
||||
mockFetchUseCase = MockFetchCollectionUseCase()
|
||||
mockToggleFavoriteUseCase = MockToggleFavoriteUseCase()
|
||||
mockDeleteUseCase = MockDeletePlantUseCase()
|
||||
|
||||
sut = CollectionViewModel(
|
||||
fetchCollectionUseCase: mockFetchUseCase,
|
||||
toggleFavoriteUseCase: mockToggleFavoriteUseCase,
|
||||
deletePlantUseCase: mockDeleteUseCase
|
||||
)
|
||||
}
|
||||
|
||||
override func tearDown() {
|
||||
sut = nil
|
||||
mockFetchUseCase = nil
|
||||
mockToggleFavoriteUseCase = nil
|
||||
mockDeleteUseCase = nil
|
||||
super.tearDown()
|
||||
}
|
||||
|
||||
// MARK: - Test Helpers
|
||||
|
||||
private func createTestPlant(
|
||||
id: UUID = UUID(),
|
||||
scientificName: String = "Monstera deliciosa",
|
||||
commonNames: [String] = ["Swiss Cheese Plant"],
|
||||
family: String = "Araceae",
|
||||
isFavorite: Bool = false
|
||||
) -> Plant {
|
||||
Plant(
|
||||
id: id,
|
||||
scientificName: scientificName,
|
||||
commonNames: commonNames,
|
||||
family: family,
|
||||
genus: "Monstera",
|
||||
identificationSource: .onDeviceML,
|
||||
isFavorite: isFavorite
|
||||
)
|
||||
}
|
||||
|
||||
// MARK: - loadPlants Tests
|
||||
|
||||
func testLoadPlants_WhenSuccessful_LoadsAndFiltersPlantsCorrectly() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"]),
|
||||
createTestPlant(scientificName: "Sansevieria trifasciata", commonNames: ["Snake Plant"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
|
||||
// When
|
||||
await sut.loadPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 3)
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
XCTAssertNil(sut.error)
|
||||
XCTAssertEqual(mockFetchUseCase.executeWithFilterCallCount, 1)
|
||||
}
|
||||
|
||||
func testLoadPlants_WhenFailed_SetsErrorState() async {
|
||||
// Given
|
||||
mockFetchUseCase.errorToThrow = FetchCollectionError.repositoryFetchFailed(NSError(domain: "test", code: -1))
|
||||
|
||||
// When
|
||||
await sut.loadPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(sut.plants.isEmpty)
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
XCTAssertNotNil(sut.error)
|
||||
}
|
||||
|
||||
func testLoadPlants_WhenAlreadyLoading_DoesNotStartAnotherLoad() async {
|
||||
// Given
|
||||
mockFetchUseCase.plantsToReturn = [createTestPlant()]
|
||||
|
||||
// When - Start first load
|
||||
let loadTask = Task {
|
||||
await sut.loadPlants()
|
||||
}
|
||||
|
||||
// Immediately start another load
|
||||
await sut.loadPlants()
|
||||
await loadTask.value
|
||||
|
||||
// Then - Should only have called execute once because second call was ignored
|
||||
XCTAssertEqual(mockFetchUseCase.executeWithFilterCallCount, 1)
|
||||
}
|
||||
|
||||
func testLoadPlants_AppliesCurrentFilter() async {
|
||||
// Given
|
||||
let customFilter = PlantFilter(isFavorite: true)
|
||||
sut.currentFilter = customFilter
|
||||
mockFetchUseCase.plantsToReturn = [createTestPlant(isFavorite: true)]
|
||||
|
||||
// When
|
||||
await sut.loadPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(mockFetchUseCase.lastFilter?.isFavorite, true)
|
||||
}
|
||||
|
||||
// MARK: - refreshPlants Tests
|
||||
|
||||
func testRefreshPlants_WhenSuccessful_RefreshesCollection() async {
|
||||
// Given
|
||||
let initialPlants = [createTestPlant(scientificName: "Plant A")]
|
||||
let refreshedPlants = [
|
||||
createTestPlant(scientificName: "Plant A"),
|
||||
createTestPlant(scientificName: "Plant B")
|
||||
]
|
||||
|
||||
mockFetchUseCase.plantsToReturn = initialPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
mockFetchUseCase.plantsToReturn = refreshedPlants
|
||||
|
||||
// When
|
||||
await sut.refreshPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 2)
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
}
|
||||
|
||||
func testRefreshPlants_WhenFailed_SetsErrorButKeepsExistingData() async {
|
||||
// Given
|
||||
let initialPlants = [createTestPlant()]
|
||||
mockFetchUseCase.plantsToReturn = initialPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
mockFetchUseCase.errorToThrow = FetchCollectionError.repositoryFetchFailed(NSError(domain: "test", code: -1))
|
||||
|
||||
// When
|
||||
await sut.refreshPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertNotNil(sut.error)
|
||||
XCTAssertFalse(sut.isLoading)
|
||||
}
|
||||
|
||||
// MARK: - toggleFavorite Tests
|
||||
|
||||
func testToggleFavorite_WhenSuccessful_UpdatesPlantFavoriteStatusOptimistically() async {
|
||||
// Given
|
||||
let plantID = UUID()
|
||||
let testPlant = createTestPlant(id: plantID, isFavorite: false)
|
||||
mockFetchUseCase.plantsToReturn = [testPlant]
|
||||
await sut.loadPlants()
|
||||
|
||||
mockToggleFavoriteUseCase.resultToReturn = true
|
||||
|
||||
// When
|
||||
await sut.toggleFavorite(plantID: plantID)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(mockToggleFavoriteUseCase.executeCallCount, 1)
|
||||
XCTAssertEqual(mockToggleFavoriteUseCase.lastPlantID, plantID)
|
||||
// After successful toggle, the plant should be marked as favorite
|
||||
if let updatedPlant = sut.plants.first(where: { $0.id == plantID }) {
|
||||
XCTAssertTrue(updatedPlant.isFavorite)
|
||||
}
|
||||
}
|
||||
|
||||
func testToggleFavorite_WhenFailed_RollsBackOptimisticUpdate() async {
|
||||
// Given
|
||||
let plantID = UUID()
|
||||
let testPlant = createTestPlant(id: plantID, isFavorite: false)
|
||||
mockFetchUseCase.plantsToReturn = [testPlant]
|
||||
await sut.loadPlants()
|
||||
|
||||
mockToggleFavoriteUseCase.errorToThrow = ToggleFavoriteError.updateFailed(NSError(domain: "test", code: -1))
|
||||
|
||||
// When
|
||||
await sut.toggleFavorite(plantID: plantID)
|
||||
|
||||
// Then
|
||||
XCTAssertNotNil(sut.error)
|
||||
// After failed toggle, the plant should be rolled back to original state (not favorite)
|
||||
if let plant = sut.plants.first(where: { $0.id == plantID }) {
|
||||
XCTAssertFalse(plant.isFavorite)
|
||||
}
|
||||
}
|
||||
|
||||
func testToggleFavorite_WhenPlantNotFound_DoesNotCallUseCase() async {
|
||||
// Given
|
||||
mockFetchUseCase.plantsToReturn = []
|
||||
await sut.loadPlants()
|
||||
|
||||
let nonExistentPlantID = UUID()
|
||||
|
||||
// When
|
||||
await sut.toggleFavorite(plantID: nonExistentPlantID)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(mockToggleFavoriteUseCase.executeCallCount, 1)
|
||||
}
|
||||
|
||||
// MARK: - deletePlant Tests
|
||||
|
||||
func testDeletePlant_WhenSuccessful_RemovesPlantWithOptimisticUpdate() async {
|
||||
// Given
|
||||
let plantID = UUID()
|
||||
let testPlants = [
|
||||
createTestPlant(id: plantID, scientificName: "Plant to Delete"),
|
||||
createTestPlant(scientificName: "Plant to Keep")
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
XCTAssertEqual(sut.plants.count, 2)
|
||||
|
||||
// When
|
||||
await sut.deletePlant(plantID: plantID)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(mockDeleteUseCase.executeCallCount, 1)
|
||||
XCTAssertEqual(mockDeleteUseCase.lastPlantID, plantID)
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertFalse(sut.plants.contains(where: { $0.id == plantID }))
|
||||
}
|
||||
|
||||
func testDeletePlant_WhenFailed_RollsBackAndAddsPlantBack() async {
|
||||
// Given
|
||||
let plantID = UUID()
|
||||
let plantToDelete = createTestPlant(id: plantID, scientificName: "Plant to Delete")
|
||||
mockFetchUseCase.plantsToReturn = [plantToDelete]
|
||||
await sut.loadPlants()
|
||||
|
||||
mockDeleteUseCase.errorToThrow = DeletePlantError.repositoryDeleteFailed(NSError(domain: "test", code: -1))
|
||||
|
||||
// When
|
||||
await sut.deletePlant(plantID: plantID)
|
||||
|
||||
// Then
|
||||
XCTAssertNotNil(sut.error)
|
||||
// Plant should be restored after failed deletion
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertTrue(sut.plants.contains(where: { $0.id == plantID }))
|
||||
}
|
||||
|
||||
// MARK: - Search Debouncing Tests
|
||||
|
||||
func testSearchText_WhenSetToEmptyString_AppliesFilterImmediately() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
sut.searchText = "Monstera"
|
||||
|
||||
// Wait for debounce
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// When
|
||||
sut.searchText = ""
|
||||
|
||||
// Then - Filter should be applied immediately for empty string
|
||||
XCTAssertEqual(sut.plants.count, 2)
|
||||
}
|
||||
|
||||
func testSearchText_WhenSet_DebouncesAndFiltersPlants() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"]),
|
||||
createTestPlant(scientificName: "Sansevieria", commonNames: ["Snake Plant"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When
|
||||
sut.searchText = "Monstera"
|
||||
|
||||
// Wait for debounce (300ms + some buffer)
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.scientificName, "Monstera deliciosa")
|
||||
}
|
||||
|
||||
func testSearchText_WhenTypingQuickly_CancelsIntermediateSearches() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When - Simulate rapid typing
|
||||
sut.searchText = "M"
|
||||
sut.searchText = "Mo"
|
||||
sut.searchText = "Mon"
|
||||
sut.searchText = "Ficus"
|
||||
|
||||
// Wait for final debounce
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then - Only final search should be applied
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.scientificName, "Ficus lyrata")
|
||||
}
|
||||
|
||||
func testSearchText_FiltersOnScientificName() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When
|
||||
sut.searchText = "lyrata"
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.scientificName, "Ficus lyrata")
|
||||
}
|
||||
|
||||
func testSearchText_FiltersOnCommonNames() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When
|
||||
sut.searchText = "Fiddle"
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.scientificName, "Ficus lyrata")
|
||||
}
|
||||
|
||||
func testSearchText_FiltersOnFamily() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", family: "Araceae"),
|
||||
createTestPlant(scientificName: "Ficus lyrata", family: "Moraceae")
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When
|
||||
sut.searchText = "Moraceae"
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.family, "Moraceae")
|
||||
}
|
||||
|
||||
func testSearchText_IsCaseInsensitive() async {
|
||||
// Given
|
||||
let testPlants = [
|
||||
createTestPlant(scientificName: "Monstera deliciosa", commonNames: ["Swiss Cheese Plant"]),
|
||||
createTestPlant(scientificName: "Ficus lyrata", commonNames: ["Fiddle Leaf Fig"])
|
||||
]
|
||||
mockFetchUseCase.plantsToReturn = testPlants
|
||||
await sut.loadPlants()
|
||||
|
||||
// When
|
||||
sut.searchText = "MONSTERA"
|
||||
try? await Task.sleep(nanoseconds: 400_000_000)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
XCTAssertEqual(sut.plants.first?.scientificName, "Monstera deliciosa")
|
||||
}
|
||||
|
||||
// MARK: - applyFilter Tests
|
||||
|
||||
func testApplyFilter_AppliesNewFilterAndReloadsCollection() async {
|
||||
// Given
|
||||
let favoritePlants = [createTestPlant(isFavorite: true)]
|
||||
mockFetchUseCase.plantsToReturn = favoritePlants
|
||||
|
||||
let newFilter = PlantFilter(isFavorite: true)
|
||||
|
||||
// When
|
||||
await sut.applyFilter(newFilter)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.currentFilter.isFavorite, true)
|
||||
XCTAssertEqual(mockFetchUseCase.lastFilter?.isFavorite, true)
|
||||
XCTAssertEqual(sut.plants.count, 1)
|
||||
}
|
||||
|
||||
func testApplyFilter_SavesFilterToPreferences() async {
|
||||
// Given
|
||||
let newFilter = PlantFilter(sortBy: .name, sortAscending: true)
|
||||
mockFetchUseCase.plantsToReturn = []
|
||||
|
||||
// When
|
||||
await sut.applyFilter(newFilter)
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.currentFilter.sortBy, .name)
|
||||
XCTAssertEqual(sut.currentFilter.sortAscending, true)
|
||||
}
|
||||
|
||||
// MARK: - toggleViewMode Tests
|
||||
|
||||
func testToggleViewMode_SwitchesFromGridToList() {
|
||||
// Given
|
||||
sut.viewMode = .grid
|
||||
|
||||
// When
|
||||
sut.toggleViewMode()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.viewMode, .list)
|
||||
}
|
||||
|
||||
func testToggleViewMode_SwitchesFromListToGrid() {
|
||||
// Given
|
||||
sut.viewMode = .list
|
||||
|
||||
// When
|
||||
sut.toggleViewMode()
|
||||
|
||||
// Then
|
||||
XCTAssertEqual(sut.viewMode, .grid)
|
||||
}
|
||||
|
||||
func testToggleViewMode_PersistsViewModePreference() {
|
||||
// Given
|
||||
sut.viewMode = .grid
|
||||
|
||||
// When
|
||||
sut.toggleViewMode()
|
||||
|
||||
// Then - The viewMode property setter should save to preferences
|
||||
XCTAssertEqual(sut.viewMode, .list)
|
||||
}
|
||||
|
||||
// MARK: - Computed Properties Tests
|
||||
|
||||
func testIsEmpty_WhenNoPlantsAndNotLoading_ReturnsTrue() async {
|
||||
// Given
|
||||
mockFetchUseCase.plantsToReturn = []
|
||||
await sut.loadPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(sut.isEmpty)
|
||||
}
|
||||
|
||||
func testIsEmpty_WhenHasPlants_ReturnsFalse() async {
|
||||
// Given
|
||||
mockFetchUseCase.plantsToReturn = [createTestPlant()]
|
||||
await sut.loadPlants()
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(sut.isEmpty)
|
||||
}
|
||||
|
||||
func testHasActiveSearch_WhenSearchTextIsEmpty_ReturnsFalse() {
|
||||
// Given
|
||||
sut.searchText = ""
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(sut.hasActiveSearch)
|
||||
}
|
||||
|
||||
func testHasActiveSearch_WhenSearchTextIsNotEmpty_ReturnsTrue() {
|
||||
// Given
|
||||
sut.searchText = "Monstera"
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(sut.hasActiveSearch)
|
||||
}
|
||||
|
||||
func testHasActiveFilters_WhenDefaultFilter_ReturnsFalse() {
|
||||
// Given
|
||||
sut.currentFilter = .default
|
||||
|
||||
// Then
|
||||
XCTAssertFalse(sut.hasActiveFilters)
|
||||
}
|
||||
|
||||
func testHasActiveFilters_WhenCustomFilter_ReturnsTrue() {
|
||||
// Given
|
||||
sut.currentFilter = PlantFilter(isFavorite: true)
|
||||
|
||||
// Then
|
||||
XCTAssertTrue(sut.hasActiveFilters)
|
||||
}
|
||||
|
||||
// MARK: - clearError Tests
|
||||
|
||||
func testClearError_RemovesErrorState() async {
|
||||
// Given
|
||||
mockFetchUseCase.errorToThrow = FetchCollectionError.repositoryFetchFailed(NSError(domain: "test", code: -1))
|
||||
await sut.loadPlants()
|
||||
XCTAssertNotNil(sut.error)
|
||||
|
||||
// When
|
||||
sut.clearError()
|
||||
|
||||
// Then
|
||||
XCTAssertNil(sut.error)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user