Files
conjure/Tests/ConjureKitTests/ShortcutRecordingTests.swift
2026-10-16 12:06:41 +02:00

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)
}