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 selectMsg := completions.SelectCompletionMsg{
212 Value: FileCompletionItem{
213 "./root/project/random.txt",
214 },
215 Insert: true,
216 }
217
218 m, msg = simulateUpdate(testEditor, selectMsg)
219 testEditor = m.(*editorCmp)
220
221 if _, ok := msg.(noopEvent); !ok {
222 t.Fatal("Expected noopEvent from cmds()")
223 }
224
225 assert.Equal(t, "I am looking for a file called ./root/project/random.txt", testEditor.textarea.Value())
226}
227
228func TestEditor_OnCompletionPathToImageEmitsAttachFileMessage(t *testing.T) {
229 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
230 fsys := fstest.MapFS{
231 "auto_completed_image.png": {
232 Data: pngMagicNumberData,
233 },
234 "random.txt": {
235 Data: []byte("Some content"),
236 },
237 }
238 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
239 model, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
240 testEditor = model.(*editorCmp)
241
242 require.NotNil(t, cmd)
243 msg := cmd()
244 require.NotNil(t, msg)
245
246 var attachmentMsg message.Attachment
247 if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
248 attachmentMsg = fpickedMsg.Attachment
249 }
250
251 assert.Equal(t, message.Attachment{
252 FilePath: "auto_completed_image.png",
253 FileName: "auto_completed_image.png",
254 MimeType: "image/png",
255 Content: pngMagicNumberData,
256 }, attachmentMsg)
257}
258
259func TestEditor_OnCompletionPathToNonImageEmitsAttachFileMessage(t *testing.T) {
260 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
261 fsys := fstest.MapFS{
262 "auto_completed_image.png": {
263 Data: pngMagicNumberData,
264 },
265 "random.txt": {
266 Data: []byte("Some content"),
267 },
268 }
269 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
270 model, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "random.txt"}, true, testEditor)
271 testEditor = model.(*editorCmp)
272
273 assert.Nil(t, cmd)
274}
275
276func TestEditor_OnPastePathToImageEmitsAttachFileMessage(t *testing.T) {
277 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
278 fsys := fstest.MapFS{
279 "image.png": {
280 Data: pngMagicNumberData,
281 },
282 "random.txt": {
283 Data: []byte("Some content"),
284 },
285 }
286 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
287 model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("image.png"))
288 testEditor = model.(*editorCmp)
289
290 require.NotNil(t, cmd)
291 msg := cmd()
292 assert.NotNil(t, msg)
293
294 var attachmentMsg message.Attachment
295 if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
296 attachmentMsg = fpickedMsg.Attachment
297 }
298
299 assert.Equal(t, message.Attachment{
300 FilePath: "image.png",
301 FileName: "image.png",
302 MimeType: "image/png",
303 Content: pngMagicNumberData,
304 }, attachmentMsg)
305}
306
307func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
308 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
309 fsys := fstest.MapFS{
310 "image.png": {
311 Data: pngMagicNumberData,
312 },
313 "random.txt": {
314 Data: []byte("Some content"),
315 },
316 }
317 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
318 model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("random.txt"))
319 testEditor = model.(*editorCmp)
320
321 assert.Nil(t, cmd)
322}
323
324// TestHelperFunctions demonstrates how to use the batch message helpers
325func TestHelperFunctions(t *testing.T) {
326 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
327 require.NotNil(t, testEditor)
328
329 // Simulate pressing the '/' key
330 testEditor.Focus()
331 keyPressMsg := tea.KeyPressMsg{
332 Text: "/",
333 }
334
335 m, cmds := testEditor.Update(keyPressMsg)
336 testEditor = m.(*editorCmp)
337
338 // Execute the command and check if it returns a BatchMsg
339 msg := cmds()
340 if batchMsg, ok := msg.(tea.BatchMsg); ok {
341 // Test our helper functions
342 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
343 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
344
345 // Test exact message helper
346 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
347 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
348
349 // Test specific completions helper
350 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
351 assert.NotNil(t, msg)
352 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
353 } else {
354 t.Fatal("Expected BatchMsg from cmds()")
355 }
356}