144 lines
6.5 KiB
Swift
144 lines
6.5 KiB
Swift
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)
|
|
}
|