diff --git a/lib/config.js b/lib/config.js index 27fa84d2a..35adadead 100644 --- a/lib/config.js +++ b/lib/config.js @@ -23,7 +23,8 @@ var parseConfig = function(configFilePath, cliOptions) { reporter: 'progress', singleRun: false, browsers: [], - proxies: {} + proxies: {}, + urlRoot: '/' }; var ADAPTER_DIR = __dirname + '/../adapter'; diff --git a/lib/launcher.js b/lib/launcher.js index ec1ddc84e..bf066eca3 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -228,8 +228,8 @@ PhantomJSBrowser.prototype = { var Launcher = function() { var browsers = []; - this.launch = function(names, port) { - var url = 'http://localhost:' + port; + this.launch = function(names, port, urlRoot) { + var url = 'http://localhost:' + port + urlRoot; var Cls, browser, id; names.forEach(function(nameOrCls) { @@ -249,7 +249,7 @@ var Launcher = function() { browser.isCaptured = false; log.info('Starting browser "%s"', browser.name || 'Custom'); - browser.start(url + '/?id=' + browser.id); + browser.start(url + '?id=' + browser.id); browsers.push(browser); }); }; diff --git a/lib/proxy.js b/lib/proxy.js new file mode 100644 index 000000000..388ff0a78 --- /dev/null +++ b/lib/proxy.js @@ -0,0 +1,61 @@ +var url = require('url'); + +var parseProxyConfig = function(proxies) { + var proxyConfig = {}; + var endsWith = function(str, suffix) { + return str.substr(-suffix.length) === suffix; + } + if (!proxies) { return; } + Object.keys(proxies).forEach(function(proxyPath) { + var proxyUrl = proxies[proxyPath]; + if (!endsWith(proxyPath, '/')) { + proxyPath = proxyPath + '/'; + } + var proxyDetails = url.parse(proxyUrl); + if (!endsWith(proxyDetails.path, '/')) { + proxyDetails.path = proxyDetails.path + '/'; + } + proxyConfig[proxyPath] = { + host: proxyDetails.hostname, + port: proxyDetails.port || '80', + baseProxyUrl: proxyDetails.path + }; + }); + return proxyConfig; +}; + +exports.parseProxyConfig = parseProxyConfig; + +/** + * Returns a handler which understands the proxies and its redirects, along with the proxy to use + * @param proxy A http-proxy.RoutingProxy object with the proxyRequest method + * @param proxies a map of routes to proxy url + * @return {Function} handler function + */ +var createProxyHandler = function(proxy, proxyConfig) { + var proxies = parseProxyConfig(proxyConfig); + var proxiesList = []; + if (proxies) { + proxiesList = Object.keys(proxies); + proxiesList.sort(); + proxiesList.reverse(); + } + return function(request, response) { + var proxiedUrl; + if (proxies) { + for (var i = 0; i < proxiesList.length; i++) { + if (request.url.indexOf(proxiesList[i]) === 0) { + proxiedUrl = proxies[proxiesList[i]]; + request.url = request.url.replace(proxiesList[i], proxiedUrl.baseProxyUrl); + break; + } + } + } + if (proxiedUrl) { + proxy.proxyRequest(request, response, {host: proxiedUrl.host, port: proxiedUrl.port}); + return true; + } + return false; + }; +}; +exports.createProxyHandler = createProxyHandler; diff --git a/lib/reporter.js b/lib/reporter.js index 205223c4e..928cc6c29 100644 --- a/lib/reporter.js +++ b/lib/reporter.js @@ -1,8 +1,8 @@ var util = require('util'); var u = require('./util'); -var createErrorFormatter = function(basePath) { - var URL_REGEXP = /http:\/\/[^\/]*\/(base|absolute)([^\?\s]*)(\?[0-9]*)?/g; +var createErrorFormatter = function(basePath, urlRoot) { + var URL_REGEXP = new RegExp('http:\\/\\/[^\\/]*' + urlRoot.replace(/\//g, '\\/')+ '(base|absolute)([^\\?\\s]*)(\\?[0-9]*)?', 'g'); return function(msg, indentation) { // remove domain and timestamp from source files @@ -260,6 +260,6 @@ exports.DotsColor = DotsColorReporter; exports.ProgressColor = ProgressColorReporter; -exports.createReporter = function(name, useColors, basePath) { - return new exports[u.ucFirst(name) + (useColors ? 'Color' : '')](createErrorFormatter(basePath)); +exports.createReporter = function(name, useColors, basePath, urlRoot) { + return new exports[u.ucFirst(name) + (useColors ? 'Color' : '')](createErrorFormatter(basePath, urlRoot)); }; diff --git a/lib/server.js b/lib/server.js index 0dcef1be4..912ee3af1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -33,9 +33,10 @@ exports.start = function(cliOptions) { } }); - var webServer = ws.createWebServer(fileList, config.basePath, config.proxies); + var webServer = ws.createWebServer(fileList, config.basePath, config.proxies, config.urlRoot); var socketServer = io.listen(webServer, { logger: logger.create('socket.io', 0), + resource: config.urlRoot + 'socket.io', transports: ['websocket', 'xhr-polling', 'jsonp-polling'] }); @@ -50,14 +51,14 @@ exports.start = function(cliOptions) { }); webServer.listen(config.port, function() { - log.info('Web server started at http://localhost:' + config.port); + log.info('Web server started at http://localhost:' + config.port + config.urlRoot); if (config.browsers && config.browsers.length) { - launcher.launch(config.browsers, config.port); + launcher.launch(config.browsers, config.port, config.urlRoot); } }); - var resultReporter = reporter.createReporter(config.reporter, config.colors, config.basePath); + var resultReporter = reporter.createReporter(config.reporter, config.colors, config.basePath, config.urlRoot); globalEmitter.bind(resultReporter); var capturedBrowsers = new browser.Collection(globalEmitter); @@ -84,7 +85,7 @@ exports.start = function(cliOptions) { var nonReady = []; if (!capturedBrowsers.length) { - log.warn('No captured browser, open http://localhost:' + config.port); + log.warn('No captured browser, open http://localhost:' + config.port + config.urlRoot); return false; } else if (capturedBrowsers.areAllReady(nonReady)) { log.debug('All browsers are ready, executing'); @@ -145,8 +146,8 @@ exports.start = function(cliOptions) { log.debug('Execution (fired by runner)'); if (!capturedBrowsers.length) { - log.warn('No captured browser, open http://localhost:' + config.port); - socket.end('No captured browser, open http://localhost:' + config.port + '\n'); + log.warn('No captured browser, open http://localhost:' + config.port + config.urlRoot); + socket.end('No captured browser, open http://localhost:' + config.port + config.urlRoot + '\n'); return; } diff --git a/lib/web-server.js b/lib/web-server.js index 650abbb58..45db9dd84 100644 --- a/lib/web-server.js +++ b/lib/web-server.js @@ -1,11 +1,11 @@ -var fs = require('fs'), - http = require('http'), - util = require('util'), +var http = require('http'), u = require('./util'), path = require('path'), + httpProxy = require('http-proxy'), + proxy = require('./proxy'), + fs = require('fs'), log = require('./logger').create('web server'), - url = require('url'), - httpProxy = require('http-proxy'); + util = require('util'); var SCRIPT_TAG = ''; var MIME_TYPE = { @@ -21,84 +21,79 @@ var setNoCacheHeaders = function(response) { }; -/** - * Web Server handler - * - * URL schema structure: - * /base/... (files in basePath, commonly project root, relative paths) - * /absolute/... (files outside of basePath, absolute paths) - * /adapter/... (testacular adapters) - */ -var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, proxy, proxies) { - - return function(request, response) { - - var files = fileList.getFiles(); - - var getProxiedPath = function(requestUrl) { - var proxiedUrl; - if (proxies) { - var proxiesList = Object.keys(proxies); - proxiesList.sort(); - proxiesList.reverse(); - for (var i = 0; i < proxiesList.length; i++) { - if (requestUrl.indexOf(proxiesList[i]) === 0) { - proxiedUrl = url.parse(proxies[proxiesList[i]]); - break; - } - } - } - return proxiedUrl; - }; +exports.createWebServer = function (fileList, baseFolder, proxies, urlRoot) { + var staticFolder = path.normalize(__dirname + '/../static'); + var adapterFolder = path.normalize(__dirname + '/../adapter'); - // helper for serving static file - var serveStaticFile = function(file, process) { - fs.readFile(file, function(error, data) { + return http.createServer(createHandler(fileList, u.normalizeWinPath(staticFolder), + u.normalizeWinPath(adapterFolder), baseFolder, + new httpProxy.RoutingProxy(), proxies, urlRoot)); +}; - if (error) { - log.warn('404: ' + file); - response.writeHead(404); - return response.end('NOT FOUND'); - } +var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, proxyFn, proxies, urlRoot) { + var testacularSrcHandler = createTestacularSourceHandler(fileList, staticFolder, adapterFolder, baseFolder, urlRoot); + var proxiedPathsHandler = proxy.createProxyHandler(proxyFn, proxies); + var sourceFileHandler = createSourceFileHandler(fileList, adapterFolder, baseFolder); + return function(request, response) { + if (testacularSrcHandler(request, response)) { + return; + } + if (proxiedPathsHandler(request, response)) { + return; + } + return sourceFileHandler(request, response); + }; +}; - // set content type - response.setHeader('Content-Type', MIME_TYPE[file.split('.').pop()] || MIME_TYPE.txt); +var serveStaticFile = function(file, response, process) { + fs.readFile(file, function(error, data) { + if (error) { + log.warn('404: ' + file); + response.writeHead(404); + return response.end('NOT FOUND'); + } - // call custom process fn to transform the data - var responseData = process && process(data.toString(), response) || data; - response.writeHead(200); + // set content type + response.setHeader('Content-Type', MIME_TYPE[file.split('.').pop()] || MIME_TYPE.txt); - log.debug('serving: ' + file); - return response.end(responseData); - }); - }; + // call custom process fn to transform the data + var responseData = process && process(data.toString(), response) || data; + response.writeHead(200); - // TODO(vojta): clean the url namespace (put everything to /__testacular__/ or so) - // TODO(vojta): no cache for testacular.js and client.html ? (updating testacular) + log.debug('serving: ' + file); + return response.end(responseData); + }); +}; - var requestedFilePath = request.url.replace(/\?.*/, '') - .replace(/^\/adapter/, adapterFolder) - .replace(/^\/absolute/, '') - .replace(/^\/base/, baseFolder); - // SERVE client.html - main entry point - if (requestedFilePath === '/') { - return serveStaticFile(staticFolder + '/client.html'); +var createTestacularSourceHandler = function(fileList, staticFolder, adapterFolder, baseFolder, urlRoot) { + return function(request, response) { + var requestUrl = request.url.replace(/\?.*/, ''); + if (requestUrl.indexOf(urlRoot) !== 0) { + return false; + } + requestUrl = requestUrl.substring(urlRoot.length - 1); + if (requestUrl === '/') { + serveStaticFile(staticFolder + '/client.html', response); + return true; } // SERVE testacular.js - if (request.url === '/testacular.js') { - return serveStaticFile(staticFolder + '/testacular.js'); + if (requestUrl === '/testacular.js') { + serveStaticFile(staticFolder + '/testacular.js', response, function(data, response) { + return data.replace('%TESTACULAR_SRC_PREFIX%', urlRoot.substring(1)); + }); + return true; } // SERVE context.html - execution context within the iframe // or runner.html - execution context without channel to the server - if (request.url === '/context.html' || request.url === '/debug.html') { - return serveStaticFile(staticFolder + request.url, function(data, response) { + if (requestUrl === '/context.html' || requestUrl === '/debug.html') { + serveStaticFile(staticFolder + requestUrl, response, function(data, response) { // never cache setNoCacheHeaders(response); - var scriptTags = files.map(function(file) { + var scriptTags = fileList.getFiles().map(function(file) { var filePath = file.path; if (!file.isUrl) { @@ -110,7 +105,7 @@ var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, filePath = '/absolute' + filePath; } - if (request.url === '/context.html') { + if (requestUrl === '/context.html') { filePath += '?' + file.mtime.getTime(); } } @@ -120,45 +115,38 @@ var createHandler = function(fileList, staticFolder, adapterFolder, baseFolder, return data.replace('%SCRIPTS%', scriptTags.join('\n')); }); + return true; } + return false; + }; +}; + +var createSourceFileHandler = function(fileList, adapterFolder, baseFolder) { + return function(request, response) { + var requestedFilePath = request.url.replace(/\?.*/, '') + .replace(/^\/adapter/, adapterFolder) + .replace(/^\/absolute/, '') + .replace(/^\/base/, baseFolder); var equalsPath = function(file) { return file.path === requestedFilePath; }; - // Check if proxied path, and if so, route it through proxy - var proxiedPath = getProxiedPath(request.url); - if (proxiedPath) { - proxiedPath.port = proxiedPath.port || '80'; - return proxy.proxyRequest(request, response, {host: proxiedPath.hostname, port: proxiedPath.port}); - } - - - // not in the file list - forbidden - if (!files.some(equalsPath)) { + if (fileList.getFiles().some(equalsPath)) { + serveStaticFile(requestedFilePath, response, function(data, response) { + if (/\?\d+/.test(request.url)) { + // files with timestamps - cache one year, rely on timestamps + response.setHeader('Cache-Control', ['public', 'max-age=31536000']); + } else { + // without timestamps - no cache (debug) + setNoCacheHeaders(response); + } + }); + return true; + } else { response.writeHead(404); - return response.end('NOT FOUND'); + response.end('NOT FOUND'); + return false; } - - // OTHERWISE - js files - return serveStaticFile(requestedFilePath, function(data, response) { - if (/\?\d+/.test(request.url)) { - // files with timestamps - cache one year, rely on timestamps - response.setHeader('Cache-Control', ['public', 'max-age=31536000']); - } else { - // without timestamps - no cache (debug) - setNoCacheHeaders(response); - } - }); - }; -}; - -exports.createWebServer = function (fileList, baseFolder, proxies) { - var staticFolder = path.normalize(__dirname + '/../static'); - var adapterFolder = path.normalize(__dirname + '/../adapter'); - var proxy = new httpProxy.RoutingProxy(); - - return http.createServer(createHandler(fileList, u.normalizeWinPath(staticFolder), - u.normalizeWinPath(adapterFolder), baseFolder, - proxy, proxies)); + } }; diff --git a/static/client.html b/static/client.html index 50503cb58..1a9fa9e1a 100644 --- a/static/client.html +++ b/static/client.html @@ -14,7 +14,7 @@