[CORD-1133] E2E GUI Tests
Change-Id: I521d0cc068a3e328f3d4f421ba4b342bea167e19
diff --git a/Jenkinsfile b/Jenkinsfile
index 3036c3a..cadc328 100644
--- a/Jenkinsfile
+++ b/Jenkinsfile
@@ -30,10 +30,19 @@
sh 'docker tag nginx nginx:candidate'
sh 'docker build --no-cache -t xosproject/xos-gui .'
sh 'docker run -p 4000:4000 --net=host --name xos-gui -d xosproject/xos-gui'
-
+ } catch (err) {
+ currentBuild.result = 'FAILURE'
+ step([$class: 'Mailer', notifyEveryUnstableBuild: true, recipients: 'teo@onlab.us', sendToIndividuals: true])
+ }
+ }
+ dir('build/platform-install') {
+ stage 'Build Mock R-CORD Config'
+ sh `ansible-playbook -i inventory/mock-rcord deploy-xos-playbook.yml`
+ }
+ dir('orchestration/xos-gui') {
+ try {
stage 'Run E2E Tests'
- sh 'curl 127.0.0.1:4000/spa/ --write-out %{http_code} --silent --output /dev/null | grep 200'
-
+ sh 'UI_URL=127.0.0.1:4000/spa/#' protractor conf/protractor.conf.js
currentBuild.result = 'SUCCESS'
} catch (err) {
currentBuild.result = 'FAILURE'
@@ -46,8 +55,10 @@
sh 'docker rmi -f nginx:candidate'
sh 'docker rmi -f nginx:latest'
}
+ }
+ dir('build/platform-install') {
+ sh `ansible-playbook -i inventory/mock-rcord teardown-playbook.yml`
echo "RESULT: ${currentBuild.result}"
}
-
}
}
\ No newline at end of file
diff --git a/conf/protractor.conf.jenkins.js b/conf/protractor.conf.jenkins.js
new file mode 100644
index 0000000..eba3848
--- /dev/null
+++ b/conf/protractor.conf.jenkins.js
@@ -0,0 +1,12 @@
+exports.config = {
+ seleniumServerJar: '/home/teone/selenium/selenium-server-standalone-2.44.0.jar',
+ capabilities: {
+ 'browserName': 'firefox'
+ },
+ specs: [
+ '../e2e/**/*.spec.js'
+ ],
+ jasmineNodeOpts: {
+ showColors: true
+ }
+};
\ No newline at end of file
diff --git a/conf/protractor.conf.js b/conf/protractor.conf.js
new file mode 100644
index 0000000..5709ce0
--- /dev/null
+++ b/conf/protractor.conf.js
@@ -0,0 +1,24 @@
+const SpecReporter = require('jasmine-spec-reporter').SpecReporter;
+
+exports.config = {
+ seleniumAddress: 'http://localhost:4444/wd/hub',
+ suites: {
+ login: '../e2e/login/*.spec.js',
+ dashboard: '../e2e/dashboard/*.spec.js',
+ keyboard: '../e2e/keyboard-shortcuts/*.spec.js',
+ crud: '../e2e/crud/*.spec.js'
+ },
+ onPrepare: function () {
+ jasmine.getEnv().addReporter(new SpecReporter({
+ spec: {
+ displayStacktrace: true
+ }
+ }));
+ },
+ jasmineNodeOpts: {
+ print: function() {},
+ showColors: true, // Use colors in the command line report.
+ defaultTimeoutInterval: (parseInt(process.env.TIMEOUT, 10) + 1000) || 30 * 1000
+ },
+ allScriptsTimeout: parseInt(process.env.TIMEOUT, 10) || 10 * 1000
+};
\ No newline at end of file
diff --git a/e2e/README.md b/e2e/README.md
new file mode 100644
index 0000000..a578c20
--- /dev/null
+++ b/e2e/README.md
@@ -0,0 +1,33 @@
+# End to end test
+
+NOTE: Require protractor to be installed as a global module.
+
+## Setup
+```
+webdriver-manager update
+webdriver-manager start
+```
+
+## Run tests
+
+_Note that this tests are designed to work with the Mock R-CORD config_
+
+```
+protractor conf/protractor.conf.js
+```
+
+Other paramenters you can pass are:
+
+| Variable Name | Description |
+|---------------|--------------------------------------------------------------|
+| UI_URL | Address of the GUI (deaults to `http://192.168.46.100/spa/#` |
+| UI_PWD | Password to login (needed only for remote connections) |
+| TIMEOUT | Time allowed for each test |
+
+ ### Test suites
+
+ If you need to run test for only a particural suite you can use:
+
+ `protractor conf/protractor.conf.js --suite login`
+
+ Suites are defined in `cong/protractor.conf.js`
\ No newline at end of file
diff --git a/e2e/crud/crud.po.js b/e2e/crud/crud.po.js
new file mode 100644
index 0000000..6568819
--- /dev/null
+++ b/e2e/crud/crud.po.js
@@ -0,0 +1,21 @@
+module.exports = new function(){
+
+ // list view
+ this.tableRows = element.all(by.repeater('item in vm.data'));
+ this.tableColumn = element(by.repeater('item in vm.data').row(0))
+ .all(by.repeater('col in vm.columns'));
+
+ this.actionsColumn = element(by.repeater('item in vm.data').row(0))
+ .element(by.css('td:last-child'));
+
+ this.deleteBtn = this.actionsColumn.all(by.tagName('a'));
+
+ this.addBtn = element(by.linkText('Add'));
+
+ // detail page
+ this.formInputs = element.all(by.repeater('field in vm.config.inputs'));
+ this.formBtn = element(by.buttonText('Save'));
+
+ this.nameField = element(by.css('[name="name"]'));
+ this.successFeedback = element(by.css('.alert.alert-success'));
+};
diff --git a/e2e/crud/crud.spec.js b/e2e/crud/crud.spec.js
new file mode 100644
index 0000000..f3660b4
--- /dev/null
+++ b/e2e/crud/crud.spec.js
@@ -0,0 +1,51 @@
+const user = require('../test_helpers/user');
+const page = require('./crud.po');
+const config = require('../test_helpers/config');
+
+describe('XOS CRUD Page', function() {
+
+ beforeEach((done) => {
+ user.login()
+ .then(() => {
+ done();
+ });
+ });
+
+ describe('list view', () => {
+ beforeEach(() => {
+ browser.get(`${config.url}/core/nodes/`);
+ });
+ it('should have a table', () => {
+ expect(page.tableRows.count()).toBe(2);
+ expect(page.tableColumn.count()).toBe(5);
+ expect(page.deleteBtn.count()).toBe(1); // per row
+ });
+
+ it('should have an add button', () => {
+ expect(page.addBtn.isDisplayed()).toBeTruthy();
+ page.addBtn.click();
+ expect(browser.getCurrentUrl()).toBe(`${config.url}/core/nodes/add`);
+ });
+ });
+
+ describe('details view', () => {
+
+ describe('for an existing model', () => {
+ beforeEach(() => {
+ browser.get(`${config.url}/core/nodes/1`);
+ });
+ it('should have a form', () => {
+ expect(page.formInputs.count()).toBe(5);
+ expect(page.formBtn.isPresent()).toBeTruthy();
+ });
+
+ it('should save the model', () => {
+ page.nameField.clear().sendKeys('test');
+ page.formBtn.click();
+ expect(page.nameField.getAttribute('value')).toBe('test');
+ expect(page.successFeedback.isDisplayed()).toBeTruthy();
+ browser.sleep(3000)
+ });
+ })
+ });
+});
\ No newline at end of file
diff --git a/e2e/dashboard/dashboard.po.js b/e2e/dashboard/dashboard.po.js
new file mode 100644
index 0000000..18c1ea8
--- /dev/null
+++ b/e2e/dashboard/dashboard.po.js
@@ -0,0 +1,8 @@
+module.exports = new function(){
+
+ this.graphTitle = element(by.css('xos-coarse-tenancy-graph h1'));
+ this.graphSvg = element(by.css('xos-coarse-tenancy-graph svg'));
+
+ this.summaryTitle = element(by.css('xos-dashboard > .row > .col-xs-12 > h2'));
+ this.summaryBoxes = element.all(by.css('.panel.panel-filled'));
+};
diff --git a/e2e/dashboard/dashboard.spec.js b/e2e/dashboard/dashboard.spec.js
new file mode 100644
index 0000000..d18d6b1
--- /dev/null
+++ b/e2e/dashboard/dashboard.spec.js
@@ -0,0 +1,12 @@
+const user = require('../test_helpers/user');
+const dashboardPage = require('./dashboard.po');
+
+describe('XOS Dashboard', function() {
+ it('should have a graph and system summary', () => {
+ user.login();
+ expect(dashboardPage.graphTitle.isPresent()).toBeTruthy();
+ expect(dashboardPage.graphSvg.isPresent()).toBeTruthy();
+ expect(dashboardPage.summaryTitle.isPresent()).toBeTruthy();
+ expect(dashboardPage.summaryBoxes.count()).toBe(3);
+ });
+});
\ No newline at end of file
diff --git a/e2e/keyboard-shortcuts/keyboard.po.js b/e2e/keyboard-shortcuts/keyboard.po.js
new file mode 100644
index 0000000..7c84e62
--- /dev/null
+++ b/e2e/keyboard-shortcuts/keyboard.po.js
@@ -0,0 +1,11 @@
+module.exports = new function(){
+
+ const body = element(by.css('body'));
+
+ this.pressKey = (key) => {
+ body.sendKeys(key);
+ };
+
+ this.sidePanel = element(by.css('xos-side-panel > section'));
+ this.searchField = element(by.model('vm.query'));
+};
diff --git a/e2e/keyboard-shortcuts/shortcut.spec.js b/e2e/keyboard-shortcuts/shortcut.spec.js
new file mode 100644
index 0000000..3756a52
--- /dev/null
+++ b/e2e/keyboard-shortcuts/shortcut.spec.js
@@ -0,0 +1,19 @@
+const user = require('../test_helpers/user');
+const page = require('./keyboard.po');
+
+describe('XOS Keyboard Shortcuts', function() {
+
+ beforeEach(() => {
+ user.login();
+ });
+
+ it('should open the side panel when ? is pressed', () => {
+ page.pressKey('?');
+ expect(page.sidePanel.getAttribute('class')).toMatch('open');
+ });
+
+ it('should select the search form when f is pressed', () => {
+ page.pressKey('f');
+ expect(page.searchField.getAttribute('placeholder')).toEqual(browser.driver.switchTo().activeElement().getAttribute('placeholder'));
+ });
+});
\ No newline at end of file
diff --git a/e2e/login/login.po.js b/e2e/login/login.po.js
new file mode 100644
index 0000000..52c2b3c
--- /dev/null
+++ b/e2e/login/login.po.js
@@ -0,0 +1,18 @@
+module.exports = new function(){
+
+ const usernameField = element(by.model('username'));
+ const passwordField = element(by.model('password'));
+ const submitBtn = element(by.css('.btn.btn-accent'));
+
+ this.sendUsername = (username) => {
+ usernameField.sendKeys(username);
+ };
+
+ this.sendPassword = (pwd) => {
+ passwordField.sendKeys(pwd);
+ };
+
+ this.submit = () => {
+ submitBtn.click();
+ }
+};
diff --git a/e2e/login/login.spec.js b/e2e/login/login.spec.js
new file mode 100644
index 0000000..cab3b15
--- /dev/null
+++ b/e2e/login/login.spec.js
@@ -0,0 +1,29 @@
+const config = require('../test_helpers/config');
+const user = require('../test_helpers/user');
+const pwd = user.pwd;
+const username = user.username;
+const loginPage = require('./login.po');
+
+describe('XOS Login page', function() {
+
+ beforeEach(() => {
+ browser.get(`${config.url}/login`);
+ });
+
+ it('should not login with a wrong password', function() {
+ loginPage.sendUsername(username);
+ loginPage.sendPassword('wrongpwd');
+ loginPage.submit();
+
+ const alert = element(by.css('.alert.alert-danger'));
+ expect(alert.isDisplayed()).toBeTruthy();
+ });
+
+ it('should login', () => {
+ loginPage.sendUsername(username);
+ loginPage.sendPassword(pwd);
+ loginPage.submit();
+
+ expect(browser.getCurrentUrl()).toEqual(`${config.url}/dashboard`);
+ });
+});
\ No newline at end of file
diff --git a/e2e/test_helpers/config.js b/e2e/test_helpers/config.js
new file mode 100644
index 0000000..53db281
--- /dev/null
+++ b/e2e/test_helpers/config.js
@@ -0,0 +1 @@
+exports.url = process.env.UI_URL || 'http://192.168.46.100/xos/#';
\ No newline at end of file
diff --git a/e2e/test_helpers/user.js b/e2e/test_helpers/user.js
new file mode 100644
index 0000000..9acf2aa
--- /dev/null
+++ b/e2e/test_helpers/user.js
@@ -0,0 +1,36 @@
+const fs = require('fs');
+const config = require('./config');
+const P = require('bluebird');
+
+const username = 'xosadmin@opencord.org';
+
+const getPwd = () => {
+
+ if (process.env.UI_PWD) {
+ return process.env.UI_PWD;
+ }
+
+ const pwdFile = fs.readFileSync('../../build/platform-install/credentials/xosadmin@opencord.org', 'utf8');
+ return pwdFile;
+};
+
+exports.pwd = getPwd();
+
+exports.username = username;
+
+exports.login = P.promisify((done) => {
+ browser.get(`${config.url}/login`);
+
+ browser.getCurrentUrl()
+ .then((url) => {
+ // NOTE login only if it is not yet
+ if (url.indexOf('login') !== -1) {
+ const loginPage = require('../login/login.po');
+ loginPage.sendUsername(username);
+ loginPage.sendPassword(getPwd());
+ loginPage.submit();
+ }
+ browser.waitForAngular();
+ done();
+ });
+});
\ No newline at end of file
diff --git a/package.json b/package.json
index e6cc74e..40be132 100644
--- a/package.json
+++ b/package.json
@@ -26,6 +26,7 @@
"babel-loader": "6.4.1",
"babel-plugin-istanbul": "2.0.3",
"base-href-webpack-plugin": "1.0.0",
+ "bluebird": "^3.5.0",
"browser-sync": "2.18.8",
"browser-sync-spa": "1.0.3",
"copy-webpack-plugin": "4.0.1",
@@ -58,6 +59,7 @@
"istanbul-instrumenter-loader": "2.0.0",
"jasmine": "2.5.3",
"jasmine-jquery": "2.1.1",
+ "jasmine-spec-reporter": "^4.0.0",
"json-loader": "0.5.4",
"karma": "1.6.0",
"karma-angular-filesort": "1.0.2",
@@ -96,6 +98,7 @@
"serve:dist:watch": "gulp serve:dist:watch",
"test": "gulp test",
"test:auto": "gulp test:auto",
+ "test:e2e": "protractor conf/protractor.conf.js",
"config": "gulp config",
"lint": "tslint -c ./tslint.json 'src/**/*.ts'"
},