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_StartFilteringOpens(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, cmds := testEditor.Update(keyPressMsg)
164 testEditor = m.(*editorCmp)
165
166 msg := cmds()
167 var openCompletionsMsg *completions.OpenCompletionsMsg
168 if batchMsg, ok := msg.(tea.BatchMsg); ok {
169 // Use our enhanced helper to check for OpenCompletionsMsg with specific completions
170 var found bool
171 openCompletionsMsg, found = assertBatchContainsOpenCompletionsMsg(t, batchMsg, []string{"file1.txt", "file2.txt"})
172 assert.True(t, found, "Expected to find OpenCompletionsMsg with specific completions in batched messages")
173 } else {
174 t.Fatal("Expected BatchMsg from cmds()")
175 }
176
177 assert.NotNil(t, openCompletionsMsg)
178 m, cmds = testEditor.Update(openCompletionsMsg)
179
180 if batchMsg, ok := msg.(tea.BatchMsg); ok {
181 assertBatchContainsExactMessage(t, batchMsg, completions.CompletionsOpenedMsg{})
182 } else {
183 t.Fatal("Expected BatchMsg from cmds()")
184 }
185
186 // Verify completions menu is open
187 assert.True(t, testEditor.isCompletionsOpen)
188 assert.Equal(t, "/", testEditor.textarea.Value())
189
190 // Now simulate typing a query to filter the completions
191 // Set the text to "/tes" and then simulate typing "t" to make "/test"
192 testEditor.textarea.SetValue("/tes")
193
194 // Simulate typing a key that would trigger filtering
195 keyPressMsg = tea.KeyPressMsg{
196 Text: "t",
197 }
198
199 m, cmds = testEditor.Update(keyPressMsg)
200 testEditor = m.(*editorCmp)
201
202 // Verify the editor still has completions open
203 assert.True(t, testEditor.isCompletionsOpen)
204
205 // The currentQuery should be updated based on what we typed
206 // In this case, it would be "test" (the word after the initial '/')
207 // Note: The actual filtering is handled by the completions component,
208 // so we're just verifying the editor's state is correct
209 assert.Equal(t, "test", testEditor.currentQuery)
210}
211
212func TestEditorAutocompletion_SelectionOfNormalPathAddsToTextAreaClosesCompletion(t *testing.T) {
213 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
214 require.NotNil(t, testEditor)
215
216 // open the completions menu by simulating a '/' key press
217 testEditor.Focus()
218 keyPressMsg := tea.KeyPressMsg{
219 Text: "/",
220 }
221
222 m, cmds := testEditor.Update(keyPressMsg)
223 testEditor = m.(*editorCmp)
224
225 msg := cmds()
226 assert.NotNil(t, msg)
227 m, cmds = testEditor.Update(msg)
228
229 // Now simulate typing a query to filter the completions
230 // Set the text to "/tes" and then simulate typing "t" to make "/test"
231 testEditor.textarea.SetValue("/tes")
232
233 // Simulate typing a key that would trigger filtering
234 keyPressMsg = tea.KeyPressMsg{
235 Text: "t",
236 }
237
238 m, cmds = testEditor.Update(keyPressMsg)
239 testEditor = m.(*editorCmp)
240
241 // The currentQuery should be updated based on what we typed
242 // In this case, it would be "test" (the word after the initial '/')
243 // Note: The actual filtering is handled by the completions component,
244 // so we're just verifying the editor's state is correct
245 assert.Equal(t, "test", testEditor.currentQuery)
246}
247
248// TestHelperFunctions demonstrates how to use the batch message helpers
249func TestHelperFunctions(t *testing.T) {
250 testEditor := newEditor(&app.App{}, mockDirLister([]string{"file1.txt", "file2.txt"}))
251 require.NotNil(t, testEditor)
252
253 // Simulate pressing the '/' key
254 testEditor.Focus()
255 keyPressMsg := tea.KeyPressMsg{
256 Text: "/",
257 }
258
259 m, cmds := testEditor.Update(keyPressMsg)
260 testEditor = m.(*editorCmp)
261
262 // Execute the command and check if it returns a BatchMsg
263 msg := cmds()
264 if batchMsg, ok := msg.(tea.BatchMsg); ok {
265 // Test our helper functions
266 found := assertBatchContainsMessage(t, batchMsg, completions.OpenCompletionsMsg{})
267 assert.True(t, found, "Expected to find OpenCompletionsMsg in batched messages")
268
269 // Test exact message helper
270 foundExact := assertBatchContainsExactMessage(t, batchMsg, completions.OpenCompletionsMsg{})
271 assert.True(t, foundExact, "Expected to find exact OpenCompletionsMsg in batched messages")
272
273 // Test specific completions helper
274 msg, foundSpecific := assertBatchContainsOpenCompletionsMsg(t, batchMsg, nil) // Just check type
275 assert.NotNil(t, msg)
276 assert.True(t, foundSpecific, "Expected to find OpenCompletionsMsg in batched messages")
277 } else {
278 t.Fatal("Expected BatchMsg from cmds()")
279 }
280}