[Intern] Deployment preparation and process tests

Also introduces some fixes and optimizations to login page
tests and cluster settings tab.

Related to blueprint ui-functional-tests-with-intern

Change-Id: I40446e454cc7a47be13b5f8ee291e110bd421182
This commit is contained in:
Nick Bogdanov 2015-08-21 13:17:02 +03:00
parent 92d8355d6e
commit e48d16d84e
14 changed files with 530 additions and 108 deletions

View File

@ -690,8 +690,10 @@ define([
constructorName: 'Volume',
urlRoot: '/api/volumes/',
getMinimalSize: function(minimum) {
var currentDisk = this.collection.disk;
var groupAllocatedSpace = currentDisk.collection.reduce(function(sum, disk) {return disk.id == currentDisk.id ? sum : sum + disk.get('volumes').findWhere({name: this.get('name')}).get('size');}, 0, this);
var currentDisk = this.collection.disk,
groupAllocatedSpace = 0;
if (currentDisk && currentDisk.collection)
groupAllocatedSpace = currentDisk.collection.reduce(function(sum, disk) {return disk.id == currentDisk.id ? sum : sum + disk.get('volumes').findWhere({name: this.get('name')}).get('size');}, 0, this);
return minimum - groupAllocatedSpace;
},
getMaxSize: function() {

View File

@ -15,46 +15,54 @@
**/
define(['underscore',
'../../helpers'], function(_, Helpers) {
'tests/functional/pages/modal',
'../../helpers'], function(_, ModalWindow, Helpers) {
'use strict';
function ClusterPage(remote) {
this.remote = remote;
this.modal = new ModalWindow(remote);
}
ClusterPage.prototype = {
constructor: ClusterPage,
goToTab: function(tabName) {
var that = this;
var self = this;
return this.remote
.then(function() {
return Helpers.clickLinkByText(
that.remote,
self.remote,
'.tabs-box .tabs a',
tabName);
});
},
removeCluster: function() {
var that = this;
removeCluster: function(clusterName) {
var self = this;
return this.remote
.then(
function() {
return this.parent
.setFindTimeout(2000)
.then(function() {
return that.goToTab('Actions');
})
.findByCssSelector('button.delete-environment-btn')
.click()
.end()
.setFindTimeout(2000)
.findByCssSelector('div.modal-content')
.findByCssSelector('button.remove-cluster-btn')
.click()
.end()
.setFindTimeout(2000)
.waitForDeletedByCssSelector('div.modal-content');
}
);
.then(function() {
return self.goToTab('Dashboard');
})
.findByCssSelector('button.delete-environment-btn')
.click()
.end()
.then(function() {
return self.modal.waitToOpen();
})
.then(function() {
return self.modal.clickFooterButton('Delete');
})
.findAllByCssSelector('div.confirm-deletion-form input[type=text]')
.then(function(confirmInputs) {
if (confirmInputs.length)
return confirmInputs[0]
.type(clusterName)
.then(function() {
return self.modal.clickFooterButton('Delete');
});
})
.end()
.then(function() {
return self.modal.waitToClose();
});
},
checkNodeRoles: function(assignRoles) {
return this.remote
@ -79,13 +87,13 @@ define(['underscore',
});
},
checkNodes: function(amount) {
var that = this;
var self = this;
return this.remote
.setFindTimeout(2000)
.then(function() {
return _.range(amount).reduce(
function(result, index) {
return that.remote
return self.remote
.setFindTimeout(1000)
.findAllByCssSelector('.node.discover > label')
.then(function(nodes) {
@ -97,6 +105,47 @@ define(['underscore',
},
true);
});
},
resetEnvironment: function(clusterName) {
var self = this;
return this.remote
.findByCssSelector('button.reset-environment-btn')
.click()
.end()
.then(function() {
return self.modal.waitToOpen();
})
.then(function() {
return self.modal.checkTitle('Reset Environment');
})
.then(function() {
return self.modal.clickFooterButton('Reset');
})
.setFindTimeout(20000)
.findAllByCssSelector('div.confirm-reset-form input[type=text]')
.then(function(confirmationInputs) {
if (confirmationInputs.length)
return confirmationInputs[0]
.type(clusterName)
.then(function() {
return self.modal.clickFooterButton('Reset');
});
})
.end()
.then(function() {
return self.modal.waitToClose();
})
.setFindTimeout(10000)
.waitForDeletedByCssSelector('div.progress-bar');
},
isTabLocked: function(tabName) {
var self = this;
return this.remote
.then(function() {
return self.goToTab(tabName);
})
.findByCssSelector('div.tab-content div.row.changes-locked')
.then(_.constant(true), _.constant(false));
}
};
return ClusterPage;

View File

@ -14,22 +14,25 @@
* under the License.
**/
define([], function() {
define(['tests/functional/pages/modal'], function(ModalWindow) {
'use strict';
function ClustersPage(remote) {
this.remote = remote;
this.modal = new ModalWindow(remote);
}
ClustersPage.prototype = {
constructor: ClustersPage,
createCluster: function(clusterName) {
var self = this;
return this.remote
.setFindTimeout(1000)
.findByClassName('create-cluster')
.click()
.end()
.setFindTimeout(2000)
.findByCssSelector('div.modal-content')
.then(function() {
return self.modal.waitToOpen();
})
.findByName('name')
.clearValue()
.type(clusterName)
@ -41,16 +44,16 @@ define([], function() {
.pressKeys('\uE007')
.pressKeys('\uE007')
.end()
.setFindTimeout(4000)
.waitForDeletedByCssSelector('div.modal-content')
.end();
.then(function() {
return self.modal.waitToClose();
});
},
clusterSelector: '.clusterbox div.name',
goToEnvironment: function(clusterName) {
var that = this;
var self = this;
return this.remote
.setFindTimeout(5000)
.findAllByCssSelector(that.clusterSelector)
.findAllByCssSelector(self.clusterSelector)
.then(function(divs) {
return divs.reduce(
function(matchFound, element) {

View File

@ -36,27 +36,27 @@ define([
});
},
getOut: function() {
var that = this;
var self = this;
return this.remote
.then(function() {
return that.welcomePage.skip();
return self.welcomePage.skip();
})
.then(function() {
return that.loginPage.logout();
return self.loginPage.logout();
});
},
getIn: function() {
var that = this;
var self = this;
return this.remote
.then(function() {
return that.loginPage.logout();
return self.loginPage.logout();
})
.then(function() {
return that.loginPage.login();
return self.loginPage.login();
})
.waitForDeletedByClassName('login-btn')
.then(function() {
return that.welcomePage.skip();
return self.welcomePage.skip();
});
},
clickLink: function(text) {
@ -66,78 +66,67 @@ define([
.click()
.end();
},
waitForModal: function() {
return this.remote
.setFindTimeout(2000)
.findByCssSelector('div.modal-content')
.end();
},
closeModal: function() {
return this.remote
.findByCssSelector('.modal-header button.close')
.click()
.end();
},
waitForElementDeletion: function(cssSelector) {
return this.remote
.setFindTimeout(2000)
.setFindTimeout(5000)
.waitForDeletedByCssSelector(cssSelector)
.catch() // For cases when element is destroyed already
.findByCssSelector(cssSelector)
.then(function() {
throw new Error('Element ' + cssSelector + ' was not destroyed');
}, _.constant(true));
},
waitForModalToClose: function() {
return this.waitForElementDeletion('div.modal-content');
.catch(function(error) {
if (error.name != 'Timeout')
throw error;
}) // For cases when element is destroyed already
.findAllByCssSelector(cssSelector)
.then(function(elements) {
if (elements.length)
throw new Error('Element ' + cssSelector + ' was not destroyed');
});
},
goToEnvironment: function(clusterName, tabName) {
var that = this;
var self = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
return self.clickLink('Environments');
})
.then(function() {
return that.clustersPage.goToEnvironment(clusterName);
return self.clustersPage.goToEnvironment(clusterName);
})
.then(function() {
if (tabName) return that.clusterPage.goToTab(tabName);
if (tabName) return self.clusterPage.goToTab(tabName);
});
},
createCluster: function(clusterName) {
var that = this;
var self = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
return self.clickLink('Environments');
})
.then(function() {
return that.clustersPage.createCluster(clusterName);
return self.clustersPage.createCluster(clusterName);
});
},
removeCluster: function(clusterName, suppressErrors) {
var that = this;
var self = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
return self.clickLink('Environments');
})
.then(function() {
return that.clustersPage.goToEnvironment(clusterName);
return self.clustersPage.goToEnvironment(clusterName);
})
.then(function() {
return that.clusterPage.removeCluster();
return self.clusterPage.removeCluster(clusterName);
})
.catch(function() {
if (!suppressErrors) throw new Error('Unable to delete cluster ' + clusterName);
});
},
doesClusterExist: function(clusterName) {
var that = this;
var self = this;
return this.remote
.setFindTimeout(2000)
.then(function() {
return that.clickLink('Environments');
return self.clickLink('Environments');
})
.findAllByCssSelector(that.clustersPage.clusterSelector)
.findAllByCssSelector(self.clustersPage.clusterSelector)
.then(function(divs) {
return divs.reduce(function(matchFound, element) {
return element.getVisibleText().then(
@ -148,19 +137,21 @@ define([
});
},
addNodesToCluster: function(nodesAmount, nodesRoles) {
var that = this;
var self = this;
return this.remote
.setFindTimeout(5000)
.findByCssSelector('a.btn-add-nodes')
.then(function() {
return self.clusterPage.goToTab('Nodes');
})
.findByCssSelector('button.btn-add-nodes')
.click()
.end()
.findByCssSelector('div.role-panel')
.end()
.then(function() {
return that.clusterPage.checkNodeRoles(nodesRoles);
return self.clusterPage.checkNodeRoles(nodesRoles);
})
.then(function() {
return that.clusterPage.checkNodes(nodesAmount);
return self.clusterPage.checkNodes(nodesAmount);
})
.findByCssSelector('button.btn-apply')
.click()
@ -168,6 +159,17 @@ define([
.setFindTimeout(2000)
.findByCssSelector('button.btn-add-nodes')
.end();
},
doesCssSelectorContainText: function(cssSelector, searchedText) {
return this.remote
.findAllByCssSelector(cssSelector)
.then(function(messages) {
return messages.reduce(function(result, message) {
return message.getVisibleText().then(function(visibleText) {
return visibleText == searchedText || result;
});
}, false)
});
}
};
return CommonMethods;

View File

@ -0,0 +1,75 @@
/*
* Copyright 2015 Mirantis, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
**/
define(['underscore', 'tests/functional/pages/modal'], function(_, ModalWindow) {
'use strict';
function DashboardPage(remote) {
this.remote = remote;
this.modal = new ModalWindow(remote);
}
DashboardPage.prototype = {
constructor: DashboardPage,
isDeploymentButtonVisible: function() {
return this.remote
.setFindTimeout(100)
.findAllByCssSelector('button.deploy-btn')
.then(function(buttons) {
return buttons.length > 0;
});
},
startDeployment: function() {
var self = this;
return this.remote
.setFindTimeout(2000)
.findByCssSelector('div.deploy-block button.deploy-btn')
.click()
.end()
.then(function() {
return self.modal.waitToOpen();
})
.then(function() {
return self.modal.checkTitle('Deploy Changes');
})
.then(function() {
return self.modal.clickFooterButton('Deploy');
})
.then(function() {
return self.modal.waitToClose();
});
},
stopDeployment: function() {
var self = this;
return this.remote
.findByCssSelector('button.stop-deployment-btn')
.click()
.end()
.then(function() {
return self.modal.waitToOpen();
})
.then(function() {
return self.modal.checkTitle('Stop Deployment');
})
.then(function() {
return self.modal.clickFooterButton('Stop');
})
.then(function() {
return self.modal.waitToClose();
});
}
};
return DashboardPage;
});

View File

@ -28,7 +28,7 @@ define([
login: function(username, password) {
username = username || Helpers.username;
password = password || Helpers.password;
var that = this;
var self = this;
return this.remote
.setWindowSize(1280, 1024)
@ -36,7 +36,7 @@ define([
.getCurrentUrl()
.then(function(url) {
if (url !== Helpers.serverUrl + '/#login') {
return that.logout();
return self.logout();
}
})
.setFindTimeout(10000)

View File

@ -0,0 +1,62 @@
define([
'intern/chai!assert'
],
function(assert) {
'use strict';
function ModalWindow(remote) {
this.remote = remote;
}
ModalWindow.prototype = {
constructor: ModalWindow,
waitToOpen: function() {
return this.remote
.setFindTimeout(2000)
.findByCssSelector('div.modal-content')
.end();
},
checkTitle: function(expectedTitle) {
return this.remote
.findByCssSelector('h4.modal-title')
.getVisibleText()
.then(function(title) {
assert.equal(title, expectedTitle, 'Unexpected modal window title');
})
.end();
},
close: function() {
return this.remote
.findByCssSelector('.modal-header button.close')
.click()
.end();
},
clickFooterButton: function(buttonText) {
return this.remote
.findAllByCssSelector('.modal-footer button')
.then(function(buttons) {
return buttons.reduce(function(result, button) {
return button.getVisibleText()
.then(function(buttonTitle) {
if (buttonTitle == buttonText)
return button.isEnabled()
.then(function(isEnabled) {
if (isEnabled) {
return button.click();
} else
throw Error('Unable to click disabled button "' + buttonText + '"');
});
return result;
});
}, null);
});
},
waitToClose: function() {
var CommonPage = require('tests/functional/pages/common'),
common = new CommonPage(this.remote);
return common.waitForElementDeletion('div.modal-content');
}
};
return ModalWindow;
}
);

View File

@ -0,0 +1,223 @@
/*
* Copyright 2015 Mirantis, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may
* not use this file except in compliance with the License. You may obtain
* a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations
* under the License.
**/
define([
'underscore',
'intern!object',
'intern/chai!assert',
'tests/functional/pages/common',
'tests/functional/pages/cluster',
'tests/functional/pages/dashboard',
'tests/functional/pages/modal'
], function(_, registerSuite, assert, Common, ClusterPage, DashboardPage, ModalWindow) {
'use strict';
registerSuite(function() {
var common,
clusterPage,
dashboardPage,
modal,
clusterName;
return {
name: 'Cluster deployment',
setup: function() {
common = new Common(this.remote);
clusterPage = new ClusterPage(this.remote);
dashboardPage = new DashboardPage(this.remote);
modal = new ModalWindow(this.remote);
clusterName = common.pickRandomName('Test Cluster');
return this.remote
.then(function() {
return common.getIn();
})
.then(function() {
return common.createCluster(clusterName);
});
},
beforeEach: function() {
return this.remote
.then(function() {
return clusterPage.goToTab('Dashboard');
});
},
teardown: function() {
return this.remote
.then(function() {
return common.removeCluster(clusterName);
});
},
'No deployment button when there are no nodes added': function() {
return this.remote
.then(function() {
return dashboardPage.isDeploymentButtonVisible();
})
.then(function(isVisible) {
assert.isFalse(isVisible, 'No deployment should be possible without nodes added');
});
},
'No controller warning': function() {
this.timeout = 120000;
return this.remote
.then(function() {
// Adding single compute
return common.addNodesToCluster(1, ['Compute']);
})
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.then(function() {
return dashboardPage.isDeploymentButtonVisible();
})
.then(function(isVisible) {
assert.isFalse(isVisible, 'No deployment should be possible without controller nodes added');
})
.findByCssSelector('div.instruction.invalid')
// Invalid configuration message is shown
.end()
.then(function() {
return common.doesCssSelectorContainText(
'div.validation-result ul.danger li',
'At least 1 Controller nodes are required (0 selected currently).');
})
.then(function(messageFound) {
assert.isTrue(messageFound, 'No controllers added warning should be shown');
});
},
'Discard changes': function() {
this.timeout = 120000;
return this.remote
.then(function() {
// Adding three controllers
return common.addNodesToCluster(1, ['Controller']);
})
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.then(function() {
return common.clickLink('Discard Changes');
})
.then(function() {
return modal.waitToOpen();
})
.then(function() {
return common.doesCssSelectorContainText('h4.modal-title', 'Discard Changes');
})
.then(function(result) {
assert.isTrue(result, 'Discard Changes confirmation modal expected');
})
.then(function() {
return modal.clickFooterButton('Discard');
})
.then(function() {
return modal.waitToClose();
})
.findByCssSelector('div.deploy-readiness a.btn-add-nodes')
// All changes discarded, add nodes button gets visible
// in deploy readiness block
.end();
},
'Start/stop deployment': function() {
this.timeout = 120000;
return this.remote
.then(function() {
return common.addNodesToCluster(3, ['Controller']);
})
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.then(function() {
return dashboardPage.startDeployment();
})
.setFindTimeout(2000)
.findAllByCssSelector('div.deploy-process div.progress')
.then(function(elements) {
assert.ok(elements.length, 'Deployment progress bar expected to appear');
})
.end()
.then(function() {
return dashboardPage.stopDeployment();
})
.then(function() {
// Progress bar disappears
return common.waitForElementDeletion('div.deploy-process div.progress');
})
// Deployment button available
.findByCssSelector('div.deploy-block button.deploy-btn')
.end()
.findByCssSelector('div.alert-warning strong')
.getVisibleText()
.then(function(alertTitle) {
assert.equal(alertTitle, 'Success', 'Deployment successfully stopped alert is expected');
})
.end()
// Reset environment button is available
.then(function() {
return clusterPage.resetEnvironment(clusterName);
});
},
'Test tabs locking after deployment completed': function() {
this.timeout = 120000;
return this.remote
.then(function() {
// Adding single controller (enough for deployment)
return common.addNodesToCluster(1, ['Controller']);
})
.then(function() {
return clusterPage.isTabLocked('Networks');
})
.then(function(isLocked) {
assert.isFalse(isLocked, 'Networks tab is not locked for undeployed cluster');
})
.then(function() {
return clusterPage.isTabLocked('Settings');
})
.then(function(isLocked) {
assert.isFalse(isLocked, 'Settings tab is not locked for undeployed cluster');
})
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.then(function() {
return dashboardPage.startDeployment();
})
.setFindTimeout(120000)
// Deployment competed
.findByCssSelector('div.horizon')
.end()
.then(function() {
return clusterPage.isTabLocked('Networks');
})
.then(function(isLocked) {
assert.isTrue(isLocked, 'Networks tab should turn locked after deployment');
})
.then(function() {
return clusterPage.isTabLocked('Settings');
})
.then(function(isLocked) {
assert.isTrue(isLocked, 'Settings tab should turn locked after deployment');
})
.then(function() {
return clusterPage.goToTab('Dashboard');
})
.then(function() {
return clusterPage.resetEnvironment(clusterName);
})
}
};
});
});

View File

@ -18,12 +18,14 @@ define([
'underscore',
'intern!object',
'intern/chai!assert',
'tests/functional/pages/modal',
'tests/functional/pages/common'
], function(_, registerSuite, assert, Common) {
], function(_, registerSuite, assert, ModalWindow, Common) {
'use strict';
registerSuite(function() {
var common,
modal,
clusterName,
nodesAmount = 4;
@ -31,6 +33,7 @@ define([
name: 'Cluster Nodes page',
setup: function() {
common = new Common(this.remote);
modal = new ModalWindow(this.remote);
clusterName = common.pickRandomName('Test Cluster');
return this.remote
@ -47,7 +50,7 @@ define([
teardown: function() {
return this.remote
.then(function() {
return common.removeCluster(clusterName, true);
return common.removeCluster(clusterName);
});
},
afterEach: function() {
@ -99,7 +102,7 @@ define([
.click()
.end()
.then(function() {
return common.waitForModal();
return modal.waitToOpen();
})
.findByCssSelector('.modal-header h4.modal-title')
.getVisibleText()
@ -108,10 +111,10 @@ define([
})
.end()
.then(function() {
return common.closeModal();
return modal.close();
})
.then(function() {
return common.waitForModalToClose();
return modal.waitToClose();
});
},
'Compact View Mode': function() {
@ -126,7 +129,7 @@ define([
.findByCssSelector('div.node-checkbox')
.click()
.findByCssSelector('i.glyphicon-ok')
// Check that node is selectable
// Check self node is selectable
.end()
.end();
},
@ -154,13 +157,13 @@ define([
return common.waitForElementDeletion('div.node-popover');
})
.then(function() {
return common.waitForModal();
return modal.waitToOpen();
})
.then(function() {
return common.closeModal();
return modal.close();
})
.then(function() {
return common.waitForModalToClose();
return modal.waitToClose();
})
.findByCssSelector('div.compact-node div.node-hardware p.btn')
// Open popover again
@ -172,14 +175,14 @@ define([
.end()
.then(function() {
// Deletion confirmation shows up
return common.waitForModal();
return modal.waitToOpen();
})
.findByCssSelector('div.modal-content button.btn-delete')
// Confirm deletion
.click()
.end()
.then(function() {
return common.waitForModalToClose();
return modal.waitToClose();
})
.findAllByCssSelector('div.compact-node')
.then(function(nodes) {

View File

@ -70,14 +70,14 @@ define([
},
'Add Cluster Nodes': function() {
var nodesAmount = 3,
that = this,
self = this,
applyButton;
return this.remote
.then(function() {
return common.goToEnvironment(clusterName);
})
.setFindTimeout(5000)
.findByCssSelector('button.btn-add-nodes')
.findByCssSelector('a.btn-add-nodes')
.click()
.end()
.findByCssSelector('button.btn-apply')
@ -113,7 +113,7 @@ define([
.then(function() {
return _.range(1, 1 + nodesAmount).reduce(
function(nodesFound, index) {
return that.remote
return self.remote
.setFindTimeout(1000)
.findByCssSelector('div.node:nth-child(' + index + ')')
.catch(function() {

View File

@ -43,7 +43,7 @@ define([
afterEach: function() {
return this.remote
.then(function() {
return common.removeCluster(clusterName, true);
return common.removeCluster(clusterName);
});
},
'Create Cluster': function() {

View File

@ -41,11 +41,10 @@ define([
.then(function() {
return loginPage.login('login', '*****');
})
.findByCssSelector('div.login-fields-box p.text-danger')
.isDisplayed()
.then(function(errorShown) {
assert.ok(errorShown, 'Error message is expected to be displayed');
});
.findAllByCssSelector('div.login-error')
.then(function(elements) {
assert.ok(elements.length, 'Error message is expected to get displayed');
});
},
'Login with proper credentials': function() {
return this.remote

View File

@ -195,10 +195,14 @@ function($, _, i18n, React, utils, models, Expression, componentMixins, controls
lockedCluster = !cluster.isAvailableForSettingsChanges(),
someSettingsEditable = _.any(settings.attributes, function(group) {return group.metadata.always_editable;}),
hasChanges = this.hasChanges(),
allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles'))));
allocatedRoles = _.uniq(_.flatten(_.union(cluster.get('nodes').pluck('roles'), cluster.get('nodes').pluck('pending_roles')))),
classes = {
row: true,
'changes-locked': lockedCluster
};
return (
<div key={this.state.key} className='row'>
<div key={this.state.key} className={utils.classNames(classes)}>
<div className='title'>{i18n('cluster_page.settings_tab.title')}</div>
<SettingSubtabs
settings={settings}

View File

@ -26,7 +26,7 @@ casper.then(function() {
var authenticated = this.evaluate(function() {
return window.app.user.get('authenticated');
}),
token = this.evaluate(function() {
token = this.evaluate(function() {
return window.app.keystoneClient.token;
});