Replace brittle localized-string selectors and broken wait helpers with a robust, identifier-first UI test infrastructure. All 41 UI tests pass on iOS 26.2 simulator (iPhone 17). Foundation: - BaseUITestCase with deterministic launch helpers (launchClean, launchOffline) - WaitHelpers (waitUntilHittable, waitUntilGone, tapWhenReady) replacing sleep() - UITestID enum mirroring AccessibilityIdentifiers from the app target - Screen objects: TabBarScreen, CameraScreen, CollectionScreen, TodayScreen, SettingsScreen, PlantDetailScreen Key fixes: - Tab navigation uses waitForExistence+tap instead of isHittable (unreliable in iOS 26 simulator) - Tests handle real app state (empty collection, no camera permission) - Increased timeouts for parallel clone execution - Added NetworkMonitorProtocol and protocol-typed DI for testability - Fixed actor-isolation issues in unit test mocks Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
200 lines
6.2 KiB
Swift
200 lines
6.2 KiB
Swift
//
|
|
// MockNotificationService.swift
|
|
// PlantGuideTests
|
|
//
|
|
// Mock implementation of NotificationServiceProtocol for unit testing.
|
|
// Provides configurable behavior and call tracking for verification.
|
|
//
|
|
|
|
import Foundation
|
|
import UserNotifications
|
|
@testable import PlantGuide
|
|
|
|
// MARK: - MockNotificationService
|
|
|
|
/// Mock implementation of NotificationServiceProtocol for testing
|
|
final actor MockNotificationService: NotificationServiceProtocol {
|
|
|
|
// MARK: - Storage
|
|
|
|
private var scheduledReminders: [UUID: (task: CareTask, plantName: String, plantID: UUID)] = [:]
|
|
private var pendingNotifications: [UNNotificationRequest] = []
|
|
|
|
// MARK: - Call Tracking
|
|
|
|
private(set) var requestAuthorizationCallCount = 0
|
|
private(set) var scheduleReminderCallCount = 0
|
|
private(set) var cancelReminderCallCount = 0
|
|
private(set) var cancelAllRemindersCallCount = 0
|
|
private(set) var cancelRemindersForTypeCallCount = 0
|
|
private(set) var updateBadgeCountCallCount = 0
|
|
private(set) var getPendingNotificationsCallCount = 0
|
|
private(set) var removeAllDeliveredNotificationsCallCount = 0
|
|
|
|
// MARK: - Error Configuration
|
|
|
|
var shouldThrowOnRequestAuthorization = false
|
|
var shouldThrowOnScheduleReminder = false
|
|
|
|
var errorToThrow: Error = NotificationError.schedulingFailed(
|
|
NSError(domain: "MockError", code: -1, userInfo: [NSLocalizedDescriptionKey: "Mock notification error"])
|
|
)
|
|
|
|
// MARK: - Return Value Configuration
|
|
|
|
var authorizationGranted = true
|
|
|
|
// MARK: - Captured Values
|
|
|
|
private(set) var lastScheduledTask: CareTask?
|
|
private(set) var lastScheduledPlantName: String?
|
|
private(set) var lastScheduledPlantID: UUID?
|
|
private(set) var lastCancelledTaskID: UUID?
|
|
private(set) var lastCancelledAllPlantID: UUID?
|
|
private(set) var lastCancelledTaskType: CareTaskType?
|
|
private(set) var lastCancelledTaskTypePlantID: UUID?
|
|
private(set) var lastBadgeCount: Int?
|
|
|
|
// MARK: - NotificationServiceProtocol
|
|
|
|
func requestAuthorization() async throws -> Bool {
|
|
requestAuthorizationCallCount += 1
|
|
if shouldThrowOnRequestAuthorization {
|
|
throw errorToThrow
|
|
}
|
|
if !authorizationGranted {
|
|
throw NotificationError.permissionDenied
|
|
}
|
|
return authorizationGranted
|
|
}
|
|
|
|
func scheduleReminder(for task: CareTask, plantName: String, plantID: UUID) async throws {
|
|
scheduleReminderCallCount += 1
|
|
lastScheduledTask = task
|
|
lastScheduledPlantName = plantName
|
|
lastScheduledPlantID = plantID
|
|
|
|
if shouldThrowOnScheduleReminder {
|
|
throw errorToThrow
|
|
}
|
|
|
|
// Validate that the scheduled date is in the future
|
|
guard task.scheduledDate > Date() else {
|
|
throw NotificationError.invalidTriggerDate
|
|
}
|
|
|
|
scheduledReminders[task.id] = (task, plantName, plantID)
|
|
}
|
|
|
|
func cancelReminder(for taskID: UUID) async {
|
|
cancelReminderCallCount += 1
|
|
lastCancelledTaskID = taskID
|
|
scheduledReminders.removeValue(forKey: taskID)
|
|
}
|
|
|
|
func cancelAllReminders(for plantID: UUID) async {
|
|
cancelAllRemindersCallCount += 1
|
|
lastCancelledAllPlantID = plantID
|
|
|
|
// Remove all reminders for this plant
|
|
let keysToRemove = scheduledReminders.filter { $0.value.plantID == plantID }.map { $0.key }
|
|
for key in keysToRemove {
|
|
scheduledReminders.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
func cancelReminders(for taskType: CareTaskType, plantID: UUID) async {
|
|
cancelRemindersForTypeCallCount += 1
|
|
lastCancelledTaskType = taskType
|
|
lastCancelledTaskTypePlantID = plantID
|
|
|
|
// Remove all reminders matching this task type and plant
|
|
let keysToRemove = scheduledReminders.filter {
|
|
$0.value.plantID == plantID && $0.value.task.type == taskType
|
|
}.map { $0.key }
|
|
for key in keysToRemove {
|
|
scheduledReminders.removeValue(forKey: key)
|
|
}
|
|
}
|
|
|
|
func updateBadgeCount(_ count: Int) async {
|
|
updateBadgeCountCallCount += 1
|
|
lastBadgeCount = count
|
|
}
|
|
|
|
func getPendingNotifications() async -> [UNNotificationRequest] {
|
|
getPendingNotificationsCallCount += 1
|
|
return pendingNotifications
|
|
}
|
|
|
|
func removeAllDeliveredNotifications() async {
|
|
removeAllDeliveredNotificationsCallCount += 1
|
|
}
|
|
|
|
func schedulePhotoReminder(for plantID: UUID, plantName: String, interval: PhotoReminderInterval) async throws {
|
|
// no-op for tests
|
|
}
|
|
|
|
func cancelPhotoReminder(for plantID: UUID) async {
|
|
// no-op for tests
|
|
}
|
|
|
|
// MARK: - Helper Methods
|
|
|
|
/// Resets all state for clean test setup
|
|
func reset() {
|
|
scheduledReminders = [:]
|
|
pendingNotifications = []
|
|
|
|
requestAuthorizationCallCount = 0
|
|
scheduleReminderCallCount = 0
|
|
cancelReminderCallCount = 0
|
|
cancelAllRemindersCallCount = 0
|
|
cancelRemindersForTypeCallCount = 0
|
|
updateBadgeCountCallCount = 0
|
|
getPendingNotificationsCallCount = 0
|
|
removeAllDeliveredNotificationsCallCount = 0
|
|
|
|
shouldThrowOnRequestAuthorization = false
|
|
shouldThrowOnScheduleReminder = false
|
|
|
|
authorizationGranted = true
|
|
|
|
lastScheduledTask = nil
|
|
lastScheduledPlantName = nil
|
|
lastScheduledPlantID = nil
|
|
lastCancelledTaskID = nil
|
|
lastCancelledAllPlantID = nil
|
|
lastCancelledTaskType = nil
|
|
lastCancelledTaskTypePlantID = nil
|
|
lastBadgeCount = nil
|
|
}
|
|
|
|
/// Gets the count of scheduled reminders
|
|
var scheduledReminderCount: Int {
|
|
scheduledReminders.count
|
|
}
|
|
|
|
/// Gets scheduled reminders for a specific plant
|
|
func reminders(for plantID: UUID) -> [CareTask] {
|
|
scheduledReminders.values
|
|
.filter { $0.plantID == plantID }
|
|
.map { $0.task }
|
|
}
|
|
|
|
/// Checks if a reminder is scheduled for a task
|
|
func hasReminder(for taskID: UUID) -> Bool {
|
|
scheduledReminders[taskID] != nil
|
|
}
|
|
|
|
/// Gets all scheduled task IDs
|
|
var scheduledTaskIDs: [UUID] {
|
|
Array(scheduledReminders.keys)
|
|
}
|
|
|
|
/// Adds a pending notification for testing getPendingNotifications
|
|
func addPendingNotification(_ request: UNNotificationRequest) {
|
|
pendingNotifications.append(request)
|
|
}
|
|
}
|