feat: add destination-specific global shortcuts
This commit is contained in:
@@ -0,0 +1,121 @@
|
||||
import AppKit
|
||||
@testable import ConjureKit
|
||||
import Testing
|
||||
|
||||
|
||||
@Test @MainActor
|
||||
func globalShortcutKeepsLegacyStorageAndScopesCustomStorage() throws {
|
||||
let suite = "GlobalShortcutsTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let commandShiftSpace = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.command, .shift]))
|
||||
defaults.set(KeyboardShortcut.commandSpace.storedValue, forKey: "globalActivationShortcut")
|
||||
defaults.set(commandShiftSpace.storedValue, forKey: "customShortcut")
|
||||
|
||||
let legacy = GlobalShortcut(defaults: defaults) {}
|
||||
let custom = GlobalShortcut(defaults: defaults, storageKey: "customShortcut") {}
|
||||
#expect(legacy.shortcut == .commandSpace)
|
||||
#expect(custom.shortcut == commandShiftSpace)
|
||||
try custom.setShortcut(nil)
|
||||
#expect(defaults.object(forKey: "customShortcut") == nil)
|
||||
#expect(KeyboardShortcut(storedValue: defaults.object(forKey: "globalActivationShortcut")) == .commandSpace)
|
||||
#expect(GlobalShortcut(defaults: defaults, action: {}).shortcut == .commandSpace)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func duplicateGlobalBindingIsRejectedWhileSuspendedWithoutChangingSavedBinding() throws {
|
||||
let suite = "GlobalShortcutsTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let candidate = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.command, .shift]))
|
||||
let previous = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.control, .option]))
|
||||
let emojiIndex = try #require(GlobalShortcuts.destinations.firstIndex(of: .command(.emoji)))
|
||||
defaults.set(candidate.storedValue, forKey: "globalActivationShortcut")
|
||||
defaults.set(previous.storedValue, forKey: "globalCommandShortcut.emoji")
|
||||
let shortcuts = GlobalShortcuts(defaults: defaults) { _ in }
|
||||
shortcuts.suspend()
|
||||
|
||||
do {
|
||||
try shortcuts.setShortcut(candidate, at: emojiIndex)
|
||||
Issue.record("A duplicate shortcut was accepted")
|
||||
} catch {
|
||||
#expect(error.localizedDescription.contains("Applications"))
|
||||
#expect(error.localizedDescription.contains("already assigned"))
|
||||
}
|
||||
let commandReturn = try #require(KeyboardShortcut(keyCode: 36, modifiers: .command))
|
||||
do {
|
||||
try shortcuts.setShortcut(commandReturn, at: emojiIndex)
|
||||
Issue.record("A Conjure action shortcut was accepted")
|
||||
} catch {
|
||||
#expect(error.localizedDescription.contains("already used by Conjure"))
|
||||
#expect(error.localizedDescription.contains("confirm uninstall"))
|
||||
}
|
||||
#expect(shortcuts.bindings[emojiIndex].shortcut == previous)
|
||||
#expect(KeyboardShortcut(storedValue: defaults.object(forKey: "globalCommandShortcut.emoji")) == previous)
|
||||
#expect(shortcuts.bindings.allSatisfy { !$0.isRegistered })
|
||||
try shortcuts.validate(candidate, at: 0)
|
||||
|
||||
try shortcuts.setShortcut(nil, at: emojiIndex)
|
||||
let restored = GlobalShortcuts(defaults: defaults) { _ in }
|
||||
#expect(restored.bindings[0].shortcut == candidate)
|
||||
#expect(restored.bindings[emojiIndex].shortcut == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func localShortcutConflictsMatchMenusActionsAndPanelNavigation() throws {
|
||||
let commandKey = try #require(KeyboardShortcut(keyCode: 0, modifiers: .command))
|
||||
for key in [",", "h", "q", "w", "x", "c", "v", "a", "m", "k", "Return", "1", "9"] {
|
||||
#expect(GlobalShortcuts.localAction(for: commandKey, keyName: key) != nil)
|
||||
}
|
||||
#expect(GlobalShortcuts.localAction(for: commandKey, keyName: "0") == nil)
|
||||
let hideOthers = try #require(KeyboardShortcut(keyCode: 0, modifiers: [.command, .option]))
|
||||
#expect(GlobalShortcuts.localAction(for: hideOthers, keyName: "H") == "Hide Others")
|
||||
let controlKey = try #require(KeyboardShortcut(keyCode: 0, modifiers: .control))
|
||||
for key in ["h", "j", "k", "l"] {
|
||||
#expect(GlobalShortcuts.localAction(for: controlKey, keyName: key) != nil)
|
||||
}
|
||||
let commandShiftSpace = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.command, .shift]))
|
||||
#expect(GlobalShortcuts.localAction(for: commandShiftSpace, keyName: "Space") == nil)
|
||||
let commandShiftK = try #require(KeyboardShortcut(keyCode: 40, modifiers: [.command, .shift]))
|
||||
#expect(GlobalShortcuts.localAction(for: commandShiftK, keyName: "K") == nil)
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func startupReportsEveryInvalidSavedBindingWithoutDiscardingThem() throws {
|
||||
let suite = "GlobalShortcutsTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let commandReturn = try #require(KeyboardShortcut(keyCode: 36, modifiers: .command))
|
||||
defaults.set("invalid", forKey: "globalActivationShortcut")
|
||||
defaults.set(commandReturn.storedValue, forKey: "globalCommandShortcut.translate")
|
||||
defaults.set(["keyCode": -1, "modifiers": 0], forKey: "globalCommandShortcut.emoji")
|
||||
let shortcuts = GlobalShortcuts(defaults: defaults) { _ in }
|
||||
|
||||
let errors = shortcuts.start()
|
||||
#expect(errors.count == 3)
|
||||
#expect(errors[0].hasPrefix("Applications:"))
|
||||
#expect(errors[1].hasPrefix("Conjure Translate:"))
|
||||
#expect(errors[1].contains("already used by Conjure"))
|
||||
#expect(errors[2].hasPrefix("Conjure Emoji:"))
|
||||
#expect(defaults.string(forKey: "globalActivationShortcut") == "invalid")
|
||||
#expect(KeyboardShortcut(storedValue: defaults.object(forKey: "globalCommandShortcut.translate")) == commandReturn)
|
||||
#expect(defaults.object(forKey: "globalCommandShortcut.emoji") != nil)
|
||||
#expect(shortcuts.bindings.allSatisfy { !$0.isRegistered })
|
||||
}
|
||||
|
||||
@Test @MainActor
|
||||
func startupRejectsEveryDuplicateEvenBeforeNativeRegistration() throws {
|
||||
let suite = "GlobalShortcutsTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let candidate = try #require(KeyboardShortcut(keyCode: 49, modifiers: [.command, .shift]))
|
||||
defaults.set(candidate.storedValue, forKey: "globalActivationShortcut")
|
||||
defaults.set(candidate.storedValue, forKey: "globalCommandShortcut.emoji")
|
||||
let shortcuts = GlobalShortcuts(defaults: defaults) { _ in }
|
||||
|
||||
let errors = shortcuts.start()
|
||||
#expect(errors.count == 2)
|
||||
#expect(errors[0].contains("Conjure Emoji"))
|
||||
#expect(errors[1].contains("Applications"))
|
||||
#expect(shortcuts.bindings.allSatisfy { !$0.isRegistered })
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
import AppKit
|
||||
import ConjureKit
|
||||
import Testing
|
||||
@testable import Conjure
|
||||
|
||||
@Test @MainActor func activationAlwaysTargetsRequestedViewAndClearsPreviousTool() throws {
|
||||
_ = NSApplication.shared
|
||||
let suite = "ConjureDestination.\(UUID().uuidString)"
|
||||
let defaults = UserDefaults(suiteName: suite)!
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let launcher = LauncherWindowController(history: LaunchHistory(defaults: defaults, now: { Date(timeIntervalSinceReferenceDate: 12_000) }))
|
||||
let window = try #require(launcher.window)
|
||||
defer { window.orderOut(nil) }
|
||||
func descendants(_ view: NSView) -> [NSView] { [view] + view.subviews.flatMap(descendants) }
|
||||
let content = try #require(window.contentView)
|
||||
let search = try #require(descendants(content).compactMap { $0 as? NSSearchField }.first)
|
||||
launcher.present(command: .emoji)
|
||||
#expect(descendants(content).contains { $0 is NSCollectionView })
|
||||
search.stringValue = "happy"
|
||||
launcher.controlTextDidChange(Notification(name: NSControl.textDidChangeNotification, object: search))
|
||||
window.orderOut(nil)
|
||||
launcher.toggle()
|
||||
#expect(!descendants(content).contains { $0 is NSCollectionView })
|
||||
#expect(search.stringValue.isEmpty)
|
||||
#expect(search.accessibilityLabel() == "Search applications, calculate, or convert units and currencies")
|
||||
launcher.toggle(command: .translate)
|
||||
#expect(search.accessibilityLabel() == "English text to translate into Japanese")
|
||||
launcher.toggle(command: .emoji)
|
||||
#expect(search.accessibilityLabel() == "Search emoji")
|
||||
#expect(search.stringValue.isEmpty)
|
||||
launcher.present()
|
||||
#expect(!descendants(content).contains { $0 is NSCollectionView })
|
||||
#expect(search.accessibilityLabel() == "Search applications, calculate, or convert units and currencies")
|
||||
}
|
||||
@@ -65,11 +65,11 @@ func shortcutModifierGuidanceDoesNotAppearAsAnError() throws {
|
||||
let suite = "Conjure.ToolFeedbackTests.\(UUID().uuidString)"
|
||||
let defaults = try #require(UserDefaults(suiteName: suite))
|
||||
defer { defaults.removePersistentDomain(forName: suite) }
|
||||
let controller = SettingsWindowController(shortcut: GlobalShortcut(defaults: defaults, action: {}))
|
||||
let controller = SettingsWindowController(shortcuts: GlobalShortcuts(defaults: defaults, action: { _ in }))
|
||||
let window = try #require(controller.window)
|
||||
let views = descendants(try #require(window.contentView))
|
||||
let recorder = try #require(views.compactMap { $0 as? NSButton }.first {
|
||||
$0.accessibilityLabel() == "Record global shortcut"
|
||||
$0.action == NSSelectorFromString("beginRecording")
|
||||
})
|
||||
let error = try #require(views.compactMap { $0 as? NSTextField }.first {
|
||||
$0.accessibilityLabel() == "Shortcut feedback"
|
||||
|
||||
Reference in New Issue
Block a user