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