[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'"
   },