/
chat-atFile.test.ts
343 lines (296 loc) · 15.7 KB
/
chat-atFile.test.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
import { expect } from '@playwright/test'
import { isWindows } from '@sourcegraph/cody-shared'
import { sidebarExplorer, sidebarSignin } from './common'
import { type ExpectedEvents, test, withPlatformSlashes } from './helpers'
// See chat-atFile.test.md for the expected behavior for this feature.
//
// NOTE: Creating new chats is slow, and setup is slow, so collapse these into fewer tests.
test.extend<ExpectedEvents>({
expectedEvents: [
'CodyInstalled',
'CodyVSCodeExtension:at-mention:executed',
'CodyVSCodeExtension:at-mention:file:executed',
],
})('@-mention file in chat', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
await chatInput.click()
await page.keyboard.type('@')
await expect(
chatPanelFrame.getByRole('heading', {
name: 'Search for a file to include, or type # for symbols...',
})
).toBeVisible()
await page.keyboard.press('Backspace')
// No results
await chatInput.fill('@definitelydoesntexist')
await expect(chatPanelFrame.getByRole('heading', { name: 'No files found' })).toBeVisible()
// Clear the input so the next test doesn't detect the same text already visible from the previous
// check (otherwise the test can pass even without the filter working).
await chatInput.clear()
// We should only match the relative visible path, not parts of the full path outside of the workspace.
// Eg. searching for "source" should not find all files if the project is inside `C:\Source`.
// TODO(dantup): After https://github.com/sourcegraph/cody/pull/2235 lands, add workspacedirectory to the test
// and assert that it contains `fixtures` to ensure this check isn't passing because the fixture folder no
// longer matches.
await chatInput.fill('@fixtures') // fixture is in the test project folder name, but not in the relative paths.
await expect(chatPanelFrame.getByRole('heading', { name: 'No files found' })).toBeVisible()
// Includes dotfiles after just "."
await chatInput.fill('@.')
await expect(chatPanelFrame.getByRole('option', { name: '.mydotfile' })).toBeVisible()
// Forward slashes
await chatInput.fill('@lib/batches/env')
await expect(
chatPanelFrame.getByRole('option', { name: withPlatformSlashes('var.go lib/batches/env') })
).toBeVisible()
// Backslashes
if (isWindows()) {
await chatInput.fill('@lib\\batches\\env')
await expect(
chatPanelFrame.getByRole('option', { name: withPlatformSlashes('var.go lib/batches/env') })
).toBeVisible()
}
// Space before @ is required unless it's at position 0
await chatInput.fill('Explain@mj')
await expect(chatPanelFrame.getByRole('option', { name: 'Main.java' })).not.toBeVisible()
await chatInput.fill('@mj')
await expect(chatPanelFrame.getByRole('option', { name: 'Main.java' })).toBeVisible()
await chatInput.fill('clear')
// Searching and clicking
await chatInput.fill('Explain @mj')
await chatPanelFrame.getByRole('option', { name: 'Main.java' }).click()
await expect(chatInput).toHaveText('Explain @Main.java ')
await expect(chatInput.getByText('@Main.java')).toHaveClass(/context-item-mention-node/)
await chatInput.press('Enter')
await expect(chatInput).toBeEmpty()
await expect(chatPanelFrame.getByText('Explain @Main.java')).toBeVisible()
await expect(chatPanelFrame.getByText(/^✨ Context:/)).toHaveCount(1)
await expect(chatInput).not.toHaveText('Explain @Main.java ')
await expect(chatPanelFrame.getByRole('option', { name: 'Main.java' })).not.toBeVisible()
// Keyboard nav through context files
await chatInput.fill('Explain @var.go')
await expect(
chatPanelFrame.getByRole('option', { name: withPlatformSlashes('var.go lib/batches/env') })
).toBeVisible()
await chatInput.press('Tab')
await expect(chatInput).toHaveText(withPlatformSlashes('Explain @lib/batches/env/var.go '))
await chatInput.focus()
await chatInput.pressSequentially('and @vgo')
await expect(
chatPanelFrame.getByRole('option', { name: withPlatformSlashes('visualize.go') })
).toBeVisible()
await chatInput.press('ArrowDown') // second item (visualize.go)
await chatInput.press('ArrowDown') // third item (.vscode/settings.json)
await chatInput.press('ArrowDown') // wraps back to first item
await chatInput.press('ArrowDown') // second item again
await chatInput.press('Tab')
await expect(chatInput).toHaveText(
withPlatformSlashes(
'Explain @lib/batches/env/var.go and @lib/codeintel/tools/lsif-visualize/visualize.go '
)
)
// Send the message and check it was included
await chatInput.press('Enter')
await expect(chatInput).toBeEmpty()
await expect(
chatPanelFrame.getByText(
withPlatformSlashes(
'Explain @lib/batches/env/var.go and @lib/codeintel/tools/lsif-visualize/visualize.go'
)
)
).toBeVisible()
// Ensure explicitly @-included context shows up as enhanced context
await expect(chatPanelFrame.getByText(/^✨ Context:/)).toHaveCount(2)
// Check pressing tab after typing a complete filename.
// https://github.com/sourcegraph/cody/issues/2200
await chatInput.focus()
await chatInput.clear()
await chatInput.pressSequentially('@Main.java')
await expect(chatPanelFrame.getByRole('option', { name: 'Main.java' })).toBeVisible()
await chatInput.press('Tab')
await expect(chatInput).toHaveText('@Main.java ')
// Check pressing tab after typing a partial filename but where that complete
// filename already exists earlier in the input.
// https://github.com/sourcegraph/cody/issues/2243
await chatInput.pressSequentially('and @Main.ja', { delay: 50 })
await chatInput.press('Tab')
await expect(chatInput).toHaveText('@Main.java and @Main.java ')
// Support @-file in mid-sentence
await chatInput.focus()
await chatInput.clear()
await chatInput.fill('Explain the file')
await chatInput.press('ArrowLeft') // 'Explain the fil|e'
await chatInput.press('ArrowLeft') // 'Explain the fi|le'
await chatInput.press('ArrowLeft') // 'Explain the f|ile'
await chatInput.press('ArrowLeft') // 'Explain the |file'
await chatInput.press('ArrowLeft') // 'Explain the| file'
await chatInput.press('Space') // 'Explain the | file'
await chatInput.pressSequentially('@Main')
await expect(chatPanelFrame.getByRole('option', { name: 'Main.java' })).toBeVisible()
await chatInput.press('Tab')
await expect(chatInput).toHaveText('Explain the @Main.java file')
// Confirm the cursor is at the end of the newly added file name with space
await page.keyboard.press('!')
await page.keyboard.press('Delete')
await expect(chatInput).toHaveText('Explain the @Main.java !file')
// "ArrowLeft" / "ArrowRight" keys alter the query input for @-mentions.
const noMatches = chatPanelFrame.getByRole('heading', { name: 'No files found' })
await chatInput.pressSequentially(' @abcdefg')
await expect(chatInput).toHaveText('Explain the @Main.java ! @abcdefgfile')
await noMatches.hover()
await expect(noMatches).toBeVisible()
await chatInput.press('ArrowLeft')
await expect(noMatches).toBeVisible()
await chatInput.press('ArrowRight')
await expect(noMatches).toBeVisible()
await chatInput.press('?')
await expect(chatInput).toHaveText('Explain the @Main.java ! @abcdefg?file')
await expect(noMatches).not.toBeVisible()
// Selection close on submit
await chatInput.press('Enter')
await expect(noMatches).not.toBeVisible()
await expect(chatInput).toBeEmpty()
// Query ends with non-alphanumeric character
// with no results should not show selector.
await chatInput.focus()
await chatInput.fill('@unknown')
await expect(noMatches).toBeVisible()
await chatInput.press('?')
await expect(chatInput).toHaveText('@unknown?')
await expect(noMatches).not.toBeVisible()
await chatInput.press('Backspace')
await expect(noMatches).toBeVisible()
})
test('editing a chat message with @-mention', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
// Send a message with an @-mention.
await chatInput.fill('Explain @mj')
await chatPanelFrame.getByRole('option', { name: 'Main.java' }).click()
await expect(chatInput).toHaveText('Explain @Main.java ')
await expect(chatInput.getByText('@Main.java')).toHaveClass(/context-item-mention-node/)
await chatInput.press('Enter')
await expect(chatInput).toBeEmpty()
await expect(chatPanelFrame.getByText('Explain @Main.java')).toBeVisible()
await expect(chatPanelFrame.getByText(/^✨ Context: 1 file/)).toHaveCount(1)
// Edit the just-sent message and resend it. Confirm it is sent with the right context items.
await chatInput.press('ArrowUp')
await expect(chatInput).toHaveText('Explain @Main.java ')
await chatInput.press('Meta+Enter')
await expect(chatPanelFrame.getByText(/^✨ Context: 1 file/)).toHaveCount(1)
// Edit it again, add a new @-mention, and resend.
await chatInput.press('ArrowUp')
await expect(chatInput).toHaveText('Explain @Main.java ')
await chatInput.pressSequentially('and @index.ht')
await chatPanelFrame.getByRole('option', { name: 'index.html' }).click()
await expect(chatInput).toHaveText('Explain @Main.java and @index.html')
await expect(chatInput.getByText('@index.html')).toHaveClass(/context-item-mention-node/)
await chatInput.press('Enter')
await expect(chatInput).toBeEmpty()
await expect(chatPanelFrame.getByText('Explain @Main.java and @index.html')).toBeVisible()
await expect(chatPanelFrame.getByText(/^✨ Context: 2 files/)).toHaveCount(1)
})
test('pressing Enter with @-mention menu open selects item, does not submit message', async ({
page,
sidebar,
}) => {
await sidebarSignin(page, sidebar)
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
await chatInput.fill('Explain @index.htm')
await expect(chatPanelFrame.getByRole('option', { name: 'index.html' })).toBeVisible()
await chatInput.press('Enter')
await expect(chatInput).toHaveText('Explain @index.html')
await expect(chatInput.getByText('@index.html')).toHaveClass(/context-item-mention-node/)
})
test('@-mention links in transcript message', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)
// Open chat.
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
// Submit a message with an @-mention.
await chatInput.fill('Hello @buzz.ts')
await chatPanelFrame.getByRole('option', { name: 'buzz.ts' }).click()
await chatInput.press('Enter')
// In the transcript, the @-mention is linked, and clicking the link opens the file.
const transcriptMessage = chatPanelFrame.getByText('Hello @buzz.ts')
const mentionLink = transcriptMessage.getByRole('link', { name: '@buzz.ts' })
await expect(mentionLink).toBeVisible()
await mentionLink.click()
const previewTab = page.getByRole('tab', { name: /buzz.ts, preview, Editor Group/ })
await previewTab.hover()
await expect(previewTab).toBeVisible()
})
test('@-mention file range', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)
// Open chat.
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
// Type a file with range.
await chatInput.fill('@buzz.ts:2-4')
await expect(chatPanelFrame.getByRole('option', { name: 'buzz.ts Lines 2-4' })).toBeVisible()
await chatPanelFrame.getByRole('option', { name: 'buzz.ts Lines 2-4' }).click()
await expect(chatInput).toHaveText('@buzz.ts:2-4 ')
// Submit the message
await chatInput.press('Enter')
// @-file range with the correct line range shows up in the chat view and it opens on click
await chatPanelFrame.getByText('✨ Context: 3 lines from 1 file').hover()
await chatPanelFrame.getByText('✨ Context: 3 lines from 1 file').click()
const chatContext = chatPanelFrame.locator('details').last()
await chatContext.getByRole('link', { name: '@buzz.ts:2-4' }).hover()
await chatContext.getByRole('link', { name: '@buzz.ts:2-4' }).click()
const previewTab = page.getByRole('tab', { name: /buzz.ts, preview, Editor Group/ })
await previewTab.hover()
await expect(previewTab).toBeVisible()
})
test.extend<ExpectedEvents>({
expectedEvents: [
'CodyInstalled',
'CodyVSCodeExtension:at-mention:executed',
'CodyVSCodeExtension:at-mention:symbol:executed',
],
})('@-mention symbol in chat', async ({ page, sidebar }) => {
await sidebarSignin(page, sidebar)
// Open chat.
await page.getByRole('button', { name: 'New Chat', exact: true }).click()
const chatPanelFrame = page.frameLocator('iframe.webview').last().frameLocator('iframe')
const chatInput = chatPanelFrame.getByRole('textbox', { name: 'Chat message' })
// Open the buzz.ts file so that VS Code starts to populate symbols.
await sidebarExplorer(page).click()
await page.getByRole('treeitem', { name: 'buzz.ts' }).locator('a').dblclick()
await page.getByRole('tab', { name: 'buzz.ts' }).hover()
// Go back to the Cody chat tab
await page.getByRole('tab', { name: 'New Chat' }).click()
// Symbol empty state
await chatInput.fill('@#')
await expect(chatPanelFrame.getByRole('heading', { name: /No symbols found/ })).toBeVisible()
// Clicking on a file in the selector should autocomplete the file in chat input with added space
await chatInput.fill('@#fizzb')
await expect(chatPanelFrame.getByRole('option', { name: 'fizzbuzz()' })).toBeVisible({
// Longer timeout because sometimes tsserver takes a while to become ready.
timeout: 15000,
})
await chatPanelFrame.getByRole('option', { name: 'fizzbuzz()' }).click()
await expect(chatInput).toHaveText('@buzz.ts:1-15#fizzbuzz() ')
// Submit the message
await chatInput.press('Enter')
// Close file.
const pinnedTab = page.getByRole('tab', { name: 'buzz.ts', exact: true })
await pinnedTab.getByRole('button', { name: /^Close/ }).click()
// @-file with the correct line range shows up in the chat view and it opens on click
await chatPanelFrame.getByText('✨ Context: 15 lines from 1 file').hover()
await chatPanelFrame.getByText('✨ Context: 15 lines from 1 file').click()
const chatContext = chatPanelFrame.locator('details').last()
await chatContext.getByRole('link', { name: '@buzz.ts:1-15' }).hover()
await chatContext.getByRole('link', { name: '@buzz.ts:1-15' }).click()
const previewTab = page.getByRole('tab', { name: /buzz.ts, preview, Editor Group/ })
await previewTab.hover()
await expect(previewTab).toBeVisible()
})