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