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 _, cmd := onPaste(filepath.Abs, testEditor, tea.PasteMsg("image.png"))
302
303 require.NotNil(t, cmd)
304 msg := cmd()
305 assert.NotNil(t, msg)
306
307 var attachmentMsg message.Attachment
308 if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
309 attachmentMsg = fpickedMsg.Attachment
310 }
311
312 assert.Equal(t, message.Attachment{
313 FilePath: imagePath,
314 FileName: "image.png",
315 MimeType: "image/png",
316 Content: pngMagicNumberData,
317 }, attachmentMsg)
318}
319
320func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
321 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
322
323 // Create a temporary directory and files for testing
324 tempDir := t.TempDir()
325
326 // Create test image file
327 imagePath := filepath.Join(tempDir, "image.png")
328 err := os.WriteFile(imagePath, pngMagicNumberData, 0o644)
329 require.NoError(t, err)
330
331 // Create test text file
332 textPath := filepath.Join(tempDir, "random.txt")
333 err = os.WriteFile(textPath, []byte("Some content"), 0o644)
334 require.NoError(t, err)
335
336 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
337
338 // Change to temp directory so paths resolve correctly
339 originalWd, err := os.Getwd()
340 require.NoError(t, err)
341 defer os.Chdir(originalWd)
342 err = os.Chdir(tempDir)
343 require.NoError(t, err)
344
345 _, cmd := onPaste(filepath.Abs, testEditor, tea.PasteMsg("random.txt"))
346
347 assert.Nil(t, cmd)
348}
349
350// TestHelperFunctions demonstrates how to use the batch message helpers
351func TestHelperFunctions(t *testing.T) {
352 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
353 require.NotNil(t, testEditor)
354
355 // Simulate pressing the '/' key
356 testEditor.Focus()
357 keyPressMsg := tea.KeyPressMsg{
358 Text: "/",
359 }
360
361 _, cmds := testEditor.Update(keyPressMsg)
362
363 // Execute the command and check if it returns a BatchMsg
364 msg := cmds()
365 if batchMsg, ok := msg.(tea.BatchMsg); ok {
366 // Test our helper functions
367 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
368 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
369
370 // Test exact message helper
371 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
372 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
373
374 // Test specific completions helper
375 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
376 assert.NotNil(t, msg)
377 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
378 } else {
379 t.Fatal("Expected BatchMsg from cmds()")
380 }
381}