- 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>
623 lines
19 KiB
Swift
623 lines
19 KiB
Swift
//
|
|
// 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)
|
|
}
|
|
}
|