import AppKit import Foundation import Testing @testable import ConjureKit @testable import Conjure @MainActor private final class RecordingRegistration { var active: KeyboardShortcut? var rejected: KeyboardShortcut? var unavailable = false var adapter: GlobalShortcut.Registration { .init(activate: { [self] candidate in if unavailable || candidate == rejected { throw RecordingFailure() } active = candidate }, unregister: { [self] in active = nil }, stop: { [self] in active = nil }) } } private struct RecordingFailure: LocalizedError { var errorDescription: String? { "Registration denied" } } @Test @MainActor func recordingOwnsCompletionCancellationAndFailureRestoration() throws { let suite = "ShortcutRecording.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let old = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.control, .option])) let candidate = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.command, .shift])) let other = try #require(KeyboardShortcut(keyCode: 0, modifiers: [.control, .option])) let states = GlobalShortcuts.destinations.map { _ in RecordingRegistration() } defaults.set(old.storedValue, forKey: GlobalShortcuts.destinations[0].storageKey) defaults.set(other.storedValue, forKey: GlobalShortcuts.destinations[1].storageKey) let bindings = GlobalShortcuts.destinations.enumerated().map { index, destination in GlobalShortcut(defaults: defaults, storageKey: destination.storageKey, registration: states[index].adapter) } let shortcuts = GlobalShortcuts(defaults: defaults, bindings: bindings) defer { shortcuts.stop() } #expect(shortcuts.start().isEmpty) #expect(states[0].active == old) #expect(states[1].active == other) shortcuts.beginRecording() shortcuts.beginRecording() // Beginning twice must not change the saved bindings. #expect(shortcuts.isRecording) #expect(states.allSatisfy { $0.active == nil }) #expect(shortcuts.cancelRecording().isEmpty) #expect(!shortcuts.isRecording) #expect(states[0].active == old) #expect(states[1].active == other) #expect(shortcuts.cancelRecording().isEmpty) shortcuts.beginRecording() states[0].rejected = candidate let failure = shortcuts.finishRecording(candidate, at: 0) #expect(failure.contains { $0.contains("Registration denied") }) #expect(!shortcuts.isRecording) #expect(bindings[0].shortcut == old) #expect(KeyboardShortcut(storedValue: defaults.object(forKey: GlobalShortcuts.destinations[0].storageKey)) == old) #expect(states[0].active == old) #expect(states[1].active == other) shortcuts.beginRecording() states[0].rejected = nil #expect(shortcuts.finishRecording(candidate, at: 0).isEmpty) #expect(!shortcuts.isRecording) #expect(states[0].active == candidate) #expect(states[1].active == other) #expect(KeyboardShortcut(storedValue: defaults.object(forKey: GlobalShortcuts.destinations[0].storageKey)) == candidate) shortcuts.beginRecording() #expect(!shortcuts.finishRecording(other, at: 0).isEmpty) // Duplicate: still restore everything. #expect(states[0].active == candidate) #expect(states[1].active == other) #expect(!shortcuts.isRecording) shortcuts.beginRecording() states[1].unavailable = true let restorationFailure = shortcuts.cancelRecording() #expect(restorationFailure.contains { $0.hasPrefix(GlobalShortcuts.destinations[1].title) }) #expect(!shortcuts.isRecording) #expect(states[0].active == candidate) #expect(states[1].active == nil) #expect(bindings[1].shortcut == other) #expect(shortcuts.finishRecording(nil, at: 0).isEmpty) // Clear outside recording must not resume other bindings. #expect(states[0].active == nil) #expect(states[1].active == nil) #expect(defaults.object(forKey: GlobalShortcuts.destinations[0].storageKey) == nil) } @Test(arguments: ["close", "focus", "destination", "escape", "invalid", "responder"]) @MainActor func recordingSettingsExitRestoresSavedShortcuts(_ exit: String) throws { _ = NSApplication.shared let suite = "RecordingExit.\(UUID().uuidString)" let defaults = UserDefaults(suiteName: suite)! defer { defaults.removePersistentDomain(forName: suite) } let shortcut = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.control, .option])) defaults.set(shortcut.storedValue, forKey: GlobalShortcuts.destinations[0].storageKey) let states = GlobalShortcuts.destinations.map { _ in RecordingRegistration() } let bindings = GlobalShortcuts.destinations.enumerated().map { index, destination in GlobalShortcut(defaults: defaults, storageKey: destination.storageKey, registration: states[index].adapter) } let shortcuts = GlobalShortcuts(defaults: defaults, bindings: bindings) defer { shortcuts.stop() } #expect(shortcuts.start().isEmpty) let settings = SettingsWindowController(shortcuts: shortcuts) let window = try #require(settings.window) defer { window.close() } func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) } let views = descendants(try #require(window.contentView)) let recorder = try #require(views.compactMap { $0 as? NSButton }.first { $0.action == NSSelectorFromString("beginRecording") }) recorder.performClick(nil) #expect(shortcuts.isRecording) #expect(states[0].active == nil) switch exit { case "close": settings.windowWillClose(Notification(name: NSWindow.willCloseNotification, object: window)) case "focus": settings.windowDidResignKey(Notification(name: NSWindow.didResignKeyNotification, object: window)) case "destination": let destination = try #require(views.compactMap { $0 as? NSPopUpButton }.first) destination.selectItem(at: 1) _ = settings.perform(destination.action, with: destination) case "responder": _ = window.makeFirstResponder(nil) default: let event = try #require(NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: [], timestamp: 1, windowNumber: window.windowNumber, context: nil, characters: exit == "escape" ? "\u{1b}" : "a", charactersIgnoringModifiers: exit == "escape" ? "\u{1b}" : "a", isARepeat: false, keyCode: exit == "escape" ? 53 : 0)) recorder.keyDown(with: event) } #expect(!shortcuts.isRecording) #expect(states[0].active == shortcut) #expect(bindings[0].isRegistered) }