1package editor
2
3import (
4 "testing"
5
6 tea "github.com/charmbracelet/bubbletea/v2"
7 "github.com/charmbracelet/crush/internal/app"
8 "github.com/charmbracelet/crush/internal/fsext"
9 "github.com/charmbracelet/crush/internal/tui/components/completions"
10 "github.com/stretchr/testify/assert"
11 "github.com/stretchr/testify/require"
12)
13
14// executeBatchCommands executes all commands in a BatchMsg and returns the resulting messages
15func executeBatchCommands(batchMsg tea.BatchMsg) []tea.Msg {
16 var messages []tea.Msg
17 for _, cmd := range batchMsg {
18 if cmd != nil {
19 msg := cmd()
20 messages = append(messages, msg)
21 }
22 }
23 return messages
24}
25
26// assertBatchContainsMessage checks if a BatchMsg contains a message of the specified type
27func assertBatchContainsMessage(t *testing.T, batchMsg tea.BatchMsg, expectedType interface{}) bool {
28 t.Helper()
29 messages := executeBatchCommands(batchMsg)
30
31 for _, msg := range messages {
32 switch expectedType.(type) {
33 case completions.OpenCompletionsMsg:
34 if _, ok := msg.(completions.OpenCompletionsMsg); ok {
35 return true
36 }
37 }
38 }
39 return false
40}
41
42// assertBatchContainsExactMessage checks if a BatchMsg contains a message with exact field values
43func assertBatchContainsExactMessage(t *testing.T, batchMsg tea.BatchMsg, expected interface{}) bool {
44 t.Helper()
45 messages := executeBatchCommands(batchMsg)
46
47 for _, msg := range messages {
48 switch expected := expected.(type) {
49 case completions.OpenCompletionsMsg:
50 if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
51 // If no specific completions are expected, just match the type
52 if len(expected.Completions) == 0 {
53 return true
54 }
55 // Compare completions if specified
56 if len(actual.Completions) == len(expected.Completions) {
57 // For simplicity, just check the count for now
58 // A more complete implementation would compare each completion
59 return true
60 }
61 }
62 default:
63 // Fallback to type checking only
64 if _, ok := msg.(completions.OpenCompletionsMsg); ok {
65 return true
66 }
67 }
68 }
69 return false
70}
71
72// assertBatchContainsOpenCompletionsMsg checks if a BatchMsg contains an OpenCompletionsMsg
73// with the expected completions. If expectedCompletions is nil, only the message type is checked.
74func assertBatchContainsOpenCompletionsMsg(t *testing.T, batchMsg tea.BatchMsg, expectedCompletions []string) (*completions.OpenCompletionsMsg, bool) {
75 t.Helper()
76 messages := executeBatchCommands(batchMsg)
77
78 for _, msg := range messages {
79 if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
80 if expectedCompletions == nil {
81 return &actual, true
82 }
83
84 // Convert actual completions to string titles for comparison
85 actualTitles := make([]string, len(actual.Completions))
86 for i, comp := range actual.Completions {
87 actualTitles[i] = comp.Title
88 }
89
90 // Check if we have the same number of completions
91 if len(actualTitles) != len(expectedCompletions) {
92 continue
93 }
94
95 // For now, just check that we have the same count
96 // A more sophisticated implementation would check the actual values
97 return &actual, true
98 }
99 }
100 return nil, false
101}
102
103func mockDirLister(paths []string) fsext.DirectoryListerResolver {
104 return func() fsext.DirectoryLister {
105 return func(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
106 return paths, false, nil
107 }
108 }
109}
110
111func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
112 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
113 require.NotNil(t, testEditor)
114
115 // Simulate pressing the '/' key
116 keyPressMsg := tea.KeyPressMsg{
117 Text: "/",
118 }
119
120 m, cmds := testEditor.Update(keyPressMsg)
121 testEditor = m.(*editorCmp)
122 cmds()
123
124 assert.True(t, testEditor.isCompletionsOpen)
125 assert.Equal(t, "/", testEditor.textarea.Value())
126}
127
128func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
129 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
130 require.NotNil(t, testEditor)
131
132 // First, give the editor focus
133 testEditor.Focus()
134
135 // Simulate pressing the '/' key when the editor is empty
136 // This should trigger the completions to open
137 keyPressMsg := tea.KeyPressMsg{
138 Text: "/",
139 }
140
141 m, cmds := testEditor.Update(keyPressMsg)
142 testEditor = m.(*editorCmp)
143 cmds()
144
145 // completions menu is open
146 assert.True(t, testEditor.isCompletionsOpen)
147 assert.Equal(t, "/", testEditor.textarea.Value())
148
149 // the query is empty (since we just opened it)
150 assert.Equal(t, "", testEditor.currentQuery)
151}
152
153func TestEditorAutocompletion_StartFilteringOpens(t *testing.T) {
154 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
155 require.NotNil(t, testEditor)
156
157 // open the completions menu by simulating a '/' key press
158 testEditor.Focus()
159 keyPressMsg := tea.KeyPressMsg{
160 Text: "/",
161 }
162
163 m, cmds := testEditor.Update(keyPressMsg)
164 testEditor = m.(*editorCmp)
165
166 msg := cmds()
167 var openCompletionsMsg *completions.OpenCompletionsMsg
168 if batchMsg, ok := msg.(tea.BatchMsg); ok {
169 // Use our enhanced helper to check for OpenCompletionsMsg with specific completions
170 var found bool
171 openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
172 assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
173 } else {
174 t.Fatal("Expected BatchMsg from cmds()")
175 }
176
177 assert.NotNil(t, openCompletionsMsg)
178 m, cmds = testEditor.Update(openCompletionsMsg)
179 if
180 msg = cmds()
181 testEditor = m.(*editorCmp)
182
183 if batchMsg, ok := msg.(tea.BatchMsg); ok {
184 assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
185 } else {
186 t.Fatal("Expected BatchMsg from cmds()")
187 }
188
189 // Verify completions menu is open
190 assert.True(t, testEditor.isCompletionsOpen)
191 assert.Equal(t, "/", testEditor.textarea.Value())
192
193 // Now simulate typing a query to filter the completions
194 // Set the text to "/tes" and then simulate typing "t" to make "/test"
195 testEditor.textarea.SetValue("/tes")
196
197 // Simulate typing a key that would trigger filtering
198 keyPressMsg = tea.KeyPressMsg{
199 Text: "t",
200 }
201
202 m, cmds = testEditor.Update(keyPressMsg)
203 msg = cmds()
204 testEditor = m.(*editorCmp)
205
206 // Verify the editor still has completions open
207 assert.True(t, testEditor.isCompletionsOpen)
208
209 // The currentQuery should be updated based on what we typed
210 // In this case, it would be "test" (the word after the initial '/')
211 // Note: The actual filtering is handled by the completions component,
212 // so we're just verifying the editor's state is correct
213 assert.Equal(t, "test", testEditor.currentQuery)
214
215 keyPressMsg = tea.KeyPressMsg{
216 Code: tea.KeyEnter,
217 }
218
219 m, cmds = testEditor.Update(keyPressMsg)
220 msg = cmds()
221 testEditor = m.(*editorCmp)
222
223 if batchMsg, ok := msg.(tea.BatchMsg); ok {
224 assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
225 } else {
226 t.Fatal("Expected BatchMsg from cmds()")
227 }
228
229 m, cmds = testEditor.Update(msg)
230 msg = cmds()
231 testEditor = m.(*editorCmp)
232 // Verify the editor still has completions open
233 assert.True(t, testEditor.isCompletionsOpen)
234}
235
236func TestEditorAutocompletion_SelectionOfNormalPathAddsToTextAreaClosesCompletion(t *testing.T) {
237 testEditor := newEditor(&app.App{}, mockDirLister([]string{"example_test.go", "file1.txt", "file2.txt"}))
238 require.NotNil(t, testEditor)
239
240 // open the completions menu by simulating a '/' key press
241 testEditor.Focus()
242 keyPressMsg := tea.KeyPressMsg{
243 Text: "/",
244 }
245
246 m, cmds := testEditor.Update(keyPressMsg)
247 testEditor = m.(*editorCmp)
248
249 msg := cmds()
250 assert.NotNil(t, msg)
251 m, cmds = testEditor.Update(msg)
252
253 // Now simulate typing a query to filter the completions
254 // Set the text to "/tes" and then simulate typing "t" to make "/test"
255 testEditor.textarea.SetValue("/tes")
256
257 // Simulate typing a key that would trigger filtering
258 keyPressMsg = tea.KeyPressMsg{
259 Text: "t",
260 }
261
262 m, cmds = testEditor.Update(keyPressMsg)
263 testEditor = m.(*editorCmp)
264
265 // The currentQuery should be updated based on what we typed
266 // In this case, it would be "test" (the word after the initial '/')
267 // Note: The actual filtering is handled by the completions component,
268 // so we're just verifying the editor's state is correct
269 assert.Equal(t, "test", testEditor.currentQuery)
270}
271
272// TestHelperFunctions demonstrates how to use the batch message helpers
273func TestHelperFunctions(t *testing.T) {
274 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
275 require.NotNil(t, testEditor)
276
277 // Simulate pressing the '/' key
278 testEditor.Focus()
279 keyPressMsg := tea.KeyPressMsg{
280 Text: "/",
281 }
282
283 m, cmds := testEditor.Update(keyPressMsg)
284 testEditor = m.(*editorCmp)
285
286 // Execute the command and check if it returns a BatchMsg
287 msg := cmds()
288 if batchMsg, ok := msg.(tea.BatchMsg); ok {
289 // Test our helper functions
290 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
291 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
292
293 // Test exact message helper
294 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
295 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
296
297 // Test specific completions helper
298 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
299 assert.NotNil(t, msg)
300 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
301 } else {
302 t.Fatal("Expected BatchMsg from cmds()")
303 }
304}