test: Add initial automated UI smoke tests (#4393) (#4694)

Signed-off-by: Keith Chong <kykchong@redhat.com>
This commit is contained in:
Keith Chong
2021-07-29 13:11:11 -04:00
committed by GitHub
parent c7738a0cae
commit 4a69cce7b1
18 changed files with 2518 additions and 0 deletions

40
ui-test/.env Normal file
View File

@@ -0,0 +1,40 @@
#
# Currently defined test environment variables. Uncomment and/or change as desired.
#
############################
# Test specific variables
############################
#
# Timeout to wait for an element to appear. The default is 60 sec.
# TEST_TIMEOUT=60000
#
# Run the tests in headless mode if true, non-headless mode if false
IS_HEADLESS=true
#
# Turn on/off tracing to the console. The default is true.
# ENABLE_CONSOLE_LOG=true
#
############################
# ArgoCD specific variables
############################
#
# URL of the ArgoCD UI to test against
ARGOCD_URL=http://localhost:4000
#
# Git repository where applications reside
GIT_REPO=https://github.com/argoproj/argocd-example-apps
#
# The name to give the app in ArgoCD
APP_NAME=myapp
#
# The project name
APP_PROJECT=default
#
# The source path of the application in the repo
SOURCE_REPO_PATH=helm-guestbook
#
# Destination cluster name
DESTINATION_CLUSTER_NAME=in-cluster
#
# Destination namespace
DESTINATION_NAMESPACE=default

6
ui-test/.gitignore vendored Normal file
View File

@@ -0,0 +1,6 @@
.vscode/
.idea/
.DS_Store
out/
node_modules/
.env

9
ui-test/.prettierrc Normal file
View File

@@ -0,0 +1,9 @@
{
"bracketSpacing": false,
"jsxSingleQuote": true,
"printWidth": 180,
"singleQuote": true,
"tabWidth": 4,
"jsxBracketSameLine": true,
"quoteProps": "consistent"
}

32
ui-test/package.json Normal file
View File

@@ -0,0 +1,32 @@
{
"name": "ui-test",
"version": "1.0.0",
"description": "UI Testing",
"main": "argocd-ui-test",
"scripts": {
"compile": "npx tsc",
"test": "node out/test001.js",
"pretest": "cp .env out/.env",
"lint": "tslint -p ."
},
"author": "Keith Chong",
"license": "Apache-2.0",
"dependencies": {
"@types/selenium-webdriver": "^4.0.9",
"assert": "^2.0.0",
"chromedriver": "^86.0.0",
"selenium-webdriver": "^4.0.0-alpha.7"
},
"devDependencies": {
"@types/mocha": "^8.0.3",
"@types/node": "^14.14.2",
"dotenv": "^8.2.0",
"mocha": "^8.2.0",
"prettier": "^1.18.2",
"tslint": "^6.1.3",
"tslint-config-prettier": "^1.18.0",
"tslint-plugin-prettier": "^2.0.1",
"typescript": "^4.0.3",
"yarn": "^1.22.10"
}
}

View File

@@ -0,0 +1,15 @@
require('dotenv').config({path: __dirname + '/.env'});
export default class Configuration {
// Test specific
public static readonly ENABLE_CONSOLE_LOG: string | undefined = process.env.ENABLE_CONSOLE_LOG;
public static readonly TEST_TIMEOUT: string | undefined = process.env.TEST_TIMEOUT;
// ArgoCD UI specific. These are for single application-based tests, so one can quickly create an app based on the environment variables
public static readonly ARGOCD_URL: string = process.env.ARGOCD_URL ? process.env.ARGOCD_URL : '';
public static readonly APP_NAME: string = process.env.APP_NAME ? process.env.APP_NAME : '';
public static readonly APP_PROJECT: string = process.env.APP_PROJECT ? process.env.APP_PROJECT : '';
public static readonly GIT_REPO: string = process.env.GIT_REPO ? process.env.GIT_REPO : '';
public static readonly SOURCE_REPO_PATH: string = process.env.SOURCE_REPO_PATH ? process.env.SOURCE_REPO_PATH : '';
public static readonly DESTINATION_CLUSTER_NAME: string = process.env.DESTINATION_CLUSTER_NAME ? process.env.DESTINATION_CLUSTER_NAME : '';
public static readonly DESTINATION_NAMESPACE: string = process.env.DESTINATION_NAMESPACE ? process.env.DESTINATION_NAMESPACE : '';
}

4
ui-test/src/Constants.ts Normal file
View File

@@ -0,0 +1,4 @@
export const TEST_TIMEOUT: number = 60000;
export const TEST_SLIDING_PANEL_TIMEOUT: number = 5000;
export const TEST_IS_NOT_VISIBLE_TIMEOUT: number = 5000;
export const ENABLE_CONSOLE_LOG: boolean = true;

View File

@@ -0,0 +1,134 @@
import Configuration from './Configuration';
import {Builder, By, until, WebDriver, WebElement} from 'selenium-webdriver';
import chrome from 'selenium-webdriver/chrome';
import * as Const from './Constants';
import {Navigation} from './navigation';
export default class UiTestUtilities {
/**
* Log a message to the console.
* @param message
*/
public static async log(message: string): Promise<void> {
let doLog = Const.ENABLE_CONSOLE_LOG;
// Config override
if (Configuration.ENABLE_CONSOLE_LOG) {
if (Configuration.ENABLE_CONSOLE_LOG === 'false') {
doLog = false;
} else {
doLog = true;
}
}
if (doLog) {
// tslint:disable-next-line:no-console
console.log(message);
}
}
public static async logError(message: string): Promise<void> {
let doLog = Const.ENABLE_CONSOLE_LOG;
// Config override
if (Configuration.ENABLE_CONSOLE_LOG) {
if (Configuration.ENABLE_CONSOLE_LOG === 'false') {
doLog = false;
} else {
doLog = true;
}
}
if (doLog) {
// tslint:disable-next-line:no-console
console.error(message);
}
}
/**
* Set up the WebDriver. Initial steps for all tests. Returns the instance of Navigation with the WebDriver.
* From there, navigate the UI. Test cases do no need to reference the instance of WebDriver since Component/Page-specific
* API methods should be called instead.
*
*/
public static async init(): Promise<Navigation> {
const options = new chrome.Options();
if (process.env.IS_HEADLESS) {
options.addArguments('headless');
}
options.addArguments('window-size=1400x1200');
const driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(options)
.build();
UiTestUtilities.log('Environment variables are:');
UiTestUtilities.log(require('dotenv').config({path: __dirname + '/../.env'}));
// Navigate to the ArgoCD URL
await driver.get(Configuration.ARGOCD_URL);
return new Navigation(driver);
}
/**
* Locate the UI Element for the given locator, and wait until it is visible
*
* @param driver
* @param locator
*/
public static async findUiElement(driver: WebDriver, locator: By): Promise<WebElement> {
try {
let timeout = Const.TEST_TIMEOUT;
if (Configuration.TEST_TIMEOUT) {
timeout = parseInt(Configuration.TEST_TIMEOUT, 10);
}
const element = await driver.wait(until.elementLocated(locator), timeout);
await driver.wait(until.elementIsVisible(element), timeout);
return element;
} catch (err) {
throw err;
}
}
/**
* Similar to until.methods and used in driver.wait, this will wait until
* the expected attribute is the same as the actual attribute on the element
*
* @param attr
* @param attrValue
*/
public static async untilAttributeIs(element: WebElement, attr: string, attrValue: string): Promise<boolean> {
const actual = await element.getAttribute(attr);
UiTestUtilities.log('Actual = ' + actual + ', expected = ' + attrValue + ', ' + (actual === attrValue));
return actual === attrValue;
}
/**
* Similar to until.methods and used in driver.wait, this function will wait until
* the element (eg. operation state) title attribute no longer is present
*
* @param element
*/
public static async untilOperationStatusDisappears(element: WebElement): Promise<boolean> {
try {
const opState = await element.getAttribute('title');
UiTestUtilities.log('Operation State = ' + opState);
return false;
} catch (err) {
UiTestUtilities.log('Status disappeared');
return true;
}
}
/**
* For clicking on elements if WebElement.click() doesn't work
*
* @param driver
* @param element
*/
public static async click(driver: WebDriver, element: WebElement): Promise<void> {
try {
// Execute synchronous script
await driver.executeScript('arguments[0].click();', element);
} catch (e) {
throw e;
}
}
}

View File

@@ -0,0 +1,205 @@
import {By, until, WebDriver} from 'selenium-webdriver';
import {Base} from '../base';
import UiTestUtilities from '../UiTestUtilities';
import * as Const from '../Constants';
const CREATE_APPLICATION_BUTTON_CREATE: By = By.xpath('.//button[@qe-id="applications-list-button-create"]');
const CREATE_APPLICATION_BUTTON_CANCEL: By = By.xpath('.//button[@qe-id="applications-list-button-cancel"]');
const CREATE_APPLICATION_FIELD_APP_NAME: By = By.xpath('.//input[@qeid="application-create-field-app-name"]');
const CREATE_APPLICATION_FIELD_PROJECT: By = By.xpath('.//input[@qe-id="application-create-field-project"]');
const CREATE_APPLICATION_FIELD_REPOSITORY_URL: By = By.xpath('.//input[@qe-id="application-create-field-repository-url"]');
const CREATE_APPLICATION_FIELD_REPOSITORY_PATH: By = By.xpath('.//input[@qe-id="application-create-field-path"]');
const CREATE_APPLICATION_DROPDOWN_DESTINATION: By = By.xpath('.//div[@qe-id="application-create-dropdown-destination"]');
const CREATE_APPLICATION_DROPDOWN_MENU_URL: By = By.xpath('.//li[@qe-id="application-create-dropdown-destination-URL"]');
const CREATE_APPLICATION_DROPDOWN_MENU_NAME: By = By.xpath('.//li[@qe-id="application-create-dropdown-destination-NAME"]');
export const DESTINATION_MENU_NAME: string = 'NAME';
export const DESTINATION_MENU_URL: string = 'URL';
const CREATE_APPLICATION_FIELD_CLUSTER_NAME: By = By.xpath('.//input[@qe-id="application-create-field-cluster-name"]');
const CREATE_APPLICATION_FIELD_CLUSTER_NAMESPACE: By = By.xpath('.//input[@qeid="application-create-field-namespace"]');
const CREATE_APPLICATION_FIELD_CLUSTER_URL: By = By.xpath('.//input[@qe-id="application-create-field-cluster-url"]');
export class ApplicationCreatePanel extends Base {
public constructor(driver: WebDriver) {
super(driver);
}
public async setAppName(appName: string): Promise<void> {
try {
const appNameField = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_APP_NAME);
await appNameField.sendKeys(appName);
} catch (err) {
throw new Error(err);
}
}
public async setProjectName(projectName: string): Promise<void> {
try {
const project = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_PROJECT);
await project.sendKeys(projectName);
} catch (err) {
throw new Error(err);
}
}
public async setSourceRepoUrl(sourceRepoUrl: string): Promise<void> {
try {
const reposUrl = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_REPOSITORY_URL);
await reposUrl.sendKeys(sourceRepoUrl);
} catch (err) {
throw new Error(err);
}
}
public async setSourceRepoPath(sourceRepoPath: string): Promise<void> {
try {
const path = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_REPOSITORY_PATH);
await path.sendKeys(sourceRepoPath);
} catch (err) {
throw new Error(err);
}
}
/**
* Convenience method to select the Destination Cluster URL menu and set the url field with destinationClusterFieldValue
*
* @param destinationClusterFieldValue
*/
public async selectDestinationClusterURLMenu(destinationClusterFieldValue: string): Promise<void> {
try {
const clusterCombo = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_DROPDOWN_DESTINATION);
// click() doesn't work. Use script
await UiTestUtilities.click(this.driver, clusterCombo);
const urlMenu = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_DROPDOWN_MENU_URL);
await urlMenu.click();
if (destinationClusterFieldValue) {
await this.setDestinationClusterUrl(destinationClusterFieldValue);
}
} catch (err) {
throw new Error(err);
}
}
/**
* Convenience method to select the Destination Cluster Name menu and set the namefield with destinationClusterFieldValue
*
* @param destinationClusterFieldValue
*/
public async selectDestinationClusterNameMenu(destinationClusterFieldValue: string): Promise<void> {
try {
const clusterCombo = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_DROPDOWN_DESTINATION);
// click() doesn't work. Use script
await UiTestUtilities.click(this.driver, clusterCombo);
const nameMenu = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_DROPDOWN_MENU_NAME);
await nameMenu.click();
if (destinationClusterFieldValue) {
await this.setDestinationClusterName(destinationClusterFieldValue);
}
} catch (err) {
throw new Error(err);
}
}
public async setDestinationClusterName(destinationClusterName: string): Promise<void> {
try {
const clusterName = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_CLUSTER_NAME);
await clusterName.sendKeys(destinationClusterName);
// await clusterName.sendKeys('\r');
} catch (err) {
throw new Error(err);
}
}
public async setDestinationClusterUrl(destinationClusterUrl: string): Promise<void> {
try {
const clusterUrl = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_CLUSTER_URL);
await clusterUrl.sendKeys(destinationClusterUrl);
} catch (err) {
throw new Error(err);
}
}
public async setDestinationNamespace(destinationNamespace: string): Promise<void> {
try {
const namespace = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_FIELD_CLUSTER_NAMESPACE);
await namespace.sendKeys(destinationNamespace);
} catch (err) {
throw new Error(err);
}
}
/**
* Click the Create button to create the app
*/
public async clickCreateButton(): Promise<void> {
try {
const createButton = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_BUTTON_CREATE);
await createButton.click();
// Wait until the Create Application Sliding Panel disappears
await this.driver.wait(until.elementIsNotVisible(createButton), Const.TEST_SLIDING_PANEL_TIMEOUT).catch(e => {
UiTestUtilities.logError('The Create Application Sliding Panel did not disappear');
throw e;
});
await this.driver.sleep(1000);
} catch (err) {
throw new Error(err);
}
}
/**
* Click the Cancel Button. Do not create the app.
*/
public async clickCancelButton(): Promise<void> {
try {
const cancelButton = await UiTestUtilities.findUiElement(this.driver, CREATE_APPLICATION_BUTTON_CANCEL);
await cancelButton.click();
// Wait until the Create Application Sliding Panel disappears
await this.driver.wait(until.elementIsNotVisible(cancelButton), Const.TEST_SLIDING_PANEL_TIMEOUT).catch(e => {
UiTestUtilities.logError('The Create Application Sliding Panel did not disappear');
throw e;
});
} catch (err) {
throw new Error(err);
}
}
/**
* Convenience method to create an application given the following inputs to the dialog
*
* TODO add Sync Policy and Sync Options and setter methods above
*
* @param appName
* @param projectName
* @param sourceRepoUrl
* @param sourceRepoPath
* @param destinationMenu
* @param destinationClusterName
* @param destinationNamespace
*/
public async createApplication(
appName: string,
projectName: string,
sourceRepoUrl: string,
sourceRepoPath: string,
destinationClusterName: string,
destinationNamespace: string
): Promise<void> {
UiTestUtilities.log('About to create application');
try {
await this.setAppName(appName);
await this.setProjectName(projectName);
await this.setSourceRepoUrl(sourceRepoUrl);
await this.setSourceRepoPath(sourceRepoPath);
await this.selectDestinationClusterNameMenu(destinationClusterName);
await this.setDestinationNamespace(destinationNamespace);
await this.clickCreateButton();
} catch (err) {
throw new Error(err);
}
}
}

View File

@@ -0,0 +1,199 @@
import {By, until, WebDriver} from 'selenium-webdriver';
import UiTestUtilities from '../UiTestUtilities';
import * as Const from '../Constants';
import {Base} from '../base';
import {ApplicationCreatePanel} from '../application-create-panel/application-create-panel';
import {ApplicationsSyncPanel, SYNC_PANEL_SYNCHRONIZE_BUTTON} from '../applications-sync-panel/applications-sync-panel';
import {PopupManager} from '../popup/popup-manager';
const NEW_APP_BUTTON: By = By.xpath('.//button[@qe-id="applications-list-button-new-app"]');
// Uncomment to use:
// const CREATE_APPLICATION_BUTTON: By = By.xpath('.//button[@qe-id="applications-list-button-create-application"]');
export class ApplicationsList extends Base {
private applicationCreatePanel: ApplicationCreatePanel;
private applicationsSyncPanel: ApplicationsSyncPanel;
private popupManager: PopupManager;
public constructor(driver: WebDriver) {
super(driver);
this.applicationCreatePanel = new ApplicationCreatePanel(driver);
this.applicationsSyncPanel = new ApplicationsSyncPanel(driver);
this.popupManager = new PopupManager(driver);
}
public async clickTile(appName: string): Promise<void> {
try {
const tile = await UiTestUtilities.findUiElement(this.driver, this.getApplicationTileLocator(appName));
await tile.click();
} catch (err) {
throw new Error(err);
}
}
/**
* Click the Add New Button
*/
public async clickNewAppButton(): Promise<ApplicationCreatePanel> {
try {
const newAppButton = await UiTestUtilities.findUiElement(this.driver, NEW_APP_BUTTON);
await newAppButton.click();
} catch (err) {
throw new Error(err);
}
return this.applicationCreatePanel;
}
/**
* Click the Sync button on the App tile
*
* @param appName
*/
public async clickSyncButtonOnApp(appName: string): Promise<ApplicationsSyncPanel> {
try {
const syncButton = await UiTestUtilities.findUiElement(this.driver, this.getSyncButtonLocatorForApp(appName));
await syncButton.click();
// Wait until the Synchronize sliding panel appears
const synchronizeButton = await this.driver.wait(until.elementLocated(SYNC_PANEL_SYNCHRONIZE_BUTTON), Const.TEST_TIMEOUT);
await this.driver.wait(until.elementIsVisible(synchronizeButton), Const.TEST_TIMEOUT);
} catch (err) {
throw new Error(err);
}
return this.applicationsSyncPanel;
}
/**
* Delete an application via the Delete button on the App tile
*
* @param appName
*/
public async clickDeleteButtonOnApp(appName: string): Promise<PopupManager> {
try {
const deleteButton = await UiTestUtilities.findUiElement(this.driver, this.getDeleteButtonLocatorForApp(appName));
await deleteButton.click();
} catch (err) {
throw new Error(err);
}
return this.popupManager;
}
public async waitUntilOperationStatusDisappearsOnApp(appName: string) {
const opStateElem = await UiTestUtilities.findUiElement(this.driver, this.getApplicationOperationsTitle(appName));
await this.driver.wait(async () => {
return UiTestUtilities.untilOperationStatusDisappears(opStateElem);
}, Const.TEST_TIMEOUT);
}
/**
* Click on the Refresh button on the App tile
*
* @param appName
*/
public async clickRefreshButtonOnApp(appName: string): Promise<void> {
try {
const refreshButton = await UiTestUtilities.findUiElement(this.driver, this.getRefreshButtonLocatorForApp(appName));
await this.driver.wait(until.elementIsVisible(refreshButton), Const.TEST_TIMEOUT);
await refreshButton.click();
} catch (err) {
throw new Error(err);
}
}
/**
* Use with wait. Wait for the health status of the app to change to Healthy
*
* @param appName
*/
public async waitForHealthStatusOnApp(appName: string): Promise<void> {
try {
const healthStatusElement = await UiTestUtilities.findUiElement(this.driver, this.getApplicationHealthTitle(appName));
await this.driver.wait(async () => {
return UiTestUtilities.untilAttributeIs(healthStatusElement, 'title', 'Healthy');
}, Const.TEST_TIMEOUT);
} catch (err) {
throw new Error(err);
}
}
/**
* Use with wait. Wait for the sync status of the app to change to Synced
*
* @param appName
*/
public async waitForSyncStatusOnApp(appName: string): Promise<void> {
try {
const statusElement = await UiTestUtilities.findUiElement(this.driver, this.getApplicationSyncTitle(appName));
await this.driver.wait(async () => {
return UiTestUtilities.untilAttributeIs(statusElement, 'title', 'Synced');
}, Const.TEST_TIMEOUT);
} catch (err) {
throw new Error(err);
}
}
/**
* Check that there are no operations associated with the app
*
* @param appName
*/
public async checkNoAdditionalOperations(appName: string): Promise<void> {
// Check if there are no operations still running
UiTestUtilities.log('Checking if there are any additional operations');
let opStateElem;
let opState;
try {
opStateElem = await this.driver.wait(until.elementLocated(this.getApplicationOperationsTitle(appName)), Const.TEST_IS_NOT_VISIBLE_TIMEOUT);
UiTestUtilities.logError('Unexpected to locate Operation element.');
opState = await opStateElem.getAttribute('title');
} catch (e) {
// ignore since we expect to not have any existing operations
}
if (opStateElem) {
throw new Error('Expecting no other operations. Actual: ' + opState);
}
}
// Locators
// By.css('#app .applications-tiles .applications-list-" + appName + "'');
private getApplicationTileLocator(appName: string): By {
return By.xpath('.//div[contains(@class,"qe-applications-list-"' + appName + ')');
}
private getSyncButtonLocatorForApp(appName: string): By {
return By.xpath('.//div[contains(@class, "qe-applications-list-' + appName + '")]//div[@class="row"]//ancestor::a[@qe-id="applications-tiles-button-sync"]');
}
private getDeleteButtonLocatorForApp(appName: string): By {
return By.xpath('.//div[contains(@class, "qe-applications-list-' + appName + '")]//div[@class="row"]//ancestor::a[@qe-id="applications-tiles-button-delete"]');
}
private getRefreshButtonLocatorForApp(appName: string): By {
return By.xpath('.//div[contains(@class, "qe-applications-list-' + appName + '")]//div[@class="row"]//ancestor::a[@qe-id="applications-tiles-button-refresh"]');
}
private getApplicationHealthTitle(appName: string): By {
return By.xpath(
'.//div[contains(@class, "qe-applications-list-' +
appName +
'")]//div[@class="row"]//div[@qe-id="applications-tiles-health-status"]//i[@qe-id="utils-health-status-title"]'
);
}
private getApplicationSyncTitle(appName: string): By {
return By.xpath(
'.//div[contains(@class, "qe-applications-list-' +
appName +
'")]//div[@class="row"]//div[@qe-id="applications-tiles-health-status"]//i[@qe-id="utils-sync-status-title"]'
);
}
private getApplicationOperationsTitle(appName: string): By {
return By.xpath(
'.//div[contains(@class, "qe-applications-list-' +
appName +
'")]//div[@class="row"]//div[@qe-id="applications-tiles-health-status"]//i[@qe-id="utils-operations-status-title"]'
);
}
}

View File

@@ -0,0 +1,35 @@
import {By, until, WebDriver} from 'selenium-webdriver';
import {Base} from '../base';
import * as Const from '../Constants';
import UiTestUtilities from '../UiTestUtilities';
export const SYNC_PANEL_SYNCHRONIZE_BUTTON: By = By.xpath('.//button[@qe-id="application-sync-panel-button-synchronize"]');
export class ApplicationsSyncPanel extends Base {
public constructor(driver: WebDriver) {
super(driver);
}
/**
* Click the Sync button
*/
public async clickSyncButton() {
try {
// Wait until the Synchronize button appears
const synchronizeButton = await this.driver.wait(until.elementLocated(SYNC_PANEL_SYNCHRONIZE_BUTTON), Const.TEST_TIMEOUT);
await this.driver.wait(until.elementIsVisible(synchronizeButton), Const.TEST_TIMEOUT);
// Check if the sync button is enabled
await this.driver.wait(until.elementIsEnabled(synchronizeButton), Const.TEST_TIMEOUT);
await synchronizeButton.click();
await this.driver.wait(until.elementIsNotVisible(synchronizeButton), Const.TEST_SLIDING_PANEL_TIMEOUT).catch(e => {
UiTestUtilities.logError('The Synchronization Sliding Panel did not disappear');
throw e;
});
UiTestUtilities.log('Synchronize sliding panel disappeared');
} catch (err) {
throw new Error(err);
}
}
}

9
ui-test/src/base.ts Normal file
View File

@@ -0,0 +1,9 @@
import {WebDriver} from 'selenium-webdriver';
export abstract class Base {
protected driver: WebDriver;
public constructor(driver: WebDriver) {
this.driver = driver;
}
}

93
ui-test/src/navigation.ts Normal file
View File

@@ -0,0 +1,93 @@
import {By, WebDriver} from 'selenium-webdriver';
import {ApplicationsList} from './applications-list/applications-list';
import UiTestUtilities from './UiTestUtilities';
import {Base} from './base';
const NAVBAR_APPLICATIONS_BUTTON: By = By.css('#app .nav-bar .argo-icon-application');
const NAVBAR_SETTINGS_BUTTON: By = By.css('#app .nav-bar .argo-icon-settings');
const NAVBAR_USER_INFO_BUTTON: By = By.css('#app .nav-bar .fa-user-circle');
const NAVBAR_DOCS_BUTTON: By = By.css('#app .nav-bar .argo-icon-docs');
export class Navigation extends Base {
private applicationsList: ApplicationsList;
public constructor(driver: WebDriver) {
super(driver);
this.applicationsList = new ApplicationsList(this.driver);
}
/**
* Click the Applications Nav Bar Button
* Return: reference to ApplicationsList page
*/
public async clickApplicationsNavBarButton(): Promise<ApplicationsList> {
try {
const navBarButton = await UiTestUtilities.findUiElement(this.driver, NAVBAR_APPLICATIONS_BUTTON);
await navBarButton.click();
} catch (err) {
throw new Error(err);
}
return this.applicationsList;
}
/**
* Click the Settings Nav Bar Button
* TODO return settings page
*/
public async clickSettingsNavBarButton() {
try {
const navBarButton = await UiTestUtilities.findUiElement(this.driver, NAVBAR_SETTINGS_BUTTON);
await navBarButton.click();
} catch (err) {
throw new Error(err);
}
}
/**
* Click the User Info Nav Bar Button
* TODO return User Info page
*/
public async clickUserInfoNavBarButton() {
try {
const navBarButton = await UiTestUtilities.findUiElement(this.driver, NAVBAR_USER_INFO_BUTTON);
await navBarButton.click();
} catch (err) {
throw new Error(err);
}
}
/**
* Click the Documentation Nav Bar Button
* TODO return docs page
*/
public async clickDocsNavBarButton() {
try {
const navBarButton = await UiTestUtilities.findUiElement(this.driver, NAVBAR_DOCS_BUTTON);
await navBarButton.click();
} catch (err) {
throw new Error(err);
}
}
/**
* Get the WebDriver. Test cases are not recommended to use this. Use Page/Component objects to perform actions
*/
public getDriver(): WebDriver {
return this.driver;
}
/**
* Call when test case is finished
*/
public async quit() {
await this.driver.quit();
}
/**
* Sleep for t milliseconds. This is not recommended for use by test cases.
* @param t
*/
public async sleep(t: number) {
await this.driver.sleep(t);
}
}

View File

@@ -0,0 +1,35 @@
import {By, WebDriver} from 'selenium-webdriver';
import {Base} from '../base';
import UiTestUtilities from '../UiTestUtilities';
// Popup Confirmation dialog
// Uncomment to use
// const POPUP_OK_BUTTON_CSS: By = By.css('#app .popup-container .qe-argo-popup-ok-button');
// const POPUP_OK_BUTTON: By = By.xpath('.//button[@qe-id="argo-popup-ok-button"]');
// const POPUP_CANCEL_BUTTON: By = By.xpath('.//button[@qe-id="argo-popup-cancel-button"]');
// Popup Prompt dialog
const DELETE_APP_POPUP_FIELD_CONFIRMATION: By = By.xpath('.//input[@qeid="name-field-delete-confirmation"]');
const DELETE_APP_PROMPT_POPUP_OK_BUTTON: By = By.xpath('.//button[@qe-id="prompt-popup-ok-button"]');
const DELETE_APP_PROMPT_POPUP_CANCEL_BUTTON: By = By.xpath('.//button[@qe-id="prompt-popup-cancel-button"]');
export class PopupManager extends Base {
public constructor(driver: WebDriver) {
super(driver);
}
public async setPromptFieldName(appName: string): Promise<void> {
const confirmationField = await UiTestUtilities.findUiElement(this.driver, DELETE_APP_POPUP_FIELD_CONFIRMATION);
await confirmationField.sendKeys(appName);
}
public async clickPromptOk(): Promise<void> {
const okButton = await UiTestUtilities.findUiElement(this.driver, DELETE_APP_PROMPT_POPUP_OK_BUTTON);
await okButton.click();
}
public async clickPromptCancel(): Promise<void> {
const cancelButton = await UiTestUtilities.findUiElement(this.driver, DELETE_APP_PROMPT_POPUP_CANCEL_BUTTON);
await cancelButton.click();
}
}

55
ui-test/src/test001.ts Normal file
View File

@@ -0,0 +1,55 @@
import Configuration from './Configuration';
import UiTestUtilities from './UiTestUtilities';
import {trace} from 'console';
import {ApplicationsList} from './applications-list/applications-list';
import {ApplicationCreatePanel} from './application-create-panel/application-create-panel';
import {ApplicationsSyncPanel} from './applications-sync-panel/applications-sync-panel';
import {PopupManager} from './popup/popup-manager';
/**
* General test that
* - creates an app based on the environment variables (see .env),
* - syncs the app
* - waits for the healthy and sync'ed states
* - deletes the app.
*
* This can be run multiple times for different apps
*
*/
async function doTest() {
const navigation = await UiTestUtilities.init();
try {
const appsList: ApplicationsList = await navigation.clickApplicationsNavBarButton();
const applicationCreatePanel: ApplicationCreatePanel = await appsList.clickNewAppButton();
UiTestUtilities.log('About to create application');
await applicationCreatePanel.setAppName(Configuration.APP_NAME);
await applicationCreatePanel.setProjectName(Configuration.APP_PROJECT);
await applicationCreatePanel.setSourceRepoUrl(Configuration.GIT_REPO);
await applicationCreatePanel.setSourceRepoPath(Configuration.SOURCE_REPO_PATH);
await applicationCreatePanel.selectDestinationClusterNameMenu(Configuration.DESTINATION_CLUSTER_NAME);
await applicationCreatePanel.setDestinationNamespace(Configuration.DESTINATION_NAMESPACE);
await applicationCreatePanel.clickCreateButton();
const appsSyncPanel: ApplicationsSyncPanel = await appsList.clickSyncButtonOnApp(Configuration.APP_NAME);
await appsSyncPanel.clickSyncButton();
await appsList.waitForHealthStatusOnApp(Configuration.APP_NAME);
await appsList.waitForSyncStatusOnApp(Configuration.APP_NAME);
await appsList.checkNoAdditionalOperations(Configuration.APP_NAME);
const popupManager: PopupManager = await appsList.clickDeleteButtonOnApp(Configuration.APP_NAME);
await popupManager.setPromptFieldName(Configuration.APP_NAME);
await popupManager.clickPromptOk();
// After deleting, wait until the delete operation finishes
await appsList.waitUntilOperationStatusDisappearsOnApp(Configuration.APP_NAME);
await UiTestUtilities.log('Test passed');
} catch (e) {
trace('Test failed ', e);
} finally {
await navigation.quit();
}
}
doTest();

27
ui-test/src/test002.ts Normal file
View File

@@ -0,0 +1,27 @@
import UiTestUtilities from './UiTestUtilities';
import {trace} from 'console';
import {ApplicationsList} from './applications-list/applications-list';
import {ApplicationCreatePanel} from './application-create-panel/application-create-panel';
/**
* Test to demo how to visit each page via the navigation bar on the left.
*
*/
async function doTest() {
const navigation = await UiTestUtilities.init();
try {
await navigation.clickDocsNavBarButton();
await navigation.clickUserInfoNavBarButton();
await navigation.clickSettingsNavBarButton();
const appsList: ApplicationsList = await navigation.clickApplicationsNavBarButton();
const applicationCreatePanel: ApplicationCreatePanel = await appsList.clickNewAppButton();
await applicationCreatePanel.clickCancelButton();
await UiTestUtilities.log('Test passed');
} catch (e) {
trace('Test failed ', e);
} finally {
await navigation.quit();
}
}
doTest();

74
ui-test/tsconfig.json Normal file
View File

@@ -0,0 +1,74 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Basic Options */
// "incremental": true, /* Enable incremental compilation */
"target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */
"module": "commonjs", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */
"lib": ["es2017"], /* Specify library files to be included in the compilation. */
"allowJs": true, /* Allow javascript files to be compiled. */
// "checkJs": true, /* Report errors in .js files. */
// "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */
// "declaration": true, /* Generates corresponding '.d.ts' file. */
// "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */
"sourceMap": true, /* Generates corresponding '.map' file. */
// "outFile": "./", /* Concatenate and emit output to single file. */
"outDir": "out", /* Redirect output structure to the directory. */
"rootDir": "src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */
// "composite": true, /* Enable project compilation */
// "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */
// "removeComments": true, /* Do not emit comments to output. */
// "noEmit": true, /* Do not emit outputs. */
// "importHelpers": true, /* Import emit helpers from 'tslib'. */
// "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */
// "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */
/* Strict Type-Checking Options */
"strict": true, /* Enable all strict type-checking options. */
"noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */
// "strictNullChecks": true, /* Enable strict null checks. */
// "strictFunctionTypes": true, /* Enable strict checking of function types. */
// "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */
// "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */
// "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */
// "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */
/* Additional Checks */
"noUnusedLocals": true, /* Report errors on unused locals. */
"noUnusedParameters": true, /* Report errors on unused parameters. */
"noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
/* Module Resolution Options */
"moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */
// "baseUrl": "./", /* Base directory to resolve non-absolute module names. */
// "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */
// "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */
// "typeRoots": [], /* List of folders to include type definitions from. */
// "types": [], /* Type declaration files to be included in compilation. */
// "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */
"esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */
// "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */
// "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
/* Source Map Options */
// "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */
// "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */
/* Experimental Options */
"experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */
// "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */
/* Advanced Options */
// "resolveJsonModule": true, /* Include modules imported with '.json' extension */
"skipLibCheck": true, /* Skip type checking of declaration files. */
"forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */
},
"exclude": [
"node_modules",
"out"
]
}

17
ui-test/tslint.json Normal file
View File

@@ -0,0 +1,17 @@
{
"extends": [
"tslint:recommended", "tslint-plugin-prettier", "tslint-config-prettier"
],
"jsRules": {},
"rules": {
"prettier": true,
"quotemark": [true, "single"],
"no-var-requires": false,
"interface-name": false,
"object-literal-sort-keys": false,
"max-line-length": [true, 200],
"array-type": false,
"max-classes-per-file": false
},
"rulesDirectory": []
}

1529
ui-test/yarn.lock Normal file

File diff suppressed because it is too large Load Diff