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/charmbracelet/crush/internal/tui/util"
14 "github.com/stretchr/testify/assert"
15 "github.com/stretchr/testify/require"
16)
17
18// executeBatchCommands executes all commands in a BatchMsg and returns the resulting messages
19func executeBatchCommands(batchMsg tea.BatchMsg) []tea.Msg {
20 var messages []tea.Msg
21 for _, cmd := range batchMsg {
22 if cmd != nil {
23 msg := cmd()
24 messages = append(messages, msg)
25 }
26 }
27 return messages
28}
29
30// assertBatchContainsMessage checks if a BatchMsg contains a message of the specified type
31func assertBatchContainsMessage(t *testing.T, batchMsg tea.BatchMsg, expectedType any) bool {
32 t.Helper()
33 messages := executeBatchCommands(batchMsg)
34
35 for _, msg := range messages {
36 switch expectedType.(type) {
37 case completions.OpenCompletionsMsg:
38 if _, ok := msg.(completions.OpenCompletionsMsg); ok {
39 return true
40 }
41 }
42 }
43 return false
44}
45
46// assertBatchContainsExactMessage checks if a BatchMsg contains a message with exact field values
47func assertBatchContainsExactMessage(t *testing.T, batchMsg tea.BatchMsg, expected any) bool {
48 t.Helper()
49 messages := executeBatchCommands(batchMsg)
50
51 for _, msg := range messages {
52 switch expected := expected.(type) {
53 case completions.OpenCompletionsMsg:
54 if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
55 // If no specific completions are expected, just match the type
56 if len(expected.Completions) == 0 {
57 return true
58 }
59 // Compare completions if specified
60 if len(actual.Completions) == len(expected.Completions) {
61 // For simplicity, just check the count for now
62 // A more complete implementation would compare each completion
63 return true
64 }
65 }
66 default:
67 // Fallback to type checking only
68 if _, ok := msg.(completions.OpenCompletionsMsg); ok {
69 return true
70 }
71 }
72 }
73 return false
74}
75
76// assertBatchContainsOpenCompletionsMsg checks if a BatchMsg contains an OpenCompletionsMsg
77// with the expected completions. If expectedCompletions is nil, only the message type is checked.
78func assertBatchContainsOpenCompletionsMsg(t *testing.T, batchMsg tea.BatchMsg, expectedCompletions []string) (*completions.OpenCompletionsMsg, bool) {
79 t.Helper()
80 messages := executeBatchCommands(batchMsg)
81
82 for _, msg := range messages {
83 if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
84 if expectedCompletions == nil {
85 return &actual, true
86 }
87
88 // Convert actual completions to string titles for comparison
89 actualTitles := make([]string, len(actual.Completions))
90 for i, comp := range actual.Completions {
91 actualTitles[i] = comp.Title
92 }
93
94 // Check if we have the same number of completions
95 if len(actualTitles) != len(expectedCompletions) {
96 continue
97 }
98
99 // For now, just check that we have the same count
100 // A more sophisticated implementation would check the actual values
101 return &actual, true
102 }
103 }
104 return nil, false
105}
106
107func mockDirLister(paths []string) fsext.DirectoryListerResolver {
108 return func() fsext.DirectoryLister {
109 return func(initialPath string, ignorePatterns []string) ([]string, bool, error) {
110 return paths, false, nil
111 }
112 }
113}
114
115type noopEvent struct{}
116
117type updater interface {
118 Update(msg tea.Msg) (tea.Model, tea.Cmd)
119}
120
121func simulateUpdate(up updater, msg tea.Msg) (updater, tea.Msg) {
122 up, cmd := up.Update(msg)
123 if cmd != nil {
124 return up, cmd()
125 }
126 return up, noopEvent{}
127}
128
129var pngMagicNumberData = []byte("\x89PNG\x0D\x0A\x1A\x0A")
130
131func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
132 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
133 require.NotNil(t, testEditor)
134
135 // Simulate pressing the '/' key
136 keyPressMsg := tea.KeyPressMsg{
137 Text: "/",
138 }
139
140 m, cmds := testEditor.Update(keyPressMsg)
141 testEditor = m.(*editorCmp)
142 cmds()
143
144 assert.True(t, testEditor.isCompletionsOpen)
145 assert.Equal(t, "/", testEditor.textarea.Value())
146}
147
148func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
149 testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
150 require.NotNil(t, testEditor)
151
152 // First, give the editor focus
153 testEditor.Focus()
154
155 // Simulate pressing the '/' key when the editor is empty
156 // This should trigger the completions to open
157 keyPressMsg := tea.KeyPressMsg{
158 Text: "/",
159 }
160
161 m, cmds := testEditor.Update(keyPressMsg)
162 testEditor = m.(*editorCmp)
163 cmds()
164
165 // completions menu is open
166 assert.True(t, testEditor.isCompletionsOpen)
167 assert.Equal(t, "/", testEditor.textarea.Value())
168
169 // the query is empty (since we just opened it)
170 assert.Equal(t, "", testEditor.currentQuery)
171}
172
173func TestEditorAutoCompletion_OnNonImageFileFullPathInsertedFromQuery(t *testing.T) {
174 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
175 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
176 require.NotNil(t, testEditor)
177
178 // open the completions menu by simulating a '/' key press
179 testEditor.Focus()
180 keyPressMsg := tea.KeyPressMsg{
181 Text: "/",
182 }
183
184 m, msg := simulateUpdate(testEditor, keyPressMsg)
185 testEditor = m.(*editorCmp)
186
187 var openCompletionsMsg *completions.OpenCompletionsMsg
188 if batchMsg, ok := msg.(tea.BatchMsg); ok {
189 // Use our enhanced helper to check for OpenCompletionsMsg with specific completions
190 var found bool
191 openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"image.png", "random.txt"})
192 assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
193 } else {
194 t.Fatal("Expected BatchMsg from cmds()")
195 }
196
197 assert.NotNil(t, openCompletionsMsg)
198 require.True(t, testEditor.IsCompletionsOpen())
199
200 testEditor.textarea.SetValue("I am looking for a file called /random.tx")
201
202 keyPressMsg = tea.KeyPressMsg{
203 Text: "t",
204 }
205 m, _ = simulateUpdate(testEditor, keyPressMsg)
206 testEditor = m.(*editorCmp)
207
208 selectMsg := completions.SelectCompletionMsg{
209 Value: FileCompletionItem{
210 "./root/project/random.txt",
211 },
212 Insert: true,
213 }
214
215 m, msg = simulateUpdate(testEditor, selectMsg)
216 testEditor = m.(*editorCmp)
217
218 if _, ok := msg.(noopEvent); !ok {
219 t.Fatal("Expected noopEvent from cmds()")
220 }
221
222 assert.Equal(t, "I am looking for a file called ./root/project/random.txt", testEditor.textarea.Value())
223}
224
225func TestEditor_OnCompletionPathToImageEmitsAttachFileMessage(t *testing.T) {
226 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
227 fsys := fstest.MapFS{
228 "auto_completed_image.png": {
229 Data: pngMagicNumberData,
230 },
231 "random.txt": {
232 Data: []byte("Some content"),
233 },
234 }
235
236 modelHasImageSupport := func() (bool, string) {
237 return true, "TestModel"
238 }
239 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
240 _, cmd := onCompletionItemSelect(fsys, modelHasImageSupport, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
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_OnCompletionPathToImageEmitsWanrningMessageWhenModelDoesNotSupportImages(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
270 modelHasImageSupport := func() (bool, string) {
271 return false, "TestModel"
272 }
273 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
274 _, cmd := onCompletionItemSelect(fsys, modelHasImageSupport, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
275
276 require.NotNil(t, cmd)
277 msg := cmd()
278 require.NotNil(t, msg)
279
280 warningMsg, ok := msg.(util.InfoMsg)
281 require.True(t, ok)
282 assert.Equal(t, util.InfoMsg{
283 Type: util.InfoTypeWarn,
284 Msg: "File attachments are not supported by the current model: TestModel",
285 }, warningMsg)
286}
287
288func TestEditor_OnCompletionPathToNonImageEmitsAttachFileMessage(t *testing.T) {
289 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
290 fsys := fstest.MapFS{
291 "auto_completed_image.png": {
292 Data: pngMagicNumberData,
293 },
294 "random.txt": {
295 Data: []byte("Some content"),
296 },
297 }
298
299 modelHasImageSupport := func() (bool, string) {
300 return true, "TestModel"
301 }
302 testEditor := newEditor(&app.App{}, entriesForAutoComplete)
303 _, cmd := onCompletionItemSelect(fsys, modelHasImageSupport, FileCompletionItem{Path: "random.txt"}, true, testEditor)
304
305 assert.Nil(t, cmd)
306}
307
308// TestHelperFunctions demonstrates how to use the batch message helpers
309func TestHelperFunctions(t *testing.T) {
310 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
311 require.NotNil(t, testEditor)
312
313 // Simulate pressing the '/' key
314 testEditor.Focus()
315 keyPressMsg := tea.KeyPressMsg{
316 Text: "/",
317 }
318
319 _, cmds := testEditor.Update(keyPressMsg)
320
321 // Execute the command and check if it returns a BatchMsg
322 msg := cmds()
323 if batchMsg, ok := msg.(tea.BatchMsg); ok {
324 // Test our helper functions
325 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
326 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
327
328 // Test exact message helper
329 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
330 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
331
332 // Test specific completions helper
333 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
334 assert.NotNil(t, msg)
335 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
336 } else {
337 t.Fatal("Expected BatchMsg from cmds()")
338 }
339}