Intern-based functional tests

This commit makes it possible to execute UI
functional tests either based on Casper or on Intern/Selenium

Change-Id: I21d342093f142a1bbe7b8ae4f2c96fad1c125a0b
This commit is contained in:
Nick Bogdanov 2015-07-08 15:46:09 +03:00
parent b723bc2cde
commit 9a23663c74
13 changed files with 802 additions and 5 deletions

View File

@ -158,7 +158,7 @@ function runIntern(params) {
return function() {
var baseDir = 'static';
var runner = './node_modules/.bin/intern-runner';
var browser = params.browser || argv.browser || 'phantomjs';
var browser = params.browser || argv.browser || 'firefox';
var options = [['config', 'tests/intern-' + browser + '.js']];
var suiteOptions = [];
['suites', 'functionalSuites'].forEach(function(suiteType) {
@ -181,7 +181,8 @@ function runIntern(params) {
};
}
gulp.task('intern:unit', runIntern({suites: argv.suites || 'static/tests/unit/**/*.js'}));
gulp.task('intern:unit', runIntern({suites: argv.suites || 'static/tests/unit/**/*.js', browser: 'phantomjs'}));
gulp.task('intern:functional', runIntern({functionalSuites: argv.suites || 'static/tests/functional/**/test_*.js'}));
gulp.task('unit-tests', function(cb) {
runSequence('selenium', 'intern:unit', function(err) {
@ -190,6 +191,13 @@ gulp.task('unit-tests', function(cb) {
});
});
gulp.task('functional-tests', function(cb) {
runSequence('selenium', 'intern:functional', function(err) {
shutdownSelenium();
cb(err);
});
});
gulp.task('jison', function() {
return gulp.src('static/expression/parser.jison')
.pipe(jison({moduleType: 'js'}))

View File

@ -35,6 +35,6 @@
"rimraf": "~2.2.8",
"run-sequence": "~1.0.2",
"selenium-standalone": "~4.4.0",
"uglify-js": "~2.4.16"
"uglify-js": "~2.4.21"
}
}

View File

@ -0,0 +1,103 @@
/*
* 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',
'../../helpers'], function(_, Helpers) {
'use strict';
function ClusterPage(remote) {
this.remote = remote;
}
ClusterPage.prototype = {
constructor: ClusterPage,
goToTab: function(tabName) {
var that = this;
return this.remote
.then(function() {
return Helpers.clickLinkByText(
that.remote,
'.tabs-box .tabs a',
tabName);
});
},
removeCluster: function() {
var that = 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');
}
);
},
checkNodeRoles: function(assignRoles) {
return this.remote
.setFindTimeout(2000)
.findAllByCssSelector('div.role-panel label')
.then(function(roles) {
return roles.reduce(
function(result, role) {
return role
.getVisibleText()
.then(function(label) {
var index = assignRoles.indexOf(label.substr(1));
if (index >= 0) {
role.click();
assignRoles.splice(index, 1);
return assignRoles.length == 0;
}
});
},
false
);
});
},
checkNodes: function(amount) {
var that = this;
return this.remote
.setFindTimeout(2000)
.then(function() {
return _.range(amount).reduce(
function(result, index) {
return that.remote
.setFindTimeout(1000)
.findAllByCssSelector('.node.discover > label')
.then(function(nodes) {
return nodes[index].click();
})
.catch(function() {
throw new Error('Failed to add ' + amount + ' nodes to the cluster');
});
},
true);
});
}
};
return ClusterPage;
});

View File

@ -0,0 +1,78 @@
/*
* 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([], function() {
'use strict';
function ClustersPage(remote) {
this.remote = remote;
}
ClustersPage.prototype = {
constructor: ClustersPage,
createCluster: function(clusterName) {
return this.remote
.setFindTimeout(1000)
.findByClassName('create-cluster')
.click()
.end()
.setFindTimeout(2000)
.findByCssSelector('div.modal-content')
.findByName('name')
.clearValue()
.type(clusterName)
.pressKeys('\uE007')
.pressKeys('\uE007')
.pressKeys('\uE007')
.pressKeys('\uE007')
.pressKeys('\uE007')
.pressKeys('\uE007')
.pressKeys('\uE007')
.end()
.setFindTimeout(4000)
.waitForDeletedByCssSelector('div.modal-content')
.end();
},
clusterSelector: '.clusterbox div.name',
goToEnvironment: function(clusterName) {
var that = this;
return this.remote
.setFindTimeout(5000)
.findAllByCssSelector(that.clusterSelector)
.then(function(divs) {
return divs.reduce(
function(matchFound, element) {
return element.getVisibleText().then(
function(name) {
if (name === clusterName) {
element.click();
return true;
}
return matchFound;
}
)},
false
);
})
.then(function(result) {
if (!result) {
throw new Error('Cluster ' + clusterName + ' not found');
}
return true;
});
}
};
return ClustersPage;
});

View File

@ -0,0 +1,132 @@
define([
'intern/node_modules/dojo/node!fs',
'../../helpers',
'tests/functional/pages/login',
'tests/functional/pages/welcome',
'tests/functional/pages/cluster',
'tests/functional/pages/clusters'
],
function(fs, Helpers, LoginPage, WelcomePage, ClusterPage, ClustersPage) {
'use strict';
function CommonMethods(remote) {
this.remote = remote;
this.loginPage = new LoginPage(remote);
this.welcomePage = new WelcomePage(remote);
this.clusterPage = new ClusterPage(remote);
this.clustersPage = new ClustersPage(remote);
}
CommonMethods.prototype = {
constructor: CommonMethods,
takeScreenshot: function(filename, error) {
return this.remote
.takeScreenshot()
.then(function(buffer) {
if (!filename) {
filename = new Date().toTimeString();
}
fs.writeFileSync('/tmp/' + filename + '.png', buffer);
if (error) {
throw error;
}
});
},
getOut: function() {
var that = this;
return this.remote
.then(function() {
return that.welcomePage.skip();
})
.then(function() {
return that.loginPage.logout();
});
},
getIn: function() {
var that = this;
return this.remote
.then(function() {
return that.loginPage.logout();
})
.then(function() {
return that.loginPage.login();
})
.waitForDeletedByClassName('login-btn')
.then(function() {
return that.welcomePage.skip();
});
},
clickLink: function(text) {
return this.remote
.setFindTimeout(1000)
.findByLinkText(text)
.click()
.end();
},
waitForModal: function() {
return this.remote
.setTimeout(2000)
.findByCssSelector('div.modal-content')
.end();
},
waitForModalToClose: function() {
return this.remote
.setTimeout(2000)
.waitForDeletedByCssSelector('div.modal-content')
.end();
},
goToEnvironment: function(clusterName) {
var that = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
})
.then(function() {
return that.clustersPage.goToEnvironment(clusterName);
});
},
createCluster: function(clusterName) {
var that = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
})
.then(function() {
return that.clustersPage.createCluster(clusterName);
});
},
removeCluster: function(clusterName, suppressErrors) {
var that = this;
return this.remote
.then(function() {
return that.clickLink('Environments');
})
.then(function() {
return that.clustersPage.goToEnvironment(clusterName);
})
.then(function() {
return that.clusterPage.removeCluster();
})
.catch(function() {
if (!suppressErrors) throw new Error('Unable to delete cluster ' + clusterName);
});
},
doesClusterExist: function(clusterName) {
var that = this;
return this.remote
.setFindTimeout(2000)
.then(function() {
return that.clickLink('Environments');
})
.findAllByCssSelector(that.clustersPage.clusterSelector)
.then(function(divs) {
return divs.reduce(function(matchFound, element) {
return element.getVisibleText().then(
function(name) {
return (name === clusterName) || matchFound;
}
)}, false);
});
}
};
return CommonMethods;
});

View File

@ -0,0 +1,84 @@
/*
* 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([
'../../helpers'
], function(Helpers) {
'use strict';
function LoginPage(remote) {
this.remote = remote;
}
LoginPage.prototype = {
constructor: LoginPage,
login: function(username, password) {
username = username || Helpers.username;
password = password || Helpers.password;
var that = this;
return this.remote
.setWindowSize(1280, 1024)
.setTimeout('page load', 20000)
.getCurrentUrl()
.then(function(url) {
if (url !== Helpers.serverUrl + '/#login') {
return that.logout();
}
})
.setFindTimeout(10000)
.findByName('username')
.clearValue()
.type(username)
.end()
.findByName('password')
.clearValue()
.type(password)
.end()
.findByClassName('login-btn')
.click()
.end();
},
logout: function() {
return this.remote
.getCurrentUrl()
.then(function(url) {
if (url.indexOf(Helpers.serverUrl) !== 0) {
return this.parent
.get(Helpers.serverUrl + '/#logout')
.setFindTimeout(10000)
.findByClassName('login-btn')
.then(function() {
return true;
});
}
})
.setFindTimeout(1000)
.findByCssSelector('li.user-icon')
.click()
.end()
.findByCssSelector('.user-popover button.btn-logout')
.click()
.end()
.findByCssSelector('.login-btn')
.then(
function() {return true},
function() {return true}
);
}
};
return LoginPage;
});

View File

@ -0,0 +1,47 @@
/*
* 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(['../../helpers'], function(Helpers) {
'use strict';
function WelcomePage(remote) {
this.remote = remote;
}
WelcomePage.prototype = {
constructor: WelcomePage,
skip: function(strictCheck) {
return this.remote
.getCurrentUrl()
.then(function(url) {
if (url == Helpers.serverUrl + '/#welcome') {
return this.parent
.setFindTimeout(2000)
.findByCssSelector('.welcome-button-box button')
.click()
.end()
.waitForDeletedByCssSelector('.welcome-button-box button')
.then(
function() {return true},
function() {return !strictCheck}
);
} else {
return true;
}
});
}
};
return WelcomePage;
});

View File

@ -0,0 +1,129 @@
/*
* 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'
], function(_, registerSuite, assert, Common, ClusterPage) {
'use strict';
registerSuite(function() {
var common,
clusterPage,
clusterName;
return {
name: 'Clusters page',
setup: function() {
common = new Common(this.remote);
clusterPage = new ClusterPage(this.remote);
clusterName = 'Test Cluster #' + Math.round(99999 * Math.random());
},
beforeEach: function() {
return this.remote
.then(function() {
return common.getIn();
})
.then(function() {
return common.createCluster(clusterName);
});
},
afterEach: function() {
return this.remote
.then(function() {
return common.removeCluster(clusterName, true);
});
},
'Remove Cluster': function() {
return this.remote
.then(function() {
return common.doesClusterExist(clusterName);
})
.then(function(result) {
assert.ok(result, 'Cluster exists');
})
.then(function() {
return common.removeCluster(clusterName);
})
.then(function() {
return common.doesClusterExist(clusterName);
})
.then(function(result) {
assert.notOk(result, 'Cluster removed successfully');
});
},
'Add Cluster Nodes': function() {
var nodesAmount = 3,
that = this,
applyButton;
return this.remote
.then(function() {
return common.goToEnvironment(clusterName);
})
.setFindTimeout(5000)
.findByCssSelector('button.btn-add-nodes')
.click()
.end()
.findByCssSelector('button.btn-apply')
.then(function(button) {
applyButton = button;
return applyButton.isEnabled().then(function(isEnabled) {
assert.isFalse(isEnabled, 'Apply button is disabled until both roles and nodes chosen');
return true;
});
})
.end()
.findByCssSelector('div.role-panel')
.end()
.then(function() {
return clusterPage.checkNodeRoles(['Controller', 'Storage - Cinder']);
})
.then(function() {
return applyButton.isEnabled().then(function(isEnabled) {
assert.isFalse(isEnabled, 'Apply button is disabled until both roles and nodes chosen');
return true;
});
})
.then(function() {
return clusterPage.checkNodes(nodesAmount);
})
.then(function() {
applyButton.click();
})
.setFindTimeout(2000)
.findByCssSelector('button.btn-add-nodes')
.end()
.then(function() {
return _.range(1, 1 + nodesAmount).reduce(
function(nodesFound, index) {
return that.remote
.setFindTimeout(1000)
.findByCssSelector('div.node:nth-child(' + index + ')')
.catch(function() {
throw new Error('Unable to find ' + index + ' node in cluster');
});
},
0
);
});
}
};
});
});

View File

@ -0,0 +1,60 @@
/*
* 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([
'intern!object',
'intern/chai!assert',
'tests/functional/pages/common'
], function(registerSuite, assert, Common) {
'use strict';
registerSuite(function() {
var common,
clusterName;
return {
name: 'Clusters page',
setup: function() {
common = new Common(this.remote);
clusterName = 'Test Cluster #' + Math.round(99999 * Math.random());
},
beforeEach: function() {
return this.remote
.then(function() {
return common.getIn();
})
.then(function() {
return common.createCluster(clusterName);
});
},
afterEach: function() {
return this.remote
.then(function() {
return common.removeCluster(clusterName, true);
});
},
'Create Cluster': function() {
return this.remote
.then(function() {
return common.doesClusterExist(clusterName);
})
.then(function(result) {
assert.ok(result, 'Newly created cluster name found in the list');
});
}
};
});
});

View File

@ -0,0 +1,59 @@
/*
* Copyright 2014 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([
'intern!object',
'intern/chai!assert',
'tests/functional/pages/login',
'tests/functional/pages/common'
], function(registerSuite, assert, LoginPage, Common) {
'use strict';
registerSuite(function() {
var loginPage, common;
return {
name: 'Login page',
setup: function() {
loginPage = new LoginPage(this.remote);
common = new Common(this.remote);
},
beforeEach: function() {
this.remote
.then(function() {
return common.getOut();
});
},
'Login with incorrect credentials': function() {
return this.remote
.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');
});
},
'Login with proper credentials': function() {
return this.remote
.then(function() {
return loginPage.login();
})
.waitForDeletedByClassName('login-btn');
}
};
});
});

View File

@ -0,0 +1,49 @@
/*
* 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([
'intern!object',
'intern/chai!assert',
'tests/functional/pages/login',
'tests/functional/pages/welcome'
], function(registerSuite, assert, LoginPage, WelcomePage) {
'use strict';
registerSuite(function() {
var loginPage,
welcomePage;
return {
name: 'Welcome page',
setup: function() {
loginPage = new LoginPage(this.remote);
welcomePage = new WelcomePage(this.remote);
},
'Skip welcome page': function() {
return this.remote
.then(function() {
return loginPage.login();
})
.then(function() {
return welcomePage.skip(true);
})
.then(function(result) {
assert.ok(result, 'Start using fuel button is present');
});
}
};
});
});

47
static/tests/helpers.js Normal file
View File

@ -0,0 +1,47 @@
/*
* Copyright 2014 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'], function(_) {
'use strict';
var serverHost = '127.0.0.1',
serverPort = process.env.SERVER_PORT || 5544,
serverUrl = 'http://' + serverHost + ':' + serverPort,
username = 'admin',
password = 'admin';
return {
username: username,
password: password,
serverUrl: serverUrl,
clickLinkByText: function(remote, cssSelector, linkText) {
return remote
.setFindTimeout(1000)
.findAllByCssSelector(cssSelector)
.then(function(links) {
return links.reduce(function(matchFound, link) {
return link.getVisibleText().then(function(text) {
if (_.trim(text) == linkText) {
link.click();
return true;
}
return matchFound;
});
}, false);
});
}
};
});

View File

@ -31,8 +31,9 @@ define(['config'], function(config) {
'host-node': 'requirejs',
'host-browser': '/vendor/bower/requirejs/require.js'
},
// A regular expression matching URLs to files that should not be included in code coverage analysis
grep: /^/,
excludeInstrumentation: /^/,
loader: config
loader: config,
reporters: ['console']
};
});