From 7ace826b63a144856140317fe34530c9f3a45baa Mon Sep 17 00:00:00 2001 From: Spencer Alger Date: Thu, 12 Jun 2014 15:35:06 -0700 Subject: [PATCH] moved devServer into a module so that it can be used programmatically --- .creds.json | 0 package.json | 10 +- src/kibana/apps/dashboard/styles/main.css | 1 - .../apps/discover/controllers/discover.js | 4 +- src/kibana/apps/discover/directives/table.js | 1 + src/kibana/apps/discover/styles/main.css | 1 - src/kibana/apps/settings/styles/main.css | 1 - .../saved_visualizations/_build_chart_data.js | 16 +-- .../bucket_aggs/date_histogram.js | 9 -- src/kibana/apps/visualize/styles/main.css | 1 - .../apps/visualize/styles/visualization.css | 1 - src/kibana/directives/paginate.js | 2 - src/kibana/styles/main.css | 1 - src/kibana/styles/theme/_theme.less | 2 +- tasks/config/connect.js | 75 ------------ tasks/maybe_connect_dev.js | 2 +- tasks/server.js | 9 +- test.js | 31 +++++ test/e2e/index.js | 66 +++++++++++ test/utils/automated_browser/_connector.js | 41 +++++++ test/utils/automated_browser/_example.js | 50 ++++++++ test/utils/automated_browser/_proxy.js | 108 ++++++++++++++++++ test/utils/automated_browser/index.js | 71 ++++++++++++ .../utils/dev_server/_amd_rapper.js | 0 .../utils/dev_server/_instrumentation.js | 0 test/utils/dev_server/index.js | 106 +++++++++++++++++ 26 files changed, 500 insertions(+), 109 deletions(-) create mode 100644 .creds.json delete mode 100644 tasks/config/connect.js create mode 100644 test.js create mode 100644 test/e2e/index.js create mode 100644 test/utils/automated_browser/_connector.js create mode 100644 test/utils/automated_browser/_example.js create mode 100644 test/utils/automated_browser/_proxy.js create mode 100644 test/utils/automated_browser/index.js rename tasks/utils/amd_rapper.js => test/utils/dev_server/_amd_rapper.js (100%) rename tasks/utils/instrumentation.js => test/utils/dev_server/_instrumentation.js (100%) create mode 100644 test/utils/dev_server/index.js diff --git a/.creds.json b/.creds.json new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/package.json b/package.json index cc9e824373fd..daeacd2d9d85 100644 --- a/package.json +++ b/package.json @@ -6,22 +6,22 @@ "main": "Gulpfile.js", "dependencies": {}, "devDependencies": { + "bluebird": "~2.0.7", + "connect": "~2.19.5", + "event-stream": "~3.1.5", "expect.js": "~0.2.0", "grunt": "~0.4.2", - "grunt-cli": "~0.1.13", - "grunt-contrib-connect": "~0.6.0", "grunt-contrib-jade": "~0.10.0", "grunt-contrib-jshint": "~0.8.0", "grunt-contrib-less": "~0.10.0", "grunt-contrib-requirejs": "~0.4.4", "grunt-contrib-watch": "~0.5.3", "grunt-mocha": "~0.4.10", + "http-proxy": "~1.1.4", "istanbul": "~0.2.4", "load-grunt-config": "~0.7.0", "lodash": "~2.4.1", - "bluebird": "~1.2.4", - "mocha": "~1.17.1", - "event-stream": "~3.1.5" + "mocha": "~1.17.1" }, "scripts": { "test": "grunt test", diff --git a/src/kibana/apps/dashboard/styles/main.css b/src/kibana/apps/dashboard/styles/main.css index 423c10de9402..648d11435b25 100644 --- a/src/kibana/apps/dashboard/styles/main.css +++ b/src/kibana/apps/dashboard/styles/main.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, diff --git a/src/kibana/apps/discover/controllers/discover.js b/src/kibana/apps/discover/controllers/discover.js index dda425f284bf..0c8f49fda5f8 100644 --- a/src/kibana/apps/discover/controllers/discover.js +++ b/src/kibana/apps/discover/controllers/discover.js @@ -323,8 +323,6 @@ define(function (require) { }; }, {}); - if (!indexPattern) return; - var columnObjects = arrayToKeys($state.columns); $scope.fields = []; @@ -332,6 +330,8 @@ define(function (require) { $scope.formatsByName = {}; $state.columns = $state.columns || []; + if (!indexPattern) return; + // Inject source into list; //$scope.fields.push({name: '_source', type: 'source', display: false}); diff --git a/src/kibana/apps/discover/directives/table.js b/src/kibana/apps/discover/directives/table.js index ab2462071575..8d44180212e7 100644 --- a/src/kibana/apps/discover/directives/table.js +++ b/src/kibana/apps/discover/directives/table.js @@ -26,6 +26,7 @@ define(function (require) { template: headerHtml, controller: function ($scope) { $scope.headerClass = function (column) { + if (!$scope.mapping) return; if ($scope.mapping[column] && !$scope.mapping[column].indexed) return; var sorting = $scope.sorting; diff --git a/src/kibana/apps/discover/styles/main.css b/src/kibana/apps/discover/styles/main.css index 4ce013bd9a7b..e1ee38de3731 100644 --- a/src/kibana/apps/discover/styles/main.css +++ b/src/kibana/apps/discover/styles/main.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, diff --git a/src/kibana/apps/settings/styles/main.css b/src/kibana/apps/settings/styles/main.css index 4239c632cc30..e9495d1a3851 100644 --- a/src/kibana/apps/settings/styles/main.css +++ b/src/kibana/apps/settings/styles/main.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, diff --git a/src/kibana/apps/visualize/saved_visualizations/_build_chart_data.js b/src/kibana/apps/visualize/saved_visualizations/_build_chart_data.js index 05676447b460..fd6b06cff0dd 100644 --- a/src/kibana/apps/visualize/saved_visualizations/_build_chart_data.js +++ b/src/kibana/apps/visualize/saved_visualizations/_build_chart_data.js @@ -66,10 +66,12 @@ define(function (require) { var row = rowStack.slice(0); var metric = bucket.value == null ? bucket.doc_count : bucket.value; - if (revColStack.length) [].push.apply(row, new Array(revColStack.length)); - - // we have a full row, minus the final metric - row.push(metric); + if (!revColStack.length) { + // we have a full row, minus the final metric + row.push(metric); + } else { + [].push.apply(row, new Array(revColStack.length + 1)); + } chartData.rows.push(row); }; @@ -184,8 +186,6 @@ define(function (require) { rows: [] }; - // debugger; - (function cleanup(obj) { if (obj.rows && obj.columns) { // this obj is a chart @@ -196,10 +196,11 @@ define(function (require) { var rows = obj.rows; var cols = obj.columns; + delete obj.rows; delete obj.columns; - obj.label = [].concat(labelStack, obj.label).filter(Boolean).join(' > '); + obj.label = [].concat(labelStack, obj.label).filter(Boolean).join(' > '); rows.forEach(function (row) { raw.rows.push([].concat(raw.splitValStack, row)); }); @@ -210,6 +211,7 @@ define(function (require) { delete obj.splits; labelStack.push(obj.label); + _.forOwn(splits, function (split) { raw.splitColumns.push(split.column); raw.splitValStack.push(split.value); diff --git a/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js b/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js index 4409c599f0e9..bfb0168348d2 100644 --- a/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js +++ b/src/kibana/apps/visualize/saved_visualizations/bucket_aggs/date_histogram.js @@ -100,15 +100,6 @@ define(function (require) { custom: true }; - agg.params.min_doc_count = { - hide: true, - custom: true, - default: 0, - write: function (selection, output) { - output.aggParams.min_doc_count = 0; - } - }; - agg.params.extended_bounds = { hide: true, default: {}, diff --git a/src/kibana/apps/visualize/styles/main.css b/src/kibana/apps/visualize/styles/main.css index 861ab4a7abfb..243b9d453f7e 100644 --- a/src/kibana/apps/visualize/styles/main.css +++ b/src/kibana/apps/visualize/styles/main.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, diff --git a/src/kibana/apps/visualize/styles/visualization.css b/src/kibana/apps/visualize/styles/visualization.css index a5d3512423ee..d792c2356cae 100644 --- a/src/kibana/apps/visualize/styles/visualization.css +++ b/src/kibana/apps/visualize/styles/visualization.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); .thumbnail > img, .thumbnail a > img, .carousel-inner > .item > img, diff --git a/src/kibana/directives/paginate.js b/src/kibana/directives/paginate.js index 48b38c5f34e2..c7b6ddb75dfd 100644 --- a/src/kibana/directives/paginate.js +++ b/src/kibana/directives/paginate.js @@ -56,8 +56,6 @@ define(function (require) { return; } - if (newPage === oldPage) return; - // setup the list of the other pages to link to $scope.otherPages = []; var width = +getOtherWidth($scope) || 5; diff --git a/src/kibana/styles/main.css b/src/kibana/styles/main.css index a25a6c60d5bb..c5cc526bc6e7 100644 --- a/src/kibana/styles/main.css +++ b/src/kibana/styles/main.css @@ -1,4 +1,3 @@ -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); @import "icon_font.css"; /*! * Font Awesome 4.0.3 by @davegandy - http://fontawesome.io - @fontawesome diff --git a/src/kibana/styles/theme/_theme.less b/src/kibana/styles/theme/_theme.less index 053cd3aad50b..46bdb38a1a18 100644 --- a/src/kibana/styles/theme/_theme.less +++ b/src/kibana/styles/theme/_theme.less @@ -2,7 +2,7 @@ // Bootswatch // ----------------------------------------------------- -@import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); +// @import url("//fonts.googleapis.com/css?family=Lato:400,700,400italic"); // Navbar ===================================================================== diff --git a/tasks/config/connect.js b/tasks/config/connect.js deleted file mode 100644 index 644829cff22b..000000000000 --- a/tasks/config/connect.js +++ /dev/null @@ -1,75 +0,0 @@ -module.exports = function (grunt) { - var instrumentationMiddleware = require('../utils/instrumentation'); - var amdRapperMiddleware = require('../utils/amd_rapper'); - - return { - dev: { - options: { - hostname: '0.0.0.0', - middleware: function (connect, options, stack) { - stack = stack || []; - - var root = grunt.config.get('root'); - - // when a request for an intrumented file comes in (?instrument=true) - // and it is included in `pattern`, it will be handled - // by this middleware - stack.push(instrumentationMiddleware({ - // root that files should be served from - root: root, - - // make file names easier to read - displayRoot: grunt.config.get('src'), - - // filter the filenames that will be served - filter: function (filename) { - // return true if the filename should be - // included in the coverage report (results are cached) - return grunt.file.isMatch([ - '**/src/**/*.js', - '!**/src/bower_components/**/*', - '!**/src/kibana/utils/{event_emitter,next_tick}.js' - ], filename); - } - })); - - // minimize code duplication (especially in the istanbul reporter) - // by allowing node_modules to be requested in an AMD rapper - stack.push(amdRapperMiddleware({ - root: root - })); - - // standard static middleware reading from the root - stack.push(connect.static(root)); - - stack.push(function (req, res, next) { - if (req.method !== 'HEAD' || req.url !== '/') return next(); - res.statusCode === 200; - res.setHeader('Pong', 'Kibana 4 Dev Server'); - res.end(); - }); - - // redirect requests for '/' to '/src/' - stack.push(function (req, res, next) { - if (req.url !== '/') return next(); - res.statusCode = 303; - res.setHeader('Location', '/src/'); - res.end(); - }); - - // allow browsing directories - stack.push( - function (req, res, next) { - // prevent chrome's stupid "this page is in spanish" - res.setHeader('Content-Language', 'en'); - next(); - }, - connect.directory(root) - ); - - return stack; - } - } - } - }; -}; \ No newline at end of file diff --git a/tasks/maybe_connect_dev.js b/tasks/maybe_connect_dev.js index baadbe481688..c8ed73a24866 100644 --- a/tasks/maybe_connect_dev.js +++ b/tasks/maybe_connect_dev.js @@ -19,7 +19,7 @@ module.exports = function (grunt) { } function onError() { - grunt.task.run(['connect:dev']); + grunt.task.run(['server']); done(); } diff --git a/tasks/server.js b/tasks/server.js index 00dc30c9c792..7cffc39c4a75 100644 --- a/tasks/server.js +++ b/tasks/server.js @@ -1,3 +1,10 @@ module.exports = function (grunt) { - grunt.registerTask('server', ['connect:dev:keepalive']); + grunt.registerTask('server', function () { + var done = this.async(); + var DevServer = require('../test/utils/dev_server'); + var server = new DevServer(); + server.listen(8000, function () { + console.log('visit http://localhost:8000'); + }); + }); }; \ No newline at end of file diff --git a/test.js b/test.js new file mode 100644 index 000000000000..61a14036933d --- /dev/null +++ b/test.js @@ -0,0 +1,31 @@ +var Proxy = require('./test/utils/proxy'); +var Promise = require('bluebird'); + +var configfile = require('fs').readFileSync(__dirname + '/src/config.js', 'utf8').replace( + /elasticsearch:[^\n]+/, + 'elasticsearch: \'http://\' + window.location.host + \'/es-proxy\',' +); + +var p = new Proxy(); +p.on('/es-proxy', { + target: 'http://localhost:9200', + middleware: function (req, res) { + // strip the prefix + req.url = req.url.replace(/^\/es-proxy/, ''); + } +}); + +p.on('/src/config.js', { + target: 'http://localhost:8000', + middleware: function (req, res) { + // overwrite the contents of the file + res.end(configfile); + } +}); + +// send all other requests to localhost:8000 +p.on('*', 'http://localhost:8000'); + +p.listen() + .then(console.log.bind(console, 'listening on %s')) + .catch(console.error.bind(console)); \ No newline at end of file diff --git a/test/e2e/index.js b/test/e2e/index.js new file mode 100644 index 000000000000..f42c9454486f --- /dev/null +++ b/test/e2e/index.js @@ -0,0 +1,66 @@ +/* jshint node:true */ +var _ = require('lodash'); +var Proxy = require('./test/utils/proxy'); +var Promise = require('bluebird'); +var readFileSync = require('fs').readFileSync; +var rel = require('path').join.bind(null, __dirname); + + +var soda = require('soda'); +var expect = require('chai').expect; + +// array of async fns that will be called when we need to close up +var closers = []; + +connectSauceLabs({ + username: SAUCE_USERNAME, + accessKey: SAUCE_ACCESSKEY, + verbose: true, + //optionally change sauce connect logfile location + logfile: null, + // optionally identity the tunnel for concurrent tunnels + tunnelIdentifier: null, + logger: function () { + console.log(' \x1b[33mSauce Connect\x1b[0m: %s', [].join.call(arguments, ', ')); + } +}) +.then(function (connectProc) { + closers.push(function () { + return Promise.promisify(connectProc.close, connectProc)() + .then(console.log.bind(console, 'Closed Sauce Connect')); + }); + + console.log('Started Sauce Connect'); +}) +.then(function () { + var browser = soda.createSauceClient({ + 'url': 'http://localhost:8000', + 'username': SAUCE_USERNAME, + 'access-key': SAUCE_ACCESSKEY, + 'os': 'Windows 2003', + 'browser': 'firefox', + 'browser-version': '7', + 'name': 'This is an example test' + }); + + browser.on('command', function(cmd, args){ + console.log(' \x1b[33m%s\x1b[0m: %s', cmd, args.join(', ')); + }); + + browser + .chain + .session() + .open('/src/#/visualize') + .waitForPageToLoad(8000) + .select('//ng-model="indexPattern.selection"') + .end(function(err){ + this.queue = null; + this.setContext('sauce:job-info={"passed": ' + (err === null) + '}', function(){ + browser.testComplete(function(){ + if (err) throw err; + }); + }); + + Promise.all(closers.map(function (fn) { return fn(); })); + }); +}); \ No newline at end of file diff --git a/test/utils/automated_browser/_connector.js b/test/utils/automated_browser/_connector.js new file mode 100644 index 000000000000..c6d25efc84a4 --- /dev/null +++ b/test/utils/automated_browser/_connector.js @@ -0,0 +1,41 @@ +var Promise = require('bluebird'); +var connectSauceLabs = Promise.promisify(require('sauce-connect-launcher')); + +var creds = require('../../../.creds'); + +module.exports = function SauceLabsConnector() { + var proc; + + var pickPort = function () { + return new Promise(function (resolve, reject) { + var server = require('http').createServer(); + server.listen(0, function () { + var port = server.address().port; + server.once('close', function () { + resolve(port); + }); + server.close(); + }); + }); + }; + + this.listen = function () { + connectSauceLabs({ + username: creds.SAUCE_USERNAME, + accessKey: creds.SAUCE_ACCESSKEY, + verbose: true, + tunnelIdentifier: null, + logger: function () { + console.log(' \x1b[33mSauce Connect\x1b[0m: %s', [].join.call(arguments, ', ')); + } + }) + .then(function (connectProc) { + proc = connectProc; + return proc; + }); + }; + + this.close = function () { + + }; +}; \ No newline at end of file diff --git a/test/utils/automated_browser/_example.js b/test/utils/automated_browser/_example.js new file mode 100644 index 000000000000..00de9481c138 --- /dev/null +++ b/test/utils/automated_browser/_example.js @@ -0,0 +1,50 @@ +var wd = require('wd'); +var chai = require('chai'); +var chaiAsPromised = require('chai-as-promised'); +var asserters = wd.asserters; +var mocha = require('mocha'); + +chai.use(chaiAsPromised); +chai.should(); +chaiAsPromised.transferPromiseness = wd.transferPromiseness; + +var url = 'http://localhost:8000'; +var access_key = '1770de66-20c2-4f3c-9d3e-3ecf01d768f4'; +var username = 'ccowan'; +var remotePort = (process.env.SAUCE) ? 4445 : 4444; + +var options = { + browserName: 'firefox', + name: 'Example Marvel Test' +}; + +before(function (done) { + this.browser = wd.promiseChainRemote('localhost', remotePort, username, access_key) + .init(options) + .then(function () { done(); }); +}); + +after(function (done) { + this.browser.quit() + .then(function () { done(); }); +}); + +describe('Welcome Screen', function () { + describe('click "Continue Free Trial"', function() { + + beforeEach(function (done) { + this.browser + .get(url+'/kibana/index.html#/dashboard/elasticsearch/marvel.overview.json') + .then(function () { done(); }); + }); + + it('should set the marvelOpts trialTimestamp attribute in localStorage', function(done) { + this.browser + .waitForElementByCss('div > .modal-body > p > a:nth-child(3)', asserters.isDisplayed, 1000) + .click() + .waitForConditionInBrowser('JSON.parse(localStorage.getItem("marvelOpts")).status === "trial"', 1000) + .then(function () { done(); }); + }); + + }); +}); \ No newline at end of file diff --git a/test/utils/automated_browser/_proxy.js b/test/utils/automated_browser/_proxy.js new file mode 100644 index 000000000000..cf8cd0be64c8 --- /dev/null +++ b/test/utils/automated_browser/_proxy.js @@ -0,0 +1,108 @@ +/* jshint node:true */ +var _ = require('lodash'); +var Promise = require('bluebird'); + +var portOptions = ( + '4000,4001,4040,4321,4502,4503,4567,5000,5001,5050,5555,5432,6000,6001,6060,' + + '6666,6543,7000,7070,7774,7777,8000,8001,8003,8031,8080,8081,8443,8765,8777,' + + '8888,9000,9001,9031,9080,9090,9876,9877,9999,49221,55001' +).split(',').map(function (p) { return _.parseInt(p); }); + +module.exports = function SauceLabsProxy(opts) { + if (!(this instanceof SauceLabsProxy)) return new SauceLabsProxy(opts); + + var proxy = this; + var pass = require('http-proxy').createProxyServer({}); + + var routes = []; + + var send404 = function (req, res) { + res.statusCode = 404; + res.end('unable to find ' + req.url); + }; + + var sendErr = function (req, res, err) { + res.statusCode = 500; + res.end('error: ' + err.message); + }; + + var server = require('http').createServer(function(req, res) { + var route = _.find(routes, function (route) { + return route.re.test(req.url); + }); + + if (!route) return send404(req, res); + + var origEnd = res.end; + var respEnded = false; + res.end = function () { + respEnded = true; + origEnd.apply(res, arguments); + }; + + Promise.cast(route.middleware && route.middleware(req, res)) + .then(function (ret) { + if (!respEnded) pass.web(req, res, { target: route.target }); + }) + .catch(function (err) { + return sendErr(res, res, err); + }); + }); + + proxy.on = function (prefix, opts) { + var route = {}; + + if (_.isRegExp(prefix)) { + route.re = prefix; + } + else if (prefix === '*') { + route.re = /.*/; + } + else { + route.re = new RegExp('^' + prefix); + } + + if (_.isString(opts)) { + route.target = opts; + } + + if (_.isPlainObject(opts)) { + _.assign(route, opts); + } + + routes.push(route); + return route; + }; + + proxy.listen = function () { + return new Promise(function (resolve, reject) { + var port; + + var nextPort = function () { + port = portOptions.shift(); + if (!port) done(new Error('No more port options available.')); + server.listen(port); + }; + + var onError = function (err) { + if (err && err.code === 'EADDRINUSE') nextPort(); + else done(err); + }; + + var onListening = function () { + done(); + }; + + var done = function (err) { + server.removeListener('error', onError); + server.removeListener('listening', onListening); + if (err) reject(err); + else resolve(port); + }; + + server.on('error', onError, true); + server.on('listening', onListening, true); + nextPort(); + }); + }; +}; \ No newline at end of file diff --git a/test/utils/automated_browser/index.js b/test/utils/automated_browser/index.js new file mode 100644 index 000000000000..00ce18ba543e --- /dev/null +++ b/test/utils/automated_browser/index.js @@ -0,0 +1,71 @@ +/* jshint node:true */ +var wd = require('wd'); +var Promise = require('bluebird'); +var readFileSync = require('fs').readFileSync; +var rel = require('path').join.bind(null, __dirname); + +var Connector = require('./_connector'); +var DevServer = require('./_dev_server'); + +var portOptions = [ + 4000, 4001, 4040, 4321, 4502, 4503, 4567, 5000, 5001, 5050, 5555, 5432, 6000, 6001, 6060, + 6666, 6543, 7000, 7070, 7774, 7777, 8000, 8001, 8003, 8031, 8080, 8081, 8443, 8765, 8777, + 8888, 9000, 9001, 9031, 9080, 9090, 9876, 9877, 9999, 49221, 55001 +]; + +var configfile = readFileSync(rel('../../src/config.js'), 'utf8').replace( + /elasticsearch:[^\n]+/, + 'elasticsearch: \'http://\' + window.location.host + \'/es-proxy\',' +); + +exports.init = function (opts) { + opts = opts || {}; + var server = new DevServer(); + + if (opts.useSauceLabs) { + var connector = new Connector(); + } + + return server.listen() + .then(function (port) { + return wd.promiseChainRemote() + .init({ + browserName: browserName || 'chrome', + baseUrl: 'http://localhost:' + port + }); + }) + .then(function (wd) { + return { + server: server, + wd: wd, + close: function () { + return Promise.all([ + wd.close + server.close() + ]); + } + } + }); +}; + +exports.usingSauceLabs = function () { + var server = new DevServer(); + + + var close = function () { + return Promise.all([ + proxy.close(), + connector.close(), + server.close() + ]); + }; + + return server.listen() + .then(function (devServerPort) { + return connector.listen() + }) + .then(function () { + + }); + }); +}; \ No newline at end of file diff --git a/tasks/utils/amd_rapper.js b/test/utils/dev_server/_amd_rapper.js similarity index 100% rename from tasks/utils/amd_rapper.js rename to test/utils/dev_server/_amd_rapper.js diff --git a/tasks/utils/instrumentation.js b/test/utils/dev_server/_instrumentation.js similarity index 100% rename from tasks/utils/instrumentation.js rename to test/utils/dev_server/_instrumentation.js diff --git a/test/utils/dev_server/index.js b/test/utils/dev_server/index.js new file mode 100644 index 000000000000..8315143f471a --- /dev/null +++ b/test/utils/dev_server/index.js @@ -0,0 +1,106 @@ +/* jshint node:true */ + +var connect = require('connect'); +var http = require('http'); +var Promise = require('bluebird'); + +var instrumentationMiddleware = require('./_instrumentation'); +var amdRapperMiddleware = require('./_amd_rapper'); +var rel = require('path').join.bind(null, __dirname); + +var proxy = require('http-proxy').createProxyServer({}); +var ROOT = rel('../../../'); +var SRC = rel('../../../src'); + +module.exports = function DevServer(opts) { + opts = opts || {}; + + var server = this; + var app = connect(); + var httpServer = http.createServer(app); + + app.use(instrumentationMiddleware({ + root: ROOT, + displayRoot: SRC, + filter: function (filename) { + return filename.match(/.*\/src\/.*\.js$/) + && !filename.match(/.*\/src\/bower_components\/.*\.js$/) + && !filename.match(/.*\/src\/kibana\/utils\/(event_emitter|next_tick)\.js$/); + } + })); + + app.use(amdRapperMiddleware({ + root: ROOT + })); + + app.use('/es-proxy', function (req, res) { + req.url = req.url.replace(/^\/es-proxy/, ''); + proxy.web(req, res, { target: 'http://localhost:' + (process.env.ES_PORT || 9200) }); + }); + + app.use(connect.static(ROOT)); + + // respond to the "maybe_start_server" pings + app.use(function (req, res, next) { + if (req.method !== 'HEAD' || req.url !== '/') return next(); + res.statusCode === 200; + res.setHeader('Pong', 'Kibana 4 Dev Server'); + res.end(); + }); + + app.use(function (req, res, next) { + if (req.url !== '/') return next(); + res.statusCode = 303; + res.setHeader('Location', '/src/'); + res.end(); + }); + + // prevent chrome's stupid "this page is in spanish" on the directories page + app.use(function (req, res, next) { + res.setHeader('Content-Language', 'en'); + next(); + }); + + // allow browsing directories + app.use(connect.directory(ROOT)); + + server.listenOnFirstOpenPort = function (ports) { + var options = ports.slice(0); + + // wrap this logic in an IIFE so that we can call it again later + return (function attempt() { + var port = options.shift(); + if (!port) return Promise.reject(new Error('None of the supplied options succeeded')); + + return server.listen(port) + // filter out EADDRINUSE errors and call attempt again + .catch(function (err) { + if (err.code === 'EADDRINUSE') return attempt(); + throw err; + }); + })(); + }; + + server.listen = function (port) { + return new Promise(function (resolve, reject) { + var done = function (err) { + httpServer.removeListener('error', done); + httpServer.removeListener('listening', done); + + // pass the error along + if (err) return reject(err); + + resolve(server.port = httpServer.address().port); + }; + + // call done with an error + httpServer.on('error', done, true); + // call done without any args + httpServer.on('listening', done, true); + + httpServer.listen(port); + }); + }; + + server.close = httpServer.close.bind(httpServer); +}; \ No newline at end of file