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	absImagePath, err := absRef(imagePath)
314	assert.NoError(t, err)
315	assert.Equal(t, message.Attachment{
316		FilePath: absImagePath,
317		FileName: "image.png",
318		MimeType: "image/png",
319		Content:  pngMagicNumberData,
320	}, attachmentMsg)
321}
322
323func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
324	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
325
326	// Create a temporary directory and files for testing
327	tempDir := t.TempDir()
328
329	// Create test image file
330	imagePath := filepath.Join(tempDir, "image.png")
331	err := os.WriteFile(imagePath, pngMagicNumberData, 0o644)
332	require.NoError(t, err)
333
334	// Create test text file
335	textPath := filepath.Join(tempDir, "random.txt")
336	err = os.WriteFile(textPath, []byte("Some content"), 0o644)
337	require.NoError(t, err)
338
339	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
340
341	// Change to temp directory so paths resolve correctly
342	originalWd, err := os.Getwd()
343	require.NoError(t, err)
344	defer os.Chdir(originalWd)
345	err = os.Chdir(tempDir)
346	require.NoError(t, err)
347
348	_, cmd := onPaste(filepath.Abs, testEditor, tea.PasteMsg("random.txt"))
349
350	assert.Nil(t, cmd)
351}
352
353// TestHelperFunctions demonstrates how to use the batch message helpers
354func TestHelperFunctions(t *testing.T) {
355	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
356	require.NotNil(t, testEditor)
357
358	// Simulate pressing the '/' key
359	testEditor.Focus()
360	keyPressMsg := tea.KeyPressMsg{
361		Text: "/",
362	}
363
364	_, cmds := testEditor.Update(keyPressMsg)
365
366	// Execute the command and check if it returns a BatchMsg
367	msg := cmds()
368	if batchMsg, ok := msg.(tea.BatchMsg); ok {
369		// Test our helper functions
370		found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
371		assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
372
373		// Test exact message helper
374		foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
375		assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
376
377		// Test specific completions helper
378		msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
379		assert.NotNil(t, msg)
380		assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
381	} else {
382		t.Fatal("Expected BatchMsg from cmds()")
383	}
384}