- ShortcutRecorderHostView: testable NSView with process(event:) drop-in; rejects modifier-only + shift-only keystrokes, aborts on Escape, records valid (keyCode + non-shift-only modifiers) per UI-SPEC Surface 2 - ShortcutRecorderView: NSViewRepresentable SwiftUI glue with @Binding shortcut + errorMessage, @MainActor-isolated Delegate protocol - KeyCapView / KeyCapRowView: key-cap chip rendering ([⌘]+[⇧]+[J]) - CollisionDetector: Cmd+Shift+J → Chrome/Slack/VS Code warning, Option+Space → Alfred/Raycast warning, uncommon combos pass through - Tests: 7 passing (modifier-only rejected, shift-only rejected, escape aborts, valid capture, cmdShiftJ collision, optionSpace collision, uncommon combo no collision) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
137 lines
4.2 KiB
Swift
137 lines
4.2 KiB
Swift
import XCTest
|
|
import AppKit
|
|
import Carbon.HIToolbox
|
|
@testable import Shell
|
|
|
|
@MainActor
|
|
final class ShortcutRecorderTests: XCTestCase {
|
|
/// `@unchecked Sendable`: mutable `var` storage is required to capture
|
|
/// the delegate callbacks for assertion, and every test interaction
|
|
/// happens on the main thread.
|
|
private final class RecordingDelegate: ShortcutRecorderHostView.Delegate, @unchecked Sendable {
|
|
var recorded: KeyboardShortcut?
|
|
var rejectedReason: String?
|
|
var aborted = false
|
|
|
|
func shortcutRecorder(
|
|
_ view: ShortcutRecorderHostView,
|
|
recorded shortcut: KeyboardShortcut
|
|
) {
|
|
recorded = shortcut
|
|
}
|
|
|
|
func shortcutRecorder(
|
|
_ view: ShortcutRecorderHostView,
|
|
rejected reason: String
|
|
) {
|
|
rejectedReason = reason
|
|
}
|
|
|
|
func shortcutRecorderDidAbort(_ view: ShortcutRecorderHostView) {
|
|
aborted = true
|
|
}
|
|
}
|
|
|
|
private func makeEvent(
|
|
keyCode: UInt16,
|
|
modifiers: NSEvent.ModifierFlags
|
|
) -> NSEvent {
|
|
NSEvent.keyEvent(
|
|
with: .keyDown,
|
|
location: .zero,
|
|
modifierFlags: modifiers,
|
|
timestamp: 0,
|
|
windowNumber: 0,
|
|
context: nil,
|
|
characters: "",
|
|
charactersIgnoringModifiers: "",
|
|
isARepeat: false,
|
|
keyCode: keyCode
|
|
)!
|
|
}
|
|
|
|
func test_modifierOnlyRejected() {
|
|
let view = ShortcutRecorderHostView()
|
|
let delegate = RecordingDelegate()
|
|
view.delegate = delegate
|
|
|
|
// Shift-key keyCode (kVK_Shift = 56) with no modifier flags.
|
|
let event = makeEvent(keyCode: UInt16(kVK_Shift), modifiers: [])
|
|
_ = view.process(event: event)
|
|
|
|
XCTAssertNotNil(delegate.rejectedReason)
|
|
XCTAssertTrue(delegate.rejectedReason!.contains("at least one regular key"))
|
|
XCTAssertNil(delegate.recorded)
|
|
}
|
|
|
|
func test_shiftOnlyRejected() {
|
|
let view = ShortcutRecorderHostView()
|
|
let delegate = RecordingDelegate()
|
|
view.delegate = delegate
|
|
|
|
let event = makeEvent(keyCode: UInt16(kVK_ANSI_J), modifiers: [.shift])
|
|
_ = view.process(event: event)
|
|
|
|
XCTAssertNotNil(delegate.rejectedReason)
|
|
XCTAssertTrue(delegate.rejectedReason!.contains("Shift alone"))
|
|
XCTAssertNil(delegate.recorded)
|
|
}
|
|
|
|
func test_escapeAborts() {
|
|
let view = ShortcutRecorderHostView()
|
|
let delegate = RecordingDelegate()
|
|
view.delegate = delegate
|
|
|
|
let event = makeEvent(keyCode: UInt16(kVK_Escape), modifiers: [])
|
|
_ = view.process(event: event)
|
|
|
|
XCTAssertTrue(delegate.aborted)
|
|
XCTAssertNil(delegate.recorded)
|
|
XCTAssertNil(delegate.rejectedReason)
|
|
}
|
|
|
|
func test_validCaptureCallsCoordinator() {
|
|
let view = ShortcutRecorderHostView()
|
|
let delegate = RecordingDelegate()
|
|
view.delegate = delegate
|
|
|
|
let event = makeEvent(keyCode: UInt16(kVK_ANSI_J), modifiers: [.command, .shift])
|
|
_ = view.process(event: event)
|
|
|
|
XCTAssertNotNil(delegate.recorded)
|
|
XCTAssertEqual(delegate.recorded?.keyCode, UInt16(kVK_ANSI_J))
|
|
XCTAssertEqual(
|
|
delegate.recorded?.modifierFlags.intersection(.deviceIndependentFlagsMask),
|
|
[.command, .shift]
|
|
)
|
|
}
|
|
|
|
func test_cmdShiftJCollision() {
|
|
let shortcut = KeyboardShortcut(
|
|
keyCode: UInt16(kVK_ANSI_J),
|
|
modifiers: [.command, .shift]
|
|
)
|
|
let warning = CollisionDetector.check(shortcut)
|
|
XCTAssertNotNil(warning)
|
|
XCTAssertEqual(warning?.apps, ["Chrome", "Slack", "VS Code"])
|
|
}
|
|
|
|
func test_optionSpaceCollision() {
|
|
let shortcut = KeyboardShortcut(
|
|
keyCode: UInt16(kVK_Space),
|
|
modifiers: [.option]
|
|
)
|
|
let warning = CollisionDetector.check(shortcut)
|
|
XCTAssertNotNil(warning)
|
|
XCTAssertEqual(warning?.apps, ["Alfred", "Raycast"])
|
|
}
|
|
|
|
func test_uncommonComboNoCollision() {
|
|
let shortcut = KeyboardShortcut(
|
|
keyCode: UInt16(kVK_F5),
|
|
modifiers: [.control]
|
|
)
|
|
XCTAssertNil(CollisionDetector.check(shortcut))
|
|
}
|
|
}
|