diff --git a/P261194666.md b/P261194666.md index feafb3e7ce2..8d1100a01c4 100644 --- a/P261194666.md +++ b/P261194666.md @@ -537,6 +537,7 @@ Based on our investigation of the language-server-runtimes repository and the pr ``` 5. **Ensure Auto-login Happens Early** (`packages/amazonq/src/lsp/activation.ts`): + ```typescript export async function activate(ctx: vscode.ExtensionContext): Promise { try { 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/chat.test.ts b/packages/amazonq/test/e2e_new/amazonq/chat.test.ts new file mode 100644 index 00000000000..5ff9f86e9f4 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/chat.test.ts @@ -0,0 +1,37 @@ +/*! + * 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 } from './framework/cleanupHelper' +import { testContext } from './utils/testContext' +import { waitForChatResponse, writeToChat } from './framework/chatHelper' +import assert from 'assert' + +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! + }) + + afterEach(async () => { + try { + await closeAllTabs(webviewView) + } catch (e) { + assert.fail(`Failed to clean up tabs: ${e}`) + } + }) + + 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/framework/chatHelper.ts b/packages/amazonq/test/e2e_new/amazonq/framework/chatHelper.ts new file mode 100644 index 00000000000..593941c16c7 --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/framework/chatHelper.ts @@ -0,0 +1,35 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView } from 'vscode-extension-tester' +import { waitForElement } from './generalHelper' + +export async function writeToChat(prompt: string, webview: WebviewView): Promise { + const chatInput = await waitForElement(webview, By.css('.mynah-chat-prompt-input')) + await chatInput.sendKeys(prompt) + const sendButton = await waitForElement(webview, By.css('.mynah-chat-prompt-button')) + await sendButton.click() + return true +} + +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 new Promise((resolve) => setTimeout(resolve, 500)) + } + + return false +} diff --git a/packages/amazonq/test/e2e_new/amazonq/framework/cleanupHelper.ts b/packages/amazonq/test/e2e_new/amazonq/framework/cleanupHelper.ts new file mode 100644 index 00000000000..68fb43daa7e --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/framework/cleanupHelper.ts @@ -0,0 +1,31 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { By, WebviewView } from 'vscode-extension-tester' + +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 new Promise((resolve) => setTimeout(resolve, 500)) + } + + const tabsContainer = await webview.findWebElements(By.css('.mynah-tabs-container')) + const allClosed = + tabsContainer.length === 0 || + (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 + } +} diff --git a/packages/amazonq/test/e2e_new/amazonq/framework/generalHelper.ts b/packages/amazonq/test/e2e_new/amazonq/framework/generalHelper.ts new file mode 100644 index 00000000000..e8b9c7ecc8b --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/framework/generalHelper.ts @@ -0,0 +1,43 @@ +/*! + * 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' + +/* Note: If multiple is set to False, then it will return the first element it finds that matches the Locator*/ +export async function waitForElement( + webview: WebviewView, + locator: By, + multiple: true, + timeout?: number +): Promise +export async function waitForElement( + webview: WebviewView, + locator: By, + multiple?: false, + timeout?: number +): Promise +export 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) +} + +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/framework/loginHelper.ts b/packages/amazonq/test/e2e_new/amazonq/framework/loginHelper.ts new file mode 100644 index 00000000000..98925fa901d --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/framework/loginHelper.ts @@ -0,0 +1,52 @@ +/*! + * 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 { waitForElement, findItemByText } from './generalHelper' +import { testContext } from '../utils/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 loginToAmazonQ is not fully autonomous as we ran into a blocker when the browser window pops up */ + +export async function loginToAmazonQ(): Promise { + const workbench = new Workbench() + await workbench.executeCommand('Amazon Q: Open Chat') + + await new Promise((resolve) => setTimeout(resolve, 5000)) + let 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...') + await new Promise((resolve) => setTimeout(resolve, 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 +} 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..b0add41c2bd --- /dev/null +++ b/packages/amazonq/test/e2e_new/amazonq/utils/setup.ts @@ -0,0 +1,13 @@ +/*! + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +import { loginToAmazonQ } from '../framework/loginHelper' + +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 loginToAmazonQ() +}) 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 +}