diff --git a/packages/amazonq/test/e2e/amazonq/VET.test.ts b/packages/amazonq/test/e2e/amazonq/VET.test.ts deleted file mode 100644 index 414f9e0b0cc..00000000000 --- a/packages/amazonq/test/e2e/amazonq/VET.test.ts +++ /dev/null @@ -1,166 +0,0 @@ -/*! - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -import { Workbench, By, WebviewView, WebElement } from 'vscode-extension-tester' -import { until } from 'selenium-webdriver' - -describe('Amazon Q E2E UI Test', function () { - // need this timeout because Amazon Q takes awhile to load - - // need this timeout - this.timeout(150000) - let webviewView: WebviewView - let workbench: Workbench - before(async function () { - /* TO-DO - possibly before the workbench executes Amazon Q: Open Chat, we can make sure that all the tabs are closed first*/ - workbench = new Workbench() - await workbench.executeCommand('Amazon Q: Open Chat') - - // need this timeout - await new Promise((resolve) => setTimeout(resolve, 5000)) - webviewView = new WebviewView() - await webviewView.switchToFrame() - - const selectableItems = await waitForElement(webviewView, By.css('.selectable-item'), true) - if (selectableItems.length === 0) { - throw new Error('No selectable login options found') - } - - const companyItem = await findItemByText(selectableItems, 'Company account') - await companyItem.click() - const signInContinue = await webviewView.findWebElement(By.css('#connection-selection-continue-button')) - await signInContinue.click() - const startUrlInput = await webviewView.findWebElement(By.id('startUrl')) - await startUrlInput.clear() - await startUrlInput.sendKeys('https://amzn.awsapps.com/start') - const UrlContinue = await webviewView.findWebElement(By.css('button.continue-button.topMargin')) - await UrlContinue.click() - console.log('Waiting for manual authentication...') - // need this timeout - await new Promise((resolve) => setTimeout(resolve, 12000)) - console.log('Manual authentication should be done') - await webviewView.switchBack() - - // AFTER AUTHENTICATION WE MUST RELOAD THE WEBVIEW BECAUSE MULTIPLE WEVIEWS CANNOT BE READ AT THE SAME TIME - const editorView = workbench.getEditorView() - console.log('editorview successfully created') - await editorView.closeAllEditors() - console.log('Closed all editors') - webviewView = new WebviewView() - console.log('Reopened webview view') - await webviewView.switchToFrame() - }) - - after(async () => { - /* - mynah-tabs-container is the css that contains all the mynah ui tabs - inside that there are two spans that have key values - inside those spans there is a div with the css mynah-tab-item-label - and finally INSIDE THAT there is a button with the css mynah-tabs-close-button, we need to click that button and close all the tabs after the test is done - - Logic: - Find all the tahs by looking for the close buttons and then close them one by one. To check if all the tabs are closed, we can check if the mynah-tabs-container is empty. - */ - try { - const closeButtons = await webviewView.findWebElements(By.css('.mynah-tabs-close-button')) - - for (const button of closeButtons) { - await button.click() - await new Promise((resolve) => setTimeout(resolve, 500)) - } - - // double check that all tabs are closed by checking if the mynah-tabs-container is empty - const tabsContainer = await webviewView.findWebElements(By.css('.mynah-tabs-container')) - if ( - tabsContainer.length === 0 || - (await tabsContainer[0].findElements(By.css('.mynah-tab-item-label'))).length === 0 - ) { - console.log('All chat tabs successfully closed') - } - } catch (error) { - console.log('Error closing tabs:', error) - } - await webviewView.switchBack() - }) - - it('Chat Prompt Test', async () => { - const chatInput = await waitForElement(webviewView, By.css('.mynah-chat-prompt-input')) - await chatInput.sendKeys('Hello, Amazon Q!') - const sendButton = await waitForElement(webviewView, By.css('.mynah-chat-prompt-button')) - await sendButton.click() - const responseReceived = await waitForChatResponse(webviewView) - if (!responseReceived) { - throw new Error('Chat response not received within timeout') - } - - console.log('Chat response detected successfully') - }) - - // Helper to wait for ui elements to load, utilizes typescript function overloading to account for all possible edge cases - async function waitForElement( - webview: WebviewView, - locator: By, - multiple: true, - timeout?: number - ): Promise - async function waitForElement( - webview: WebviewView, - locator: By, - multiple?: false, - timeout?: number - ): Promise - async function waitForElement( - webview: WebviewView, - locator: By, - multiple = false, - timeout = 15000 - ): Promise { - const driver = webview.getDriver() - await driver.wait(until.elementsLocated(locator), timeout) - return multiple ? await webview.findWebElements(locator) : await webview.findWebElement(locator) - } - - // Helper to find item by text content - async function findItemByText(items: WebElement[], text: string) { - for (const item of items) { - const titleDivs = await item.findElements(By.css('.title')) - for (const titleDiv of titleDivs) { - const titleText = await titleDiv.getText() - if (titleText?.trim().startsWith(text)) { - return item - } - } - } - throw new Error(`Item with text "${text}" not found`) - } - - /* My Idea: Basically the conversation container's css is .mynah-chat-items-conversation-container - Instead of looking for a specific message like how we look for other elements in the test, - I can check how many elements there are in our specific conversation container. If there is 2 elements, - we can assume that the chat response has been generated. The challenge is, we must grab the latest - conversation container, as there can be multiple conversations in the webview. */ - async function waitForChatResponse(webview: WebviewView, timeout = 15000): Promise { - const startTime = Date.now() - - while (Date.now() - startTime < timeout) { - const conversationContainers = await webview.findWebElements( - By.css('.mynah-chat-items-conversation-container') - ) - - if (conversationContainers.length > 0) { - const latestContainer = conversationContainers[conversationContainers.length - 1] - - const chatItems = await latestContainer.findElements(By.css('*')) - - if (chatItems.length >= 2) { - return true - } - } - await new Promise((resolve) => setTimeout(resolve, 500)) - } - - return false - } -}) diff --git a/packages/amazonq/test/e2e_new/amazonq/helpers/pinContextHelper.ts b/packages/amazonq/test/e2e_new/amazonq/helpers/pinContextHelper.ts new file mode 100644 index 00000000000..aa58ea2d75d --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/helpers/pinContextHelper.ts @@ -0,0 +1,106 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView } from 'vscode-extension-tester' +import { sleep, waitForElement } from '../utils/generalUtils' +import { WebElement } from 'vscode-extension-tester' + +/** + * Clicks the "Pin Context" button in the chat interface + * @param webview The WebviewView instance + * @returns Promise True if button was found and clicked, false otherwise + */ +export async function clickPinContextButton(webview: WebviewView): Promise { + try { + const topBar = await waitForElement(webview, By.css('.mynah-prompt-input-top-bar')) + const buttons = await topBar.findElements( + By.css('.mynah-button.mynah-button-secondary.fill-state-always.status-clear.mynah-ui-clickable-item') + ) + // double check the label to make sure it says "Pin Context" + for (const button of buttons) { + const label = await button.findElement(By.css('.mynah-button-label')) + const labelText = await label.getText() + console.log('THE BUTTON TEXT LABEL IS:', labelText) + if (labelText === '@Pin Context') { + console.log('Found Pin Context button, clicking...') + await button.click() + return true + } + } + console.log('Pin Context button not found') + return false + } catch (e) { + console.error('Error clicking Pin Context button:', e) + return false + } +} + +/** + * Lists all the possible Pin Context menu items in the console. + * @param webview The WebviewView instance + * @returns Promise Returns the items as a WebElement List and the labels in a string array + */ +export async function getPinContextMenuItems(webview: WebviewView): Promise<{ items: WebElement[]; labels: string[] }> { + try { + const menuList = await waitForElement(webview, By.css('.mynah-detailed-list-items-block')) + await sleep(3000) + const menuListItems = await menuList.findElements(By.css('.mynah-detailed-list-item.mynah-ui-clickable-item')) + const labels: string[] = [] + + for (const item of menuListItems) { + try { + const textWrapper = await item.findElement(By.css('.mynah-detailed-list-item-text')) + const nameElement = await textWrapper.findElement(By.css('.mynah-detailed-list-item-name')) + const labelText = await nameElement.getText() + labels.push(labelText) + console.log('Menu item found:', labelText) + } catch (e) { + labels.push('') + console.log('Could not get text for menu item') + } + } + + return { items: menuListItems, labels } + } catch (e) { + console.error('Error getting Pin Context menu items:', e) + return { items: [], labels: [] } + } +} + +/** + * Clicks a specific item in the Pin Context menu by its label text + * @param webview The WebviewView instance + * @param itemName The text label of the menu item to click + * @returns Promise True if the item was found and clicked, false otherwise + * + * NOTE: To find all possible text labels, you can call getPinContextMenuItems + */ +export async function clickPinContextMenuItem(webview: WebviewView, itemName: string): Promise { + try { + const menuList = await waitForElement(webview, By.css('.mynah-detailed-list-items-block')) + await sleep(3000) + const menuListItems = await menuList.findElements(By.css('.mynah-detailed-list-item.mynah-ui-clickable-item')) + for (const item of menuListItems) { + try { + const textWrapper = await item.findElement(By.css('.mynah-detailed-list-item-text')) + const nameElement = await textWrapper.findElement(By.css('.mynah-detailed-list-item-name')) + const labelText = await nameElement.getText() + + if (labelText === itemName) { + console.log(`Clicking Pin Context menu item: ${itemName}`) + await item.click() + return true + } + } catch (e) { + continue + } + } + + console.log(`Pin Context menu item not found: ${itemName}`) + return false + } catch (e) { + console.error(`Error clicking Pin Context menu item ${itemName}:`, e) + return false + } +} diff --git a/packages/amazonq/test/e2e_new/amazonq/helpers/quickActionsHelper.ts b/packages/amazonq/test/e2e_new/amazonq/helpers/quickActionsHelper.ts new file mode 100644 index 00000000000..70f2d53e54b --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/helpers/quickActionsHelper.ts @@ -0,0 +1,74 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import { By, WebElement, WebviewView } from 'vscode-extension-tester' +import { writeToChat } from '../utils/generalUtils' +import { sleep, waitForElements } from '../utils/generalUtils' + +/** + * Gets all quick action command menu items + * @param webview The WebviewView instance + * @returns Promise<{items: WebElement[], texts: string[]}> Array of menu items and their text labels + */ +export async function getQuickActionsCommands(webview: WebviewView): Promise<{ items: WebElement[]; texts: string[] }> { + try { + await writeToChat('/', webview, false) + await sleep(2000) + + const menuItems = await waitForElements( + webview, + By.css('.mynah-detailed-list-item.mynah-ui-clickable-item.target-command'), + 10000 + ) + + const menuTexts = [] + for (let i = 0; i < menuItems.length; i++) { + try { + const text = await menuItems[i].getText() + menuTexts.push(text) + console.log(`Command ${i + 1}: ${text}`) + } catch (e) { + menuTexts.push('') + console.log(`Could not get text for command ${i + 1}`) + } + } + + console.log(`Found ${menuItems.length} quick action command items`) + return { items: menuItems, texts: menuTexts } + } catch (e) { + console.error('Error getting quick action commands:', e) + return { items: [], texts: [] } + } +} + +/** + * Clicks a specific quick action command by name + * @param webview The WebviewView instance + * @param commandName The name of the command to click + * @returns Promise True if command was found and clicked, false otherwise + */ +export async function clickQuickActionsCommand(webview: WebviewView, commandName: string): Promise { + try { + const { items, texts } = await getQuickActionsCommands(webview) + if (items.length === 0) { + console.log('No quick action commands found to click') + return false + } + const indexToClick = texts.findIndex((text) => text === commandName) + + if (indexToClick === -1) { + console.log(`Command "${commandName}" not found`) + return false + } + console.log(`Clicking on command: ${commandName}`) + await items[indexToClick].click() + await sleep(3000) + console.log('Command clicked successfully') + return true + } catch (e) { + console.error('Error clicking quick action command:', e) + return false + } +} diff --git a/packages/amazonq/test/e2e_new/amazonq/helpers/switchModelHelper.ts b/packages/amazonq/test/e2e_new/amazonq/helpers/switchModelHelper.ts new file mode 100644 index 00000000000..fa67ad002b4 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/helpers/switchModelHelper.ts @@ -0,0 +1,52 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView, WebElement } from 'vscode-extension-tester' +import { waitForElement } from '../utils/generalUtils' + +/** + * Lists all available model options in the dropdown + * @param webviewView The WebviewView instance + */ +export async function listModels(webviewView: WebviewView): Promise { + try { + const selectElement = await waitForElement(webviewView, By.css('.mynah-form-input.auto-width')) + const options = await selectElement.findElements(By.css('option')) + const optionTexts = await Promise.all(options.map(async (option) => await option.getText())) + + console.log('Available model options:', optionTexts) + } catch (e) { + console.error('Error listing model options:', e) + throw e + } +} + +/** + * Selects a specific model from the dropdown by name + * @param webviewView The WebviewView instance + * @param modelName The exact name of the model to select + */ +export async function selectModel(webviewView: WebviewView, modelName: string): Promise { + try { + const selectElement = await waitForElement(webviewView, By.css('.mynah-form-input.auto-width')) + await selectElement.click() + const options = await selectElement.findElements(By.css('option')) + let targetOption: WebElement | undefined + for (const option of options) { + const optionText = await option.getText() + if (optionText === modelName) { + targetOption = option + break + } + } + if (!targetOption) { + throw new Error(`Model option "${modelName}" not found`) + } + await targetOption.click() + console.log(`Selected model option: ${modelName}`) + } catch (e) { + console.error('Error selecting model option:', e) + throw e + } +} diff --git a/packages/amazonq/test/e2e_new/amazonq/tests/chat.test.ts b/packages/amazonq/test/e2e_new/amazonq/tests/chat.test.ts new file mode 100644 index 00000000000..1f9cdc44634 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/tests/chat.test.ts @@ -0,0 +1,36 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import '../utils/setup' +import { WebviewView } from 'vscode-extension-tester' +import { testContext } from '../utils/testContext' +import { clearChat, waitForChatResponse, writeToChat } from '../utils/generalUtils' +import { closeAllTabs } from '../utils/cleanupUtils' + +describe('Amazon Q Chat Basic Functionality', function () { + // this timeout is the general timeout for the entire test suite + this.timeout(150000) + let webviewView: WebviewView + + before(async function () { + webviewView = testContext.webviewView + }) + + after(async function () { + await closeAllTabs(webviewView) + }) + + afterEach(async () => { + await clearChat(webviewView) + }) + + it('Chat Prompt Test', async () => { + await writeToChat('Hello, Amazon Q!', webviewView) + const responseReceived = await waitForChatResponse(webviewView) + if (!responseReceived) { + throw new Error('Chat response not received within timeout') + } + console.log('Chat response detected successfully') + }) +}) diff --git a/packages/amazonq/test/e2e_new/amazonq/tests/pinContext.test.ts b/packages/amazonq/test/e2e_new/amazonq/tests/pinContext.test.ts new file mode 100644 index 00000000000..4e6c9c5a477 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/tests/pinContext.test.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import '../utils/setup' +import { WebviewView } from 'vscode-extension-tester' +import { closeAllTabs, dismissOverlayIfPresent } from '../utils/cleanupUtils' +import { testContext } from '../utils/testContext' +import { clickPinContextButton, getPinContextMenuItems, clickPinContextMenuItem } from '../helpers/pinContextHelper' +import { clearChat } from '../utils/generalUtils' + +describe('Amazon Q Pin Context Functionality', function () { + // this timeout is the general timeout for the entire test suite + this.timeout(150000) + let webviewView: WebviewView + + before(async function () { + webviewView = testContext.webviewView + }) + + after(async function () { + await closeAllTabs(webviewView) + }) + + afterEach(async () => { + await dismissOverlayIfPresent(webviewView) + await clearChat(webviewView) + }) + + it('Pin Context Test', async () => { + await clickPinContextButton(webviewView) + await getPinContextMenuItems(webviewView) + await clickPinContextMenuItem(webviewView, '@workspace') + }) +}) diff --git a/packages/amazonq/test/e2e_new/amazonq/tests/quickActions.test.ts b/packages/amazonq/test/e2e_new/amazonq/tests/quickActions.test.ts new file mode 100644 index 00000000000..e27fe447d24 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/tests/quickActions.test.ts @@ -0,0 +1,34 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import '../utils/setup' +import { WebviewView } from 'vscode-extension-tester' +import { closeAllTabs, dismissOverlayIfPresent } from '../utils/cleanupUtils' +import { testContext } from '../utils/testContext' +import { clickQuickActionsCommand } from '../helpers/quickActionsHelper' +import { clearChat } from '../utils/generalUtils' + +describe('Amazon Q Chat Quick Actions Functionality', function () { + // this timeout is the general timeout for the entire test suite + this.timeout(150000) + let webviewView: WebviewView + + before(async function () { + webviewView = testContext.webviewView + }) + + after(async function () { + await closeAllTabs(webviewView) + }) + + afterEach(async () => { + // before closing the tabs, make sure that any overlays have been dismissed + await dismissOverlayIfPresent(webviewView) + await clearChat(webviewView) + }) + + it('Quick Actions Test', async () => { + await clickQuickActionsCommand(webviewView, 'dev') + }) +}) diff --git a/packages/amazonq/test/e2e_new/amazonq/tests/switchModel.test.ts b/packages/amazonq/test/e2e_new/amazonq/tests/switchModel.test.ts new file mode 100644 index 00000000000..41cbbf0aec5 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/tests/switchModel.test.ts @@ -0,0 +1,28 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import '../utils/setup' +import { WebviewView } from 'vscode-extension-tester' +import { testContext } from '../utils/testContext' +import { listModels, selectModel } from '../helpers/switchModelHelper' +import { closeAllTabs } from '../utils/cleanupUtils' + +describe('Amazon Q Switch Model Functionality', function () { + // this timeout is the general timeout for the entire test suite + this.timeout(150000) + let webviewView: WebviewView + + before(async function () { + webviewView = testContext.webviewView + }) + + after(async function () { + await closeAllTabs(webviewView) + }) + + it('Switch Model Test', async () => { + await listModels(webviewView) + await selectModel(webviewView, 'Claude Sonnet 3.7') + }) +}) diff --git a/packages/amazonq/test/e2e_new/amazonq/utils/authUtils.ts b/packages/amazonq/test/e2e_new/amazonq/utils/authUtils.ts new file mode 100644 index 00000000000..226288be667 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/authUtils.ts @@ -0,0 +1,63 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { Workbench, By, WebviewView } from 'vscode-extension-tester' +import { findItemByText, sleep, waitForElements } from './generalUtils' +import { testContext } from './testContext' + +/* Completes the entire Amazon Q login flow + +Currently, the function will +1. Open AmazonQ +2. Clicks Company Account +3. Inputs the Start URL +4. IMPORTANT: you must click manually open yourself when the popup window asks to open the browser and complete the authentication in the browser** + +TO-DO: Currently this signInToAmazonQ is not fully autonomous as we ran into a blocker when the browser window pops up */ +export async function signInToAmazonQ(): Promise { + const workbench = new Workbench() + await workbench.executeCommand('Amazon Q: Open Chat') + + await sleep(5000) + let webviewView = new WebviewView() + await webviewView.switchToFrame() + + const selectableItems = await waitForElements(webviewView, By.css('.selectable-item')) + if (selectableItems.length === 0) { + throw new Error('No selectable login options found') + } + + // find the button / input + click the button / input + const companyItem = await findItemByText(selectableItems, 'Company account') + await companyItem.click() + + const signInContinue = await webviewView.findWebElement(By.css('#connection-selection-continue-button')) + await signInContinue.click() + + const startUrlInput = await webviewView.findWebElement(By.id('startUrl')) + await startUrlInput.clear() + await startUrlInput.sendKeys('https://amzn.awsapps.com/start') + + const UrlContinue = await webviewView.findWebElement(By.css('button.continue-button.topMargin')) + await UrlContinue.click() + + console.log('Waiting for manual authentication...') + await sleep(12000) + console.log('Manual authentication should be done') + await webviewView.switchBack() + + const editorView = workbench.getEditorView() + await editorView.closeAllEditors() + webviewView = new WebviewView() + await webviewView.switchToFrame() + + testContext.workbench = workbench + testContext.webviewView = webviewView +} + +/* NOTE: The workbench and webviewView is grabbed directly from testContext because we are under the assumption that if you want to log out +you've already logged in before. */ +export async function signOutFromAmazonQ(workbench: Workbench): Promise { + await workbench.executeCommand('Amazon Q: Sign Out') +} diff --git a/packages/amazonq/test/e2e_new/amazonq/utils/cleanupUtils.ts b/packages/amazonq/test/e2e_new/amazonq/utils/cleanupUtils.ts new file mode 100644 index 00000000000..8f5866754a8 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/cleanupUtils.ts @@ -0,0 +1,62 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView } from 'vscode-extension-tester' +import { sleep } from './generalUtils' + +/** + * Closes all open chat tabs + * @param webview The WebviewView instance + * @returns Promise True if all tabs were successfully closed + * @throws Error if tabs could not be closed + */ +export async function closeAllTabs(webview: WebviewView): Promise { + try { + const closeButtons = await webview.findWebElements(By.css('.mynah-tabs-close-button')) + + for (const button of closeButtons) { + await button.click() + await sleep(500) + } + + const tabsContainer = await webview.findWebElements(By.css('.mynah-tabs-container')) + const allClosed = + tabsContainer.length === 1 || + (await tabsContainer[0].findElements(By.css('.mynah-tab-item-label'))).length === 0 + + if (allClosed) { + console.log('All chat tabs successfully closed') + return true + } else { + throw new Error('Failed to close all tabs') + } + } catch (error) { + console.error('Error closing tabs:', error) + throw error + } +} + +/** + * Attempts to dismiss any open overlays + * @param webview The WebviewView instance + * @returns Promise True if overlay was dismissed or none was present, false if dismissal failed + */ +export async function dismissOverlayIfPresent(webview: WebviewView): Promise { + try { + const overlays = await webview.findWebElements(By.css('.mynah-overlay.mynah-overlay-open')) + if (overlays.length > 0) { + console.log('Overlay detected, attempting to dismiss...') + const driver = webview.getDriver() + await driver.executeScript('document.body.click()') + + await sleep(1000) + const overlaysAfter = await webview.findWebElements(By.css('.mynah-overlay.mynah-overlay-open')) + return overlaysAfter.length === 0 + } + return true + } catch (e) { + console.log('Error while trying to dismiss overlay:', e) + return false + } +} diff --git a/packages/amazonq/test/e2e_new/amazonq/utils/generalUtils.ts b/packages/amazonq/test/e2e_new/amazonq/utils/generalUtils.ts new file mode 100644 index 00000000000..b5e259fe5f2 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/generalUtils.ts @@ -0,0 +1,125 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView, WebElement } from 'vscode-extension-tester' +import { until } from 'selenium-webdriver' + +/** + * General sleep function to wait for a specified amount of time + * @param timeout Time in miliseconds + */ +export async function sleep(timeout: number) { + await new Promise((resolve) => setTimeout(resolve, timeout)) +} + +/** + * Waits for an element to be located, if there are multiple elements with the same locator it will just return the first one + * @param webview The WebviewView instance + * @param locator The selenium locator + * @param timeout The timeout in milliseconds (Optional) + * @returns Promise Returns the element found + */ +export async function waitForElement(webview: WebviewView, locator: By, timeout?: number): Promise { + const driver = webview.getDriver() + await driver.wait(until.elementsLocated(locator), timeout) + return await webview.findWebElement(locator) +} + +/** + * Waits for multiple elements with the same css selector to be located + * @param webview The WebviewView instance + * @param locator The selenium locator + * @param timeout The timeout in milliseconds (Optional) + * @returns Promise Returns an array of elements found + */ +export async function waitForElements(webview: WebviewView, locator: By, timeout?: number): Promise { + const driver = webview.getDriver() + await driver.wait(until.elementsLocated(locator), timeout) + return await webview.findWebElements(locator) +} + +/** + * Writes text to the chat input and optionally sends it + * @param prompt The text to write in the chat input + * @param webview The WebviewView instance + * @param send Whether to click the send button (defaults to true) + * @returns Promise True if successful + */ +export async function writeToChat(prompt: string, webview: WebviewView, send = true): Promise { + const chatInput = await waitForElement(webview, By.css('.mynah-chat-prompt-input')) + await chatInput.sendKeys(prompt) + if (send === true) { + const sendButton = await waitForElement(webview, By.css('.mynah-chat-prompt-button')) + await sendButton.click() + } + return true +} + +/** + * Waits for a chat response to be generated + * @param webview The WebviewView instance + * @param timeout The timeout in milliseconds + * @returns Promise True if a response was detected, false if timeout occurred + */ +export async function waitForChatResponse(webview: WebviewView, timeout = 15000): Promise { + const startTime = Date.now() + + while (Date.now() - startTime < timeout) { + const conversationContainers = await webview.findWebElements(By.css('.mynah-chat-items-conversation-container')) + + if (conversationContainers.length > 0) { + const latestContainer = conversationContainers[conversationContainers.length - 1] + + const chatItems = await latestContainer.findElements(By.css('*')) + + if (chatItems.length >= 2) { + return true + } + } + await sleep(500) + } + + return false +} + +/** + * Clears the text in the chat input field + * @param webview The WebviewView instance + * @returns Promise True if successful, false if an error occurred + */ +export async function clearChat(webview: WebviewView): Promise { + try { + const chatInput = await waitForElement(webview, By.css('.mynah-chat-prompt-input')) + await chatInput.sendKeys( + process.platform === 'darwin' + ? '\uE03D\u0061' // Command+A on macOS + : '\uE009\u0061' // Ctrl+A on Windows/Linux + ) + await chatInput.sendKeys('\uE003') // Backspace + return true + } catch (e) { + console.error('Error clearing chat input:', e) + return false + } +} + +/** + * Finds an item based on the text + * @param items WebElement array to search + * @param text The text of the item + * @returns Promise The first element that contains the specified text + * TO-DO: Make this function more general by eliminated the By.css('.title') + */ +export async function findItemByText(items: WebElement[], text: string) { + for (const item of items) { + const titleDivs = await item.findElements(By.css('.title')) + for (const titleDiv of titleDivs) { + const titleText = await titleDiv.getText() + if (titleText?.trim().startsWith(text)) { + return item + } + } + } + throw new Error(`Item with text "${text}" not found`) +} diff --git a/packages/amazonq/test/e2e_new/amazonq/utils/setup.ts b/packages/amazonq/test/e2e_new/amazonq/utils/setup.ts new file mode 100644 index 00000000000..cf0805c2a69 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/setup.ts @@ -0,0 +1,17 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { signInToAmazonQ } from './authUtils' +import { testContext } from './testContext' +import { closeAllTabs } from './cleanupUtils' + +before(async function () { + this.timeout(60000) + console.log('\n\n*** MANUAL INTERVENTION REQUIRED ***') + console.log('When prompted, you must manually click to open the browser and complete authentication') + console.log('You have 60 seconds to complete this step\n\n') + await signInToAmazonQ() + const webviewView = testContext.webviewView + await closeAllTabs(webviewView) +}) diff --git a/packages/amazonq/test/e2e_new/amazonq/utils/testContext.ts b/packages/amazonq/test/e2e_new/amazonq/utils/testContext.ts new file mode 100644 index 00000000000..1feb527f4e3 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/testContext.ts @@ -0,0 +1,30 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { Workbench, WebviewView } from 'vscode-extension-tester' + +export interface TestContext { + workbench: Workbench + webviewView: WebviewView +} + +export const testContext = new Proxy({} as TestContext, { + get(target, prop) { + if (prop in target && target[prop as keyof TestContext] !== undefined) { + return target[prop as keyof TestContext] + } + throw new Error( + `TestContext.${String(prop)} is undefined. Make sure setup.ts has properly initialized the test context.` + ) + }, + set(target, prop, value) { + target[prop as keyof TestContext] = value + return true + }, +}) + +export function initializeTestContext(workbench: Workbench, webviewView: WebviewView): void { + testContext.workbench = workbench + testContext.webviewView = webviewView +}