diff --git a/.agent-os/specs/2025-07-22-phase1-completion/tasks.md b/.agent-os/specs/2025-07-22-phase1-completion/tasks.md index 283b96b..76faba1 100644 --- a/.agent-os/specs/2025-07-22-phase1-completion/tasks.md +++ b/.agent-os/specs/2025-07-22-phase1-completion/tasks.md @@ -7,50 +7,60 @@ These are the tasks to be completed for the spec detailed in @.agent-os/specs/20 ## Tasks -- [ ] 1. **Implement Pause/Resume UI Controls** - - [ ] 1.1 Write tests for pause/resume button components and state management - - [ ] 1.2 Add pause/resume buttons to main automation control panel in ClickItViewModel - - [ ] 1.3 Integrate pause/resume functionality with ElapsedTimeManager and ClickCoordinator - - [ ] 1.4 Update visual feedback overlay to reflect pause/resume states - - [ ] 1.5 Ensure session statistics preservation during pause state - - [ ] 1.6 Verify all tests pass and UI responds correctly - -- [ ] 2. **Build Enhanced Preset Management System** - - [ ] 2.1 Write tests for PresetManager and PresetConfiguration data structures - - [ ] 2.2 Create PresetManager class with UserDefaults integration for save/load functionality - - [ ] 2.3 Design and implement preset management UI components (save, load, delete, custom naming) - - [ ] 2.4 Add preset validation logic to ensure saved configurations are valid - - [ ] 2.5 Integrate preset system with ClickItViewModel and all configuration properties - - [ ] 2.6 Implement preset selection dropdown and management interface - - [ ] 2.7 Add preset export/import capability for backup and sharing - - [ ] 2.8 Verify all tests pass and preset system works end-to-end - -- [ ] 3. **Develop Comprehensive Error Recovery System** - - [ ] 3.1 Write tests for ErrorRecoveryManager and error detection mechanisms - - [ ] 3.2 Create ErrorRecoveryManager to monitor system state and handle failures - - [ ] 3.3 Implement automatic retry logic for click failures and permission issues - - [ ] 3.4 Add error notification system with clear user feedback and recovery status - - [ ] 3.5 Integrate error recovery hooks into ClickCoordinator and automation loops - - [ ] 3.6 Implement graceful degradation strategies when recovery fails - - [ ] 3.7 Add system health monitoring for permissions and resource availability - - [ ] 3.8 Verify all tests pass and error recovery works under failure conditions - -- [ ] 4. **Optimize Performance for Sub-10ms Timing** - - [ ] 4.1 Write performance benchmark tests for timing accuracy and resource usage - - [ ] 4.2 Implement HighPrecisionTimer system with optimized timing loops - - [ ] 4.3 Profile and optimize memory usage to meet <50MB RAM target - - [ ] 4.4 Optimize CPU usage to achieve <5% idle target with efficient background processing - - [ ] 4.5 Add real-time performance monitoring and metrics collection - - [ ] 4.6 Implement automated performance validation and regression testing - - [ ] 4.7 Create performance dashboard for user visibility into timing accuracy - - [ ] 4.8 Verify all performance targets met and benchmarks pass consistently - -- [ ] 5. **Implement Advanced CPS Randomization** - - [ ] 5.1 Write tests for CPSRandomizer and timing pattern generation - - [ ] 5.2 Create CPSRandomizer with configurable variance and distribution patterns - - [ ] 5.3 Add UI controls for randomization settings and pattern selection - - [ ] 5.4 Implement statistical distributions (normal, uniform) for natural timing variation - - [ ] 5.5 Integrate randomization with AutomationConfiguration and clicking loops - - [ ] 5.6 Add validation to ensure randomization doesn't break timing requirements - - [ ] 5.7 Implement anti-detection patterns to avoid automation signature detection - - [ ] 5.8 Verify all tests pass and randomization produces human-like patterns \ No newline at end of file +- [x] 1. **Implement Pause/Resume UI Controls** + - [x] 1.1 Write tests for pause/resume button components and state management + - [x] 1.2 Add pause/resume buttons to main automation control panel in ClickItViewModel + - [x] 1.3 Integrate pause/resume functionality with ElapsedTimeManager and ClickCoordinator + - [x] 1.4 Update visual feedback overlay to reflect pause/resume states + - [x] 1.5 Ensure session statistics preservation during pause state + - [x] 1.6 Verify all tests pass and UI responds correctly + +- [ ] 2. **🚨 EMERGENCY: Enhance Emergency Stop System** (HIGH PRIORITY) + - [ ] 2.1 Write tests for enhanced emergency stop functionality + - [ ] 2.2 Implement multiple emergency stop key options (ESC, F1, Cmd+Period, Space) + - [ ] 2.3 Add configurable emergency stop key selection in settings + - [ ] 2.4 Implement immediate stop with <50ms response time guarantee + - [ ] 2.5 Add visual confirmation of emergency stop activation + - [ ] 2.6 Ensure emergency stop works even when app is in background + - [ ] 2.7 Add emergency stop status to automation panel and overlay + - [ ] 2.8 Verify emergency stop reliability across all automation states + +- [ ] 3. **Build Enhanced Preset Management System** + - [ ] 3.1 Write tests for PresetManager and PresetConfiguration data structures + - [ ] 3.2 Create PresetManager class with UserDefaults integration for save/load functionality + - [ ] 3.3 Design and implement preset management UI components (save, load, delete, custom naming) + - [ ] 3.4 Add preset validation logic to ensure saved configurations are valid + - [ ] 3.5 Integrate preset system with ClickItViewModel and all configuration properties + - [ ] 3.6 Implement preset selection dropdown and management interface + - [ ] 3.7 Add preset export/import capability for backup and sharing + - [ ] 3.8 Verify all tests pass and preset system works end-to-end + +- [ ] 4. **Develop Comprehensive Error Recovery System** + - [ ] 4.1 Write tests for ErrorRecoveryManager and error detection mechanisms + - [ ] 4.2 Create ErrorRecoveryManager to monitor system state and handle failures + - [ ] 4.3 Implement automatic retry logic for click failures and permission issues + - [ ] 4.4 Add error notification system with clear user feedback and recovery status + - [ ] 4.5 Integrate error recovery hooks into ClickCoordinator and automation loops + - [ ] 4.6 Implement graceful degradation strategies when recovery fails + - [ ] 4.7 Add system health monitoring for permissions and resource availability + - [ ] 4.8 Verify all tests pass and error recovery works under failure conditions + +- [ ] 5. **Optimize Performance for Sub-10ms Timing** + - [ ] 5.1 Write performance benchmark tests for timing accuracy and resource usage + - [ ] 5.2 Implement HighPrecisionTimer system with optimized timing loops + - [ ] 5.3 Profile and optimize memory usage to meet <50MB RAM target + - [ ] 5.4 Optimize CPU usage to achieve <5% idle target with efficient background processing + - [ ] 5.5 Add real-time performance monitoring and metrics collection + - [ ] 5.6 Implement automated performance validation and regression testing + - [ ] 5.7 Create performance dashboard for user visibility into timing accuracy + - [ ] 5.8 Verify all performance targets met and benchmarks pass consistently + +- [ ] 6. **Implement Advanced CPS Randomization** + - [ ] 6.1 Write tests for CPSRandomizer and timing pattern generation + - [ ] 6.2 Create CPSRandomizer with configurable variance and distribution patterns + - [ ] 6.3 Add UI controls for randomization settings and pattern selection + - [ ] 6.4 Implement statistical distributions (normal, uniform) for natural timing variation + - [ ] 6.5 Integrate randomization with AutomationConfiguration and clicking loops + - [ ] 6.6 Add validation to ensure randomization doesn't break timing requirements + - [ ] 6.7 Implement anti-detection patterns to avoid automation signature detection + - [ ] 6.8 Verify all tests pass and randomization produces human-like patterns \ No newline at end of file diff --git a/Sources/ClickIt/UI/Components/StatusHeaderCard.swift b/Sources/ClickIt/UI/Components/StatusHeaderCard.swift index f2e8cdf..d3c463d 100644 --- a/Sources/ClickIt/UI/Components/StatusHeaderCard.swift +++ b/Sources/ClickIt/UI/Components/StatusHeaderCard.swift @@ -66,28 +66,71 @@ struct StatusHeaderCard: View { ) } - // Primary Action Button - Button(action: { - if viewModel.isRunning { - viewModel.stopAutomation() - } else { - viewModel.startAutomation() - } - }) { - HStack(spacing: 8) { - Image(systemName: viewModel.isRunning ? "stop.fill" : "play.fill") - .font(.system(size: 16, weight: .medium)) + // Control Buttons + if viewModel.isRunning || viewModel.isPaused { + // Running/Paused state: Show Pause/Resume and Stop buttons + HStack(spacing: 12) { + // Pause/Resume Button + Button(action: { + if viewModel.canPause { + viewModel.pauseAutomation() + } else if viewModel.canResume { + viewModel.resumeAutomation() + } + }) { + HStack(spacing: 6) { + Image(systemName: viewModel.isPaused ? "play.fill" : "pause.fill") + .font(.system(size: 14, weight: .medium)) + + Text(viewModel.isPaused ? "Resume" : "Pause") + .font(.subheadline) + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .frame(height: 36) + } + .buttonStyle(.bordered) + .disabled(!viewModel.canPause && !viewModel.canResume) + .tint(viewModel.isPaused ? .green : .orange) - Text(viewModel.isRunning ? "Stop Automation" : "Start Automation") - .font(.headline) - .fontWeight(.medium) + // Stop Button + Button(action: { + viewModel.stopAutomation() + }) { + HStack(spacing: 6) { + Image(systemName: "stop.fill") + .font(.system(size: 14, weight: .medium)) + + Text("Stop") + .font(.subheadline) + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .frame(height: 36) + } + .buttonStyle(.borderedProminent) + .tint(.red) + } + } else { + // Ready state: Show Start button + Button(action: { + viewModel.startAutomation() + }) { + HStack(spacing: 8) { + Image(systemName: "play.fill") + .font(.system(size: 16, weight: .medium)) + + Text("Start Automation") + .font(.headline) + .fontWeight(.medium) + } + .frame(maxWidth: .infinity) + .frame(height: 44) } - .frame(maxWidth: .infinity) - .frame(height: 44) + .buttonStyle(.borderedProminent) + .disabled(!viewModel.canStartAutomation) + .tint(.green) } - .buttonStyle(.borderedProminent) - .disabled(!viewModel.canStartAutomation && !viewModel.isRunning) - .tint(viewModel.isRunning ? .red : .green) } .padding(16) .background(Color(NSColor.controlBackgroundColor)) diff --git a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift index 663832d..b6a63c4 100644 --- a/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift +++ b/Sources/ClickIt/UI/ViewModels/ClickItViewModel.swift @@ -15,6 +15,7 @@ class ClickItViewModel: ObservableObject { // MARK: - Published Properties @Published var targetPoint: CGPoint? @Published var isRunning = false + @Published var isPaused = false @Published var appStatus: AppStatus = .ready // Configuration Properties @@ -69,6 +70,14 @@ class ClickItViewModel: ObservableObject { return total >= 1 && total <= 3600 // 1 second to 60 minutes } + var canPause: Bool { + isRunning && !isPaused + } + + var canResume: Bool { + isPaused && !isRunning + } + // MARK: - Dependencies private let clickCoordinator = ClickCoordinator.shared @@ -151,9 +160,83 @@ class ClickItViewModel: ObservableObject { clickCoordinator.stopAutomation() cancelTimer() // Also cancel any active timer isRunning = false + isPaused = false appStatus = .ready } + func pauseAutomation() { + guard isRunning && !isPaused else { return } + + clickCoordinator.stopAutomation() + ElapsedTimeManager.shared.pauseTracking() + + // Update visual feedback to show paused state (dimmed) + if showVisualFeedback, let point = targetPoint { + VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: false) + } + + isRunning = false + isPaused = true + appStatus = .paused + } + + func resumeAutomation() { + guard isPaused && !isRunning else { return } + + // Resume elapsed time tracking + ElapsedTimeManager.shared.resumeTracking() + + // Restart automation with current configuration + guard let point = targetPoint else { return } + + let config = AutomationConfiguration( + location: point, + clickType: clickType, + clickInterval: Double(totalMilliseconds) / 1000.0, + targetApplication: nil, + maxClicks: durationMode == .clickCount ? maxClicks : nil, + maxDuration: durationMode == .timeLimit ? durationSeconds : nil, + stopOnError: stopOnError, + randomizeLocation: randomizeLocation, + locationVariance: CGFloat(randomizeLocation ? locationVariance : 0), + showVisualFeedback: showVisualFeedback, + useDynamicMouseTracking: false + ) + + clickCoordinator.startAutomation(with: config) + isRunning = true + isPaused = false + appStatus = .running + + // Update visual feedback to show active state + if showVisualFeedback { + VisualFeedbackOverlay.shared.updateOverlay(at: point, isActive: true) + } + } + + // MARK: - Testing Methods + func startAutomationForTesting() { + guard let point = targetPoint else { return } + + let config = AutomationConfiguration( + location: point, + clickType: clickType, + clickInterval: Double(totalMilliseconds) / 1000.0, + targetApplication: nil, + maxClicks: durationMode == .clickCount ? maxClicks : nil, + maxDuration: durationMode == .timeLimit ? durationSeconds : nil, + stopOnError: stopOnError, + randomizeLocation: randomizeLocation, + locationVariance: CGFloat(randomizeLocation ? locationVariance : 0), + showVisualFeedback: false, // Disable visual feedback for tests + useDynamicMouseTracking: false + ) + + clickCoordinator.startAutomation(with: config) + isRunning = true + appStatus = .running + } + func resetConfiguration() { intervalHours = 0 intervalMinutes = 0 @@ -189,9 +272,10 @@ class ClickItViewModel: ObservableObject { guard let self = self else { return } // Sync ViewModel state with ClickCoordinator state - if !isActive && self.isRunning { + if !isActive && (self.isRunning || self.isPaused) { print("ClickItViewModel: Automation stopped externally (e.g., DELETE key), updating UI state") self.isRunning = false + self.isPaused = false self.appStatus = .ready // Also cancel any active timer when automation stops self.cancelTimer() @@ -349,6 +433,7 @@ enum TimerMode { enum AppStatus { case ready case running + case paused case error(String) var displayText: String { @@ -357,6 +442,8 @@ enum AppStatus { return "Ready" case .running: return "Running" + case .paused: + return "Paused" case .error(let message): return "Error: \(message)" } @@ -368,6 +455,8 @@ enum AppStatus { return .green case .running: return .blue + case .paused: + return .orange case .error: return .red } diff --git a/Tests/ClickItTests/PauseResumeTests.swift b/Tests/ClickItTests/PauseResumeTests.swift new file mode 100644 index 0000000..55c6ee7 --- /dev/null +++ b/Tests/ClickItTests/PauseResumeTests.swift @@ -0,0 +1,307 @@ +// +// PauseResumeTests.swift +// ClickItTests +// +// Created by ClickIt on 2025-07-22. +// Copyright © 2025 ClickIt. All rights reserved. +// + +import XCTest +import Combine +@testable import ClickIt + +final class PauseResumeTests: XCTestCase { + + var cancellables: Set! + + override func setUp() { + super.setUp() + cancellables = Set() + } + + override func tearDown() { + cancellables = nil + super.tearDown() + } + + // MARK: - ClickItViewModel Pause/Resume State Tests + + @MainActor + func testInitialPauseState() { + let viewModel = ClickItViewModel() + + XCTAssertFalse(viewModel.isPaused, "ViewModel should not be paused initially") + XCTAssertFalse(viewModel.isRunning, "ViewModel should not be running initially") + XCTAssertTrue(viewModel.canPause == false, "Should not be able to pause when not running") + XCTAssertTrue(viewModel.canResume == false, "Should not be able to resume when not running") + } + + @MainActor + func testPauseButtonAvailability() { + let viewModel = ClickItViewModel() + + // Initially should not be able to pause or resume + XCTAssertFalse(viewModel.canPause, "Cannot pause when not running") + XCTAssertFalse(viewModel.canResume, "Cannot resume when not running") + + // Set up mock target point to enable automation + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + // After starting, should be able to pause + viewModel.startAutomationForTesting() + XCTAssertTrue(viewModel.canPause, "Should be able to pause when running") + XCTAssertFalse(viewModel.canResume, "Should not be able to resume when running (not paused)") + + // After pausing, should be able to resume + viewModel.pauseAutomation() + XCTAssertFalse(viewModel.canPause, "Should not be able to pause when already paused") + XCTAssertTrue(viewModel.canResume, "Should be able to resume when paused") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testPauseActionWhenRunning() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + XCTAssertTrue(viewModel.isRunning, "Should be running after start") + + viewModel.pauseAutomation() + XCTAssertFalse(viewModel.isRunning, "Should not be running after pause") + XCTAssertTrue(viewModel.isPaused, "Should be paused after pause action") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testResumeActionWhenPaused() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + viewModel.pauseAutomation() + XCTAssertTrue(viewModel.isPaused, "Should be paused") + + viewModel.resumeAutomation() + XCTAssertFalse(viewModel.isPaused, "Should not be paused after resume") + XCTAssertTrue(viewModel.isRunning, "Should be running after resume") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testStopClearsPauseState() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + viewModel.pauseAutomation() + XCTAssertTrue(viewModel.isPaused, "Should be paused") + + viewModel.stopAutomation() + XCTAssertFalse(viewModel.isPaused, "Pause state should be cleared after stop") + XCTAssertFalse(viewModel.isRunning, "Should not be running after stop") + } + + // MARK: - ElapsedTimeManager Integration Tests + + @MainActor + func testElapsedTimeManagerPauseResume() async { + let timeManager = ElapsedTimeManager.shared + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + // Start automation and let time elapse + viewModel.startAutomationForTesting() + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + + let timeBeforePause = timeManager.elapsedTime + XCTAssertGreaterThan(timeBeforePause, 0.1, "Time should have elapsed before pause") + + // Pause and verify time stops progressing + viewModel.pauseAutomation() + XCTAssertFalse(timeManager.isTracking, "TimeManager should not be tracking when paused") + + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + let timeAfterPause = timeManager.elapsedTime + XCTAssertEqual(timeAfterPause, timeBeforePause, accuracy: 0.05, "Time should not progress during pause") + + // Resume and verify time continues + viewModel.resumeAutomation() + XCTAssertTrue(timeManager.isTracking, "TimeManager should be tracking when resumed") + + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + let timeAfterResume = timeManager.elapsedTime + XCTAssertGreaterThan(timeAfterResume, timeAfterPause, "Time should progress after resume") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testSessionTimePreservationDuringPause() async { + let timeManager = ElapsedTimeManager.shared + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds + + let sessionTimeBeforePause = timeManager.currentSessionTime + XCTAssertGreaterThan(sessionTimeBeforePause, 0.2, "Session time should accumulate") + + // Pause and verify session time is preserved + viewModel.pauseAutomation() + let sessionTimeDuringPause = timeManager.currentSessionTime + XCTAssertEqual(sessionTimeDuringPause, sessionTimeBeforePause, accuracy: 0.05, "Session time should be preserved during pause") + + // Wait and ensure time doesn't progress during pause + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + let sessionTimeStillPaused = timeManager.currentSessionTime + XCTAssertEqual(sessionTimeStillPaused, sessionTimeBeforePause, accuracy: 0.05, "Session time should remain constant during pause") + + // Resume and verify time continues from preserved point + viewModel.resumeAutomation() + try? await Task.sleep(nanoseconds: 200_000_000) // 0.2 seconds + + let sessionTimeAfterResume = timeManager.currentSessionTime + XCTAssertGreaterThan(sessionTimeAfterResume, sessionTimeBeforePause, "Session time should continue from pause point") + + // Clean up + viewModel.stopAutomation() + } + + // MARK: - ClickCoordinator Integration Tests + + @MainActor + func testClickCoordinatorPauseIntegration() { + let coordinator = ClickCoordinator.shared + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + // Start automation + viewModel.startAutomationForTesting() + XCTAssertTrue(coordinator.isActive, "Coordinator should be active when automation starts") + + // Pause should stop coordinator + viewModel.pauseAutomation() + XCTAssertFalse(coordinator.isActive, "Coordinator should be inactive when paused") + + // Resume should restart coordinator + viewModel.resumeAutomation() + XCTAssertTrue(coordinator.isActive, "Coordinator should be active when resumed") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testStatisticsPreservationDuringPause() async { + let coordinator = ClickCoordinator.shared + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + + // Let some clicks happen + try? await Task.sleep(nanoseconds: 500_000_000) // 0.5 seconds + + let statisticsBeforePause = coordinator.getSessionStatistics() + let clickCountBeforePause = statisticsBeforePause.totalClicks + + // Pause and verify statistics are preserved + viewModel.pauseAutomation() + let statisticsDuringPause = coordinator.getSessionStatistics() + XCTAssertEqual(statisticsDuringPause.totalClicks, clickCountBeforePause, "Click count should be preserved during pause") + XCTAssertFalse(statisticsDuringPause.isActive, "Statistics should show inactive during pause") + + // Resume and verify statistics continue from preserved state + viewModel.resumeAutomation() + try? await Task.sleep(nanoseconds: 300_000_000) // 0.3 seconds + + let statisticsAfterResume = coordinator.getSessionStatistics() + XCTAssertGreaterThanOrEqual(statisticsAfterResume.totalClicks, clickCountBeforePause, "Click count should continue from pause point") + XCTAssertTrue(statisticsAfterResume.isActive, "Statistics should show active after resume") + + // Clean up + viewModel.stopAutomation() + } + + // MARK: - Edge Case Tests + + @MainActor + func testPauseWhenNotRunning() { + let viewModel = ClickItViewModel() + + // Should not crash or change state when pausing while not running + viewModel.pauseAutomation() + XCTAssertFalse(viewModel.isPaused, "Should not be paused when wasn't running") + XCTAssertFalse(viewModel.isRunning, "Should not be running") + } + + @MainActor + func testResumeWhenNotPaused() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + // Resume when not paused should not start automation + viewModel.resumeAutomation() + XCTAssertFalse(viewModel.isRunning, "Should not start running from resume when not paused") + + // But if running and not paused, resume should be no-op + viewModel.startAutomationForTesting() + XCTAssertTrue(viewModel.isRunning, "Should be running after start") + + viewModel.resumeAutomation() + XCTAssertTrue(viewModel.isRunning, "Should still be running after resume") + XCTAssertFalse(viewModel.isPaused, "Should not be paused") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testMultiplePauseCalls() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + viewModel.pauseAutomation() + XCTAssertTrue(viewModel.isPaused, "Should be paused after first pause") + + // Multiple pause calls should not change state + viewModel.pauseAutomation() + viewModel.pauseAutomation() + XCTAssertTrue(viewModel.isPaused, "Should remain paused after multiple pause calls") + XCTAssertFalse(viewModel.isRunning, "Should not be running") + + // Clean up + viewModel.stopAutomation() + } + + @MainActor + func testMultipleResumeCalls() { + let viewModel = ClickItViewModel() + viewModel.setTargetPoint(CGPoint(x: 100, y: 100)) + + viewModel.startAutomationForTesting() + viewModel.pauseAutomation() + viewModel.resumeAutomation() + XCTAssertFalse(viewModel.isPaused, "Should not be paused after resume") + XCTAssertTrue(viewModel.isRunning, "Should be running after resume") + + // Multiple resume calls should not change state + viewModel.resumeAutomation() + viewModel.resumeAutomation() + XCTAssertFalse(viewModel.isPaused, "Should remain not paused after multiple resume calls") + XCTAssertTrue(viewModel.isRunning, "Should remain running") + + // Clean up + viewModel.stopAutomation() + } +} \ No newline at end of file