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
114type noopEvent struct{}
115
116type updater interface {
117 Update(msg tea.Msg) (tea.Model, tea.Cmd)
118}
119
120func simulateUpdate(up updater, msg tea.Msg) (updater, tea.Msg) {
121 up, cmd := up.Update(msg)
122 if cmd != nil {
123 return up, cmd()
124 }
125 return up, noopEvent{}
126}
127
128var pngMagicNumberData = []byte("\x89PNG\x0D\x0A\x1A\x0A")
129
130func mockResolveAbs(path string) (string, error) {
131 return path, nil
132}
133
134func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
135 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
136 require.NotNil(t, testEditor)
137
138 // Simulate pressing the '/' key
139 keyPressMsg := tea.KeyPressMsg{
140 Text: "/",
141 }
142
143 m, cmds := testEditor.Update(keyPressMsg)
144 testEditor = m.(*editorCmp)
145 cmds()
146
147 assert.True(t, testEditor.isCompletionsOpen)
148 assert.Equal(t, "/", testEditor.textarea.Value())
149}
150
151func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
152 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
153 require.NotNil(t, testEditor)
154
155 // First, give the editor focus
156 testEditor.Focus()
157
158 // Simulate pressing the '/' key when the editor is empty
159 // This should trigger the completions to open
160 keyPressMsg := tea.KeyPressMsg{
161 Text: "/",
162 }
163
164 m, cmds := testEditor.Update(keyPressMsg)
165 testEditor = m.(*editorCmp)
166 cmds()
167
168 // completions menu is open
169 assert.True(t, testEditor.isCompletionsOpen)
170 assert.Equal(t, "/", testEditor.textarea.Value())
171
172 // the query is empty (since we just opened it)
173 assert.Equal(t, "", testEditor.currentQuery)
174}
175
176func TestEditorAutoCompletion_OnNonImageFileFullPathInsertedFromQuery(t *testing.T) {
177 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
178 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
179 require.NotNil(t, testEditor)
180
181 // open the completions menu by simulating a '/' key press
182 testEditor.Focus()
183 keyPressMsg := tea.KeyPressMsg{
184 Text: "/",
185 }
186
187 m, msg := simulateUpdate(testEditor, keyPressMsg)
188 testEditor = m.(*editorCmp)
189
190 var openCompletionsMsg *completions.OpenCompletionsMsg
191 if batchMsg, ok := msg.(tea.BatchMsg); ok {
192 // Use our enhanced helper to check for OpenCompletionsMsg with specific completions
193 var found bool
194 openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"image.png", "random.txt"})
195 assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
196 } else {
197 t.Fatal("Expected BatchMsg from cmds()")
198 }
199
200 assert.NotNil(t, openCompletionsMsg)
201 require.True(t, testEditor.IsCompletionsOpen())
202
203 testEditor.textarea.SetValue("I am looking for a file called /random.tx")
204
205 keyPressMsg = tea.KeyPressMsg{
206 Text: "t",
207 }
208 m, msg = simulateUpdate(testEditor, keyPressMsg)
209 testEditor = m.(*editorCmp)
210
211
212 selectMsg := completions.SelectCompletionMsg{
213 Value: FileCompletionItem{
214 "./root/project/random.txt",
215 },
216 Insert: true,
217 }
218
219 m, msg = simulateUpdate(testEditor, selectMsg)
220 testEditor = m.(*editorCmp)
221
222 if _, ok := msg.(noopEvent); !ok {
223 t.Fatal("Expected noopEvent from cmds()")
224 }
225
226 assert.Equal(t, "I am looking for a file called ./root/project/random.txt", testEditor.textarea.Value())
227}
228
229func TestEditor_OnCompletionPathToImageEmitsAttachFileMessage(t *testing.T) {
230 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
231 fsys := fstest.MapFS{
232 "auto_completed_image.png": {
233 Data: pngMagicNumberData,
234 },
235 "random.txt": {
236 Data: []byte("Some content"),
237 },
238 }
239 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
240 model, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
241 testEditor = model.(*editorCmp)
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 model, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "random.txt"}, true, testEditor)
272 testEditor = model.(*editorCmp)
273
274 assert.Nil(t, cmd)
275}
276
277func TestEditor_OnPastePathToImageEmitsAttachFileMessage(t *testing.T) {
278 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
279 fsys := fstest.MapFS{
280 "image.png": {
281 Data: pngMagicNumberData,
282 },
283 "random.txt": {
284 Data: []byte("Some content"),
285 },
286 }
287 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
288 model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("image.png"))
289 testEditor = model.(*editorCmp)
290
291 require.NotNil(t, cmd)
292 msg := cmd()
293 assert.NotNil(t, msg)
294
295 var attachmentMsg message.Attachment
296 if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
297 attachmentMsg = fpickedMsg.Attachment
298 }
299
300 assert.Equal(t, message.Attachment{
301 FilePath: "image.png",
302 FileName: "image.png",
303 MimeType: "image/png",
304 Content: pngMagicNumberData,
305 }, attachmentMsg)
306}
307
308func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
309 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
310 fsys := fstest.MapFS{
311 "image.png": {
312 Data: pngMagicNumberData,
313 },
314 "random.txt": {
315 Data: []byte("Some content"),
316 },
317 }
318 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
319 model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("random.txt"))
320 testEditor = model.(*editorCmp)
321
322 assert.Nil(t, cmd)
323}
324
325// TestHelperFunctions demonstrates how to use the batch message helpers
326func TestHelperFunctions(t *testing.T) {
327 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
328 require.NotNil(t, testEditor)
329
330 // Simulate pressing the '/' key
331 testEditor.Focus()
332 keyPressMsg := tea.KeyPressMsg{
333 Text: "/",
334 }
335
336 m, cmds := testEditor.Update(keyPressMsg)
337 testEditor = m.(*editorCmp)
338
339 // Execute the command and check if it returns a BatchMsg
340 msg := cmds()
341 if batchMsg, ok := msg.(tea.BatchMsg); ok {
342 // Test our helper functions
343 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
344 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
345
346 // Test exact message helper
347 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
348 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
349
350 // Test specific completions helper
351 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
352 assert.NotNil(t, msg)
353 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
354 } else {
355 t.Fatal("Expected BatchMsg from cmds()")
356 }
357}