Skip to content

Commit 33437c3

Browse files
authored
Merge branch 'feature/ui-e2e-tests' into quick-actions
2 parents bfbce96 + b020242 commit 33437c3

File tree

5 files changed

+357
-14
lines changed

5 files changed

+357
-14
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@
3030
"testE2E": "npm run testE2E -w packages/ --if-present",
3131
"test:ui:prepare": "./node_modules/.bin/extest get-vscode -s ~/.vscode-test-resources -n && extest get-chromedriver -s ~/.vscode-test-resources -n",
3232
"test:ui:install": "cd packages/amazonq && npm run package 2>&1 | grep -o 'VSIX Version: [^ ]*' | cut -d' ' -f3 | xargs -I{} bash -c 'cd ../../ && ./node_modules/.bin/extest install-vsix -f amazon-q-vscode-{}.vsix -e packages/amazonq/test/e2e_new/amazonq/resources -s ~/.vscode-test-resources'",
33-
"test:ui:run": "npm run testCompile && ./node_modules/.bin/extest run-tests -s ~/.vscode-test-resources -e packages/amazonq/test/e2e_new/amazonq/resources packages/amazonq/dist/test/e2e_new/amazonq/tests/*.js",
33+
"test:ui:run": "npm run testCompile && ./node_modules/.bin/extest run-tests -s ~/.vscode-test-resources -e packages/amazonq/test/e2e_new/amazonq/resources packages/amazonq/dist/test/e2e_new/amazonq/tests/*.test.js",
3434
"test:ui": "npm run test:ui:prepare && npm run test:ui:install && npm run test:ui:run",
3535
"testInteg": "npm run testInteg -w packages/ --if-present",
3636
"package": "npm run package -w packages/toolkit -w packages/amazonq",
Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,286 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import { WebviewView, By, WebElement } from 'vscode-extension-tester'
6+
import { waitForElement } from '../utils/generalUtils'
7+
8+
/**
9+
* Clicks the tools to get to the MCP server overlay
10+
* @param webviewView The WebviewView instance
11+
* @returns Promise<boolean> True if tools button was found and clicked, false otherwise
12+
*/
13+
export async function clickToolsButton(webviewView: WebviewView): Promise<void> {
14+
try {
15+
const navWrapper = await waitForElement(webviewView, By.css('.mynah-nav-tabs-wrapper.mynah-ui-clickable-item'))
16+
const buttonsWrapper = await navWrapper.findElement(By.css('.mynah-nav-tabs-bar-buttons-wrapper'))
17+
const buttons = await buttonsWrapper.findElements(
18+
By.css('.mynah-button.mynah-button-secondary.fill-state-always.mynah-ui-clickable-item')
19+
)
20+
for (const button of buttons) {
21+
const icon = await button.findElement(By.css('i.mynah-ui-icon.mynah-ui-icon-tools'))
22+
if (icon) {
23+
await button.click()
24+
await webviewView.getDriver().actions().move({ x: 0, y: 0 }).perform()
25+
}
26+
}
27+
console.log('Tools button not found')
28+
} catch (e) {
29+
console.error('Error clicking tools button:', e)
30+
}
31+
}
32+
33+
/**
34+
* Clicks the add button in the MCP server configuration panel
35+
* @param webviewView The WebviewView instance
36+
* @returns Promise<boolean> True if add button was found and clicked, false otherwise
37+
*/
38+
export async function clickMCPAddButton(webviewView: WebviewView): Promise<void> {
39+
try {
40+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
41+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
42+
const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container'))
43+
const addButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-plus)'))
44+
await addButton.click()
45+
} catch (e) {
46+
console.error('Error clicking the MCP add button:', e)
47+
}
48+
}
49+
50+
/**
51+
* Configures an MCP server with the provided settings
52+
* @param webviewView The WebviewView instance
53+
* @param config Configuration object with optional parameters
54+
* @returns Promise<boolean> True if configuration was successful, false otherwise
55+
* Note: I have the default settings in the defaultConfig
56+
*/
57+
interface MCPServerConfig {
58+
scope?: 'global' | 'workspace'
59+
name?: string
60+
transport?: number
61+
command?: string
62+
args?: string[]
63+
environmentVariable?: { name: string; value: string }
64+
timeout?: number
65+
}
66+
67+
const defaultConfig: MCPServerConfig = {
68+
scope: 'global',
69+
name: 'aws-documentation',
70+
transport: 0,
71+
command: 'uvx',
72+
args: ['awslabs.aws-documentation-mcp-server@latest'],
73+
timeout: 0,
74+
}
75+
76+
// Each name maps to an index in the '.mynah-form-input-wrapper' array
77+
const formItemsMap = {
78+
SCOPE: 0,
79+
NAME: 1,
80+
TRANSPORT: 2,
81+
COMMAND: 3,
82+
ARGS: 4,
83+
ENV_VARS: 6,
84+
TIMEOUT: 9,
85+
} as const
86+
87+
type McpFormItem = keyof typeof formItemsMap
88+
89+
async function selectScope(container: WebElement, scope: string) {
90+
try {
91+
const radioLabels = await container.findElements(
92+
By.css('.mynah-form-input-radio-label.mynah-ui-clickable-item')
93+
)
94+
if (scope === 'global') {
95+
const globalOption = radioLabels[0]
96+
await globalOption.click()
97+
} else {
98+
const workspaceOption = radioLabels[1]
99+
await workspaceOption.click()
100+
}
101+
} catch (e) {
102+
console.error('Error selecting the scope:', e)
103+
throw e
104+
}
105+
}
106+
107+
async function inputName(container: WebElement, name: string) {
108+
try {
109+
const input = await container.findElement(By.css('.mynah-form-input'))
110+
await input.sendKeys(name)
111+
} catch (e) {
112+
console.error('Error inputing the name:', e)
113+
throw e
114+
}
115+
}
116+
117+
async function selectTransport(container: WebElement, transport: number) {
118+
try {
119+
const selectElement = await container.findElement(By.css('select'))
120+
const options = await selectElement.findElements(By.css('option'))
121+
const optionIndex = transport
122+
await options[optionIndex].click()
123+
} catch (e) {
124+
console.error('Error selecting the transport:', e)
125+
throw e
126+
}
127+
}
128+
129+
async function inputCommand(container: WebElement, command: string) {
130+
try {
131+
const input = await container.findElement(By.css('.mynah-form-input'))
132+
await input.sendKeys(command)
133+
} catch (e) {
134+
console.error('Error inputing the command:', e)
135+
throw e
136+
}
137+
}
138+
139+
async function inputArgs(container: WebElement, args: string[]) {
140+
try {
141+
const input = await container.findElement(By.css('.mynah-form-input'))
142+
const addButton = await container.findElement(By.css('.mynah-form-item-list-add-button'))
143+
for (let i = 0; i < args.length; i++) {
144+
await input.sendKeys(args[i])
145+
await addButton.click()
146+
}
147+
} catch (e) {
148+
console.error('Error inputing the arguments:', e)
149+
throw e
150+
}
151+
}
152+
153+
async function inputEnvironmentVariables(container: WebElement, environmentVariable?: { name: string; value: string }) {
154+
try {
155+
if (environmentVariable) {
156+
const envInputs = await container.findElements(By.css('.mynah-form-input'))
157+
await envInputs[0].sendKeys(environmentVariable.name)
158+
await envInputs[1].sendKeys(environmentVariable.value)
159+
const addButton = await container.findElement(By.css('.mynah-form-item-list-add-button'))
160+
await addButton.click()
161+
} else {
162+
console.log('No environmental variables for this configuration')
163+
}
164+
} catch (e) {
165+
console.error('Error inputing the environment variables:', e)
166+
throw e
167+
}
168+
}
169+
170+
async function inputTimeout(container: WebElement, timeout: number) {
171+
try {
172+
const input = await container.findElement(By.css('.mynah-form-input'))
173+
await input.clear()
174+
await input.sendKeys(timeout.toString())
175+
} catch (e) {
176+
console.error('Error inputing the timeout:', e)
177+
throw e
178+
}
179+
}
180+
181+
async function processFormItem(mcpFormItem: McpFormItem, container: WebElement, config: MCPServerConfig) {
182+
switch (mcpFormItem) {
183+
case 'SCOPE':
184+
await selectScope(container, config.scope!)
185+
break
186+
case 'NAME':
187+
await inputName(container, config.name!)
188+
break
189+
case 'TRANSPORT':
190+
await selectTransport(container, config.transport!)
191+
break
192+
case 'COMMAND':
193+
await inputCommand(container, config.command!)
194+
break
195+
case 'ARGS':
196+
await inputArgs(container, config.args!)
197+
break
198+
case 'ENV_VARS':
199+
await inputEnvironmentVariables(container, config.environmentVariable)
200+
break
201+
case 'TIMEOUT':
202+
await inputTimeout(container, config.timeout!)
203+
break
204+
}
205+
}
206+
207+
export async function configureMCPServer(webviewView: WebviewView, config: MCPServerConfig = {}): Promise<void> {
208+
const mergedConfig = { ...defaultConfig, ...config }
209+
try {
210+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
211+
const sheetBody = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
212+
const filtersWrapper = await sheetBody.findElement(By.css('.mynah-detailed-list-filters-wrapper'))
213+
const formContainer = await filtersWrapper.findElement(By.css('.mynah-chat-item-form-items-container'))
214+
const items = await formContainer.findElements(By.css('.mynah-form-input-wrapper'))
215+
216+
for (const formItem of Object.keys(formItemsMap) as McpFormItem[]) {
217+
const index = formItemsMap[formItem]
218+
if (index < items.length) {
219+
await processFormItem(formItem, items[index], mergedConfig)
220+
}
221+
}
222+
} catch (e) {
223+
console.log('Error configuring the MCP Server')
224+
}
225+
}
226+
227+
export async function saveMCPServerConfiguration(webviewView: WebviewView): Promise<void> {
228+
try {
229+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
230+
const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
231+
const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper'))
232+
const saveButton = await filterActions.findElement(
233+
By.css('.mynah-button.fill-state-always.status-primary.mynah-ui-clickable-item')
234+
)
235+
await saveButton.click()
236+
} catch (e) {
237+
console.error('Error saving the MCP server configuration:', e)
238+
}
239+
}
240+
241+
export async function cancelMCPServerConfiguration(webviewView: WebviewView): Promise<void> {
242+
try {
243+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
244+
const body = await sheetWrapper.findElement(By.css('.mynah-sheet-body'))
245+
const filterActions = await body.findElement(By.css('.mynah-detailed-list-filter-actions-wrapper'))
246+
const saveButton = await filterActions.findElement(
247+
By.css('.mynah-button.mynah-button-secondary.mynah-button-border.fill-state-always.mynah-ui-clickable-item')
248+
)
249+
await saveButton.click()
250+
} catch (e) {
251+
console.error('Error saving the MCP server configuration:', e)
252+
}
253+
}
254+
255+
/**
256+
* Clicks the refresh button in the MCP server configuration panel
257+
* @param webviewView The WebviewView instance
258+
* @returns Promise<boolean> True if refresh button was found and clicked, false otherwise
259+
*/
260+
export async function clickMCPRefreshButton(webviewView: WebviewView): Promise<void> {
261+
try {
262+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
263+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
264+
const actionsContainer = await header.findElement(By.css('.mynah-sheet-header-actions-container'))
265+
const refreshButton = await actionsContainer.findElement(By.css('button:has(i.mynah-ui-icon-refresh)'))
266+
await refreshButton.click()
267+
} catch (e) {
268+
console.error('Error clicking the MCP refresh button:', e)
269+
}
270+
}
271+
272+
/**
273+
* Clicks the close/cancel button in the MCP server configuration panel
274+
* @param webviewView The WebviewView instance
275+
* @returns Promise<boolean> True if close button was found and clicked, false otherwise
276+
*/
277+
export async function clickMCPCloseButton(webviewView: WebviewView): Promise<void> {
278+
try {
279+
const sheetWrapper = await waitForElement(webviewView, By.id('mynah-sheet-wrapper'))
280+
const header = await sheetWrapper.findElement(By.css('.mynah-sheet-header'))
281+
const cancelButton = await header.findElement(By.css('button:has(i.mynah-ui-icon-cancel)'))
282+
await webviewView.getDriver().executeScript('arguments[0].click()', cancelButton)
283+
} catch (e) {
284+
console.error('Error closing the MCP overlay:', e)
285+
}
286+
}

packages/amazonq/test/e2e_new/amazonq/tests/inline.test.ts

Lines changed: 17 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
import '../utils/setup'
66
import { Workbench, EditorView, InputBox, TextEditor, WebviewView, Key } from 'vscode-extension-tester'
77
import { testContext } from '../utils/testContext'
8-
import { pressKey, createNewTextFile, writeToTextEditor } from '../utils/generalUtils'
8+
import { createNewTextFile, writeToTextEditor, sleep } from '../utils/generalUtils'
99
import assert from 'assert'
1010

1111
describe('Amazon Q Inline Completion / Chat Functionality', function () {
@@ -20,25 +20,31 @@ describe('Amazon Q Inline Completion / Chat Functionality', function () {
2020
webviewView = testContext.webviewView
2121
await webviewView.switchBack()
2222
workbench = testContext.workbench
23-
2423
editorView = new EditorView()
2524
testContext.editorView = editorView
26-
2725
textEditor = await createNewTextFile(workbench, editorView)
2826
})
29-
30-
it('Inline Test', async () => {
31-
await writeToTextEditor(textEditor, 'Select Me')
27+
after(async function () {
28+
// Switch back to Webview Iframe when dealing with external webviews from Amazon Q.
29+
await editorView.closeAllEditors()
30+
await webviewView.switchToFrame()
31+
})
32+
it('Inline Test Shortcut', async () => {
33+
await writeToTextEditor(textEditor, 'def factorial(n):')
3234
const text = await textEditor.getText()
33-
assert.equal(text, 'Select Me')
35+
assert.equal(text, 'def factorial(n): ')
3436
await textEditor.clearText()
3537

38+
const textBefore = await textEditor.getText()
3639
await workbench.executeCommand('Amazon Q: Inline Chat')
3740
const input = new InputBox()
38-
await input.sendKeys('Write a simple sentece')
41+
await input.sendKeys('Generate the fibonacci sequence through iteration')
3942
await input.sendKeys(Key.ENTER)
40-
const driver = textEditor.getDriver()
41-
await pressKey(driver, 'ENTER')
42-
await pressKey(driver, 'TAB')
43+
// Must wait for response to be generated.
44+
await sleep(8000)
45+
46+
const textAfter = await textEditor.getText()
47+
assert(textAfter.length > textBefore.length, 'Amazon Q should have generated code')
48+
await textEditor.clearText()
4349
})
4450
})
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/*!
2+
* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved.
3+
* SPDX-License-Identifier: Apache-2.0
4+
*/
5+
import '../utils/setup'
6+
import { WebviewView } from 'vscode-extension-tester'
7+
import { testContext } from '../utils/testContext'
8+
import {
9+
clickMCPAddButton,
10+
clickMCPCloseButton,
11+
clickMCPRefreshButton,
12+
clickToolsButton,
13+
configureMCPServer,
14+
saveMCPServerConfiguration,
15+
} from '../helpers/mcpHelper'
16+
import { closeAllTabs } from '../utils/cleanupUtils'
17+
18+
describe('Amazon Q MCP Functionality', function () {
19+
// this timeout is the general timeout for the entire test suite
20+
this.timeout(150000)
21+
let webviewView: WebviewView
22+
23+
before(async function () {
24+
webviewView = testContext.webviewView
25+
})
26+
27+
after(async function () {
28+
await closeAllTabs(webviewView)
29+
})
30+
31+
it('Test Amazon Q MCP Servers and Built-in Tools Access', async () => {
32+
await clickToolsButton(webviewView)
33+
await clickMCPCloseButton(webviewView)
34+
})
35+
36+
it('Add new MCP Server', async () => {
37+
await clickToolsButton(webviewView)
38+
await clickMCPAddButton(webviewView)
39+
await configureMCPServer(webviewView)
40+
await saveMCPServerConfiguration(webviewView)
41+
await clickMCPCloseButton(webviewView)
42+
})
43+
44+
it('Refresh MCP Server', async () => {
45+
await clickToolsButton(webviewView)
46+
await clickMCPRefreshButton(webviewView)
47+
await clickMCPCloseButton(webviewView)
48+
})
49+
})

0 commit comments

Comments
 (0)