-
Notifications
You must be signed in to change notification settings - Fork 747
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 17 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,32 @@ | ||
| /*! | ||
| * 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' | ||
|
|
||
| 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 () => { | ||
| await closeAllTabs(webviewView) | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| }) | ||
|
|
||
| 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,50 @@ | ||
| /*! | ||
| * 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' | ||
|
|
||
| /* Writes a prompt to the chat input and waits for a response | ||
| Logic: | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Finds the chat input element using the .mynah-chat-prompt-input CSS selector, | ||
| sends the provided prompt test, clicks the send button, and waits for a chat | ||
| response. Returns true if successful, throws an error if the response times out */ | ||
|
|
||
| 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 | ||
| } | ||
|
|
||
| /* Waits for a chat response and outputs whether the response is "correct" | ||
|
||
| Logic: | ||
| The overall conversation container's css is .mynah-chat-items-conversation-container. | ||
| Within that container we can check how many elements exist. If there is 2 elements, | ||
| we can assume that the chat response has been generated. However, we must grab the | ||
| latest conversation container, as there can be multiple conversations in the webview. */ | ||
|
|
||
| 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,34 @@ | ||
| /*! | ||
| * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. | ||
| * SPDX-License-Identifier: Apache-2.0 | ||
| */ | ||
| import { By, WebviewView } from 'vscode-extension-tester' | ||
|
|
||
| /* Finds all the tabs by looking for the close buttons and then closes them one by one. | ||
| Logic: | ||
| 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 to avoid memory from a previous test. To double | ||
| check if all the tabs are closed, we can check if the mynah-tabs-container is empty. */ | ||
|
|
||
| export async function closeAllTabs(webview: WebviewView) { | ||
| 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)) | ||
| } | ||
|
|
||
| // double check that all tabs are closed by checking if the mynah-tabs-container is empty | ||
| const tabsContainer = await webview.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) | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /*! | ||
| * 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' | ||
|
|
||
| /* Waits for an element (or multiple elements) to appear based on the parameters | ||
| Logic: | ||
| The function utilizes the Selenium wait driver. We can call that driver from our | ||
| WebviewView but it can also be called on parts of the VSCode Editor that are not | ||
| part of the WebviewView. | ||
| (TO-DO: create a more general function that can be called on any part of the VSCode | ||
| Editor. Will do when a use case appears for it.*/ | ||
|
|
||
| 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? |
||
| } | ||
|
|
||
| /* General function for finding WebElement by their text content | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| Logic: | ||
| It searches through an array of WebElements and looks for an element with | ||
| the ".tittle" CSS class within each item. Compares the text content and returns | ||
| the first matching parent element, or throws an error if not found. */ | ||
|
|
||
| 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,53 @@ | ||
| /*! | ||
| * 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 | ||
| Documentation: https://quip-amazon.com/PoJOAyt4ja8H/Authentication-for-UI-Tests-Documentation */ | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
|
|
||
| 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) // Increase timeout to 60 seconds | ||
| 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() | ||
| }) |
| 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 { Workbench, WebviewView } from 'vscode-extension-tester' | ||
|
|
||
| export interface TestContext { | ||
| workbench?: Workbench | ||
laura-codess marked this conversation as resolved.
Outdated
Show resolved
Hide resolved
|
||
| webviewView?: WebviewView | ||
| } | ||
|
|
||
| // arr to store shared context | ||
| export const testContext: TestContext = {} | ||
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.
why do we need to assert defined here?
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.
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.
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.
he's asking why did you use this https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#non-null-assertion-operator
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.
yes, thanks for the link!