editor_test.go

  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, 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, "I am looking for a file called ./root/project/random.txt", testEditor.textarea.Value())
227}
228
229func TestEditor_OnCompletionPathToImageEmitsAttachFileMessage(t *testing.T) {
230	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
231	fsys := fstest.MapFS{
232		"auto_completed_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 := onCompletionItemSelect(fsys, FileCompletionItem{Path: "auto_completed_image.png"}, true, testEditor)
241	testEditor = model.(*editorCmp)
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	model, cmd := onCompletionItemSelect(fsys, FileCompletionItem{Path: "random.txt"}, true, testEditor)
272	testEditor = model.(*editorCmp)
273
274	assert.Nil(t, cmd)
275}
276
277func TestEditor_OnPastePathToImageEmitsAttachFileMessage(t *testing.T) {
278	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
279	fsys := fstest.MapFS{
280		"image.png": {
281			Data: pngMagicNumberData,
282		},
283		"random.txt": {
284			Data: []byte("Some content"),
285		},
286	}
287	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
288	model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("image.png"))
289	testEditor = model.(*editorCmp)
290
291	require.NotNil(t, cmd)
292	msg := cmd()
293	assert.NotNil(t, msg)
294
295	var attachmentMsg message.Attachment
296	if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
297		attachmentMsg = fpickedMsg.Attachment
298	}
299
300	assert.Equal(t, message.Attachment{
301		FilePath: "image.png",
302		FileName: "image.png",
303		MimeType: "image/png",
304		Content:  pngMagicNumberData,
305	}, attachmentMsg)
306}
307
308func TestEditor_OnPastePathToNonImageEmitsAttachFileMessage(t *testing.T) {
309	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
310	fsys := fstest.MapFS{
311		"image.png": {
312			Data: pngMagicNumberData,
313		},
314		"random.txt": {
315			Data: []byte("Some content"),
316		},
317	}
318	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
319	model, cmd := onPaste(fsys, mockResolveAbs, testEditor, tea.PasteMsg("random.txt"))
320	testEditor = model.(*editorCmp)
321
322	assert.Nil(t, cmd)
323}
324
325// TestHelperFunctions demonstrates how to use the batch message helpers
326func TestHelperFunctions(t *testing.T) {
327	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
328	require.NotNil(t, testEditor)
329
330	// Simulate pressing the '/' key
331	testEditor.Focus()
332	keyPressMsg := tea.KeyPressMsg{
333		Text: "/",
334	}
335
336	m, cmds := testEditor.Update(keyPressMsg)
337	testEditor = m.(*editorCmp)
338
339	// Execute the command and check if it returns a BatchMsg
340	msg := cmds()
341	if batchMsg, ok := msg.(tea.BatchMsg); ok {
342		// Test our helper functions
343		found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
344		assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
345
346		// Test exact message helper
347		foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
348		assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
349
350		// Test specific completions helper
351		msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
352		assert.NotNil(t, msg)
353		assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
354	} else {
355		t.Fatal("Expected BatchMsg from cmds()")
356	}
357}