diff --git a/grunt.js b/grunt.js index 30fc2c58f..226995944 100644 --- a/grunt.js +++ b/grunt.js @@ -15,7 +15,7 @@ module.exports = function(grunt) { grunt.initConfig({ pkg: '', files: { - server: ['lib/*.js'], + server: ['lib/**/*.js'], client: ['static/testacular.src.js'], jasmine: ['adapter/jasmine.src.js'], mocha: ['adapter/mocha.src.js'], diff --git a/lib/launcher.js b/lib/launcher.js index 39b0cd6ba..c691e3d1a 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -1,305 +1,10 @@ -var spawn = require('child_process').spawn; -var path = require('path'); var log = require('./logger').create('launcher'); -var env = process.env; -var fs = require('fs'); -var util = require('./util'); - -var generate = { - id: function() { - return Math.floor(Math.random() * 100000000); - } -}; - - -// inspired by https://github.com/admc/jellyfish/blob/master/lib/browsers.js -var Browser = function(id) { - - var exitCallback = function() {}; - - this._getCommand = function() { - return path.normalize(env[this.ENV_CMD] || this.DEFAULT_CMD[process.platform]); - }; - - this._execCommand = function(cmd, args) { - log.debug(cmd + ' ' + args.join(' ')); - this._process = spawn(cmd, args); - - var errorOutput = ''; - this._process.stderr.on('data', function(data) { - errorOutput += data.toString(); - }); - - var self = this; - this._process.on('exit', function(code) { - if (code) { - log.error('Cannot start %s\n\t%s', self.name, errorOutput); - } - - log.debug('Cleaning %s', self._tempDir); - spawn('rm', ['-rf', self._tempDir]).on('exit', exitCallback); - }); - }; - - this._getOptions = function(url) { - return [url]; - }; - - this.id = id; - this.isCaptured = false; - - this._tempDir = path.normalize((env.TMPDIR || env.TMP || env.TEMP || '/tmp') + '/testacular-' + id.toString()); - - try { - log.debug('Creating temp dir at ' + this._tempDir); - fs.mkdirSync(this._tempDir); - } catch (e) {} - - - this.start = function(url) { - this._execCommand(this._getCommand(), this._getOptions(url)); - }; - - this.kill = function(callback) { - exitCallback = callback || function() {}; - - if (this._process.exitCode === null) { - this._process.kill(); - } else { - process.nextTick(exitCallback); - } - }; -}; - - -var ChromeBrowser = function() { - Browser.apply(this, arguments); - - this._getOptions = function(url) { - // Chrome CLI options - // http://peter.sh/experiments/chromium-command-line-switches/ - return [ - '--user-data-dir=' + this._tempDir, - '--no-default-browser-check', - '--no-first-run', - '--disable-default-apps', - url - ]; - }; -}; - -ChromeBrowser.prototype = { - name: 'Chrome', - - DEFAULT_CMD: { - linux: 'google-chrome', - darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', - win32: process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe' - }, - ENV_CMD: 'CHROME_BIN' -}; - - -var ChromeCanaryBrowser = function() { - ChromeBrowser.apply(this, arguments); - - var parentOptions = this._getOptions; - this._getOptions = function(url) { - // disable crankshaft optimizations, as it causes lot of memory leaks (as of Chrome 23.0) - return parentOptions.call(this, url).concat(['--js-flags="--nocrankshaft --noopt"']); - }; -}; - -ChromeCanaryBrowser.prototype = { - name: 'ChromeCanary', - - DEFAULT_CMD: { - linux: 'google-chrome-canary', - darwin: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', - win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe' - }, - ENV_CMD: 'CHROME_CANARY_BIN' -}; - - -var FirefoxBrowser = function(id) { - Browser.apply(this, arguments); - - this.start = function(url) { - var self = this; - var command = this._getCommand(); - var errorOutput = ''; - - // passing url, because on Windows and Linux, -CreateProfile is ignored and started FF - // (if there is already any running instance) - var p = spawn(command, ['-CreateProfile', 'testacular-' + id + ' ' + self._tempDir, url]); - - p.stderr.on('data', function(data) { - errorOutput += data.toString(); - }); - - p.on('exit', function() { - var match = /at\s\'(.*)[\/\\]prefs\.js\'/.exec(errorOutput); - - if (match) { - var profile = self._tempDir = match[1]; - var prefs = 'user_pref("browser.shell.checkDefaultBrowser", false);\n' + - 'user_pref("browser.bookmarks.restore_default_bookmarks", false);\n'; - - fs.createWriteStream(profile + '/prefs.js', {flags: 'a'}).write(prefs); - self._execCommand(command, ['-profile', profile, url]); - } else { - // we don't have to start Firefox, as -CreateProfile cmd probably already did - log.warn('Cannot create Firefox profile, reusing current.'); - } - }); - }; -}; - -FirefoxBrowser.prototype = { - name: 'Firefox', - - DEFAULT_CMD: { - linux: 'firefox', - darwin: '/Applications/Firefox.app/Contents/MacOS/firefox-bin', - win32: process.env.ProgramFiles + '\\Mozilla Firefox\\firefox.exe' - }, - ENV_CMD: 'FIREFOX_BIN' -}; - - -var OperaBrowser = function() { - Browser.apply(this, arguments); - - this._getOptions = function(url) { - // Opera CLI options - // http://www.opera.com/docs/switches/ - return [ - '-pd', this._tempDir, - '-nomail', - url - ]; - }; - - this.start = function(url) { - var self = this; - var prefs = 'Opera Preferences version 2.1\n' + - '[User Prefs]\n' + - 'Show Default Browser Dialog=0\n' + - 'Startup Type=2\n' + // use homepage - 'Home URL=about:blank\n' + - 'Show Close All But Active Dialog=0\n' + - 'Show Close All Dialog=0\n' + - 'Show Crash Log Upload Dialog=0\n' + - 'Show Delete Mail Dialog=0\n' + - 'Show Download Manager Selection Dialog=0\n' + - 'Show Geolocation License Dialog=0\n' + - 'Show Mail Error Dialog=0\n' + - 'Show New Opera Dialog=0\n' + - 'Show Problem Dialog=0\n' + - 'Show Progress Dialog=0\n' + - 'Show Validation Dialog=0\n' + - 'Show Widget Debug Info Dialog=0\n' + - 'Show Startup Dialog=0\n' + - 'Show E-mail Client=0\n' + - 'Show Mail Header Toolbar=0\n' + - 'Show Setupdialog On Start=0\n' + - 'Enable Usage Statistics=0\n' + - '[Install]\n' + - 'Newest Used Version=1.00.0000\n' + - '[State]\n' + - 'Accept License=1\n'; - - var prefsFile = this._tempDir + '/operaprefs.ini'; - fs.writeFile(prefsFile, prefs, function(err) { - // TODO(vojta): handle error - self._execCommand(self._getCommand(), self._getOptions(url)); - }); - }; -}; - -OperaBrowser.prototype = { - name: 'Opera', - - DEFAULT_CMD: { - linux: 'opera', - darwin: '/Applications/Opera.app/Contents/MacOS/Opera', - win32: process.env.ProgramFiles + '\\Opera\\opera.exe' - }, - ENV_CMD: 'OPERA_BIN' -}; - - -var SafariBrowser = function() { - Browser.apply(this, arguments); - - this.start = function(url) { - var HTML_TPL = path.normalize(__dirname + '/../static/safari.html'); - var self = this; - - fs.readFile(HTML_TPL, function(err, data) { - var content = data.toString().replace('%URL%', url); - var staticHtmlPath = self._tempDir + '/redirect.html'; - - fs.writeFile(staticHtmlPath, content, function(err) { - self._execCommand(self._getCommand(), [staticHtmlPath]); - }); - }); - }; -}; - -SafariBrowser.prototype = { - name: 'Safari', - - DEFAULT_CMD: { - darwin: '/Applications/Safari.app/Contents/MacOS/Safari' - }, - ENV_CMD: 'SAFARI_BIN' -}; - - -var PhantomJSBrowser = function() { - Browser.apply(this, arguments); - - this.start = function(url) { - // create the js file, that will open testacular - var captureFile = this._tempDir + '/capture.js'; - var captureCode = '(new WebPage()).open("' + url + '");'; - fs.createWriteStream(captureFile).end(captureCode); - - // and start phantomjs - this._execCommand(this._getCommand(), [captureFile]); - }; -}; - -PhantomJSBrowser.prototype = { - name: 'PhantomJS', - - DEFAULT_CMD: { - linux: 'phantomjs', - darwin: '/usr/local/bin/phantomjs', - win32: process.env.ProgramFiles + '\\PhantomJS\\phantomjs.exe' - }, - ENV_CMD: 'PHANTOMJS_BIN' -}; - - -var IEBrowser = function() { - Browser.apply(this, arguments); -}; - -IEBrowser.prototype = { - name: 'IE', - DEFAULT_CMD: { - win32: process.env.ProgramFiles + '\\Internet Explorer\\iexplore.exe' - }, - ENV_CMD: 'IE_BIN' -}; +var BaseBrowser = require('./launchers/Base'); var ScriptBrowser = function(id, script) { - Browser.apply(this, arguments); + BaseBrowser.apply(this, arguments); this.name = script; @@ -312,13 +17,14 @@ var ScriptBrowser = function(id, script) { var Launcher = function() { var browsers = []; + this.launch = function(names, port, urlRoot) { var url = 'http://localhost:' + port + urlRoot; var Cls, browser; names.forEach(function(name) { Cls = exports[name + 'Browser'] || ScriptBrowser; - browser = new Cls(generate.id(), name); + browser = new Cls(Launcher.generateId(), name); log.info('Starting browser "%s"', browser.name || 'Custom'); browser.start(url + '?id=' + browser.id); @@ -326,6 +32,7 @@ var Launcher = function() { }); }; + this.kill = function(callback) { var remaining = 0; var finish = function() { @@ -345,12 +52,14 @@ var Launcher = function() { }); }; + this.areAllCaptured = function() { return !browsers.some(function(browser) { return !browser.isCaptured; }); }; + this.markCaptured = function(id) { browsers.forEach(function(browser) { if (browser.id === id) { @@ -360,13 +69,18 @@ var Launcher = function() { }; }; +Launcher.generateId = function() { + return Math.floor(Math.random() * 100000000); +}; + +// PUBLISH exports.Launcher = Launcher; -exports.BaseBrowser = Browser; -exports.ChromeBrowser = ChromeBrowser; -exports.ChromeCanaryBrowser = ChromeCanaryBrowser; -exports.FirefoxBrowser = FirefoxBrowser; -exports.OperaBrowser = OperaBrowser; -exports.SafariBrowser = SafariBrowser; -exports.PhantomJSBrowser = PhantomJSBrowser; -exports.IEBrowser = IEBrowser; + +exports.ChromeBrowser = require('./launchers/Chrome'); +exports.ChromeCanaryBrowser = require('./launchers/ChromeCanary'); +exports.FirefoxBrowser = require('./launchers/Firefox'); +exports.IEBrowser = require('./launchers/IE'); +exports.OperaBrowser = require('./launchers/Opera'); +exports.PhantomJSBrowser = require('./launchers/PhantomJS'); +exports.SafariBrowser = require('./launchers/Safari'); diff --git a/lib/launchers/Base.js b/lib/launchers/Base.js new file mode 100644 index 000000000..088160bf9 --- /dev/null +++ b/lib/launchers/Base.js @@ -0,0 +1,73 @@ +var spawn = require('child_process').spawn; +var path = require('path'); +var log = require('../logger').create('launcher'); +var fs = require('fs'); +var env = process.env; + + +var BaseBrowser = function(id) { + + var exitCallback = function() {}; + + this.id = id; + this.isCaptured = false; + + this._tempDir = path.normalize((env.TMPDIR || env.TMP || env.TEMP || '/tmp') + '/testacular-' + + id.toString()); + + try { + log.debug('Creating temp dir at ' + this._tempDir); + fs.mkdirSync(this._tempDir); + } catch (e) {} + + + this.start = function(url) { + this._execCommand(this._getCommand(), this._getOptions(url)); + }; + + + this.kill = function(callback) { + exitCallback = callback || function() {}; + + if (this._process.exitCode === null) { + this._process.kill(); + } else { + process.nextTick(exitCallback); + } + }; + + + this._getCommand = function() { + return path.normalize(env[this.ENV_CMD] || this.DEFAULT_CMD[process.platform]); + }; + + + this._execCommand = function(cmd, args) { + log.debug(cmd + ' ' + args.join(' ')); + this._process = spawn(cmd, args); + + var errorOutput = ''; + this._process.stderr.on('data', function(data) { + errorOutput += data.toString(); + }); + + var self = this; + this._process.on('exit', function(code) { + if (code) { + log.error('Cannot start %s\n\t%s', self.name, errorOutput); + } + + log.debug('Cleaning %s', self._tempDir); + spawn('rm', ['-rf', self._tempDir]).on('exit', exitCallback); + }); + }; + + + this._getOptions = function(url) { + return [url]; + }; +}; + + +// PUBLISH +module.exports = BaseBrowser; diff --git a/lib/launchers/Chrome.js b/lib/launchers/Chrome.js new file mode 100644 index 000000000..48aa8733f --- /dev/null +++ b/lib/launchers/Chrome.js @@ -0,0 +1,33 @@ +var BaseBrowser = require('./Base'); + + +var ChromeBrowser = function() { + BaseBrowser.apply(this, arguments); + + this._getOptions = function(url) { + // Chrome CLI options + // http://peter.sh/experiments/chromium-command-line-switches/ + return [ + '--user-data-dir=' + this._tempDir, + '--no-default-browser-check', + '--no-first-run', + '--disable-default-apps', + url + ]; + }; +}; + +ChromeBrowser.prototype = { + name: 'Chrome', + + DEFAULT_CMD: { + linux: 'google-chrome', + darwin: '/Applications/Google Chrome.app/Contents/MacOS/Google Chrome', + win32: process.env.LOCALAPPDATA + '\\Google\\Chrome\\Application\\chrome.exe' + }, + ENV_CMD: 'CHROME_BIN' +}; + + +// PUBLISH +module.exports = ChromeBrowser; diff --git a/lib/launchers/ChromeCanary.js b/lib/launchers/ChromeCanary.js new file mode 100644 index 000000000..a51318875 --- /dev/null +++ b/lib/launchers/ChromeCanary.js @@ -0,0 +1,27 @@ +var ChromeBrowser = require('./Chrome'); + + +var ChromeCanaryBrowser = function() { + ChromeBrowser.apply(this, arguments); + + var parentOptions = this._getOptions; + this._getOptions = function(url) { + // disable crankshaft optimizations, as it causes lot of memory leaks (as of Chrome 23.0) + return parentOptions.call(this, url).concat(['--js-flags="--nocrankshaft --noopt"']); + }; +}; + +ChromeCanaryBrowser.prototype = { + name: 'ChromeCanary', + + DEFAULT_CMD: { + linux: 'google-chrome-canary', + darwin: '/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary', + win32: process.env.LOCALAPPDATA + '\\Google\\Chrome SxS\\Application\\chrome.exe' + }, + ENV_CMD: 'CHROME_CANARY_BIN' +}; + + +// PUBLISH +module.exports = ChromeCanaryBrowser; diff --git a/lib/launchers/Firefox.js b/lib/launchers/Firefox.js new file mode 100644 index 000000000..37e1ed60b --- /dev/null +++ b/lib/launchers/Firefox.js @@ -0,0 +1,57 @@ +var spawn = require('child_process').spawn; +var log = require('../logger').create('launcher'); +var fs = require('fs'); + +var BaseBrowser = require('./Base'); + + +var PREFS = + 'user_pref("browser.shell.checkDefaultBrowser", false);\n' + + 'user_pref("browser.bookmarks.restore_default_bookmarks", false);\n'; + +var FirefoxBrowser = function(id) { + BaseBrowser.apply(this, arguments); + + this.start = function(url) { + var self = this; + var command = this._getCommand(); + var errorOutput = ''; + + // passing url, because on Windows and Linux, -CreateProfile is ignored and started FF + // (if there is already any running instance) + var p = spawn(command, ['-CreateProfile', 'testacular-' + id + ' ' + self._tempDir, url]); + + p.stderr.on('data', function(data) { + errorOutput += data.toString(); + }); + + p.on('exit', function() { + var match = /at\s\'(.*)[\/\\]prefs\.js\'/.exec(errorOutput); + + if (match) { + var profile = self._tempDir = match[1]; + + fs.createWriteStream(profile + '/prefs.js', {flags: 'a'}).write(PREFS); + self._execCommand(command, ['-profile', profile, url]); + } else { + // we don't have to start Firefox, as -CreateProfile cmd probably already did + log.warn('Cannot create Firefox profile, reusing current.'); + } + }); + }; +}; + +FirefoxBrowser.prototype = { + name: 'Firefox', + + DEFAULT_CMD: { + linux: 'firefox', + darwin: '/Applications/Firefox.app/Contents/MacOS/firefox-bin', + win32: process.env.ProgramFiles + '\\Mozilla Firefox\\firefox.exe' + }, + ENV_CMD: 'FIREFOX_BIN' +}; + + +// PUBLISH +module.exports = FirefoxBrowser; diff --git a/lib/launchers/IE.js b/lib/launchers/IE.js new file mode 100644 index 000000000..5dfa69931 --- /dev/null +++ b/lib/launchers/IE.js @@ -0,0 +1,17 @@ +var BaseBrowser = require('./Base'); + +var IEBrowser = function() { + BaseBrowser.apply(this, arguments); +}; + +IEBrowser.prototype = { + name: 'IE', + DEFAULT_CMD: { + win32: process.env.ProgramFiles + '\\Internet Explorer\\iexplore.exe' + }, + ENV_CMD: 'IE_BIN' +}; + + +// PUBLISH +module.exports = IEBrowser; diff --git a/lib/launchers/Opera.js b/lib/launchers/Opera.js new file mode 100644 index 000000000..db2b35fb5 --- /dev/null +++ b/lib/launchers/Opera.js @@ -0,0 +1,72 @@ +var fs = require('fs'); + +var BaseBrowser = require('./Base'); + + +var PREFS = + 'Opera Preferences version 2.1\n' + + '[User Prefs]\n' + + 'Show Default Browser Dialog=0\n' + + 'Startup Type=2\n' + // use homepage + 'Home URL=about:blank\n' + + 'Show Close All But Active Dialog=0\n' + + 'Show Close All Dialog=0\n' + + 'Show Crash Log Upload Dialog=0\n' + + 'Show Delete Mail Dialog=0\n' + + 'Show Download Manager Selection Dialog=0\n' + + 'Show Geolocation License Dialog=0\n' + + 'Show Mail Error Dialog=0\n' + + 'Show New Opera Dialog=0\n' + + 'Show Problem Dialog=0\n' + + 'Show Progress Dialog=0\n' + + 'Show Validation Dialog=0\n' + + 'Show Widget Debug Info Dialog=0\n' + + 'Show Startup Dialog=0\n' + + 'Show E-mail Client=0\n' + + 'Show Mail Header Toolbar=0\n' + + 'Show Setupdialog On Start=0\n' + + 'Enable Usage Statistics=0\n' + + '[Install]\n' + + 'Newest Used Version=1.00.0000\n' + + '[State]\n' + + 'Accept License=1\n'; + + +var OperaBrowser = function() { + BaseBrowser.apply(this, arguments); + + this._getOptions = function(url) { + // Opera CLI options + // http://www.opera.com/docs/switches/ + return [ + '-pd', this._tempDir, + '-nomail', + url + ]; + }; + + this.start = function(url) { + var self = this; + + var prefsFile = this._tempDir + '/operaprefs.ini'; + fs.writeFile(prefsFile, PREFS, function(err) { + // TODO(vojta): handle error + self._execCommand(self._getCommand(), self._getOptions(url)); + }); + }; +}; + +OperaBrowser.prototype = { + name: 'Opera', + + DEFAULT_CMD: { + linux: 'opera', + darwin: '/Applications/Opera.app/Contents/MacOS/Opera', + win32: process.env.ProgramFiles + '\\Opera\\opera.exe' + }, + ENV_CMD: 'OPERA_BIN' +}; + + +// PUBLISH +module.exports = OperaBrowser; diff --git a/lib/launchers/PhantomJS.js b/lib/launchers/PhantomJS.js new file mode 100644 index 000000000..8822b093a --- /dev/null +++ b/lib/launchers/PhantomJS.js @@ -0,0 +1,33 @@ +var fs = require('fs'); + +var BaseBrowser = require('./Base'); + + +var PhantomJSBrowser = function() { + BaseBrowser.apply(this, arguments); + + this.start = function(url) { + // create the js file, that will open testacular + var captureFile = this._tempDir + '/capture.js'; + var captureCode = '(new WebPage()).open("' + url + '");'; + fs.createWriteStream(captureFile).end(captureCode); + + // and start phantomjs + this._execCommand(this._getCommand(), [captureFile]); + }; +}; + +PhantomJSBrowser.prototype = { + name: 'PhantomJS', + + DEFAULT_CMD: { + linux: 'phantomjs', + darwin: '/usr/local/bin/phantomjs', + win32: process.env.ProgramFiles + '\\PhantomJS\\phantomjs.exe' + }, + ENV_CMD: 'PHANTOMJS_BIN' +}; + + +// PUBLISH +module.exports = PhantomJSBrowser; diff --git a/lib/launchers/Safari.js b/lib/launchers/Safari.js new file mode 100644 index 000000000..264fbcc2c --- /dev/null +++ b/lib/launchers/Safari.js @@ -0,0 +1,35 @@ +var fs = require('fs'); + +var BaseBrowser = require('./Base'); + + +var SafariBrowser = function() { + BaseBrowser.apply(this, arguments); + + this.start = function(url) { + var HTML_TPL = path.normalize(__dirname + '/../static/safari.html'); + var self = this; + + fs.readFile(HTML_TPL, function(err, data) { + var content = data.toString().replace('%URL%', url); + var staticHtmlPath = self._tempDir + '/redirect.html'; + + fs.writeFile(staticHtmlPath, content, function(err) { + self._execCommand(self._getCommand(), [staticHtmlPath]); + }); + }); + }; +}; + +SafariBrowser.prototype = { + name: 'Safari', + + DEFAULT_CMD: { + darwin: '/Applications/Safari.app/Contents/MacOS/Safari' + }, + ENV_CMD: 'SAFARI_BIN' +}; + + +// PUBLISH +module.exports = SafariBrowser; diff --git a/test/unit/launcher.spec.coffee b/test/unit/launcher.spec.coffee index 92c492569..ade639586 100644 --- a/test/unit/launcher.spec.coffee +++ b/test/unit/launcher.spec.coffee @@ -10,7 +10,7 @@ describe 'launcher', -> beforeEach util.disableLogger beforeEach -> - mockSpawn = jasmine.createSpy 'exec' + mockSpawn = jasmine.createSpy 'spawn' mockSpawn._processes = [] mockSpawn.andCallFake (cmd, args) -> process = new events.EventEmitter @@ -23,16 +23,18 @@ describe 'launcher', -> mocks = child_process: spawn: mockSpawn + './logger': require '../../lib/logger' + globals = global: global process: nextTick: process.nextTick, platform: 'linux', env: TMPDIR: '/temp' - m = loadFile __dirname + '/../../lib/launcher.js', mocks, globals + m = loadFile __dirname + '/../../lib/launcher.js', mocks, globals, true e = m.exports # mock out id generator lastGeneratedId = 0 - m.generate.id = -> + e.Launcher.generateId = -> ++lastGeneratedId