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