Merge branch 'master' of github.com:elastic/kibana into fix/relativePathsInKibana

This commit is contained in:
spalger 2015-11-16 15:24:22 -06:00
commit 4e7ae9fafd
61 changed files with 1935 additions and 135 deletions

View file

@ -1,2 +1,3 @@
src/fixtures
test/fixtures/scenarios
optimize

2
.gitignore vendored
View file

@ -10,6 +10,7 @@ target
.idea
*.iml
*.log
/test/output
/esvm
.htpasswd
.eslintcache
@ -19,3 +20,4 @@ webpackstats.json
config/kibana.dev.yml
coverage
selenium
.babelcache.json

1
.npmrc Normal file
View file

@ -0,0 +1 @@
save-exact=true

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

@ -43,8 +43,10 @@ module.exports = function (grunt) {
lintThese: [
'Gruntfile.js',
'<%= root %>/tasks/**/*.js',
'<%= root %>/test/**/*.js',
'<%= src %>/**/*.js',
'!<%= src %>/fixtures/**/*.js'
'!<%= src %>/fixtures/**/*.js',
'!<%= root %>/test/fixtures/scenarios/**/*.js'
],
deepModules: {
'caniuse-db': '1.0.30000265',

View file

@ -4,6 +4,10 @@
# The host to bind the server to.
# server.host: "0.0.0.0"
# A value to use as a XSRF token. This token is sent back to the server on each request
# and required if you want to execute requests from other clients (like curl).
# server.xsrf.token: ""
# If you are running kibana behind a proxy, and want to mount it at a path,
# specify that path here. The basePath can't end in a slash.
# server.basePath: ""
@ -26,8 +30,8 @@
# used by the Kibana server to perform maintenance on the kibana_index at startup. Your Kibana
# users will still need to authenticate with Elasticsearch (which is proxied through
# the Kibana server)
# elasticsearch.username: user
# elasticsearch.password: pass
# elasticsearch.username: "user"
# elasticsearch.password: "pass"
# SSL for outgoing requests from the Kibana Server to the browser (PEM formatted)
# server.ssl.cert: /path/to/your/server.crt

0
optimize/.empty Normal file
View file

View file

@ -172,7 +172,7 @@
"npm": "2.11.0",
"portscanner": "1.0.0",
"simple-git": "1.8.0",
"sinon": "1.16.1",
"sinon": "1.17.2",
"source-map": "0.4.4",
"wreck": "6.2.0"
},

View file

@ -1,2 +1,5 @@
require('babel/register')(require('../optimize/babelOptions').node);
// load the babel options seperately so that they can modify the process.env
// before calling babel/register
const babelOptions = require('../optimize/babelOptions').node;
require('babel/register')(babelOptions);
require('./cli');

View file

@ -1,4 +1,9 @@
var fromRoot = require('requirefrom')('src/utils')('fromRoot');
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,
@ -6,7 +11,7 @@ exports.webpack = {
optional: ['runtime']
};
exports.node = Object.assign({
exports.node = cloneDeep({
ignore: [
fromRoot('src'),
/[\\\/](node_modules|bower_components)[\\\/]/

View file

@ -1,7 +1,14 @@
var cloneDeep = require('lodash').cloneDeep;
var fromRoot = require('path').resolve.bind(null, __dirname, '../../');
if (!process.env.BABEL_CACHE_PATH) {
process.env.BABEL_CACHE_PATH = fromRoot('optimize/.babelcache.json');
}
exports.webpack = {
stage: 1,
nonStandard: false,
optional: ['runtime']
};
exports.node = Object.assign({}, exports.webpack);
exports.node = cloneDeep(exports.webpack);

View file

@ -13,7 +13,12 @@ describe('plugins/elasticsearch', function () {
before(function () {
kbnServer = new KbnServer({
server: { autoListen: false },
server: {
autoListen: false,
xsrf: {
disableProtection: true
}
},
logging: { quiet: true },
plugins: {
scanDirs: [
@ -104,5 +109,3 @@ describe('plugins/elasticsearch', function () {
});
});

View file

@ -93,7 +93,7 @@
<h1>No results found <i aria-hidden="true" class="fa fa-meh-o"></i></h1>
<p>
Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here's some ideas:
Unfortunately I could not find any results matching your search. I tried really hard. I looked all over the place and frankly, I just couldn't find anything good. Help me, help you. Here are some ideas:
</p>
<div class="shard-failures" ng-show="failures">

View file

@ -5,8 +5,9 @@ let path = require('path');
let utils = require('requirefrom')('src/utils');
let fromRoot = utils('fromRoot');
const randomBytes = require('crypto').randomBytes;
module.exports = Joi.object({
module.exports = () => Joi.object({
pkg: Joi.object({
version: Joi.string().default(Joi.ref('$version')),
buildNum: Joi.number().default(Joi.ref('$buildNum')),
@ -40,7 +41,11 @@ module.exports = Joi.object({
origin: ['*://localhost:9876'] // karma test server
}),
otherwise: Joi.boolean().default(false)
})
}),
xsrf: Joi.object({
token: Joi.string().default(randomBytes(32).toString('hex')),
disableProtection: Joi.boolean().default(false),
}).default(),
}).default(),
logging: Joi.object().keys({

View file

@ -1,6 +1,6 @@
module.exports = function (kbnServer) {
let Config = require('./Config');
let schema = require('./schema');
let schema = require('./schema')();
kbnServer.config = new Config(schema, kbnServer.settings || {});
};

View file

@ -0,0 +1,145 @@
import expect from 'expect.js';
import { fromNode as fn } from 'bluebird';
import { resolve } from 'path';
import KbnServer from '../../KbnServer';
const nonDestructiveMethods = ['GET'];
const destructiveMethods = ['POST', 'PUT', 'DELETE'];
const src = resolve.bind(null, __dirname, '../../../../src');
describe('xsrf request filter', function () {
function inject(kbnServer, opts) {
return fn(cb => {
kbnServer.server.inject(opts, (resp) => {
cb(null, resp);
});
});
}
const makeServer = async function (token) {
const kbnServer = new KbnServer({
server: { autoListen: false, xsrf: { token } },
plugins: { scanDirs: [src('plugins')] },
logging: { quiet: true },
optimize: { enabled: false },
});
await kbnServer.ready();
kbnServer.server.route({
path: '/xsrf/test/route',
method: [...nonDestructiveMethods, ...destructiveMethods],
handler: function (req, reply) {
reply(null, 'ok');
}
});
return kbnServer;
};
describe('issuing tokens', function () {
const token = 'secur3';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
it('sends a token when rendering an app', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
});
expect(resp.payload).to.contain(`"xsrfToken":"${token}"`);
});
});
context('without configured token', function () {
let kbnServer;
beforeEach(async () => kbnServer = await makeServer());
afterEach(async () => await kbnServer.close());
it('responds with a random token', async function () {
var resp = await inject(kbnServer, {
method: 'GET',
url: '/app/kibana',
});
expect(resp.payload).to.match(/"xsrfToken":".{64}"/);
});
});
context('with configured token', function () {
const token = 'mytoken';
let kbnServer;
beforeEach(async () => kbnServer = await makeServer(token));
afterEach(async () => await kbnServer.close());
for (const method of nonDestructiveMethods) {
context(`nonDestructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('ignores invalid tokens', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.headers).to.not.have.property('kbn-xsrf-token');
});
});
}
for (const method of destructiveMethods) {
context(`destructiveMethod: ${method}`, function () { // eslint-disable-line no-loop-func
it('accepts requests with the correct token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': token,
},
});
expect(resp.statusCode).to.be(200);
expect(resp.payload).to.be('ok');
});
it('rejects requests without a token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Missing XSRF token"/);
});
it('rejects requests with an invalid token', async function () {
const resp = await inject(kbnServer, {
url: '/xsrf/test/route',
method: method,
headers: {
'kbn-xsrf-token': `invalid:${token}`,
},
});
expect(resp.statusCode).to.be(403);
expect(resp.payload).to.match(/"Invalid XSRF token"/);
});
});
}
});
});

View file

@ -121,4 +121,6 @@ module.exports = function (kbnServer, server, config) {
.permanent(true);
}
});
return kbnServer.mixin(require('./xsrf'));
};

20
src/server/http/xsrf.js Normal file
View file

@ -0,0 +1,20 @@
import { forbidden } from 'boom';
export default function (kbnServer, server, config) {
const token = config.get('server.xsrf.token');
const disabled = config.get('server.xsrf.disableProtection');
server.decorate('reply', 'issueXsrfToken', function () {
return token;
});
server.ext('onPostAuth', function (req, reply) {
if (disabled || req.method === 'get') return reply.continue();
const attempt = req.headers['kbn-xsrf-token'];
if (!attempt) return reply(forbidden('Missing XSRF token'));
if (attempt !== token) return reply(forbidden('Invalid XSRF token'));
return reply.continue();
});
}

View file

@ -21,12 +21,13 @@ module.exports = class KbnLogger {
}
init(readstream, emitter, callback) {
readstream
.pipe(this.squeeze)
.pipe(this.format)
.pipe(this.dest);
emitter.on('stop', _.noop);
this.output = readstream.pipe(this.squeeze).pipe(this.format);
this.output.pipe(this.dest);
emitter.on('stop', () => {
this.output.unpipe(this.dest);
});
callback();
}

View file

@ -19,7 +19,7 @@ module.exports = async function (kbnServer, server, config) {
let path = [];
async function initialize(id) {
const initialize = async function (id) {
let plugin = plugins.byId[id];
if (includes(path, id)) {

View file

@ -69,7 +69,7 @@ module.exports = async (kbnServer, server, config) => {
}
server.decorate('reply', 'renderApp', function (app) {
let payload = {
const payload = {
app: app,
nav: uiExports.apps,
version: kbnServer.version,
@ -77,6 +77,7 @@ module.exports = async (kbnServer, server, config) => {
buildSha: config.get('pkg.buildSha'),
basePath: config.get('server.basePath'),
vars: defaults(app.getInjectedVars(), defaultInjectedVars),
xsrfToken: this.issueXsrfToken(),
};
return this.view(app.templateName, {

View file

@ -0,0 +1,132 @@
import $ from 'jquery';
import expect from 'expect.js';
import { stub } from 'auto-release-sinon';
import ngMock from 'ngMock';
import xsrfChromeApi from '../xsrf';
const xsrfHeader = 'kbn-xsrf-token';
const xsrfToken = 'xsrfToken';
describe('chrome xsrf apis', function () {
describe('#getXsrfToken()', function () {
it('exposes the token', function () {
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
expect(chrome.getXsrfToken()).to.be(xsrfToken);
});
});
context('jQuery support', function () {
it('adds a global jQuery prefilter', function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, {});
expect($.ajaxPrefilter.callCount).to.be(1);
});
context('jQuery prefilter', function () {
let prefilter;
const xsrfToken = 'xsrfToken';
beforeEach(function () {
stub($, 'ajaxPrefilter');
xsrfChromeApi({}, { xsrfToken });
prefilter = $.ajaxPrefilter.args[0][0];
});
it('sets the kbn-xsrf-token header', function () {
const setHeader = stub();
prefilter({}, {}, { setRequestHeader: setHeader });
expect(setHeader.callCount).to.be(1);
expect(setHeader.args[0]).to.eql([
xsrfHeader,
xsrfToken
]);
});
it('can be canceled by setting the kbnXsrfToken option', function () {
const setHeader = stub();
prefilter({ kbnXsrfToken: false }, {}, { setRequestHeader: setHeader });
expect(setHeader.callCount).to.be(0);
});
});
context('Angular support', function () {
let $http;
let $httpBackend;
beforeEach(function () {
stub($, 'ajaxPrefilter');
const chrome = {};
xsrfChromeApi(chrome, { xsrfToken });
ngMock.module(chrome.$setupXsrfRequestInterceptor);
});
beforeEach(ngMock.inject(function ($injector) {
$http = $injector.get('$http');
$httpBackend = $injector.get('$httpBackend');
$httpBackend
.when('POST', '/api/test')
.respond('ok');
}));
afterEach(function () {
$httpBackend.verifyNoOutstandingExpectation();
$httpBackend.verifyNoOutstandingRequest();
});
it('injects a kbn-xsrf-token header on every request', function () {
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === xsrfToken;
}).respond(200, '');
$http.post('/api/test');
$httpBackend.flush();
});
it('skips requests with the kbnXsrfToken set falsey', function () {
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return !(xsrfHeader in headers);
}).respond(200, '');
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: 0
});
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: ''
});
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: false
});
$httpBackend.flush();
});
it('accepts alternate tokens to use', function () {
const customToken = `custom:${xsrfToken}`;
$httpBackend.expectPOST('/api/test', undefined, function (headers) {
return headers[xsrfHeader] === customToken;
}).respond(200, '');
$http({
method: 'POST',
url: '/api/test',
kbnXsrfToken: customToken
});
$httpBackend.flush();
});
});
});
});

View file

@ -24,6 +24,7 @@ module.exports = function (chrome, internals) {
a.href = chrome.addBasePath('/elasticsearch');
return a.href;
}()))
.config(chrome.$setupXsrfRequestInterceptor)
.directive('kbnChrome', function ($rootScope) {
return {
template: function ($el) {
@ -45,7 +46,7 @@ module.exports = function (chrome, internals) {
controller: function ($scope, $rootScope, $location, $http) {
// are we showing the embedded version of the chrome?
chrome.setVisible(!Boolean($location.search().embed));
internals.setVisibleDefault(!$location.search().embed);
// listen for route changes, propogate to tabs
var onRouteChange = function () {

View file

@ -10,6 +10,9 @@ module.exports = function (chrome, internals) {
* determines if the Kibana chrome should be displayed
*/
var def = true;
internals.setVisibleDefault = (_def) => def = Boolean(_def);
/**
* @param {boolean} display - should the chrome be displayed
* @return {chrome}
@ -23,7 +26,7 @@ module.exports = function (chrome, internals) {
* @return {boolean} - display state of the chrome
*/
chrome.getVisible = function () {
if (_.isUndefined(internals.visible)) return true;
if (_.isUndefined(internals.visible)) return def;
return internals.visible;
};
};

View file

@ -0,0 +1,29 @@
import $ from 'jquery';
import { set } from 'lodash';
export default function (chrome, internals) {
chrome.getXsrfToken = function () {
return internals.xsrfToken;
};
$.ajaxPrefilter(function ({ kbnXsrfToken = internals.xsrfToken }, originalOptions, jqXHR) {
if (kbnXsrfToken) {
jqXHR.setRequestHeader('kbn-xsrf-token', kbnXsrfToken);
}
});
chrome.$setupXsrfRequestInterceptor = function ($httpProvider) {
$httpProvider.interceptors.push(function () {
return {
request: function (opts) {
const { kbnXsrfToken = internals.xsrfToken } = opts;
if (kbnXsrfToken) {
set(opts, ['headers', 'kbn-xsrf-token'], kbnXsrfToken);
}
return opts;
}
};
});
};
}

View file

@ -2,7 +2,7 @@
<div class="content" chrome-context >
<nav
ng-style="::{ background: chrome.getNavBackground() }"
ng-class="::{ show: chrome.getVisible() }"
ng-class="{ show: chrome.getVisible() }"
class="hide navbar navbar-inverse navbar-static-top">
<!-- Mobile navbar -->

View file

@ -18,6 +18,7 @@ var internals = _.defaults(
rootController: null,
rootTemplate: null,
showAppsLink: null,
xsrfToken: null,
brand: null,
nav: [],
applicationClasses: []
@ -30,6 +31,7 @@ $('<link>').attr({
}).appendTo('head');
require('./api/apps')(chrome, internals);
require('./api/xsrf')(chrome, internals);
require('./api/nav')(chrome, internals);
require('./api/angular')(chrome, internals);
require('./api/controls')(chrome, internals);

View file

@ -20,7 +20,7 @@ describe('ui/courier/fetch/strategy/search', () => {
index: ['logstash-123'],
type: 'blah',
search_type: 'blah2',
body: 'hm this is the body'
body: { foo: 'bar', $foo: 'bar' }
}
];
}));
@ -32,6 +32,14 @@ describe('ui/courier/fetch/strategy/search', () => {
});
describe('#reqsFetchParamsToBody()', () => {
it('filters out any body properties that begin with $', () => {
let value;
search.reqsFetchParamsToBody(reqsFetchParams).then(val => value = val);
$rootScope.$apply();
expect(_.includes(value, 'foo')).to.be(true);
expect(_.includes(value, '$foo')).to.be(false);
});
context('when indexList is not empty', () => {
it('includes the index', () => {
let value;

View file

@ -2,6 +2,7 @@ define(function (require) {
return function FetchStrategyForSearch(Private, Promise, timefilter) {
var _ = require('lodash');
var angular = require('angular');
var toJson = require('ui/utils/aggressive_parse').toJson;
return {
clientMethod: 'msearch',
@ -42,7 +43,7 @@ define(function (require) {
ignore_unavailable: true
})
+ '\n'
+ angular.toJson(fetchParams.body || {});
+ toJson(fetchParams.body || {}, angular.toJson);
});
})
.then(function (requests) {

View file

@ -3,6 +3,7 @@ describe('ui/index_patterns/_calculate_indices', () => {
const sinon = require('auto-release-sinon');
const expect = require('expect.js');
const ngMock = require('ngMock');
const moment = require('moment');
let Promise;
let $rootScope;
@ -38,7 +39,7 @@ describe('ui/index_patterns/_calculate_indices', () => {
function run({ start = undefined, stop = undefined } = {}) {
calculateIndices('wat-*-no', '@something', start, stop);
$rootScope.$apply();
config = _.first(es.fieldStats.firstCall.args);
config = _.first(es.fieldStats.lastCall.args);
constraints = config.body.index_constraints;
}
@ -68,6 +69,14 @@ describe('ui/index_patterns/_calculate_indices', () => {
it('max_value is gte', () => {
expect(constraints['@something'].max_value).to.have.property('gte');
});
it('max_value is set to original if not a moment object', () => {
expect(constraints['@something'].max_value.gte).to.equal('1234567890');
});
it('max_value is set to moment.valueOf if given a moment object', () => {
const start = moment();
run({ start });
expect(constraints['@something'].max_value.gte).to.equal(start.valueOf());
});
});
context('when given stop', () => {
@ -78,6 +87,14 @@ describe('ui/index_patterns/_calculate_indices', () => {
it('min_value is lte', () => {
expect(constraints['@something'].min_value).to.have.property('lte');
});
it('min_value is set to original if not a moment object', () => {
expect(constraints['@something'].min_value.lte).to.equal('1234567890');
});
it('max_value is set to moment.valueOf if given a moment object', () => {
const stop = moment();
run({ stop });
expect(constraints['@something'].min_value.lte).to.equal(stop.valueOf());
});
});
});

View file

@ -2,6 +2,11 @@ define(function (require) {
const _ = require('lodash');
const moment = require('moment');
// gets parsed value if given arg is a moment object
function timeValue(val) {
return moment.isMoment(val) ? val.valueOf() : val;
}
return function CalculateIndicesFactory(Promise, es) {
// Uses the field stats api to determine the names of indices that need to
@ -17,10 +22,10 @@ define(function (require) {
function getFieldStats(pattern, timeFieldName, start, stop) {
const constraints = {};
if (start) {
constraints.max_value = { gte: moment(start).valueOf() };
constraints.max_value = { gte: timeValue(start) };
}
if (stop) {
constraints.min_value = { lte: moment(stop).valueOf() };
constraints.min_value = { lte: timeValue(stop) };
}
return es.fieldStats({

View file

@ -1,4 +1,8 @@
/* global mocha */
// chrome expects to be loaded first, let it get its way
var chrome = require('ui/chrome');
var Nonsense = require('Nonsense');
var sinon = require('sinon');
var $ = require('jquery');
@ -6,8 +10,6 @@ var _ = require('lodash');
var parse = require('url').parse;
var StackTraceMapper = require('ui/StackTraceMapper');
var chrome = require('ui/chrome');
/*** the vislib tests have certain style requirements, so lets make sure they are met ***/
$('body').attr('id', 'test-harness-body'); // so we can make high priority selectors

View file

@ -0,0 +1,81 @@
describe('aggressiveParse', () => {
const _ = require('lodash');
const expect = require('expect.js');
const sinon = require('sinon');
const aggressiveParse = require('ui/utils/aggressive_parse');
let object;
let jsonFn;
let result;
beforeEach(() => {
object = Object.freeze({
foo: 'bar',
nums: { two: 2, $three: 3 },
another: { level: { $deep: 'inception' } },
$no: 'wai'
});
jsonFn = sinon.stub().returns('{"foo":"bar","$foo":"bar"}');
});
describe('#toJson()', () => {
it('returns serialized version of object', () => {
result = aggressiveParse.toJson(object);
result = JSON.parse(result);
expect(_.get(result, 'foo')).to.equal(object.foo);
expect(_.get(result, 'nums.two')).to.equal(object.nums.two);
expect(_.has(result, 'another.level')).to.be(true);
});
it('does not include any properties that begin with $', () => {
result = aggressiveParse.toJson(object);
result = JSON.parse(result);
expect(_.has(result, '$no')).to.be(false);
expect(_.has(result, 'nums.$three')).to.be(false);
expect(_.has(result, 'another.level.$deep')).to.be(false);
});
context('with arity of 2', () => {
beforeEach(() => {
result = aggressiveParse.toJson(object, jsonFn);
result = JSON.parse(result);
});
it('sends first argument to custom json function', () => {
expect(jsonFn.calledWith(object)).to.be(true);
});
it('serializes the json returned by jsonFn', () => {
expect(_.get(result, 'foo')).to.equal('bar');
});
it('still does not include any properties that begin with $', () => {
expect(result).not.to.have.property('$foo');
});
});
context('with arity of 3', () => {
beforeEach(() => {
result = aggressiveParse.toJson({foo: 'bar'}, undefined, 2);
});
it('formats the json string with the number of spaces given', () => {
const formattedJson = JSON.stringify({foo: 'bar'}, null, 2);
expect(result).to.be(formattedJson);
});
});
});
describe('#replacer()', () => {
it('returns undefined if key begins with $', () => {
result = aggressiveParse.replacer('$foo', 'bar');
expect(result).to.be(undefined);
});
it('returns value if key does not being with $', () => {
result = aggressiveParse.replacer('foo', 'bar');
expect(result).to.equal('bar');
});
});
});

View file

@ -0,0 +1,27 @@
import { isString, startsWith } from 'lodash';
/**
* Serializes the given object into a JSON string
*
* All properties that begin with $ throughout the entire object are omitted.
* If a custom JSON serializer function is passed, then the given object is
* passed through it before being re-stringified with the native stringify.
*
* The space argument is passed unaltered to the native stringify.
*/
export function toJson(object, jsonFn, space) {
if (jsonFn) {
// We reparse the stringified json so that we can lean on JSON.stringify's
// avoiding-infinite-recursion capabilities when stripping out any
// remaining properties that begin with a dollar sign ($)
object = JSON.parse(jsonFn(object));
}
return JSON.stringify(object, replacer, space);
};
/**
* Returns the given value if the key does not begin with a dollar sign ($)
*/
export function replacer(key, value) {
return isString(key) && startsWith(key, '$') ? undefined : value;
}

View file

@ -668,16 +668,21 @@ define(function (require) {
* @return {undefined}
*/
Data.prototype._normalizeOrdered = function () {
if (!this.data.ordered || !this.data.ordered.date) return;
var data = this.getVisData();
var self = this;
var missingMin = this.data.ordered.min == null;
var missingMax = this.data.ordered.max == null;
data.forEach(function (d) {
if (!d.ordered || !d.ordered.date) return;
if (missingMax || missingMin) {
var extent = d3.extent(this.xValues());
if (missingMin) this.data.ordered.min = extent[0];
if (missingMax) this.data.ordered.max = extent[1];
}
var missingMin = d.ordered.min == null;
var missingMax = d.ordered.max == null;
if (missingMax || missingMin) {
var extent = d3.extent(self.xValues());
if (missingMin) d.ordered.min = extent[0];
if (missingMax) d.ordered.max = extent[1];
}
});
};
/**

View file

@ -8,6 +8,7 @@ module.exports = function (grunt) {
grunt.registerTask('_build:babelOptions', function () {
unlink(srcFile);
rename(buildFile, srcFile);
grunt.file.mkdir('build/kibana/optimize');
});
};

View file

@ -18,6 +18,9 @@ module.exports = function (grunt) {
sha: grunt.config.get('buildSha')
},
repository: pkg.repository,
engines: {
node: pkg.engines.node
},
dependencies: defaults({}, pkg.dependencies, deepModules)
}, null, ' ')
);

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,9 @@ 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,
'--env.name=development',
'--elasticsearch.url=' + format(uiConfig.servers.elasticsearch),
'--logging.json=false'
]
},
@ -89,7 +90,7 @@ module.exports = function (grunt) {
'-jar',
'selenium/selenium-server-standalone-2.47.1.jar',
'-port',
uiConfig.webdriver.port
uiConfig.servers.webdriver.port
]
},
@ -105,7 +106,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

@ -1,4 +1,3 @@
/*eslint-disable*/
module.exports = [{
'index': {
'_index': 'logstash-2015.09.17',

View file

@ -1,4 +1,3 @@
/*eslint-disable*/
module.exports = [{
'index': {
'_index': 'logstash-2015.09.18',

View file

@ -0,0 +1,60 @@
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 () {
var waitTime = 10000;
return settingsPage.getTimeBasedIndexPatternCheckbox(waitTime);
})
.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);
});

0
test/output/.empty Normal file
View file

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,198 @@
// 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;
}
var defaultTimeout = 60000;
Common.prototype = {
constructor: Common,
navigateToApp: function (appName, testStatusPage) {
var self = this;
var appUrl = getUrl(config.servers.kibana, config.apps[appName]);
var doNavigation = function (url) {
return self.tryForTime(defaultTimeout, 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(defaultTimeout, 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;
// by default, give the app 10 seconds to load
timeout = timeout || 10000;
// 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;
return self.remote.setFindTimeout(defaultTimeout)
.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,290 @@
// 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 = 60000;
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 () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="index.isTimeBased"]');
},
getTimeBasedIndexPatternCheckbox: function (timeout) {
timeout = timeout || defaultTimeout;
// fail faster since we're sometimes checking that it doesn't exist
return this.remote.setFindTimeout(timeout)
.findByCssSelector('input[ng-model="index.nameIsPattern"]');
},
getIndexPatternField: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('[ng-model="index.name"]');
},
getTimeFieldNameField: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('select[ng-model="index.timeField"]');
},
selectTimeFieldOption: function (selection) {
var self = this;
return self.getTimeFieldNameField().click()
.then(function () {
return self.getTimeFieldNameField().click();
})
.then(function () {
return self.getTimeFieldOption(selection);
});
},
getTimeFieldOption: function (selection) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('option[label="' + selection + '"]').click();
},
getCreateButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn');
},
clickCreateButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('.btn').click();
},
clickDefaultIndexButton: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-warning.ng-scope').click();
},
clickDeletePattern: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-danger.ng-scope').click();
},
getIndexPageHeading: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1.title.ng-binding.ng-isolate-scope');
},
getConfigureHeader: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('h1');
},
getTableHeader: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findAllByCssSelector('table.table.table-condensed thead tr th');
},
sortBy: function (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 (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 () {
var self = this;
var selector = 'li.kbn-settings-tab.active a small';
return self.remote.setFindTimeout(defaultTimeout)
.findByCssSelector(selector).getVisibleText()
.then(function (theText) {
// the value has () around it, remove them
return theText.replace(/\((.*)\)/, '$1');
});
},
getPageSize: function () {
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 () {
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 (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 (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 (name) {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('div.actions a.btn.btn-xs.btn-default[href$="/' + name + '"]')
.then(function (button) {
return button.click();
});
},
increasePopularity: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-default[aria-label="Plus"]')
.then(function (button) {
return button.click();
});
},
getPopularity: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('input[ng-model="editor.field.count"]')
.then(function (input) {
return input.getProperty('value');
});
},
controlChangeCancel: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-primary[aria-label="Cancel"]')
.then(function (button) {
return button.click();
});
},
controlChangeSave: function () {
return this.remote.setFindTimeout(defaultTimeout)
.findByCssSelector('button.btn.btn-success.ng-binding[aria-label="Update Field"]')
.then(function (button) {
return button.click();
});
},
setPageSize: function (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 () {
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');
}
});
});
});
},
removeIndexPattern: function () {
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));
};