diff --git a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift index 22cdae3f7..80cda1ee9 100644 --- a/apps/macos/Sources/Clawdis/CritterStatusLabel.swift +++ b/apps/macos/Sources/Clawdis/CritterStatusLabel.swift @@ -631,3 +631,81 @@ enum CritterIconRenderer { canvas.context.restoreGState() } } + +#if DEBUG +@MainActor +extension CritterStatusLabel { + static func exerciseForTesting() async { + var label = CritterStatusLabel( + isPaused: false, + isSleeping: false, + isWorking: true, + earBoostActive: false, + blinkTick: 1, + sendCelebrationTick: 1, + gatewayStatus: .running(details: nil), + animationsEnabled: true, + iconState: .workingMain(.tool(.bash))) + + _ = label.body + _ = label.iconImage + _ = label.tickTaskID + label.tick(Date()) + label.resetMotion() + label.blink() + label.wiggle() + label.wiggleLegs() + label.wiggleEars() + label.scurry() + label.scheduleRandomTimers(from: Date()) + _ = label.gatewayNeedsAttention + _ = label.gatewayBadgeColor + + label.isPaused = true + _ = label.iconImage + + label.isPaused = false + label.isSleeping = true + _ = label.iconImage + + label.isSleeping = false + label.iconState = .idle + _ = label.iconImage + + let failed = CritterStatusLabel( + isPaused: false, + isSleeping: false, + isWorking: false, + earBoostActive: false, + blinkTick: 0, + sendCelebrationTick: 0, + gatewayStatus: .failed("boom"), + animationsEnabled: false, + iconState: .idle) + _ = failed.gatewayNeedsAttention + _ = failed.gatewayBadgeColor + + let stopped = CritterStatusLabel( + isPaused: false, + isSleeping: false, + isWorking: false, + earBoostActive: false, + blinkTick: 0, + sendCelebrationTick: 0, + gatewayStatus: .stopped, + animationsEnabled: false, + iconState: .idle) + _ = stopped.gatewayNeedsAttention + _ = stopped.gatewayBadgeColor + + _ = CritterIconRenderer.makeIcon( + blink: 0.6, + legWiggle: 0.8, + earWiggle: 0.4, + earScale: 1.4, + earHoles: true, + eyesClosedLines: true, + badge: .init(symbolName: "gearshape.fill", prominence: .secondary)) + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/HoverHUD.swift b/apps/macos/Sources/Clawdis/HoverHUD.swift index 04e414c5d..d3482362a 100644 --- a/apps/macos/Sources/Clawdis/HoverHUD.swift +++ b/apps/macos/Sources/Clawdis/HoverHUD.swift @@ -215,6 +215,7 @@ final class HoverHUDController { } private func installDismissMonitor() { + if ProcessInfo.processInfo.isRunningTests { return } guard self.dismissMonitor == nil, let window else { return } self.dismissMonitor = NSEvent.addGlobalMonitorForEvents(matching: [ .leftMouseDown, diff --git a/apps/macos/Sources/Clawdis/PermissionManager.swift b/apps/macos/Sources/Clawdis/PermissionManager.swift index 9d8269df5..d7d1ab4e0 100644 --- a/apps/macos/Sources/Clawdis/PermissionManager.swift +++ b/apps/macos/Sources/Clawdis/PermissionManager.swift @@ -271,6 +271,9 @@ final class PermissionMonitor { private func startMonitoring() { Task { await self.checkStatus(force: true) } + if ProcessInfo.processInfo.isRunningTests { + return + } self.monitorTimer = Timer.scheduledTimer(withTimeInterval: 1.0, repeats: true) { [weak self] _ in guard let self else { return } Task { @MainActor in diff --git a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift index 795eeace0..9d49861c6 100644 --- a/apps/macos/Sources/Clawdis/VoicePushToTalk.swift +++ b/apps/macos/Sources/Clawdis/VoicePushToTalk.swift @@ -25,6 +25,7 @@ final class VoicePushToTalkHotkey: @unchecked Sendable { } func setEnabled(_ enabled: Bool) { + if ProcessInfo.processInfo.isRunningTests { return } self.withMainThread { [weak self] in guard let self else { return } if enabled { diff --git a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift index 2dad089fb..f8e91056d 100644 --- a/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift +++ b/apps/macos/Sources/Clawdis/VoiceWakeOverlay.swift @@ -855,3 +855,51 @@ private final class TranscriptNSTextView: NSTextView { super.keyDown(with: event) } } + +#if DEBUG +@MainActor +extension VoiceWakeOverlayController { + static func exerciseForTesting() async { + let controller = VoiceWakeOverlayController(enableUI: false) + let token = controller.startSession( + source: .wakeWord, + transcript: "Hello", + attributed: nil, + forwardEnabled: true, + isFinal: false) + + controller.updatePartial(token: token, transcript: "Hello world") + controller.presentFinal(token: token, transcript: "Final", autoSendAfter: nil) + controller.userBeganEditing() + controller.endEditing() + controller.updateText("Edited text") + + _ = controller.makeAttributed(from: "Attributed") + _ = controller.targetFrame() + _ = controller.measuredHeight() + _ = controller.dismissTargetFrame( + for: NSRect(x: 0, y: 0, width: 120, height: 60), + reason: .empty, + outcome: .empty) + _ = controller.dismissTargetFrame( + for: NSRect(x: 0, y: 0, width: 120, height: 60), + reason: .explicit, + outcome: .sent) + _ = controller.dismissTargetFrame( + for: NSRect(x: 0, y: 0, width: 120, height: 60), + reason: .explicit, + outcome: .empty) + + controller.beginSendUI(token: token, sendChime: .none) + try? await Task.sleep(nanoseconds: 350_000_000) + + controller.scheduleAutoSend(token: token, after: 10) + controller.autoSendTask?.cancel() + controller.autoSendTask = nil + controller.autoSendToken = nil + + controller.dismiss(token: token, reason: .explicit, outcome: .sent) + controller.bringToFrontIfVisible() + } +} +#endif diff --git a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift index c64aed36a..8729823ab 100644 --- a/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift +++ b/apps/macos/Sources/Clawdis/WebChatSwiftUI.swift @@ -230,6 +230,7 @@ final class WebChatSwiftUIWindowController { } private func installDismissMonitor() { + if ProcessInfo.processInfo.isRunningTests { return } guard self.dismissMonitor == nil, self.window != nil else { return } self.dismissMonitor = NSEvent.addGlobalMonitorForEvents( matching: [.leftMouseDown, .rightMouseDown, .otherMouseDown]) diff --git a/apps/macos/Tests/ClawdisIPCTests/CritterIconRendererTests.swift b/apps/macos/Tests/ClawdisIPCTests/CritterIconRendererTests.swift index 243837a72..13f24abcf 100644 --- a/apps/macos/Tests/ClawdisIPCTests/CritterIconRendererTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/CritterIconRendererTests.swift @@ -30,5 +30,8 @@ struct CritterIconRendererTests { #expect(image.tiffRepresentation != nil) } -} + @Test func critterStatusLabelExercisesHelpers() async { + await CritterStatusLabel.exerciseForTesting() + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/HoverHUDControllerTests.swift b/apps/macos/Tests/ClawdisIPCTests/HoverHUDControllerTests.swift new file mode 100644 index 000000000..4d306d4c6 --- /dev/null +++ b/apps/macos/Tests/ClawdisIPCTests/HoverHUDControllerTests.swift @@ -0,0 +1,26 @@ +import AppKit +import Testing +@testable import Clawdis + +@Suite(.serialized) +@MainActor +struct HoverHUDControllerTests { + @Test func hoverHUDControllerPresentsAndDismisses() async { + let controller = HoverHUDController() + controller.setSuppressed(false) + + controller.statusItemHoverChanged( + inside: true, + anchorProvider: { NSRect(x: 10, y: 10, width: 24, height: 24) }) + try? await Task.sleep(nanoseconds: 260_000_000) + + controller.panelHoverChanged(inside: true) + controller.panelHoverChanged(inside: false) + controller.statusItemHoverChanged( + inside: false, + anchorProvider: { NSRect(x: 10, y: 10, width: 24, height: 24) }) + + controller.dismiss(reason: "test") + controller.setSuppressed(true) + } +} diff --git a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeOverlayControllerTests.swift b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeOverlayControllerTests.swift index a7b75de4b..117399e29 100644 --- a/apps/macos/Tests/ClawdisIPCTests/VoiceWakeOverlayControllerTests.swift +++ b/apps/macos/Tests/ClawdisIPCTests/VoiceWakeOverlayControllerTests.swift @@ -5,7 +5,7 @@ import Testing @Suite(.serialized) @MainActor struct VoiceWakeOverlayControllerTests { - @Test func overlayControllerLifecycleWithoutUI() { + @Test func overlayControllerLifecycleWithoutUI() async { let controller = VoiceWakeOverlayController(enableUI: false) let token = controller.startSession( source: .wakeWord, @@ -22,6 +22,7 @@ struct VoiceWakeOverlayControllerTests { controller.updateLevel(token: token, -0.5) #expect(controller.model.level == 0) + try? await Task.sleep(nanoseconds: 120_000_000) controller.updateLevel(token: token, 2.0) #expect(controller.model.level == 1) @@ -60,4 +61,8 @@ struct VoiceWakeOverlayControllerTests { controller.updateLevel(token: token, 0.9) #expect(controller.model.level == 0.9) } + + @Test func overlayControllerExercisesHelpers() async { + await VoiceWakeOverlayController.exerciseForTesting() + } }