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
114func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
115	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
116	require.NotNil(t, testEditor)
117
118	// Simulate pressing the '/' key
119	keyPressMsg := tea.KeyPressMsg{
120		Text: "/",
121	}
122
123	m, cmds := testEditor.Update(keyPressMsg)
124	testEditor = m.(*editorCmp)
125	cmds()
126
127	assert.True(t, testEditor.isCompletionsOpen)
128	assert.Equal(t, "/", testEditor.textarea.Value())
129}
130
131func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
132	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
133	require.NotNil(t, testEditor)
134
135	// First, give the editor focus
136	testEditor.Focus()
137
138	// Simulate pressing the '/' key when the editor is empty
139	// This should trigger the completions to open
140	keyPressMsg := tea.KeyPressMsg{
141		Text: "/",
142	}
143
144	m, cmds := testEditor.Update(keyPressMsg)
145	testEditor = m.(*editorCmp)
146	cmds()
147
148	// completions menu is open
149	assert.True(t, testEditor.isCompletionsOpen)
150	assert.Equal(t, "/", testEditor.textarea.Value())
151
152	// the query is empty (since we just opened it)
153	assert.Equal(t, "", testEditor.currentQuery)
154}
155
156func TestEditorAutoCompletion_WIP(t *testing.T) {
157	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
158	require.NotNil(t, testEditor)
159
160	// open the completions menu by simulating a '/' key press
161	testEditor.Focus()
162	keyPressMsg := tea.KeyPressMsg{
163		Text: "/",
164	}
165
166	m, msg := simulateUpdate(testEditor, keyPressMsg)
167	testEditor = m.(*editorCmp)
168
169	var openCompletionsMsg *completions.OpenCompletionsMsg
170	if batchMsg, ok := msg.(tea.BatchMsg); ok {
171		// Use our enhanced helper to check for OpenCompletionsMsg with specific completions
172		var found bool
173		openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
174		assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
175	} else {
176		t.Fatal("Expected BatchMsg from cmds()")
177	}
178
179	assert.NotNil(t, openCompletionsMsg)
180}
181
182type noopEvent struct{}
183
184type updater interface {
185	Update(msg tea.Msg) (tea.Model, tea.Cmd)
186}
187
188func simulateUpdate(up updater, msg tea.Msg) (updater, tea.Msg) {
189	up, cmd := up.Update(msg)
190	if cmd != nil {
191		return up, cmd()
192	}
193	return up, noopEvent{}
194}
195
196var pngMagicNumberData = []byte("\x89PNG\x0D\x0A\x1A\x0A")
197
198func TestEditor_OnPasteEmitsAttachFileMessage(t *testing.T) {
199	entriesForAutoComplete := mockDirLister([]string{"image.png", "random.txt"})
200	fsys := fstest.MapFS{
201		"image.png": {
202			Data: pngMagicNumberData,
203		},
204		"random.txt": {
205			Data: []byte("Some content"),
206		},
207	}
208	resolveAbs := func(path string) (string, error) {
209		return path, nil
210	}
211	testEditor := newEditor(&app.App{}, entriesForAutoComplete)
212	model, cmd := onPaste(fsys, resolveAbs, testEditor, tea.PasteMsg("image.png"))
213	testEditor = model.(*editorCmp)
214
215	require.NotNil(t, cmd)
216	msg := cmd()
217	assert.NotNil(t, msg)
218
219	var attachmentMsg message.Attachment
220	if fpickedMsg, ok := msg.(filepicker.FilePickedMsg); ok {
221		attachmentMsg = fpickedMsg.Attachment
222	}
223
224	assert.Equal(t, message.Attachment{
225		FilePath: "image.png",
226		FileName: "image.png",
227		MimeType: "image/png",
228		Content: pngMagicNumberData,
229	}, attachmentMsg)
230}
231
232/*
233func TestEditorAutocompletion_StartFilteringOpens(t *testing.T) {
234	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
235	require.NotNil(t, testEditor)
236
237	// open the completions menu by simulating a '/' key press
238	testEditor.Focus()
239	keyPressMsg := tea.KeyPressMsg{
240		Text: "/",
241	}
242
243	m, cmds := testEditor.Update(keyPressMsg)
244	testEditor = m.(*editorCmp)
245
246	msg := cmds()
247	var openCompletionsMsg *completions.OpenCompletionsMsg
248	if batchMsg, ok := msg.(tea.BatchMsg); ok {
249		// Use our enhanced helper to check for OpenCompletionsMsg with specific completions
250		var found bool
251		openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
252		assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
253	} else {
254		t.Fatal("Expected BatchMsg from cmds()")
255	}
256
257	assert.NotNil(t, openCompletionsMsg)
258	m, cmds = testEditor.Update(openCompletionsMsg)
259
260	msg = cmds()
261	testEditor = m.(*editorCmp)
262
263	if batchMsg, ok := msg.(tea.BatchMsg); ok {
264		assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
265	} else {
266		t.Fatal("Expected BatchMsg from cmds()")
267	}
268
269	// Verify completions menu is open
270	assert.True(t, testEditor.isCompletionsOpen)
271	assert.Equal(t, "/", testEditor.textarea.Value())
272
273	// Now simulate typing a query to filter the completions
274	// Set the text to "/tes" and then simulate typing "t" to make "/test"
275	testEditor.textarea.SetValue("/tes")
276
277	// Simulate typing a key that would trigger filtering
278	keyPressMsg = tea.KeyPressMsg{
279		Text: "t",
280	}
281
282	m, cmds = testEditor.Update(keyPressMsg)
283	msg = cmds()
284	testEditor = m.(*editorCmp)
285
286	// Verify the editor still has completions open
287	assert.True(t, testEditor.isCompletionsOpen)
288
289	// The currentQuery should be updated based on what we typed
290	// In this case, it would be "test" (the word after the initial '/')
291	// Note: The actual filtering is handled by the completions component,
292	// so we're just verifying the editor's state is correct
293	assert.Equal(t, "test", testEditor.currentQuery)
294
295	keyPressMsg = tea.KeyPressMsg{
296		Code: tea.KeyEnter,
297	}
298
299	m, cmds = testEditor.Update(keyPressMsg)
300	msg = cmds()
301	testEditor = m.(*editorCmp)
302
303	if batchMsg, ok := msg.(tea.BatchMsg); ok {
304		assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
305	} else {
306		t.Fatal("Expected BatchMsg from cmds()")
307	}
308
309	m, cmds = testEditor.Update(msg)
310	msg = cmds()
311	testEditor = m.(*editorCmp)
312	// Verify the editor still has completions open
313	assert.True(t, testEditor.isCompletionsOpen)
314}
315*/
316
317func TestEditorAutocompletion_SelectionOfNormalPathAddsToTextAreaClosesCompletion(t *testing.T) {
318	testEditor := newEditor(&app.App{}, mockDirLister([]string{"example_test.go", "file1.txt", "file2.txt"}))
319	require.NotNil(t, testEditor)
320
321	// open the completions menu by simulating a '/' key press
322	testEditor.Focus()
323	keyPressMsg := tea.KeyPressMsg{
324		Text: "/",
325	}
326
327	m, cmds := testEditor.Update(keyPressMsg)
328	testEditor = m.(*editorCmp)
329
330	msg := cmds()
331	assert.NotNil(t, msg)
332	m, cmds = testEditor.Update(msg)
333
334	// Now simulate typing a query to filter the completions
335	// Set the text to "/tes" and then simulate typing "t" to make "/test"
336	testEditor.textarea.SetValue("/tes")
337
338	// Simulate typing a key that would trigger filtering
339	keyPressMsg = tea.KeyPressMsg{
340		Text: "t",
341	}
342
343	m, cmds = testEditor.Update(keyPressMsg)
344	testEditor = m.(*editorCmp)
345
346	// The currentQuery should be updated based on what we typed
347	// In this case, it would be "test" (the word after the initial '/')
348	// Note: The actual filtering is handled by the completions component,
349	// so we're just verifying the editor's state is correct
350	assert.Equal(t, "test", testEditor.currentQuery)
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	m, cmds := testEditor.Update(keyPressMsg)
365	testEditor = m.(*editorCmp)
366
367	// Execute the command and check if it returns a BatchMsg
368	msg := cmds()
369	if batchMsg, ok := msg.(tea.BatchMsg); ok {
370		// Test our helper functions
371		found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
372		assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
373
374		// Test exact message helper
375		foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
376		assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
377
378		// Test specific completions helper
379		msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
380		assert.NotNil(t, msg)
381		assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
382	} else {
383		t.Fatal("Expected BatchMsg from cmds()")
384	}
385}