Skip to content
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

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

Merged
merged 1 commit into from
Jul 29, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions ui-test/.env
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
#
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For consistency with backend e2e tests please rename this folder to test/smoke.

Copy link
Contributor Author

@keithchong keithchong Dec 18, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The ui-test folder is at the 'top level', as a sibling to the ui folder because both are Node projects. Moving this to test/smoke will mean these tests will be 'mixed in' with the go test cases at a lower level. Should we keep those tests homogeneous?

# 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 changes: 6 additions & 0 deletions ui-test/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
.vscode/
.idea/
.DS_Store
out/
node_modules/
.env
9 changes: 9 additions & 0 deletions ui-test/.prettierrc
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"bracketSpacing": false,
"jsxSingleQuote": true,
"printWidth": 180,
"singleQuote": true,
"tabWidth": 4,
"jsxBracketSameLine": true,
"quoteProps": "consistent"
}
32 changes: 32 additions & 0 deletions ui-test/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
15 changes: 15 additions & 0 deletions ui-test/src/Configuration.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 4 additions & 0 deletions ui-test/src/Constants.ts
Original file line number Diff line number Diff line change
@@ -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;
134 changes: 134 additions & 0 deletions ui-test/src/UiTestUtilities.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
import Configuration from './Configuration';
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a big fan of page object pattern. The idea is to group page-specific methods into a class that represents a page and class instance can be created by navigating to the page so that test look as following:

// initializes web driver and returns instance of `Navigation` class that holds`WebDriver` instance
const navigation = utils.init();
// navigates to `/applications` page, waits for navigation to complete and returns `ApplicationsList` class
const appListPage = navigation.goToAppList();
// clicks new app buttons and returns `NewApplicationDialog` that holds link to sliding panel element
const newAppDialog = await appListPage.newAppButtonClick();
// locates app name element and sends 'myApp' keys
await newAppDialog.setAppName('myApp');
...

ApplicationsList:

export class ApplicationsList {
  panel: WebElement;
  public async setAppName(val: string) {
    findUiElement(Const.CREATE_APPLICATION_FIELD_APP_NAME);
    await appNameField.sendKeys(appName);
  }
}

This approach was very helpful in one of my previous projects and helped us to effectively structure test utility functions.

Copy link
Contributor Author

@keithchong keithchong Dec 15, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @alexmt, what's the naming convention for file names in ArgoCD? For example,

export class VersionPanel extends React.Component<VersionPanelProps, {copyState: CopyState}> {
, the file name is version-info-panel.tsx and the class name defined is VersionPanel. Although, not all files contain classes is probably one reason why VersionPanel.tsx is not used (I'm thinking Java, of course).

I'll try to follow the same structure as in the actual code, eg. I'll create the applications-list folder that contains the applications-list.ts file.

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;
}
}
}
Loading