editor_test.go

  1package editor
  2
  3import (
  4	"testing"
  5
  6	tea "github.com/charmbracelet/bubbletea/v2"
  7	"github.com/charmbracelet/crush/internal/app"
  8	"github.com/charmbracelet/crush/internal/fsext"
  9	"github.com/charmbracelet/crush/internal/tui/components/completions"
 10	"github.com/stretchr/testify/assert"
 11	"github.com/stretchr/testify/require"
 12)
 13
 14// executeBatchCommands executes all commands in a BatchMsg and returns the resulting messages
 15func executeBatchCommands(batchMsg tea.BatchMsg) []tea.Msg {
 16	var messages []tea.Msg
 17	for _, cmd := range batchMsg {
 18		if cmd != nil {
 19			msg := cmd()
 20			messages = append(messages, msg)
 21		}
 22	}
 23	return messages
 24}
 25
 26// assertBatchContainsMessage checks if a BatchMsg contains a message of the specified type
 27func assertBatchContainsMessage(t *testing.T, batchMsg tea.BatchMsg, expectedType interface{}) bool {
 28	t.Helper()
 29	messages := executeBatchCommands(batchMsg)
 30
 31	for _, msg := range messages {
 32		switch expectedType.(type) {
 33		case completions.OpenCompletionsMsg:
 34			if _, ok := msg.(completions.OpenCompletionsMsg); ok {
 35				return true
 36			}
 37		}
 38	}
 39	return false
 40}
 41
 42// assertBatchContainsExactMessage checks if a BatchMsg contains a message with exact field values
 43func assertBatchContainsExactMessage(t *testing.T, batchMsg tea.BatchMsg, expected interface{}) bool {
 44	t.Helper()
 45	messages := executeBatchCommands(batchMsg)
 46
 47	for _, msg := range messages {
 48		switch expected := expected.(type) {
 49		case completions.OpenCompletionsMsg:
 50			if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
 51				// If no specific completions are expected, just match the type
 52				if len(expected.Completions) == 0 {
 53					return true
 54				}
 55				// Compare completions if specified
 56				if len(actual.Completions) == len(expected.Completions) {
 57					// For simplicity, just check the count for now
 58					// A more complete implementation would compare each completion
 59					return true
 60				}
 61			}
 62		default:
 63			// Fallback to type checking only
 64			if _, ok := msg.(completions.OpenCompletionsMsg); ok {
 65				return true
 66			}
 67		}
 68	}
 69	return false
 70}
 71
 72// assertBatchContainsOpenCompletionsMsg checks if a BatchMsg contains an OpenCompletionsMsg
 73// with the expected completions. If expectedCompletions is nil, only the message type is checked.
 74func assertBatchContainsOpenCompletionsMsg(t *testing.T, batchMsg tea.BatchMsg, expectedCompletions []string) (*completions.OpenCompletionsMsg, bool) {
 75	t.Helper()
 76	messages := executeBatchCommands(batchMsg)
 77
 78	for _, msg := range messages {
 79		if actual, ok := msg.(completions.OpenCompletionsMsg); ok {
 80			if expectedCompletions == nil {
 81				return &actual, true
 82			}
 83
 84			// Convert actual completions to string titles for comparison
 85			actualTitles := make([]string, len(actual.Completions))
 86			for i, comp := range actual.Completions {
 87				actualTitles[i] = comp.Title
 88			}
 89
 90			// Check if we have the same number of completions
 91			if len(actualTitles) != len(expectedCompletions) {
 92				continue
 93			}
 94
 95			// For now, just check that we have the same count
 96			// A more sophisticated implementation would check the actual values
 97			return &actual, true
 98		}
 99	}
100	return nil, false
101}
102
103func mockDirLister(paths []string) fsext.DirectoryListerResolver {
104	return func() fsext.DirectoryLister {
105		return func(initialPath string, ignorePatterns []string, limit int) ([]string, bool, error) {
106			return paths, false, nil
107		}
108	}
109}
110
111func TestEditorTypingForwardSlashOpensCompletions(t *testing.T) {
112	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
113	require.NotNil(t, testEditor)
114
115	// Simulate pressing the '/' key
116	keyPressMsg := tea.KeyPressMsg{
117		Text: "/",
118	}
119
120	m, cmds := testEditor.Update(keyPressMsg)
121	testEditor = m.(*editorCmp)
122	cmds()
123
124	assert.True(t, testEditor.isCompletionsOpen)
125	assert.Equal(t, "/", testEditor.textarea.Value())
126}
127
128func TestEditorAutocompletionWithEmptyInput(t *testing.T) {
129	testEditor := newEditor(&app.App{}, mockDirLister([]string{}))
130	require.NotNil(t, testEditor)
131
132	// First, give the editor focus
133	testEditor.Focus()
134
135	// Simulate pressing the '/' key when the editor is empty
136	// This should trigger the completions to open
137	keyPressMsg := tea.KeyPressMsg{
138		Text: "/",
139	}
140
141	m, cmds := testEditor.Update(keyPressMsg)
142	testEditor = m.(*editorCmp)
143	cmds()
144
145	// completions menu is open
146	assert.True(t, testEditor.isCompletionsOpen)
147	assert.Equal(t, "/", testEditor.textarea.Value())
148
149	// the query is empty (since we just opened it)
150	assert.Equal(t, "", testEditor.currentQuery)
151}
152
153func TestEditorAutoCompletion_WIP(t *testing.T) {
154	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
155	require.NotNil(t, testEditor)
156
157	// open the completions menu by simulating a '/' key press
158	testEditor.Focus()
159	keyPressMsg := tea.KeyPressMsg{
160		Text: "/",
161	}
162
163	m, msg := simulateUpdate(testEditor, keyPressMsg)
164	testEditor = m.(*editorCmp)
165
166	var openCompletionsMsg *completions.OpenCompletionsMsg
167	if batchMsg, ok := msg.(tea.BatchMsg); ok {
168		// Use our enhanced helper to check for OpenCompletionsMsg with specific completions
169		var found bool
170		openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
171		assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
172	} else {
173		t.Fatal("Expected BatchMsg from cmds()")
174	}
175
176	assert.NotNil(t, openCompletionsMsg)
177}
178
179type noopEvent struct{}
180
181type updater interface {
182	Update(msg tea.Msg) (tea.Model, tea.Cmd)
183}
184
185func simulateUpdate(up updater, msg tea.Msg) (updater, tea.Msg) {
186	up, cmd := up.Update(msg)
187	if cmd != nil {
188		return up, cmd()
189	}
190	return up, noopEvent{}
191}
192
193/*
194func TestEditorAutocompletion_StartFilteringOpens(t *testing.T) {
195	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
196	require.NotNil(t, testEditor)
197
198	// open the completions menu by simulating a '/' key press
199	testEditor.Focus()
200	keyPressMsg := tea.KeyPressMsg{
201		Text: "/",
202	}
203
204	m, cmds := testEditor.Update(keyPressMsg)
205	testEditor = m.(*editorCmp)
206	
207	msg := cmds()
208	var openCompletionsMsg *completions.OpenCompletionsMsg
209	if batchMsg, ok := msg.(tea.BatchMsg); ok {
210		// Use our enhanced helper to check for OpenCompletionsMsg with specific completions
211		var found bool
212		openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
213		assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
214	} else {
215		t.Fatal("Expected BatchMsg from cmds()")
216	}
217
218	assert.NotNil(t, openCompletionsMsg)
219	m, cmds = testEditor.Update(openCompletionsMsg)
220 
221	msg = cmds()
222	testEditor = m.(*editorCmp)
223
224	if batchMsg, ok := msg.(tea.BatchMsg); ok {
225		assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
226	} else {
227		t.Fatal("Expected BatchMsg from cmds()")
228	}
229
230	// Verify completions menu is open
231	assert.True(t, testEditor.isCompletionsOpen)
232	assert.Equal(t, "/", testEditor.textarea.Value())
233
234	// Now simulate typing a query to filter the completions
235	// Set the text to "/tes" and then simulate typing "t" to make "/test"
236	testEditor.textarea.SetValue("/tes")
237
238	// Simulate typing a key that would trigger filtering
239	keyPressMsg = tea.KeyPressMsg{
240		Text: "t",
241	}
242
243	m, cmds = testEditor.Update(keyPressMsg)
244	msg = cmds()
245	testEditor = m.(*editorCmp)
246
247	// Verify the editor still has completions open
248	assert.True(t, testEditor.isCompletionsOpen)
249
250	// The currentQuery should be updated based on what we typed
251	// In this case, it would be "test" (the word after the initial '/')
252	// Note: The actual filtering is handled by the completions component,
253	// so we're just verifying the editor's state is correct
254	assert.Equal(t, "test", testEditor.currentQuery)
255
256	keyPressMsg = tea.KeyPressMsg{
257		Code: tea.KeyEnter,
258	}
259
260	m, cmds = testEditor.Update(keyPressMsg)
261	msg = cmds()
262	testEditor = m.(*editorCmp)
263
264	if batchMsg, ok := msg.(tea.BatchMsg); ok {
265		assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
266	} else {
267		t.Fatal("Expected BatchMsg from cmds()")
268	}
269
270	m, cmds = testEditor.Update(msg)
271	msg = cmds()
272	testEditor = m.(*editorCmp)
273	// Verify the editor still has completions open
274	assert.True(t, testEditor.isCompletionsOpen)
275}
276*/
277
278func TestEditorAutocompletion_SelectionOfNormalPathAddsToTextAreaClosesCompletion(t *testing.T) {
279	testEditor := newEditor(&app.App{}, mockDirLister([]string{"example_test.go", "file1.txt", "file2.txt"}))
280	require.NotNil(t, testEditor)
281
282	// open the completions menu by simulating a '/' key press
283	testEditor.Focus()
284	keyPressMsg := tea.KeyPressMsg{
285		Text: "/",
286	}
287
288	m, cmds := testEditor.Update(keyPressMsg)
289	testEditor = m.(*editorCmp)
290	
291	msg := cmds()
292	assert.NotNil(t, msg)
293	m, cmds = testEditor.Update(msg)
294
295	// Now simulate typing a query to filter the completions
296	// Set the text to "/tes" and then simulate typing "t" to make "/test"
297	testEditor.textarea.SetValue("/tes")
298
299	// Simulate typing a key that would trigger filtering
300	keyPressMsg = tea.KeyPressMsg{
301		Text: "t",
302	}
303
304	m, cmds = testEditor.Update(keyPressMsg)
305	testEditor = m.(*editorCmp)
306
307	// The currentQuery should be updated based on what we typed
308	// In this case, it would be "test" (the word after the initial '/')
309	// Note: The actual filtering is handled by the completions component,
310	// so we're just verifying the editor's state is correct
311	assert.Equal(t, "test", testEditor.currentQuery)
312}
313
314// TestHelperFunctions demonstrates how to use the batch message helpers
315func TestHelperFunctions(t *testing.T) {
316	testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
317	require.NotNil(t, testEditor)
318
319	// Simulate pressing the '/' key
320	testEditor.Focus()
321	keyPressMsg := tea.KeyPressMsg{
322		Text: "/",
323	}
324
325	m, cmds := testEditor.Update(keyPressMsg)
326	testEditor = m.(*editorCmp)
327
328	// Execute the command and check if it returns a BatchMsg
329	msg := cmds()
330	if batchMsg, ok := msg.(tea.BatchMsg); ok {
331		// Test our helper functions
332		found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
333		assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
334
335		// Test exact message helper
336		foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
337		assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
338
339		// Test specific completions helper
340		msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
341		assert.NotNil(t, msg)
342		assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
343	} else {
344		t.Fatal("Expected BatchMsg from cmds()")
345	}
346}