Tests/OrgEditorAppKitTests/KeyTests.swift
162 lines · 6372 bytes
1import AppKit
2import OrgCore
3import Testing
4@testable import OrgApp
5@testable import OrgEditorAppKit
6
7@MainActor
8struct KeyTests {
9 static let codes: [Character: UInt16] = ["a": 0, "s": 1, "f": 3, "g": 5, "z": 6, "x": 7, "c": 8, "w": 13, "q": 12, "t": 17]
10
11 /// Presses a key. `control` sends the control character, as macOS does.
12 func press(_ h: Harness, _ key: Character, control: Bool = false, option: UInt = 0, typed: String? = nil) {
13 var flags: NSEvent.ModifierFlags = []
14 if control { flags.insert(.control) }
15 if option != 0 { flags = NSEvent.ModifierFlags(rawValue: flags.rawValue | NSEvent.ModifierFlags.option.rawValue | option) }
16 let ignoring = String(key)
17 let characters = typed ?? (control ? String(UnicodeScalar(UInt8(key.asciiValue! & 0x1f))) : ignoring)
18 send(h, characters: characters, ignoring: ignoring, code: Self.codes[key] ?? 0, flags: flags)
19 }
20
21 func send(_ h: Harness, characters: String, ignoring: String, code: UInt16, flags: NSEvent.ModifierFlags = []) {
22 let event = NSEvent.keyEvent(
23 with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0, windowNumber: h.window.windowNumber,
24 context: nil, characters: characters, charactersIgnoringModifiers: ignoring, isARepeat: false, keyCode: code
25 )!
26 h.textView.keyDown(with: event)
27 }
28
29 @Test func prefixSequenceRunsAnOrgCommand() {
30 let h = Harness("* a\n")
31 var pending: [String?] = []
32 var next: [(key: String, title: String)] = []
33 h.editor.onKeysPending = { keys, continuations in
34 pending.append(keys)
35 if keys != nil { next = continuations }
36 }
37 h.caret(at: 1)
38 press(h, "c", control: true)
39 #expect(pending == ["C-c"])
40 #expect(next.contains { $0.key == "C-t" && $0.title == "Cycle TODO State" })
41 press(h, "t", control: true)
42 #expect(h.string == "* TODO a\n")
43 #expect(pending == ["C-c", nil])
44 h.checkInSync()
45 }
46
47 @Test func undefinedSequenceReportsAMessage() {
48 let h = Harness("text\n")
49 var messages: [String] = []
50 h.editor.onMessage = { messages.append($0) }
51 press(h, "c", control: true)
52 press(h, "z", control: true)
53 #expect(messages == ["C-c C-z is undefined"])
54 press(h, "x", control: true)
55 press(h, "g", control: true)
56 #expect(messages.last == "Quit")
57 #expect(h.string == "text\n")
58 }
59
60 @Test func tabFoldsOnHeadingsAndIndentsElsewhere() {
61 let h = Harness(outline)
62 h.caret(at: 1)
63 send(h, characters: "\t", ignoring: "\t", code: 48)
64 #expect(h.visibleLines == ["* a\n", "* c\n", "text\n"])
65 h.caret(at: h.offset(of: "text"))
66 send(h, characters: "\t", ignoring: "\t", code: 48)
67 #expect(h.string.hasSuffix("\ttext\n"))
68 }
69
70 @Test func markExtendsMovementAndKillTakesTheRegion() {
71 let h = Harness("abc\n")
72 h.caret(at: 0)
73 send(h, characters: "\0", ignoring: " ", code: 49, flags: .control)
74 #expect(h.editor.markActive)
75 press(h, "f", control: true)
76 press(h, "f", control: true)
77 #expect(h.textView.selectedRange() == NSRange(location: 0, length: 2))
78 press(h, "w", control: true)
79 #expect(h.string == "c\n")
80 #expect(!h.editor.markActive)
81 h.checkInSync()
82 }
83
84 @Test func leftOptionIsMetaRightOptionTypes() {
85 let h = Harness("one two\n")
86 h.caret(at: 0)
87 press(h, "f", option: KeyEvents.leftOption, typed: "ƒ")
88 #expect(h.caret == 3)
89 press(h, "f", option: KeyEvents.rightOption, typed: "ƒ")
90 #expect(h.string == "oneƒ two\n")
91 }
92
93 @Test func appCommandsGoToTheApp() {
94 let h = Harness("text\n")
95 var commands: [String] = []
96 h.editor.onAppCommand = { commands.append($0) }
97 press(h, "x", control: true)
98 press(h, "s", control: true)
99 #expect(commands == ["app.save"])
100 }
101
102 @Test func userKeymapOverridesThePreset() throws {
103 let h = Harness("* a\n")
104 var problems: [String] = []
105 let user = try Keymap(toml: "[[bind]]\nkeys = \"C-c C-t\"\ncommand = \"org.priority.up\"\n", problems: &problems)
106 h.editor.keymap = .layered([KeymapPreset.emacs.keymap, user])
107 h.caret(at: 1)
108 press(h, "c", control: true)
109 press(h, "t", control: true)
110 #expect(h.string == "* [#B] a\n")
111 }
112
113 @Test func promptsAskAndRunAgainWithTheAnswer() {
114 let h = Harness("* a\n")
115 var asked: [Prompt] = []
116 var reply: ((String?) -> Void)?
117 h.editor.onPrompt = { prompt, answer in
118 asked.append(prompt)
119 reply = answer
120 }
121 h.caret(at: 1)
122 press(h, "c", control: true)
123 press(h, "q", control: true)
124 #expect(asked.map(\.key) == ["tags"])
125 reply?("x")
126 #expect(h.string.hasPrefix("* a ") && h.string.hasSuffix(":x:\n"))
127 h.checkInSync()
128 }
129
130 @Test func everyPresetCommandExists() {
131 for preset in KeymapPreset.allCases {
132 for binding in preset.keymap.bindings {
133 #expect(EditorCommands.command(binding.command) != nil, "\(preset): \(binding.command)")
134 }
135 }
136 }
137
138 @Test func everyEmacsImportCommandExists() {
139 for (emacs, command) in EmacsImport.commands {
140 #expect(EditorCommands.command(command) != nil, "\(emacs): \(command)")
141 }
142 }
143}
144
145@MainActor
146struct ListKeyTests {
147 @Test func metaReturnInsertsAnItemInAListAndAHeadingElsewhere() {
148 let h = Harness("* a\n- x\n")
149 let event = { (flags: NSEvent.ModifierFlags) in
150 NSEvent.keyEvent(with: .keyDown, location: .zero, modifierFlags: flags, timestamp: 0, windowNumber: h.window.windowNumber,
151 context: nil, characters: "\r", charactersIgnoringModifiers: "\r", isARepeat: false, keyCode: 36)!
152 }
153 let meta = NSEvent.ModifierFlags(rawValue: NSEvent.ModifierFlags.option.rawValue | KeyEvents.leftOption)
154 h.caret(at: 7)
155 h.textView.keyDown(with: event(meta))
156 #expect(h.string == "* a\n- x\n- \n")
157 h.caret(at: 3)
158 h.textView.keyDown(with: event(meta))
159 #expect(h.string.hasPrefix("* a\n* \n"))
160 h.checkInSync()
161 }
162}