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("/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, "./root/project/random.txt", testEditor.textarea.Value())
227}
228
229func TestEditor_OnPastePathToImageEmitsAttachFileMessage(t *testing.T) {
230 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
231 fsys := fstest.MapFS{
232 "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 := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("image.png"))
241 testEditor = model.(*editorCmp)
242
243 require.NotNil(t, cmd)
244 msg := cmd()
245 assert.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: "image.png",
254 FileName: "image.png",
255 MimeType: "image/png",
256 Content: pngMagicNumberData,
257 }, attachmentMsg)
258}
259
260func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
261 entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
262 fsys := fstest.MapFS{
263 "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 := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("random.txt"))
272 testEditor = model.(*editorCmp)
273
274 assert.Nil(t, cmd)
275}
276
277// TestHelperFunctions demonstrates how to use the batch message helpers
278func TestHelperFunctions(t *testing.T) {
279 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
280 require.NotNil(t, testEditor)
281
282 // Simulate pressing the '/' key
283 testEditor.Focus()
284 keyPressMsg := tea.KeyPressMsg{
285 Text: "/",
286 }
287
288 m, cmds := testEditor.Update(keyPressMsg)
289 testEditor = m.(*editorCmp)
290
291 // Execute the command and check if it returns a BatchMsg
292 msg := cmds()
293 if batchMsg, ok := msg.(tea.BatchMsg); ok {
294 // Test our helper functions
295 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
296 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
297
298 // Test exact message helper
299 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
300 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
301
302 // Test specific completions helper
303 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
304 assert.NotNil(t, msg)
305 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
306 } else {
307 t.Fatal("Expected BatchMsg from cmds()")
308 }
309}