Merge branch '4.2' into implement/xsrfProtectionFor4.2

This commit is contained in:
spalger 2015-11-11 12:30:38 -06:00
commit 92d30802f9
25 changed files with 1425 additions and 95 deletions

1
.gitignore vendored
View file

@ -10,6 +10,7 @@ target
.idea
*.iml
*.log
/test/output
/esvm
.htpasswd
installedPlugins

View file

@ -102,6 +102,41 @@ The standard `npm run test` task runs several sub tasks and can take several min
</dd>
</dl>
### Functional UI Testing
#### Handy references
- https://theintern.github.io/
- https://theintern.github.io/leadfoot/Element.html
#### Running tests using npm task:
*The Selenium server that is started currently only runs the tests in Firefox*
To runt the functional UI tests, execute the following command:
`npm run test:ui`
The task above takes a little time to start the servers. You can also start the servers and leave them running, and then run the tests separately:
`npm run test:ui:server` will start the server required to run the selenium tests, leave this open
`npm run test:ui:runner` will run the frontend tests and close when complete
#### Running tests locally with your existing (and already running) ElasticSearch, Kibana, and Selenium Server:
Set your es and kibana ports in `test/intern.js` to 9220 and 5620, respecitively. You can configure your Selenium server to run the tests on Chrome,IE, or other browsers here.
Once you've got the services running, execute the following:
`npm run test:ui:runner`
#### General notes:
- Using Page Objects pattern (https://theintern.github.io/intern/#writing-functional-test)
- At least the initial tests for the Settings, Discover, and Visualize tabs all depend on a very specific set of logstash-type data (generated with makelogs). Since that is a static set of data, all the Discover and Visualize tests use a specific Absolute time range. This gaurantees the same results each run.
- These tests have been developed and tested with Chrome and Firefox browser. In theory, they should work on all browsers (that's the benefit of Intern using Leadfoot).
- These tests should also work with an external testing service like https://saucelabs.com/ or https://www.browserstack.com/ but that has not been tested.
### Submit a pull request

View file

@ -1,6 +1,9 @@
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,

View file

@ -1,6 +1,9 @@
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,

View file

@ -44,7 +44,7 @@ module.exports = function (grunt) {
purge: true,
config: {
http: {
port: uiConfig.elasticsearch.port
port: uiConfig.servers.elasticsearch.port
}
}
}

View file

@ -33,8 +33,8 @@ module.exports = function (grunt) {
},
cmd: /^win/.test(platform) ? '.\\bin\\kibana.bat' : './bin/kibana',
args: [
'--server.port=' + uiConfig.kibana.port,
'--elasticsearch.url=' + format(uiConfig.elasticsearch),
'--server.port=' + uiConfig.servers.kibana.port,
'--elasticsearch.url=' + format(uiConfig.servers.elasticsearch),
'--logging.json=false'
]
},
@ -89,7 +89,7 @@ module.exports = function (grunt) {
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'-port',
uiConfig.webdriver.port
uiConfig.servers.webdriver.port
]
},
@ -105,7 +105,7 @@ module.exports = function (grunt) {
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'-port',
uiConfig.webdriver.port
uiConfig.servers.webdriver.port
]
},

View file

@ -21,11 +21,11 @@ describe('scenario manager', function () {
it('should be able to load scenarios', function () {
return manager.load('makelogs')
.then(function () {
expect(create.getCall(0).args[0].index).to.be('logstash-2015.09.17');
expect(create.getCall(1).args[0].index).to.be('logstash-2015.09.18');
expect(bulk.called).to.be(true);
});
.then(function () {
expect(create.getCall(0).args[0].index).to.be('logstash-2015.09.17');
expect(create.getCall(1).args[0].index).to.be('logstash-2015.09.18');
expect(bulk.called).to.be(true);
});
});
it('should be able to delete all indices', function () {
@ -55,6 +55,52 @@ describe('scenario manager', function () {
});
});
it('should load if the index does not exist', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var throwError = sinon.stub(manager.client, 'count', Promise.reject);
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.calledWith(id)).to.be(true);
load.restore();
throwError.restore();
});
});
it('should load if the index is empty', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var returnZero = sinon.stub(manager.client, 'count', function () {
return Promise.resolve({
'count': 0
});
});
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.calledWith(id)).to.be(true);
load.restore();
returnZero.restore();
});
});
it('should not load if the index is not empty', function () {
var load = sinon.stub(manager, 'load', Promise.resolve);
var returnOne = sinon.stub(manager.client, 'count', function () {
return Promise.resolve({
'count': 1
});
});
var id = 'makelogs';
return manager.loadIfEmpty(id).then(function () {
expect(load.called).to.be(false);
load.restore();
returnOne.restore();
});
});
afterEach(function () {
bulk.restore();
create.restore();
@ -62,12 +108,40 @@ describe('scenario manager', function () {
});
});
it('should throw an error if the scenario is not defined', function () {
expect(manager.load).withArgs('makelogs').to.throwError();
describe('load', function () {
it('should reject if the scenario is not specified', function () {
return manager.load()
.then(function () {
throw new Error('Promise should reject')
})
.catch(function () { return; });
});
it('should reject if the scenario is not defined', function () {
return manager.load('idonotexist')
.then(function () {
throw new Error('Promise should reject')
})
.catch(function () { return; });
});
});
it('should throw an error if an index is not defined when clearing', function () {
expect(manager.unload).to.throwError();
describe('unload', function () {
it('should reject if the scenario is not specified', function () {
return manager.unload()
.then(function () {
throw new Error('Promise should reject')
})
.catch(function () { return; });
});
it('should reject if the scenario is not defined', function () {
return manager.unload('idonotexist')
.then(function () {
throw new Error('Promise should reject')
})
.catch(function () { return; });
});
});
it('should throw an error if an es server is not specified', function () {

View file

@ -2,23 +2,27 @@ var path = require('path');
var rootDir = path.join(__dirname, 'scenarios');
module.exports = {
makelogs: {
baseDir: path.join(rootDir, 'makelogs'),
bulk: [{
indexDefinition: 'makelogsIndexDefinition.js',
indexName: 'logstash-2015.09.17',
source: 'logstash-2015.09.17.js'
}, {
indexDefinition: 'makelogsIndexDefinition.js',
indexName: 'logstash-2015.09.18',
source: 'logstash-2015.09.18.js'
}]
},
emptyKibana: {
baseDir: path.join(rootDir, 'emptyKibana'),
bulk: [{
indexName: '.kibana',
source: 'kibana.js'
}]
scenarios: {
makelogs: {
baseDir: path.join(rootDir, 'makelogs'),
bulk: [{
indexName: 'logstash-2015.09.17',
indexDefinition: 'makelogsIndexDefinition.js',
source: 'logstash-2015.09.17.js'
}, {
indexName: 'logstash-2015.09.18',
indexDefinition: 'makelogsIndexDefinition.js',
source: 'logstash-2015.09.18.js'
}]
},
emptyKibana: {
baseDir: path.join(rootDir, 'emptyKibana'),
bulk: [{
indexName: '.kibana',
indexDefinition: 'kibanaDefinition.js',
source: 'kibana.js',
haltOnFailure: false
}]
}
}
};

View file

@ -1,6 +1,7 @@
var path = require('path');
var config = require('./config');
var elasticsearch = require('elasticsearch');
var Promise = require('bluebird');
var config = require('./config').scenarios;
function ScenarioManager(server) {
if (!server) throw new Error('No server defined');
@ -16,28 +17,33 @@ function ScenarioManager(server) {
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
ScenarioManager.prototype.load = function (id) {
var scenario = config[id];
if (!scenario) throw new Error('No scenario found for ' + id);
var self = this;
var scenario = config[id];
if (!scenario) return Promise.reject('No scenario found for ' + id);
return Promise.all(scenario.bulk.map(function mapBulk(bulk) {
var loadIndexDefinition;
if (bulk.indexDefinition) {
var body = require(path.join(scenario.baseDir, bulk.indexDefinition));
loadIndexDefinition = self.client.indices.create({
index: bulk.indexName,
body: require(path.join(scenario.baseDir, bulk.indexDefinition))
body: body
});
} else {
loadIndexDefinition = Promise.resolve();
}
return loadIndexDefinition.then(function bulkRequest() {
self.client.bulk({
body: require(path.join(scenario.baseDir, bulk.source)),
return loadIndexDefinition
.then(function bulkRequest() {
var body = require(path.join(scenario.baseDir, bulk.source));
return self.client.bulk({
body: body
});
})
.catch(function (err) {
if (bulk.haltOnFailure === false) return;
throw err;
});
}));
};
@ -48,7 +54,7 @@ ScenarioManager.prototype.load = function (id) {
*/
ScenarioManager.prototype.unload = function (id) {
var scenario = config[id];
if (!scenario) throw new Error('Expected index');
if (!scenario) return Promise.reject('No scenario found for ' + id);
var indices = scenario.bulk.map(function mapBulk(bulk) {
return bulk.indexName;
@ -67,7 +73,8 @@ ScenarioManager.prototype.unload = function (id) {
ScenarioManager.prototype.reload = function (id) {
var self = this;
return this.unload(id).then(function load() {
return self.unload(id)
.then(function load() {
return self.load(id);
});
};
@ -82,4 +89,32 @@ ScenarioManager.prototype.deleteAll = function () {
});
};
module.exports = ScenarioManager;
/**
* Load a testing scenario if not already loaded
* @param {string} id The scenario id to load
* @return {Promise} A promise that is resolved when elasticsearch has a response
*/
ScenarioManager.prototype.loadIfEmpty = function (id) {
var self = this;
var scenario = config[id];
if (!scenario) throw new Error('No scenario found for ' + id);
var self = this;
return Promise.all(scenario.bulk.map(function mapBulk(bulk) {
var loadIndexDefinition;
return self.client.count({
index: bulk.indexName
})
.then(function handleCountResponse(response) {
if (response.count === 0) {
return self.load(id);
}
})
}))
.catch(function (reason) {
return self.load(id);
});
};
module.exports = ScenarioManager;

View file

@ -0,0 +1,16 @@
module.exports = {
settings: {
number_of_shards: 1,
number_of_replicas: 1
},
mappings: {
config: {
properties: {
buildNum: {
type: 'string',
index: 'not_analyzed'
}
}
}
}
}

View file

@ -0,0 +1,59 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
return function (bdd, scenarioManager) {
bdd.describe('user input reactions', function () {
var common;
var settingsPage;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
});
bdd.beforeEach(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.it('should hide time-based index pattern when time-based option is unchecked', function () {
var self = this;
return settingsPage.getTimeBasedEventsCheckbox()
.then(function (selected) {
// uncheck the 'time-based events' checkbox
return selected.click();
})
// try to find the checkbox (this shouldn fail)
.then(function () {
return settingsPage.getTimeBasedIndexPatternCheckbox();
})
.then(function () {
// we expect the promise above to fail
var handler = common.handleError(self);
var msg = 'Found time based index pattern checkbox';
handler(msg);
})
.catch(function () {
// we expect this failure since checkbox should be hidden
return;
});
});
bdd.it('should enable creation after selecting time field', function () {
// select a time field and check that Create button is enabled
return settingsPage.selectTimeFieldOption('@timestamp')
.then(function () {
return settingsPage.getCreateButton().isEnabled()
.then(function (enabled) {
expect(enabled).to.be.ok();
});
})
.catch(common.handleError(this));
});
});
};
});

View file

@ -0,0 +1,104 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('creating and deleting default index', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote;
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.describe('index pattern creation', function indexPatternCreation() {
bdd.before(function () {
return settingsPage.createIndexPattern();
});
bdd.it('should have index pattern in page header', function pageHeader() {
return settingsPage.getIndexPageHeading().getVisibleText()
.then(function (patternName) {
expect(patternName).to.be('logstash-*');
})
.catch(common.handleError(this));
});
bdd.it('should have index pattern in url', function url() {
return common.tryForTime(5000, function () {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.contain('logstash-*');
});
})
.catch(common.handleError(this));
});
bdd.it('should have expected table headers', function checkingHeader() {
return settingsPage.getTableHeader()
.then(function (headers) {
var expectedHeaders = [
'name',
'type',
'format',
'analyzed',
'indexed',
'controls'
];
// 6 name type format analyzed indexed controls
expect(headers.length).to.be(expectedHeaders.length);
var comparedHeaders = headers.map(function compareHead(header, i) {
return header.getVisibleText()
.then(function (text) {
expect(text).to.be(expectedHeaders[i]);
});
});
return Promise.all(comparedHeaders);
})
.catch(common.handleError(this));
});
});
bdd.describe('index pattern deletion', function indexDelete() {
bdd.before(function () {
var expectedAlertText = 'Are you sure you want to remove this index pattern?';
return settingsPage.removeIndexPattern()
.then(function (alertText) {
expect(alertText).to.be(expectedAlertText);
});
});
bdd.it('should return to index pattern creation page', function returnToPage() {
return common.tryForTime(5000, function () {
return settingsPage.getCreateButton();
})
.catch(common.handleError(this));
});
bdd.it('should remove index pattern from url', function indexNotInUrl() {
// give the url time to settle
return common.tryForTime(5000, function () {
return remote.getCurrentUrl()
.then(function (currentUrl) {
expect(currentUrl).to.not.contain('logstash-*');
})
})
.catch(common.handleError(this));
});
});
});
};
});

View file

@ -0,0 +1,111 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
//var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('index result popularity', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote;
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.beforeEach(function be() {
return settingsPage.createIndexPattern();
});
bdd.afterEach(function ae() {
return settingsPage.removeIndexPattern();
});
bdd.describe('change popularity', function indexPatternCreation() {
var fieldName = 'geo.coordinates';
// set the page size to All again, https://github.com/elastic/kibana/issues/5030
// TODO: remove this after issue #5030 is closed
function fix5030() {
return settingsPage.setPageSize('All')
.then(function () {
return common.sleep(1000);
});
}
bdd.beforeEach(function () {
// increase Popularity of geo.coordinates
return settingsPage.setPageSize('All')
.then(function () {
return common.sleep(1000);
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
.then(function increasePopularity() {
return settingsPage.increasePopularity();
});
});
bdd.afterEach(function () {
// Cancel saving the popularity change (we didn't make a change in this case, just checking the value)
return settingsPage.controlChangeCancel();
});
bdd.it('should update the popularity input', function () {
return settingsPage.getPopularity()
.then(function (popularity) {
expect(popularity).to.be('1');
})
.catch(common.handleError(this));
});
bdd.it('should be reset on cancel', function pageHeader() {
// Cancel saving the popularity change
return settingsPage.controlChangeCancel()
.then(function () {
return fix5030();
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
// check that its 0 (previous increase was cancelled)
.then(function getPopularity() {
return settingsPage.getPopularity()
})
.then(function (popularity) {
expect(popularity).to.be('0');
})
.catch(common.handleError(this));
});
bdd.it('can be saved', function pageHeader() {
// Saving the popularity change
return settingsPage.controlChangeSave()
.then(function () {
return fix5030();
})
.then(function openControlsByName() {
return settingsPage.openControlsByName(fieldName);
})
// check that its 0 (previous increase was cancelled)
.then(function getPopularity() {
return settingsPage.getPopularity();
})
.then(function (popularity) {
expect(popularity).to.be('1');
})
.catch(common.handleError(this));
});
}); // end 'change popularity'
}); // end index result popularity
};
});

View file

@ -0,0 +1,134 @@
define(function (require) {
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
var expect = require('intern/dojo/node!expect.js');
var Promise = require('bluebird');
return function (bdd, scenarioManager) {
bdd.describe('index result field sort', function describeIndexTests() {
var common;
var settingsPage;
var remote;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
remote = this.remote
return scenarioManager.reload('emptyKibana')
});
var columns = [{
heading: 'name',
first: '@message',
last: 'xss.raw',
selector: function () {
return settingsPage.getTableRow(0, 0).getVisibleText()
}
}, {
heading: 'type',
first: '_source',
last: 'string',
selector: function () {
return settingsPage.getTableRow(0, 1).getVisibleText()
}
}];
columns.forEach(function (col) {
bdd.describe('sort by heading - ' + col.heading, function indexPatternCreation() {
bdd.before(function () {
return settingsPage.navigateTo()
});
bdd.beforeEach(function () {
return settingsPage.createIndexPattern();
});
bdd.afterEach(function () {
return settingsPage.removeIndexPattern();
});
bdd.it('should sort ascending', function pageHeader() {
return settingsPage.sortBy(col.heading)
.then(function getText() {
return col.selector();
})
.then(function (rowText) {
expect(rowText).to.be(col.first);
})
.catch(common.handleError(this));
});
bdd.it('should sort descending', function pageHeader() {
return settingsPage.sortBy(col.heading)
.then(function sortAgain() {
return settingsPage.sortBy(col.heading);
})
.then(function getText() {
return col.selector();
})
.then(function (rowText) {
expect(rowText).to.be(col.last);
})
.catch(common.handleError(this));
});
});
});
bdd.describe('field list pagination', function () {
var expectedDefaultPageSize = 25;
var expectedFieldCount = 85;
var expectedLastPageCount = 10;
var pages = [1, 2, 3, 4];
bdd.before(function () {
return settingsPage.navigateTo()
.then(function () {
return settingsPage.createIndexPattern();
});
});
bdd.after(function () {
return settingsPage.removeIndexPattern();
});
bdd.it('makelogs data should have expected number of fields', function () {
return settingsPage.getFieldsTabCount()
.then(function (tabCount) {
expect(tabCount).to.be('' + expectedFieldCount);
})
.catch(common.handleError(this));
});
bdd.it('should have correct default page size selected', function () {
return settingsPage.getPageSize()
.then(function (pageSize) {
expect(pageSize).to.be('' + expectedDefaultPageSize);
})
.catch(common.handleError(this));
});
bdd.it('should have the correct number of rows per page', function () {
var pageCount = Math.ceil(expectedFieldCount / expectedDefaultPageSize);
var chain = pages.reduce(function (chain, val) {
return chain.then(function () {
return settingsPage.goToPage(val)
.then(function () {
return common.sleep(1000);
})
.then(function () {
return settingsPage.getPageFieldCount();
})
.then(function (pageCount) {
var expectedSize = (val < 4) ? expectedDefaultPageSize : expectedLastPageCount;
expect(pageCount.length).to.be(expectedSize);
});
});
}, Promise.resolve());
return chain.catch(common.handleError(this));
});
}); // end describe pagination
}); // end index result field sort
};
});

View file

@ -0,0 +1,64 @@
define(function (require) {
var expect = require('intern/dojo/node!expect.js');
var Common = require('../../../support/pages/Common');
var SettingsPage = require('../../../support/pages/SettingsPage');
return function (bdd, scenarioManager) {
bdd.describe('initial state', function () {
var common;
var settingsPage;
bdd.before(function () {
common = new Common(this.remote);
settingsPage = new SettingsPage(this.remote);
return scenarioManager.reload('emptyKibana')
.then(function () {
return settingsPage.navigateTo();
});
});
bdd.it('should load with time pattern checked', function () {
return settingsPage.getTimeBasedEventsCheckbox().isSelected()
.then(function (selected) {
expect(selected).to.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should load with name pattern unchecked', function () {
return settingsPage.getTimeBasedIndexPatternCheckbox().isSelected()
.then(function (selected) {
expect(selected).to.not.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should contain default index pattern', function () {
var defaultPattern = 'logstash-*';
return settingsPage.getIndexPatternField().getProperty('value')
.then(function (pattern) {
expect(pattern).to.be(defaultPattern);
})
.catch(common.handleError(this));
});
bdd.it('should not select the time field', function () {
return settingsPage.getTimeFieldNameField().isSelected()
.then(function (timeFieldIsSelected) {
expect(timeFieldIsSelected).to.not.be.ok();
})
.catch(common.handleError(this));
});
bdd.it('should not be enable creation', function () {
return settingsPage.getCreateButton().isEnabled()
.then(function (enabled) {
expect(enabled).to.not.be.ok();
})
.catch(common.handleError(this));
});
});
};
});

View file

@ -0,0 +1,38 @@
define(function (require) {
var bdd = require('intern!bdd');
var config = require('intern').config;
var url = require('intern/dojo/node!url');
var ScenarioManager = require('intern/dojo/node!../../../fixtures/scenarioManager');
var initialStateTest = require('./_initial_state');
var creationChangesTest = require('./_creation_form_changes');
var indexPatternCreateDeleteTest = require('./_index_pattern_create_delete');
var indexPatternResultsSortTest = require('./_index_pattern_results_sort');
var indexPatternPopularityTest = require('./_index_pattern_popularity');
bdd.describe('settings app', function () {
var scenarioManager = new ScenarioManager(url.format(config.servers.elasticsearch));
// on setup, we create an settingsPage instance
// that we will use for all the tests
bdd.before(function () {
return scenarioManager.reload('emptyKibana')
.then(function () {
return scenarioManager.loadIfEmpty('makelogs')
})
});
bdd.after(function () {
return scenarioManager.unload('makelogs')
.then(function () {
scenarioManager.unload('emptyKibana');
});
});
initialStateTest(bdd, scenarioManager);
creationChangesTest(bdd, scenarioManager);
indexPatternCreateDeleteTest(bdd, scenarioManager);
indexPatternResultsSortTest(bdd, scenarioManager);
indexPatternPopularityTest(bdd, scenarioManager);
});
});

View file

@ -1,21 +0,0 @@
define(function (require) {
var registerSuite = require('intern!object');
var expect = require('intern/dojo/node!expect.js');
var config = require('intern').config;
var getUrl = require('intern/dojo/node!../utils/getUrl');
registerSuite(function () {
return {
'status': function () {
return this.remote
.get(getUrl(config.kibana, 'status'))
.setFindTimeout(60000)
.findByCssSelector('.plugin_status_breakdown')
.getVisibleText()
.then(function (text) {
expect(text.indexOf('plugin:kibana Ready')).to.be.above(-1);
});
}
};
});
});

View file

@ -0,0 +1,30 @@
define(function (require) {
var bdd = require('intern!bdd');
var expect = require('intern/dojo/node!expect.js');
var config = require('intern').config;
var Common = require('../../support/pages/Common');
bdd.describe('status page', function () {
var common;
bdd.before(function () {
common = new Common(this.remote);
// load the status page
return common.navigateToApp('statusPage', false);
});
bdd.it('should show the kibana plugin as ready', function () {
var self = this;
return common.tryForTime(6000, function () {
return self.remote
.findByCssSelector('.plugin_status_breakdown')
.getVisibleText()
.then(function (text) {
expect(text.indexOf('plugin:kibana Ready')).to.be.above(-1);
})
})
.catch(common.handleError(self));
});
});
});

View file

@ -3,6 +3,7 @@ define(function (require) {
var _ = require('intern/dojo/node!lodash');
return _.assign({
debug: false,
capabilities: {
'selenium-version': '2.47.1',
'idle-timeout': 30
@ -10,8 +11,17 @@ define(function (require) {
environments: [{
browserName: 'firefox'
}],
tunnelOptions: serverConfig.webdriver,
functionalSuites: ['test/functional/status.js'],
excludeInstrumentation: /(fixtures|node_modules)\//
tunnelOptions: serverConfig.servers.webdriver,
functionalSuites: [
'test/functional/status_page/index',
'test/functional/apps/settings/index'
],
excludeInstrumentation: /(fixtures|node_modules)\//,
loaderOptions: {
paths: {
'bluebird': './node_modules/bluebird/js/browser/bluebird.js',
'moment': './node_modules/moment/moment.js'
}
}
}, serverConfig);
});

View file

@ -1,17 +1,42 @@
var kibanaURL = '/app/kibana';
module.exports = {
webdriver: {
protocol: process.env.TEST_UI_WEBDRIVER_PROTOCOL || 'http',
hostname: process.env.TEST_UI_WEBDRIVER_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_WEBDRIVER_PORT, 10) || 4444
servers: {
webdriver: {
protocol: process.env.TEST_UI_WEBDRIVER_PROTOCOL || 'http',
hostname: process.env.TEST_UI_WEBDRIVER_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_WEBDRIVER_PORT, 10) || 4444
},
kibana: {
protocol: process.env.TEST_UI_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_UI_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620
},
elasticsearch: {
protocol: process.env.TEST_UI_ES_PROTOCOL || 'http',
hostname: process.env.TEST_UI_ES_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220
}
},
kibana: {
protocol: process.env.TEST_UI_KIBANA_PROTOCOL || 'http',
hostname: process.env.TEST_UI_KIBANA_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_KIBANA_PORT, 10) || 5620
},
elasticsearch: {
protocol: process.env.TEST_UI_ES_PROTOCOL || 'http',
hostname: process.env.TEST_UI_ES_HOSTNAME || 'localhost',
port: parseInt(process.env.TEST_UI_ES_PORT, 10) || 9220
apps: {
statusPage: {
pathname: 'status'
},
discover: {
pathname: kibanaURL,
hash: '/discover',
},
visualize: {
pathname: kibanaURL,
hash: '/visualize',
},
dashboard: {
pathname: kibanaURL,
hash: '/dashboard',
},
settings: {
pathname: kibanaURL,
hash: '/settings'
}
}
};

View file

@ -0,0 +1,199 @@
// in test/support/pages/Common.js
define(function (require) {
var config = require('intern').config;
var Promise = require('bluebird');
var moment = require('moment');
var getUrl = require('intern/dojo/node!../../utils/getUrl');
var fs = require('intern/dojo/node!fs');
var path = require('intern/dojo/node!path');
function Common(remote) {
this.remote = remote;
}
Common.prototype = {
constructor: Common,
navigateToApp: function (appName, testStatusPage) {
var self = this;
var urlTimeout = 10000;
var appUrl = getUrl(config.servers.kibana, config.apps[appName]);
var doNavigation = function (url) {
return self.tryForTime(urlTimeout, function () {
// since we're using hash URLs, always reload first to force re-render
return self.remote.get(url)
.then(function () {
return self.remote.refresh();
})
.then(function () {
if (testStatusPage !== false) {
return self.checkForKibanaApp()
.then(function (kibanaLoaded) {
if (!kibanaLoaded) throw new Error('Kibana is not loaded, retrying');
});
}
})
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
var navSuccessful = new RegExp(appUrl).test(currentUrl);
if (!navSuccessful) throw new Error('App failed to load: ' + appName);
})
});
};
return doNavigation(appUrl)
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
var lastUrl = currentUrl;
return self.tryForTime(urlTimeout, function () {
// give the app time to update the URL
return self.sleep(500)
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (currentUrl) {
if (lastUrl !== currentUrl) {
lastUrl = currentUrl;
throw new Error('URL changed, waiting for it to settle');
}
});
});
});
},
runScript: function (fn, timeout) {
var self = this;
timeout = timeout || 2000;
// wait for deps on window before running script
return self.remote
.setExecuteAsyncTimeout(timeout)
.executeAsync(function (done) {
var interval = setInterval(function () {
var ready = (document.readyState === 'complete');
var hasJQuery = !!window.$;
if (ready && hasJQuery) {
console.log('doc ready, jquery loaded');
clearInterval(interval);
done();
}
}, 10);
}).then(function () {
return self.remote.execute(fn);
});
},
getApp: function () {
var self = this;
var loadTimeout = 5000;
return self.tryForTime(3000, function () {
return self.remote.setFindTimeout(loadTimeout)
.findByCssSelector('.content > .application');
})
.then(function () {
return self.runScript(function () {
var $ = window.$;
var $scope = $('.content > .application').scope();
return $scope ? $scope.chrome.getApp() : {};
});
});
},
checkForKibanaApp: function () {
var self = this;
return self.getApp()
.then(function (app) {
var appId = app.id;
self.debug('current application: ' + appId);
return appId === 'kibana';
})
.catch(function (err) {
self.debug('kibana check failed');
self.debug(err);
// not on the kibana app...
return false;
});
},
tryForTime: function (timeout, block) {
var self = this;
var start = Date.now();
var retryDelay = 500;
var lastTry = 0;
function attempt() {
lastTry = Date.now();
if (lastTry - start > timeout) {
throw new Error('timeout');
}
return Promise
.try(block)
.then(function tryForTimeSuccess() {
self.debug('tryForTime success in about ' + (lastTry - start) + ' ms');
return (lastTry - start);
})
.catch(function tryForTimeCatch(err) {
self.debug('tryForTime failure, retry in ' + retryDelay + 'ms - ' + err.message);
return Promise.delay(retryDelay).then(attempt);
});
}
return Promise.try(attempt);
},
log: function (logString) {
console.log(moment().format('HH:mm:ss.SSS') + ': ' + logString);
},
debug: function (logString) {
if (config.debug) this.log(logString);
},
sleep: function (sleepMilliseconds) {
this.debug('sleeping for ' + sleepMilliseconds + 'ms');
return Promise.resolve().delay(sleepMilliseconds);
},
handleError: function (testObj) {
var self = this;
var testName = (testObj.parent) ? [testObj.parent.name, testObj.name].join('_') : testObj.name;
return function (reason) {
var now = Date.now();
var filename = ['failure', now, testName].join('_') + '.png';
return self.saveScreenshot(filename)
.finally(function () {
throw new Error(reason);
});
};
},
saveScreenshot: function (filename) {
var self = this;
var outDir = path.resolve('test', 'output');
return self.remote.takeScreenshot()
.then(function writeScreenshot(data) {
var filepath = path.resolve(outDir, filename);
self.debug('Test Failed, taking screenshot "' + filepath + '"');
fs.writeFileSync(filepath, data);
})
.catch(function (err) {
self.log('SCREENSHOT FAILED: ' + err);
});
}
};
return Common;
});

View file

@ -0,0 +1,54 @@
// in test/support/pages/HeaderPage.js
define(function (require) {
var Common = require('./Common');
var common;
// the page object is created as a constructor
// so we can provide the remote Command object
// at runtime
function HeaderPage(remote) {
this.remote = remote;
common = new Common(this.remote);
}
var defaultTimeout = 5000;
HeaderPage.prototype = {
constructor: HeaderPage,
clickSelector: function (selector) {
var self = this.remote;
return common.tryForTime(5000, function () {
return self.setFindTimeout(defaultTimeout)
.findByCssSelector(selector)
.then(function (tab) {
return tab.click();
});
});
},
clickDiscover: function () {
common.debug('click Discover tab');
this.clickSelector('a[href*=\'discover\']');
},
clickVisualize: function () {
common.debug('click Visualize tab');
this.clickSelector('a[href*=\'visualize\']');
},
clickDashboard: function () {
common.debug('click Dashboard tab');
this.clickSelector('a[href*=\'dashboard\']');
},
clickSettings: function () {
common.debug('click Settings tab');
this.clickSelector('a[href*=\'settings\']');
}
};
return HeaderPage;
});

View file

@ -0,0 +1,331 @@
// in test/support/pages/SettingsPage.js
define(function (require) {
// the page object is created as a constructor
// so we can provide the remote Command object
// at runtime
var Promise = require('bluebird');
var Common = require('./Common');
var defaultTimeout = 5000;
var common;
function SettingsPage(remote) {
this.remote = remote;
common = new Common(this.remote);
}
SettingsPage.prototype = {
constructor: SettingsPage,
navigateTo: function () {
return common.navigateToApp('settings');
},
getTimeBasedEventsCheckbox: function getTimeBasedEventsCheckbox() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="index.isTimeBased"]');
},
getTimeBasedIndexPatternCheckbox: function getTimeBasedIndexPatternCheckbox() {
// fail faster since we're sometimes checking that it doesn't exist
return this.remote.setFindTimeout(defaultTimeout / 2)
.findByCssSelector('input[ng-model="index.nameIsPattern"]');
},
getIndexPatternField: function getIndexPatternField() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('[ng-model="index.name"]');
},
getTimeFieldNameField: function getTimeFieldNameField() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('select[ng-model="index.timeField"]');
},
selectTimeFieldOption: function selectTimeFieldOption(selection) {
var self = this;
return common.tryForTime(defaultTimeout * 2, function () {
return self.getTimeFieldNameField().click();
})
.then(function () {
return self.getTimeFieldNameField().click();
})
.then(function () {
return self.getTimeFieldOption(selection);
})
// DEBUGGING
.catch(function (err) {
common.debug('selectTimeFieldOption: FAILED to create index pattern');
return common.checkForKibanaApp()
.then(function (onKibana) {
common.debug('onKibana')
common.debug(onKibana)
})
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (url) {
common.debug('FAILED on url ' + url);
throw err
})
});
},
getTimeFieldOption: function getTimeFieldOption(selection) {
return this.remote.setFindTimeout(defaultTimeout * 2)
.findByCssSelector('option[label="' + selection + '"]').click();
},
getCreateButton: function getCreateButton() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn');
},
clickCreateButton: function clickCreateButton() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn').click();
},
clickDefaultIndexButton: function clickDefaultIndexButton() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-warning.ng-scope').click();
},
clickDeletePattern: function clickDeletePattern() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-danger.ng-scope').click();
},
getIndexPageHeading: function getIndexPageHeading() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1.title.ng-binding.ng-isolate-scope');
},
getConfigureHeader: function getConfigureHeader() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1');
},
getTableHeader: function getTableHeader() {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('table.table.table-condensed thead tr th');
},
sortBy: function sortBy(columnName) {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('table.table.table-condensed thead tr th span')
.then(function (chartTypes) {
function getChartType(chart) {
return chart.getVisibleText()
.then(function (chartString) {
if (chartString === columnName) {
return chart.click();
}
});
}
var getChartTypesPromises = chartTypes.map(getChartType);
return Promise.all(getChartTypesPromises);
});
},
getTableRow: function getTableRow(rowNumber, colNumber) {
return this.remote.setFindTimeout(defaultTimeout)
// passing in zero-based index, but adding 1 for css 1-based indexes
.findByCssSelector('div.agg-table-paginated table.table.table-condensed tbody tr:nth-child(' +
(rowNumber + 1) + ') td.ng-scope:nth-child(' +
(colNumber + 1) + ') span.ng-binding'
);
},
getFieldsTabCount: function getFieldsTabCount() {
var self = this;
var selector = 'li.kbn-settings-tab.active a small';
var getText = function () {
return self.remote.setFindTimeout(defaultTimeout)
.findByCssSelector(selector).getVisibleText();
}
return common.tryForTime(defaultTimeout * 4, function () {
return getText();
})
.then(function () {
return getText();
})
.then(function (theText) {
// the value has () around it, remove them
return theText.replace(/\((.*)\)/, '$1');
});
},
getPageSize: function getPageSize() {
var selectedItemLabel = '';
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('select.ng-pristine.ng-valid.ng-untouched option')
.then(function (chartTypes) {
function getChartType(chart) {
var thisChart = chart;
return chart.isSelected()
.then(function (isSelected) {
if (isSelected === true) {
return thisChart.getProperty('label')
.then(function (theLabel) {
selectedItemLabel = theLabel;
});
}
});
}
var getChartTypesPromises = chartTypes.map(getChartType);
return Promise.all(getChartTypesPromises);
})
.then(function () {
return selectedItemLabel;
});
},
getPageFieldCount: function getPageFieldCount() {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('div.agg-table-paginated table.table.table-condensed tbody tr td.ng-scope:nth-child(1) span.ng-binding');
},
goToPage: function goToPage(pageNum) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('ul.pagination-other-pages-list.pagination-sm.ng-scope li.ng-scope:nth-child(' +
(pageNum + 1) + ') a.ng-binding'
)
.then(function (page) {
return page.click();
});
},
openControlsRow: function openControlsRow(row) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('table.table.table-condensed tbody tr:nth-child(' +
(row + 1) + ') td.ng-scope div.actions a.btn.btn-xs.btn-default i.fa.fa-pencil'
)
.then(function (page) {
return page.click();
});
},
openControlsByName: function openControlsByName(name) {
return this.remote.setFindTimeout(defaultTimeout * 2)
.findByCssSelector('div.actions a.btn.btn-xs.btn-default[href$="/' + name + '"]')
.then(function (button) {
return button.click();
});
},
increasePopularity: function increasePopularity() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-default[aria-label="Plus"]')
.then(function (button) {
return button.click();
});
},
getPopularity: function getPopularity() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="editor.field.count"]')
.then(function (input) {
return input.getProperty('value');
});
},
controlChangeCancel: function controlChangeCancel() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-primary[aria-label="Cancel"]')
.then(function (button) {
return button.click();
});
},
controlChangeSave: function controlChangeSave() {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-success.ng-binding[aria-label="Update Field"]')
.then(function (button) {
return button.click();
});
},
setPageSize: function setPageSize(size) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('form.form-inline.pagination-size.ng-scope.ng-pristine.ng-valid div.form-group option[label="' + size + '"]')
.then(function (button) {
return button.click();
});
},
createIndexPattern: function createIndexPattern() {
var self = this;
return common.tryForTime(defaultTimeout, function () {
return self.selectTimeFieldOption('@timestamp')
.then(function () {
return self.getCreateButton().click();
});
})
.then(function () {
return common.tryForTime(defaultTimeout, function () {
return self.remote.getCurrentUrl()
.then(function (currentUrl) {
if (!currentUrl.match(/indices\/.+\?/)) {
throw new Error('Index pattern not created');
}
});
});
})
// DEBUGGING
.catch(function (err) {
common.debug('createIndexPattern: FAILED to create index pattern');
return common.checkForKibanaApp()
.then(function (onKibana) {
common.debug('onKibana')
common.debug(onKibana)
})
.then(function () {
return self.remote.getCurrentUrl();
})
.then(function (url) {
common.debug('FAILED on url ' + url);
throw err
})
});
},
removeIndexPattern: function removeIndexPattern() {
var self = this;
var alertText;
return common.tryForTime(defaultTimeout, function () {
return self.clickDeletePattern()
.then(function () {
return self.remote.getAlertText();
})
.then(function (text) {
alertText = text;
})
.then(function () {
return self.remote.acceptAlert();
});
})
.then(function () {
return common.tryForTime(defaultTimeout, function () {
return self.remote.getCurrentUrl()
.then(function (currentUrl) {
if (currentUrl.match(/indices\/.+\?/)) {
throw new Error('Index pattern not removed');
}
});
});
})
.then(function () {
return alertText;
});
}
};
return SettingsPage;
});

View file

@ -2,17 +2,36 @@ var expect = require('expect.js');
var getUrl = require('../getUrl');
describe('getUrl', function () {
it('should be able to convert a config and a path to a url', function () {
expect(getUrl({
it('should convert to a url', function () {
var url = getUrl({
protocol: 'http',
hostname: 'localhost',
}, {
pathname: 'foo'
});
expect(url).to.be('http://localhost/foo');
});
it('should convert to a secure url with port', function () {
var url = getUrl({
protocol: 'http',
hostname: 'localhost',
port: 9220
}, 'foo')).to.be('http://localhost:9220/foo');
}, {
pathname: 'foo'
});
expect(url).to.be('http://localhost:9220/foo');
});
it('should convert to a secure hashed url', function () {
expect(getUrl({
protocol: 'https',
hostname: 'localhost',
}, 'foo')).to.be('https://localhost/foo');
}, {
pathname: 'foo',
hash: 'bar'
})).to.be('https://localhost/foo#bar');
});
});

View file

@ -1,7 +1,6 @@
var _ = require('lodash');
var url = require('url');
/**
* Converts a config and a pathname to a url
* @param {object} config A url config
@ -11,11 +10,14 @@ var url = require('url');
* hostname: 'localhost',
* port: 9220
* }
* @param {string} pathname The requested path
* @param {object} app The params to append
* example:
* {
* pathname: 'app/kibana',
* hash: '/discover'
* }
* @return {string}
*/
module.exports = function getPage(config, pathname) {
return url.format(_.assign(config, {
pathname: pathname
}));
module.exports = function getPage(config, app) {
return url.format(_.assign(config, app));
};