-
Notifications
You must be signed in to change notification settings - Fork 746
feat(amazonq): Initializing E2E UI Testing Framework #7685
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
98688bf
5959a38
4540f16
f1afe95
4fa90ec
e1c33d8
ea46464
66f198d
dd62e27
866478a
35c656f
ed3c220
824635e
b30f7f4
5da6336
c8ccbae
74011d2
a4d6b32
d2bc9dd
693404d
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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! | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why do we need to assert defined here?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sorry, Im a bit confused on what you mean by assert defined? the testContext is used to keep track of the state that we're in. So we need to grab the webview from testContext because that testContext.webview can be updated depending on what webview we're looking at.
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. he's asking why did you use this https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. yes, thanks for the link! |
||
| }) | ||
|
|
||
| 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') | ||
| }) | ||
| }) | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<boolean> { | ||
| 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<boolean> { | ||
| 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 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<boolean> { | ||
| 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 | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<WebElement[]> | ||
| export async function waitForElement( | ||
| webview: WebviewView, | ||
| locator: By, | ||
| multiple?: false, | ||
| timeout?: number | ||
| ): Promise<WebElement> | ||
| export async function waitForElement( | ||
| webview: WebviewView, | ||
| locator: By, | ||
| multiple = false, | ||
| timeout = 15000 | ||
| ): Promise<WebElement | WebElement[]> { | ||
| const driver = webview.getDriver() | ||
| await driver.wait(until.elementsLocated(locator), timeout) | ||
| return multiple ? await webview.findWebElements(locator) : await webview.findWebElement(locator) | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if multiple is false, which element does it pick? What gives one element higher priority over another?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. if multiple is false, it will just pick the first element it finds. my goal for the waitForElement function when multiple is false when the writer knows that there is a single element (ie. a button) but the function is flexible enough that it can also be used to find multiple elements (ie. a menu list)
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. not blocking, but I am also curious what might make something the 'first' element it finds. For example, does it mean it appears first the in the raw html? If so, do we know what determines that? |
||
| } | ||
|
|
||
| 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`) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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<void> { | ||
| 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)) | ||
laura-codess marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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 | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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() | ||
| }) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think you meant to modify this file? Might have to explicitly exclude it from PR.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If its happening on pre-commit hook, you can use -n to bypass that.