Skip to content

Commit 2baf460

Browse files
authored
feat(amazonq): Improve the button clicking logic, replaced the MCP test suite (#7881)
## Changes I researched how we can make a very robust button clicking logic using Selenium best practices. I added this new general function into generalUtils.ts and replaced the MCP test suite with this logic so it no longer flakes. --- - Treat all work as PUBLIC. Private `feature/x` branches will not be squash-merged at release time. - Your code changes must meet the guidelines in [CONTRIBUTING.md](https://github.com/aws/aws-toolkit-vscode/blob/master/CONTRIBUTING.md#guidelines). - License: I confirm that my contribution is made under the terms of the Apache 2.0 license.
1 parent 2d028ed commit 2baf460

File tree

3 files changed

+80
-48
lines changed

3 files changed

+80
-48
lines changed

packages/amazonq/test/e2e_new/amazonq/helpers/mcpHelper.ts

Lines changed: 31 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -3,31 +3,21 @@
33
* SPDX-License-Identifier: Apache-2.0
44
*/
55
import { WebviewView, By, WebElement } from 'vscode-extension-tester'
6-
import { waitForElement } from '../utils/generalUtils'
6+
import { clickButton, waitForElement } from '../utils/generalUtils'
7+
import { dismissOverlayIfPresent } from '../utils/cleanupUtils'
78

89
/**
910
* Clicks the tools to get to the MCP server overlay
1011
* @param webviewView The WebviewView instance
1112
* @returns Promise<boolean> True if tools button was found and clicked, false otherwise
1213
*/
1314
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-
}
15+
await clickButton(
16+
webviewView,
17+
'[data-testid="tab-bar-buttons-wrapper"]',
18+
'[data-testid="tab-bar-button"] .mynah-ui-icon-tools',
19+
'tools button'
20+
)
3121
}
3222

3323
/**
@@ -36,15 +26,7 @@ export async function clickToolsButton(webviewView: WebviewView): Promise<void>
3626
* @returns Promise<boolean> True if add button was found and clicked, false otherwise
3727
*/
3828
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-
}
29+
await clickButton(webviewView, '.mynah-sheet-header-actions-container', 'i.mynah-ui-icon-plus', 'MCP add button')
4830
}
4931

5032
/**
@@ -225,17 +207,12 @@ export async function configureMCPServer(webviewView: WebviewView, config: MCPSe
225207
}
226208

227209
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-
}
210+
await clickButton(
211+
webviewView,
212+
'[data-testid="chat-item-action-button"][action-id="save-mcp"]',
213+
'span.mynah-button-label',
214+
'save button'
215+
)
239216
}
240217

241218
export async function cancelMCPServerConfiguration(webviewView: WebviewView): Promise<void> {
@@ -259,13 +236,21 @@ export async function cancelMCPServerConfiguration(webviewView: WebviewView): Pr
259236
*/
260237
export async function clickMCPRefreshButton(webviewView: WebviewView): Promise<void> {
261238
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()
239+
// First dismiss any overlay that might be present
240+
await dismissOverlayIfPresent(webviewView)
241+
242+
await clickButton(
243+
webviewView,
244+
'.mynah-sheet-header-actions-container',
245+
'i.mynah-ui-icon-refresh',
246+
'MCP refresh button'
247+
)
248+
249+
// Dismiss any overlay that might appear after clicking
250+
await dismissOverlayIfPresent(webviewView)
267251
} catch (e) {
268252
console.error('Error clicking the MCP refresh button:', e)
253+
throw e
269254
}
270255
}
271256

@@ -276,11 +261,10 @@ export async function clickMCPRefreshButton(webviewView: WebviewView): Promise<v
276261
*/
277262
export async function clickMCPCloseButton(webviewView: WebviewView): Promise<void> {
278263
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)
264+
await dismissOverlayIfPresent(webviewView)
265+
await clickButton(webviewView, '.mynah-sheet-header', 'i.mynah-ui-icon-cancel', 'MCP close button')
283266
} catch (e) {
284267
console.error('Error closing the MCP overlay:', e)
268+
throw e
285269
}
286270
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ import {
99
clickMCPAddButton,
1010
clickMCPCloseButton,
1111
clickMCPRefreshButton,
12-
clickToolsButton,
1312
configureMCPServer,
1413
saveMCPServerConfiguration,
14+
clickToolsButton,
1515
} from '../helpers/mcpHelper'
1616
import { closeAllTabs } from '../utils/cleanupUtils'
1717

packages/amazonq/test/e2e_new/amazonq/utils/generalUtils.ts

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,54 @@ export async function waitForElements(webview: WebviewView, locator: By, timeout
3939
return await webview.findWebElements(locator)
4040
}
4141

42+
/**
43+
* Robust button clicking function that locates a button by its wrapper and content, then clicks it
44+
* @param webviewView The WebviewView instance
45+
* @param buttonWrapperSelector CSS selector for the button's wrapper element
46+
* @param buttonContentSelector CSS selector for the content inside the button (icon, text, etc.)
47+
* @param buttonName Descriptive name for the button (used in error messages)
48+
* @param timeout Timeout in milliseconds (defaults to 5000)
49+
* @returns Promise<void>
50+
*/
51+
export async function clickButton(
52+
webviewView: WebviewView,
53+
buttonWrapperSelector: string,
54+
buttonContentSelector: string,
55+
buttonName: string = 'button',
56+
timeout: number = 5000
57+
): Promise<void> {
58+
try {
59+
const buttonWrapper = await webviewView
60+
.getDriver()
61+
.wait(until.elementLocated(By.css(buttonWrapperSelector)), timeout, `${buttonName} wrapper not found`)
62+
63+
await webviewView
64+
.getDriver()
65+
.wait(until.elementIsVisible(buttonWrapper), timeout, `${buttonName} wrapper not visible`)
66+
67+
const buttonContent = await webviewView
68+
.getDriver()
69+
.wait(until.elementLocated(By.css(buttonContentSelector)), timeout, `${buttonName} content not found`)
70+
71+
const button = await buttonContent.findElement(By.xpath('./..'))
72+
await webviewView.getDriver().wait(until.elementIsEnabled(button), timeout, `${buttonName} not clickable`)
73+
await button.click()
74+
await webviewView.getDriver().sleep(300)
75+
} catch (e) {
76+
console.error(`Failed to click ${buttonName}:`, {
77+
error: e,
78+
timestamp: new Date().toISOString(),
79+
})
80+
try {
81+
const screenshot = await webviewView.getDriver().takeScreenshot()
82+
console.log(`Screenshot taken at time of ${buttonName} failure`, screenshot)
83+
} catch (screenshotError) {
84+
console.error('Failed to take error screenshot:', screenshotError)
85+
}
86+
throw new Error(`Failed to click ${buttonName}: ${e}`)
87+
}
88+
}
89+
4290
/**
4391
* Presses a single key globally
4492
* @param driver The WebDriver instance

0 commit comments

Comments
 (0)