editor_test.go

  1package editor
  2
  3import (
  4	"os"
  5	"path/filepath"
  6	"testing"
  7	"testing/fstest"
  8
  9	tea "github.com/charmbracelet/bubbletea/v2"
 10	"github.com/charmbracelet/crush/internal/app"
 11	"github.com/charmbracelet/crush/internal/fsext"
 12	"github.com/charmbracelet/crush/internal/message"
 13	"github.com/charmbracelet/crush/internal/tui/components/completions"
 14	"github.com/charmbracelet/crush/internal/tui/components/dialogs/filepicker"
 15	"github.com/stretchr/testify/assert"
 16	"github.com/stretchr/testify/require"
 17)
 18
 19// executeBatchCommands executes all commands in a BatchMsg and returns the resulting messages
 20func executeBatchCommands(batchMsg tea.BatchMsg) []tea.Msg {
 21	var messages []tea.Msg
 22	for _, cmd := range batchMsg {
 23		if cmd != nil {
 24			msg := cmd()
 25			messages = append(messages, msg)
 26		}
 27	}
 28	return messages
 29}
 30
 31// assertBatchContainsMessage checks if a BatchMsg contains a message of the specified type
 32func assertBatchContainsMessage(t *testing.T, batchMsg tea.BatchMsg, expectedType interface{}) bool {
 33	t.Helper()
 34	messages := executeBatchCommands(batchMsg)
 35
 36	for _, msg := range messages {
 37		switch expectedType.(type) {
 38		case completions.OpenCompletionsMsg:
 39			if _, ok := msg.(completions.OpenCompletionsMsg); ok {
 40				return true
 41			}
 42		}
 43	}
 44	return false
 45}
 46
 47// assertBatchContainsExactMessage checks if a BatchMsg contains a message with exact field values
 48func assertBatchContainsExactMessage(t *testing.T, batchMsg tea.BatchMsg, expected interface{}) bool {
 49	t.Helper()
 50	messages := executeBatchCommands(batchMsg)
 51
 52	for _, msg := range messages {
 53		switch expected := expected.(type) {
 54		case completions.OpenCompletionsMsg:
 55			if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
 56				// If no specific completions are expected, just match the type
 57				if len(expected.Completions) == 0 {
 58					return true
 59				}
 60				// Compare completions if specified
 61				if len(actual.Completions) == len(expected.Completions) {
 62					// For simplicity, just check the count for now
 63					// A more complete implementation would compare each completion
 64					return true
 65				}
 66			}
 67		default:
 68			// Fallback to type checking only
 69			if _, ok := msg.(completions.OpenCompletionsMsg); ok {
 70				return true
 71			}
 72		}
 73	}
 74	return false
 75}
 76
 77// assertBatchContainsOpenCompletionsMsg checks if a BatchMsg contains an OpenCompletionsMsg
 78// with the expected completions. If expectedCompletions is nil, only the message type is checked.
 79func assertBatchContainsOpenCompletionsMsg(t *testing.T, batchMsg tea.BatchMsg, expectedCompletions []string) (*completions.OpenCompletionsMsg, bool) {
 80	t.Helper()
 81	messages := executeBatchCommands(batchMsg)
 82
 83	for _, msg := range messages {
 84		if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
 85			if expectedCompletions == nil {
 86				return &actual, true
 87			}
 88
 89			// Convert actual completions to string titles for comparison
 90			actualTitles := make([]string, len(actual.Completions))
 91			for i, comp := range actual.Completions {
 92				actualTitles[i] = comp.Title
 93			}
 94
 95			// Check if we have the same number of completions
 96			if len(actualTitles) != len(expectedCompletions) {
 97				continue
 98			}
 99
100			// For now, just check that we have the same count
101			// A more sophisticated implementation would check the actual values
102			return &actual, true
103		}
104	}
105	return nil, false
106}
107
108func mockDirLister(paths []string) fsext.DirectoryListerResolver {
109	return func() fsext.DirectoryLister {
110		return func(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
111			return paths, false, nil
112		}
113	}
114}
115
116type noopEvent struct{}
117
118type updater interface {
119	Update(msg tea.Msg) (tea.Model, tea.Cmd)
120}
121
122func simulateUpdate(up updater, msg tea.Msg) (updater, tea.Msg) {
123	up, cmd := up.Update(msg)
124	if cmd != nil {
125		return up, cmd()
126	}
127	return up, noopEvent{}
128}
129
130var pngMagicNumberData = []byte("\x89PNG\x0D\x0A\x1A\x0A")
131
132func mockResolveAbs(path string) (string, error) {
133	return path, nil
134}
135
136func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
137	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
138	require.NotNil(t, testEditor)
139
140	// Simulate pressing the '/' key
141	keyPressMsg := tea.KeyPressMsg{
142		Text: "/",
143	}
144
145	m, cmds := testEditor.Update(keyPressMsg)
146	testEditor = m.(*editorCmp)
147	cmds()
148
149	assert.True(t, testEditor.isCompletionsOpen)
150	assert.Equal(t, "/", testEditor.textarea.Value())
151}
152
153func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
154	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
155	require.NotNil(t, testEditor)
156
157	// First, give the editor focus
158	testEditor.Focus()
159
160	// Simulate pressing the '/' key when the editor is empty
161	// This should trigger the completions to open
162	keyPressMsg := tea.KeyPressMsg{
163		Text: "/",
164	}
165
166	m, cmds := testEditor.Update(keyPressMsg)
167	testEditor = m.(*editorCmp)
168	cmds()
169
170	// completions menu is open
171	assert.True(t, testEditor.isCompletionsOpen)
172	assert.Equal(t, "/", testEditor.textarea.Value())
173
174	// the query is empty (since we just opened it)
175	assert.Equal(t, "", testEditor.currentQuery)
176}
177
178func TestEditorAutoCompletion_OnNonImageFileFullPathInsertedFromQuery(t *testing.T) {
179	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
180	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
181	require.NotNil(t, testEditor)
182
183	// open the completions menu by simulating a '/' key press
184	testEditor.Focus()
185	keyPressMsg := tea.KeyPressMsg{
186		Text: "/",
187	}
188
189	m, msg := simulateUpdate(testEditor, keyPressMsg)
190	testEditor = m.(*editorCmp)
191
192	var openCompletionsMsg *completions.OpenCompletionsMsg
193	if batchMsg, ok := msg.(tea.BatchMsg); ok {
194		// Use our enhanced helper to check for OpenCompletionsMsg with specific completions
195		var found bool
196		openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"image.png", "random.txt"})
197		assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
198	} else {
199		t.Fatal("Expected BatchMsg from cmds()")
200	}
201
202	assert.NotNil(t, openCompletionsMsg)
203	require.True(t, testEditor.IsCompletionsOpen())
204
205	testEditor.textarea.SetValue("I am looking for a file called /random.tx")
206
207	keyPressMsg = tea.KeyPressMsg{
208		Text: "t",
209	}
210	m, _ = simulateUpdate(testEditor, keyPressMsg)
211	testEditor = m.(*editorCmp)
212
213	selectMsg := completions.SelectCompletionMsg{
214		Value: FileCompletionItem{
215			"./root/project/random.txt",
216		},
217		Insert: true,
218	}
219
220	m, msg = simulateUpdate(testEditor, selectMsg)
221	testEditor = m.(*editorCmp)
222
223	if _, ok := msg.(noopEvent); !ok {
224		t.Fatal("Expected noopEvent from cmds()")
225	}
226
227	assert.Equal(t, "I am looking for a file called ./root/project/random.txt", testEditor.textarea.Value())
228}
229
230func TestEditor_OnCompletionPathToImageEmitsAttachFileMessage(t *testing.T) {
231	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
232	fsys := fstest.MapFS{
233		"auto_completed_image.png": {
234			Data: pngMagicNumberData,
235		},
236		"random.txt": {
237			Data: []byte("Some content"),
238		},
239	}
240	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
241	_, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
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	_, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "random.txt"}, true, testEditor)
272
273	assert.Nil(t, cmd)
274}
275
276func TestEditor_OnPastePathToImageEmitsAttachFileMessage(t *testing.T) {
277	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
278
279	// Create a temporary directory and files for testing
280	tempDir := t.TempDir()
281
282	// Create test image file
283	imagePath := filepath.Join(tempDir, "image.png")
284	err := os.WriteFile(imagePath, pngMagicNumberData, 0o644)
285	require.NoError(t, err)
286
287	// Create test text file
288	textPath := filepath.Join(tempDir, "random.txt")
289	err = os.WriteFile(textPath, []byte("Some content"), 0o644)
290	require.NoError(t, err)
291
292	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
293
294	// Change to temp directory so paths resolve correctly
295	originalWd, err := os.Getwd()
296	require.NoError(t, err)
297	defer os.Chdir(originalWd)
298	err = os.Chdir(tempDir)
299	require.NoError(t, err)
300
301	absRef := filepath.Abs
302	_, cmd := onPaste(absRef, testEditor, tea.PasteMsg("image.png"))
303
304	require.NotNil(t, cmd)
305	msg := cmd()
306	assert.NotNil(t, msg)
307
308	var attachmentMsg message.Attachment
309	if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
310		attachmentMsg = fpickedMsg.Attachment
311	}
312
313	assert.NoError(t, err)
314
315	// Create a copy of the attachment for comparison, but use the actual FilePath from the message
316	// This handles the case on macOS where the path might have a "/private" prefix
317	expectedAttachment := message.Attachment{
318		FilePath: attachmentMsg.FilePath, // Use the actual path from the message
319		FileName: "image.png",
320		MimeType: "image/png",
321		Content:  pngMagicNumberData,
322	}
323
324	assert.Equal(t, expectedAttachment, attachmentMsg)
325}
326
327func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
328	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
329
330	// Create a temporary directory and files for testing
331	tempDir := t.TempDir()
332
333	// Create test image file
334	imagePath := filepath.Join(tempDir, "image.png")
335	err := os.WriteFile(imagePath, pngMagicNumberData, 0o644)
336	require.NoError(t, err)
337
338	// Create test text file
339	textPath := filepath.Join(tempDir, "random.txt")
340	err = os.WriteFile(textPath, []byte("Some content"), 0o644)
341	require.NoError(t, err)
342
343	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
344
345	// Change to temp directory so paths resolve correctly
346	originalWd, err := os.Getwd()
347	require.NoError(t, err)
348	defer os.Chdir(originalWd)
349	err = os.Chdir(tempDir)
350	require.NoError(t, err)
351
352	_, cmd := onPaste(filepath.Abs, testEditor, tea.PasteMsg("random.txt"))
353
354	assert.Nil(t, cmd)
355}
356
357// TestHelperFunctions demonstrates how to use the batch message helpers
358func TestHelperFunctions(t *testing.T) {
359	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
360	require.NotNil(t, testEditor)
361
362	// Simulate pressing the '/' key
363	testEditor.Focus()
364	keyPressMsg := tea.KeyPressMsg{
365		Text: "/",
366	}
367
368	_, cmds := testEditor.Update(keyPressMsg)
369
370	// Execute the command and check if it returns a BatchMsg
371	msg := cmds()
372	if batchMsg, ok := msg.(tea.BatchMsg); ok {
373		// Test our helper functions
374		found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
375		assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
376
377		// Test exact message helper
378		foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
379		assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
380
381		// Test specific completions helper
382		msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
383		assert.NotNil(t, msg)
384		assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
385	} else {
386		t.Fatal("Expected BatchMsg from cmds()")
387	}
388}