diff --git a/.eslintrc b/.eslintrc index e3578aadf..d6622030e 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,3 +1,4 @@ { - "extends": "standard" + "extends": "standard", + "parser": "babel-eslint" } diff --git a/client/karma.js b/client/karma.js index 9caf386b3..176538cd2 100644 --- a/client/karma.js +++ b/client/karma.js @@ -113,7 +113,13 @@ var Karma = function (socket, iframe, opener, navigator, location) { // we are not going to execute at all this.error = function (msg, url, line) { hasError = true - socket.emit('karma_error', url ? msg + '\nat ' + url + (line ? ':' + line : '') : msg) + var message = msg + + if (url) { + message = msg + '\nat ' + url + (line ? ':' + line : '') + } + + socket.emit('karma_error', message) this.complete() return false } diff --git a/gruntfile.js b/gruntfile.js index 2285372d7..365be2df1 100644 --- a/gruntfile.js +++ b/gruntfile.js @@ -28,15 +28,18 @@ module.exports = function (grunt) { }, mochaTest: { options: { - ui: 'bdd', + require: [ + 'babel/register' + ], reporter: 'dot', - quite: false, + ui: 'bdd', + quiet: false, colors: true }, unit: { src: [ - 'test/unit/mocha-globals.coffee', - 'test/unit/**/*.coffee' + 'test/unit/mocha-globals.js', + 'test/unit/**/*.spec.js' ] } }, @@ -64,6 +67,9 @@ module.exports = function (grunt) { } }, eslint: { + options: { + quiet: true + }, target: [ '<%= files.server %>', '<%= files.grunt %>', @@ -72,23 +78,6 @@ module.exports = function (grunt) { 'test/**/*.js' ] }, - coffeelint: { - unittests: { - files: { - src: ['test/unit/**/*.coffee'] - } - }, - taskstests: { - files: { - src: ['test/tasks/**/*.coffee'] - } - }, - options: { - max_line_length: { - value: 100 - } - } - }, 'npm-publish': { options: { requires: ['build'], @@ -127,7 +116,7 @@ module.exports = function (grunt) { grunt.registerTask('build', ['browserify:client']) grunt.registerTask('default', ['build', 'test', 'lint']) - grunt.registerTask('lint', ['eslint', 'coffeelint']) + grunt.registerTask('lint', ['eslint']) grunt.registerTask('release', 'Build, bump and publish to NPM.', function (type) { grunt.task.run([ diff --git a/package.json b/package.json index e8490f3cd..8bf00a5bd 100644 --- a/package.json +++ b/package.json @@ -215,7 +215,7 @@ "chokidar": "^1.0.1", "colors": "^1.1.0", "connect": "^3.3.5", - "core-js": "^0.9.17", + "core-js": "^1.0.1", "di": "^0.0.1", "dom-serialize": "^2.2.0", "expand-braces": "^0.1.1", @@ -235,24 +235,24 @@ }, "devDependencies": { "LiveScript": "^1.3.0", + "babel": "^5.6.23", + "babel-eslint": "^4.0.5", "chai": "^2.3.0", "chai-as-promised": "^5.0.0", "chai-subset": "^1.0.1", - "coffee-errors": "^0.8.6", "coffee-script": "^1.9.2", "cucumber": "^0.5.2", - "eslint": "^0.24.1", - "eslint-config-standard": "^3.4.1", - "eslint-plugin-react": "^2.5.0", + "eslint": "^1.0.0", + "eslint-config-standard": "^4.0.0", + "eslint-plugin-react": "^3.2.0", "grunt": "^0.4", "grunt-auto-release": "^0.0.6", "grunt-browserify": "^3.8.0", "grunt-bump": "^0.3.1", - "grunt-coffeelint": "^0.0.13", "grunt-contrib-watch": "^0.6.1", "grunt-conventional-changelog": "^1.2.2", "grunt-cucumberjs": "^0.7.0", - "grunt-eslint": "^16.0.0", + "grunt-eslint": "^17.0.0", "grunt-mocha-test": "^0.12.7", "grunt-npm": "0.0.2", "karma-browserify": "^4.1.2", diff --git a/test/.eslintrc b/test/.eslintrc index 3c8fcd734..32dfd9f91 100644 --- a/test/.eslintrc +++ b/test/.eslintrc @@ -3,6 +3,8 @@ "mocha": true }, "globals": { - "expect": true + "expect": true, + "sinon": true, + "scheduleNextTick": true } } diff --git a/test/unit/browser.spec.coffee b/test/unit/browser.spec.coffee deleted file mode 100644 index 73eaea72a..000000000 --- a/test/unit/browser.spec.coffee +++ /dev/null @@ -1,509 +0,0 @@ -#============================================================================== -# lib/browser.js module -#============================================================================== -describe 'Browser', -> - e = require '../../lib/events' - Browser = require '../../lib/browser' - Collection = require '../../lib/browser_collection' - createMockTimer = require './mocks/timer' - - browser = collection = emitter = socket = null - socketId = 0 - - mkSocket = -> - s = new e.EventEmitter - s.id = socketId++ - return s - - beforeEach -> - socket = mkSocket() - emitter = new e.EventEmitter - collection = new Collection emitter - - - it 'should set fullName and name', -> - fullName = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + - '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7' - browser = new Browser 'id', fullName, collection, emitter, socket - expect(browser.name).to.equal 'Chrome 16.0.912 (Mac OS X 10.6.8)' - expect(browser.fullName).to.equal fullName - - - #========================================================================== - # Browser.init - #========================================================================== - describe 'init', -> - - it 'should emit "browser_register"', -> - spyRegister = sinon.spy() - emitter.on 'browser_register', spyRegister - browser = new Browser 12345, '', collection, emitter, socket - browser.init() - - expect(spyRegister).to.have.been.called - expect(spyRegister.args[0][0]).to.equal browser - - - it 'should ad itself into the collection', -> - browser = new Browser 12345, '', collection, emitter, socket - browser.init() - - expect(collection.length).to.equal 1 - collection.forEach (browserInCollection) -> - expect(browserInCollection).to.equal browser - - - #========================================================================== - # Browser.toString - #========================================================================== - describe 'toString', -> - - it 'should return browser name', -> - fullName = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + - '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7' - browser = new Browser 'id', fullName, collection, emitter, socket - expect(browser.toString()).to.equal 'Chrome 16.0.912 (Mac OS X 10.6.8)' - - it 'should return verbatim user agent string for unrecognized browser', -> - fullName = 'NonexistentBot/1.2.3' - browser = new Browser 'id', fullName, collection, emitter, socket - expect(browser.toString()).to.equal 'NonexistentBot/1.2.3' - - - #========================================================================== - # Browser.onKarmaError - #========================================================================== - describe 'onKarmaError', -> - - beforeEach -> - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - - - it 'should set lastResult.error and fire "browser_error"', -> - spy = sinon.spy() - emitter.on 'browser_error', spy - browser.state = Browser.STATE_EXECUTING - - browser.onKarmaError() - expect(browser.lastResult.error).to.equal true - expect(spy).to.have.been.called - - - it 'should ignore if browser not executing', -> - spy = sinon.spy() - emitter.on 'browser_error', spy - browser.state = Browser.STATE_READY - - browser.onKarmaError() - expect(browser.lastResult.error).to.equal false - expect(spy).not.to.have.been.called - - - #========================================================================== - # Browser.onInfo - #========================================================================== - describe 'onInfo', -> - - beforeEach -> - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - - - it 'should emit "browser_log"', -> - spy = sinon.spy() - emitter.on 'browser_log', spy - - browser.state = Browser.STATE_EXECUTING - browser.onInfo {log: 'something', type: 'info'} - expect(spy).to.have.been.calledWith browser, 'something', 'info' - - - it 'should ignore if browser not executing', -> - spy = sinon.spy() - emitter.on 'browser_dump', spy - - browser.state = Browser.STATE_READY - browser.onInfo {dump: 'something'} - browser.onInfo {total: 20} - - expect(browser.lastResult.total).to.equal 0 - expect(spy).not.to.have.been.called - - - #========================================================================== - # Browser.onStart - #========================================================================== - describe 'onStart', -> - - beforeEach -> - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - - - it 'should set total count of specs', -> - browser.state = Browser.STATE_EXECUTING - browser.onStart {total: 20} - expect(browser.lastResult.total).to.equal 20 - - - it 'should emit "browser_start"', -> - spy = sinon.spy() - emitter.on 'browser_start', spy - - browser.state = Browser.STATE_EXECUTING - browser.onStart {total: 20} - - expect(spy).to.have.been.calledWith browser, {total: 20} - - - #========================================================================== - # Browser.onComplete - #========================================================================== - describe 'onComplete', -> - - beforeEach -> - sinon.stub Date, 'now' - Date.now.returns 12345 - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - - afterEach -> Date.now.restore() - - - it 'should set isReady to true', -> - browser.state = Browser.STATE_EXECUTING - browser.onComplete() - expect(browser.isReady()).to.equal true - - - it 'should fire "browsers_change" event', -> - spy = sinon.spy() - emitter.on 'browsers_change', spy - - browser.state = Browser.STATE_EXECUTING - browser.onComplete() - expect(spy).to.have.been.calledWith collection - - - it 'should ignore if browser not executing', -> - spy = sinon.spy() - emitter.on 'browsers_change', spy - emitter.on 'browser_complete', spy - - browser.state = Browser.STATE_READY - browser.onComplete() - expect(spy).not.to.have.been.called - - - it 'should set totalTime', -> - Date.now.returns 12347 # the default spy return 12345 - - browser.state = Browser.STATE_EXECUTING - browser.onComplete() - - expect(browser.lastResult.totalTime).to.equal 2 - - - it 'should error the result if zero tests executed', -> - browser.state = Browser.STATE_EXECUTING - browser.onComplete() - - expect(browser.lastResult.error).to.equal true - - - #========================================================================== - # Browser.onDisconnect - #========================================================================== - describe 'onDisconnect', -> - timer = null - - beforeEach -> - timer = createMockTimer() - browser = new Browser 'fake-id', 'full name', collection, emitter, socket, timer, 10 - browser.init() - - - it 'should remove from parent collection', -> - expect(collection.length).to.equal 1 - - browser.onDisconnect 'socket.io-reason', socket - expect(collection.length).to.equal 0 - - - it 'should complete if browser executing', -> - spy = sinon.spy() - emitter.on 'browser_complete', spy - browser.state = Browser.STATE_EXECUTING - - browser.onDisconnect 'socket.io-reason', socket - timer.wind 20 - - expect(browser.lastResult.disconnected).to.equal true - expect(spy).to.have.been.called - - - it 'should not complete if browser not executing', -> - spy = sinon.spy() - emitter.on 'browser_complete', spy - browser.state = Browser.STATE_READY - - browser.onDisconnect 'socket.io-reason', socket - expect(spy).not.to.have.been.called - - - #========================================================================== - # Browser.reconnect - #========================================================================== - describe 'reconnect', -> - - it 'should cancel disconnecting', -> - timer = createMockTimer() - - browser = new Browser 'id', 'Chrome 19.0', collection, emitter, socket, timer, 10 - browser.init() - browser.state = Browser.STATE_EXECUTING - - browser.onDisconnect 'socket.io-reason', socket - browser.reconnect mkSocket() - - timer.wind 10 - expect(browser.state).to.equal Browser.STATE_EXECUTING - - - it 'should ignore disconnects on old sockets, but accept other messages', -> - # IE on polling sometimes reconnect on another socket (before disconnecting) - - browser = new Browser 'id', 'Chrome 19.0', collection, emitter, socket, null, 0 - browser.init() - browser.state = Browser.STATE_EXECUTING - - browser.reconnect mkSocket() - - # still accept results on the old socket - socket.emit 'result', {success: true} - expect(browser.lastResult.success).to.equal 1 - - socket.emit 'karma_error', {} - expect(browser.lastResult.error).to.equal true - - # should be ignored, keep executing - socket.emit 'disconnect', 'socket.io reason' - expect(browser.state).to.equal Browser.STATE_EXECUTING - - - it 'should reconnect a disconnected browser', -> - browser = new Browser 'id', 'Chrome 25.0', collection, emitter, socket, null, 10 - browser.state = Browser.STATE_DISCONNECTED - - browser.reconnect mkSocket() - - expect(browser.isReady()).to.equal true - - - #========================================================================== - # Browser.onResult - #========================================================================== - describe 'onResult', -> - - createSuccessResult = -> - {success: true, suite: [], log: []} - - createFailedResult = -> - {success: false, suite: [], log: []} - - createSkippedResult = -> - {success: true, skipped: true, suite: [], log: []} - - beforeEach -> - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - - - it 'should update lastResults', -> - browser.state = Browser.STATE_EXECUTING - browser.onResult createSuccessResult() - browser.onResult createSuccessResult() - browser.onResult createFailedResult() - browser.onResult createSkippedResult() - - expect(browser.lastResult.success).to.equal 2 - expect(browser.lastResult.failed).to.equal 1 - expect(browser.lastResult.skipped).to.equal 1 - - - it 'should ignore if not running', -> - browser.state = Browser.STATE_READY - browser.onResult createSuccessResult() - browser.onResult createSuccessResult() - browser.onResult createFailedResult() - - expect(browser.lastResult.success).to.equal 0 - expect(browser.lastResult.failed).to.equal 0 - - - it 'should update netTime', -> - browser.state = Browser.STATE_EXECUTING - browser.onResult {time: 3, suite: [], log: []} - browser.onResult {time: 1, suite: [], log: []} - browser.onResult {time: 5, suite: [], log: []} - - expect(browser.lastResult.netTime).to.equal 9 - - - it 'should accept array of results', -> - browser.state = Browser.STATE_EXECUTING - browser.onResult [createSuccessResult(), createSuccessResult(), - createFailedResult(), createSkippedResult()] - - expect(browser.lastResult.success).to.equal 2 - expect(browser.lastResult.failed).to.equal 1 - expect(browser.lastResult.skipped).to.equal 1 - - - #========================================================================== - # Browser.serialize - #========================================================================== - describe 'serialize', -> - - it 'should return plain object with only name, id, isReady properties', -> - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - browser.state = Browser.STATE_READY - browser.name = 'Browser 1.0' - browser.id = '12345' - - expect(browser.serialize()).to.deep.equal {id: '12345', name: 'Browser 1.0', isReady: true} - - - #========================================================================== - # Browser.serialize - #========================================================================== - describe 'execute', -> - it 'should emit execute and change state to EXECUTING', -> - spyExecute = sinon.spy() - config = {} - browser = new Browser 'fake-id', 'full name', collection, emitter, socket - socket.on 'execute', spyExecute - browser.execute config - - expect(browser.isReady()).to.equal false - expect(spyExecute).to.have.been.calledWith config - - - #========================================================================== - # Browser higher level tests for reconnecting - #========================================================================== - describe 'scenario:', -> - - it 'reconnecting during the run', -> - timer = createMockTimer() - browser = new Browser 'fake-id', 'full name', collection, emitter, socket, timer, 10 - browser.init() - browser.state = Browser.STATE_EXECUTING - socket.emit 'result', {success: true, suite: [], log: []} - socket.emit 'disconnect', 'socket.io reason' - expect(browser.isReady()).to.equal false - - newSocket = mkSocket() - browser.reconnect newSocket - expect(browser.isReady()).to.equal false - - newSocket.emit 'result', {success: false, suite: [], log: []} - newSocket.emit 'complete' - expect(browser.isReady()).to.equal true - expect(browser.lastResult.success).to.equal 1 - expect(browser.lastResult.failed).to.equal 1 - - - it 'disconecting during the run', -> - spy = sinon.spy() - emitter.on 'browser_complete', spy - timer = createMockTimer() - browser = new Browser 'fake-id', 'full name', collection, emitter, socket, timer, 10 - browser.init() - browser.state = Browser.STATE_EXECUTING - socket.emit 'result', {success: true, suite: [], log: []} - socket.emit 'disconnect', 'socket.io reason' - - timer.wind 10 - expect(browser.lastResult.disconnected).to.equal true - expect(spy).to.have.been.calledWith browser - - - it 'restarting a disconnected browser', -> - timer = createMockTimer() - browser = new Browser 'fake-id', 'Chrome 31.0', collection, emitter, socket, timer, 10 - browser.init() - - browser.execute() - socket.emit 'start', {total: 10} - socket.emit 'result', {success: true, suite: [], log: []} - socket.emit 'result', {success: false, suite: [], log: []} - socket.emit 'result', {skipped: true, suite: [], log: []} - socket.emit 'disconnect', 'socket.io reason' - timer.wind 10 # wait-for reconnecting delay - expect(browser.state).to.equal Browser.STATE_DISCONNECTED - expect(browser.disconnectsCount).to.equal 1 - - newSocket = mkSocket() - emitter.on 'browser_register', -> browser.execute() - - # reconnect on a new socket (which triggers re-execution) - browser.reconnect newSocket - expect(browser.state).to.equal Browser.STATE_EXECUTING - newSocket.emit 'start', {total: 11} - socket.emit 'result', {success: true, suite: [], log: []} - - # expected cleared last result (should not include the results from previous run) - expect(browser.lastResult.total).to.equal 11 - expect(browser.lastResult.success).to.equal 1 - expect(browser.lastResult.failed).to.equal 0 - expect(browser.lastResult.skipped).to.equal 0 - - - it 'keeping multiple active sockets', -> - # If there is a new connection (socket) for an already connected browser, - # we need to keep the old socket, in the case that the new socket will disconnect. - browser = new Browser 'fake-id', 'Chrome 31.0', collection, emitter, socket, null, 10 - browser.init() - browser.execute() - - # A second connection... - newSocket = mkSocket() - browser.reconnect newSocket - - # Disconnect the second connection... - browser.onDisconnect 'socket.io-reason', newSocket - expect(browser.state).to.equal Browser.STATE_EXECUTING - - # It should still be listening on the old socket. - socket.emit 'result', {success: true, suite: [], log: []} - expect(browser.lastResult.success).to.equal 1 - - it 'complete only once after reconnect on the same socket', -> - # If there is a new connection on the same socket, - # we should emit complete message only once. - browser = new Browser 'fake-id', 'Chrome 31.0', collection, emitter, socket, null, 10 - browser.onComplete = sinon.spy() - browser.init() - browser.execute() - - # A second connection... - browser.reconnect socket - - socket.emit 'result', {success: true, suite: [], log: []} - socket.emit 'complete' - - expect(browser.onComplete.callCount).to.equal 1 - - it 'disconnect when no message during the run', -> - timer = createMockTimer() - browser = new Browser 'fake-id', 'Chrome 31.0', collection, emitter, socket, timer, 10, 20 - browser.init() - browser.execute() - - spyBrowserComplete = sinon.spy() - emitter.on 'browser_complete', spyBrowserComplete - - socket.emit 'start', {total: 11} - socket.emit 'result', {success: true, suite: [], log: []} - - timer.wind 20 - expect(browser.state).to.equal Browser.STATE_DISCONNECTED - expect(browser.disconnectsCount).to.equal 1 - expect(spyBrowserComplete).to.have.been.called diff --git a/test/unit/browser.spec.js b/test/unit/browser.spec.js new file mode 100644 index 000000000..6f7ef2762 --- /dev/null +++ b/test/unit/browser.spec.js @@ -0,0 +1,488 @@ +describe('Browser', () => { + var collection + var emitter + var socket + var e = require('../../lib/events') + var Browser = require('../../lib/browser') + var Collection = require('../../lib/browser_collection') + var createMockTimer = require('./mocks/timer') + + var browser = collection = emitter = socket = null + var socketId = 0 + + var mkSocket = () => { + var s = new e.EventEmitter() + socketId = socketId + 1 + s.id = socketId + return s + } + + beforeEach(() => { + socket = mkSocket() + emitter = new e.EventEmitter() + collection = new Collection(emitter) + }) + + it('should set fullName and name', () => { + var fullName = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7' + browser = new Browser('id', fullName, collection, emitter, socket) + expect(browser.name).to.equal('Chrome 16.0.912 (Mac OS X 10.6.8)') + expect(browser.fullName).to.equal(fullName) + }) + + describe('init', () => { + it('should emit "browser_register"', () => { + var spyRegister = sinon.spy() + emitter.on('browser_register', spyRegister) + browser = new Browser(12345, '', collection, emitter, socket) + browser.init() + + expect(spyRegister).to.have.been.called + expect(spyRegister.args[0][0]).to.equal(browser) + }) + + it('should ad itself into the collection', () => { + browser = new Browser(12345, '', collection, emitter, socket) + browser.init() + + expect(collection.length).to.equal(1) + collection.forEach(browserInCollection => { + expect(browserInCollection).to.equal(browser) + }) + }) + }) + + describe('toString', () => { + it('should return browser name', () => { + var fullName = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7' + browser = new Browser('id', fullName, collection, emitter, socket) + expect(browser.toString()).to.equal('Chrome 16.0.912 (Mac OS X 10.6.8)') + }) + + it('should return verbatim user agent string for unrecognized browser', () => { + var fullName = 'NonexistentBot/1.2.3' + browser = new Browser('id', fullName, collection, emitter, socket) + expect(browser.toString()).to.equal('NonexistentBot/1.2.3') + }) + }) + + describe('onKarmaError', () => { + beforeEach(() => { + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + }) + + it('should set lastResult.error and fire "browser_error"', () => { + var spy = sinon.spy() + emitter.on('browser_error', spy) + browser.state = Browser.STATE_EXECUTING + + browser.onKarmaError() + expect(browser.lastResult.error).to.equal(true) + expect(spy).to.have.been.called + }) + + it('should ignore if browser not executing', () => { + var spy = sinon.spy() + emitter.on('browser_error', spy) + browser.state = Browser.STATE_READY + + browser.onKarmaError() + expect(browser.lastResult.error).to.equal(false) + expect(spy).not.to.have.been.called + }) + }) + + describe('onInfo', () => { + beforeEach(() => { + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + }) + + it('should emit "browser_log"', () => { + var spy = sinon.spy() + emitter.on('browser_log', spy) + + browser.state = Browser.STATE_EXECUTING + browser.onInfo({log: 'something', type: 'info'}) + expect(spy).to.have.been.calledWith(browser, 'something', 'info') + }) + + it('should ignore if browser not executing', () => { + var spy = sinon.spy() + emitter.on('browser_dump', spy) + + browser.state = Browser.STATE_READY + browser.onInfo({dump: 'something'}) + browser.onInfo({total: 20}) + + expect(browser.lastResult.total).to.equal(0) + expect(spy).not.to.have.been.called + }) + }) + + describe('onStart', () => { + beforeEach(() => { + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + }) + + it('should set total count of specs', () => { + browser.state = Browser.STATE_EXECUTING + browser.onStart({total: 20}) + expect(browser.lastResult.total).to.equal(20) + }) + + it('should emit "browser_start"', () => { + var spy = sinon.spy() + emitter.on('browser_start', spy) + + browser.state = Browser.STATE_EXECUTING + browser.onStart({total: 20}) + + expect(spy).to.have.been.calledWith(browser, {total: 20}) + }) + }) + + describe('onComplete', () => { + beforeEach(() => { + sinon.stub(Date, 'now') + Date.now.returns(12345) + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + }) + + afterEach(() => { + Date.now.restore() + }) + + it('should set isReady to true', () => { + browser.state = Browser.STATE_EXECUTING + browser.onComplete() + expect(browser.isReady()).to.equal(true) + }) + + it('should fire "browsers_change" event', () => { + var spy = sinon.spy() + emitter.on('browsers_change', spy) + + browser.state = Browser.STATE_EXECUTING + browser.onComplete() + expect(spy).to.have.been.calledWith(collection) + }) + + it('should ignore if browser not executing', () => { + var spy = sinon.spy() + emitter.on('browsers_change', spy) + emitter.on('browser_complete', spy) + + browser.state = Browser.STATE_READY + browser.onComplete() + expect(spy).not.to.have.been.called + }) + + it('should set totalTime', () => { + Date.now.returns(12347) // the default spy return 12345 + + browser.state = Browser.STATE_EXECUTING + browser.onComplete() + + expect(browser.lastResult.totalTime).to.equal(2) + }) + + it('should error the result if zero tests executed', () => { + browser.state = Browser.STATE_EXECUTING + browser.onComplete() + + expect(browser.lastResult.error).to.equal(true) + }) + }) + + describe('onDisconnect', () => { + var timer = null + + beforeEach(() => { + timer = createMockTimer() + browser = new Browser('fake-id', 'full name', collection, emitter, socket, timer, 10) + browser.init() + }) + + it('should remove from parent collection', () => { + expect(collection.length).to.equal(1) + + browser.onDisconnect('socket.io-reason', socket) + expect(collection.length).to.equal(0) + }) + + it('should complete if browser executing', () => { + var spy = sinon.spy() + emitter.on('browser_complete', spy) + browser.state = Browser.STATE_EXECUTING + + browser.onDisconnect('socket.io-reason', socket) + timer.wind(20) + + expect(browser.lastResult.disconnected).to.equal(true) + expect(spy).to.have.been.called + }) + + it('should not complete if browser not executing', () => { + var spy = sinon.spy() + emitter.on('browser_complete', spy) + browser.state = Browser.STATE_READY + + browser.onDisconnect('socket.io-reason', socket) + expect(spy).not.to.have.been.called + }) + }) + + describe('reconnect', () => { + it('should cancel disconnecting', () => { + var timer = createMockTimer() + + browser = new Browser('id', 'Chrome 19.0', collection, emitter, socket, timer, 10) + browser.init() + browser.state = Browser.STATE_EXECUTING + + browser.onDisconnect('socket.io-reason', socket) + browser.reconnect(mkSocket()) + + timer.wind(10) + expect(browser.state).to.equal(Browser.STATE_EXECUTING) + }) + + it('should ignore disconnects on old sockets, but accept other messages', () => { + // IE on polling sometimes reconnect on another socket (before disconnecting) + + browser = new Browser('id', 'Chrome 19.0', collection, emitter, socket, null, 0) + browser.init() + browser.state = Browser.STATE_EXECUTING + + browser.reconnect(mkSocket()) + + // still accept results on the old socket + socket.emit('result', {success: true}) + expect(browser.lastResult.success).to.equal(1) + + socket.emit('karma_error', {}) + expect(browser.lastResult.error).to.equal(true) + + // should be ignored, keep executing + socket.emit('disconnect', 'socket.io reason') + expect(browser.state).to.equal(Browser.STATE_EXECUTING) + }) + + it('should reconnect a disconnected browser', () => { + browser = new Browser('id', 'Chrome 25.0', collection, emitter, socket, null, 10) + browser.state = Browser.STATE_DISCONNECTED + + browser.reconnect(mkSocket()) + + expect(browser.isReady()).to.equal(true) + }) + }) + + describe('onResult', () => { + var createSuccessResult = () => { + return {success: true, suite: [], log: []} + } + + var createFailedResult = () => { + return {success: false, suite: [], log: []} + } + + var createSkippedResult = () => { + return {success: true, skipped: true, suite: [], log: []} + } + + beforeEach(() => { + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + }) + + it('should update lastResults', () => { + browser.state = Browser.STATE_EXECUTING + browser.onResult(createSuccessResult()) + browser.onResult(createSuccessResult()) + browser.onResult(createFailedResult()) + browser.onResult(createSkippedResult()) + + expect(browser.lastResult.success).to.equal(2) + expect(browser.lastResult.failed).to.equal(1) + expect(browser.lastResult.skipped).to.equal(1) + }) + + it('should ignore if not running', () => { + browser.state = Browser.STATE_READY + browser.onResult(createSuccessResult()) + browser.onResult(createSuccessResult()) + browser.onResult(createFailedResult()) + + expect(browser.lastResult.success).to.equal(0) + expect(browser.lastResult.failed).to.equal(0) + }) + + it('should update netTime', () => { + browser.state = Browser.STATE_EXECUTING + browser.onResult({time: 3, suite: [], log: []}) + browser.onResult({time: 1, suite: [], log: []}) + browser.onResult({time: 5, suite: [], log: []}) + + expect(browser.lastResult.netTime).to.equal(9) + }) + + it('should accept array of results', () => { + browser.state = Browser.STATE_EXECUTING + browser.onResult([ + createSuccessResult(), createSuccessResult(), + createFailedResult(), createSkippedResult() + ]) + + expect(browser.lastResult.success).to.equal(2) + expect(browser.lastResult.failed).to.equal(1) + expect(browser.lastResult.skipped).to.equal(1) + }) + }) + + describe('serialize', () => { + it('should return plain object with only name, id, isReady properties', () => { + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + browser.state = Browser.STATE_READY + browser.name = 'Browser 1.0' + browser.id = '12345' + + expect(browser.serialize()).to.deep.equal({id: '12345', name: 'Browser 1.0', isReady: true}) + }) + }) + + describe('execute', () => { + it('should emit execute and change state to EXECUTING', () => { + var spyExecute = sinon.spy() + var config = {} + browser = new Browser('fake-id', 'full name', collection, emitter, socket) + socket.on('execute', spyExecute) + browser.execute(config) + + expect(browser.isReady()).to.equal(false) + expect(spyExecute).to.have.been.calledWith(config) + }) + }) + + describe('scenario:', () => { + it('reconnecting during the run', () => { + var timer = createMockTimer() + browser = new Browser('fake-id', 'full name', collection, emitter, socket, timer, 10) + browser.init() + browser.state = Browser.STATE_EXECUTING + socket.emit('result', {success: true, suite: [], log: []}) + socket.emit('disconnect', 'socket.io reason') + expect(browser.isReady()).to.equal(false) + + var newSocket = mkSocket() + browser.reconnect(newSocket) + expect(browser.isReady()).to.equal(false) + + newSocket.emit('result', {success: false, suite: [], log: []}) + newSocket.emit('complete') + expect(browser.isReady()).to.equal(true) + expect(browser.lastResult.success).to.equal(1) + expect(browser.lastResult.failed).to.equal(1) + }) + + it('disconecting during the run', () => { + var spy = sinon.spy() + emitter.on('browser_complete', spy) + var timer = createMockTimer() + browser = new Browser('fake-id', 'full name', collection, emitter, socket, timer, 10) + browser.init() + browser.state = Browser.STATE_EXECUTING + socket.emit('result', {success: true, suite: [], log: []}) + socket.emit('disconnect', 'socket.io reason') + + timer.wind(10) + expect(browser.lastResult.disconnected).to.equal(true) + expect(spy).to.have.been.calledWith(browser) + }) + + it('restarting a disconnected browser', () => { + var timer = createMockTimer() + browser = new Browser('fake-id', 'Chrome 31.0', collection, emitter, socket, timer, 10) + browser.init() + + browser.execute() + socket.emit('start', {total: 10}) + socket.emit('result', {success: true, suite: [], log: []}) + socket.emit('result', {success: false, suite: [], log: []}) + socket.emit('result', {skipped: true, suite: [], log: []}) + socket.emit('disconnect', 'socket.io reason') + timer.wind(10) // wait-for reconnecting delay + expect(browser.state).to.equal(Browser.STATE_DISCONNECTED) + expect(browser.disconnectsCount).to.equal(1) + + var newSocket = mkSocket() + emitter.on('browser_register', () => browser.execute()) + + // reconnect on a new socket (which triggers re-execution) + browser.reconnect(newSocket) + expect(browser.state).to.equal(Browser.STATE_EXECUTING) + newSocket.emit('start', {total: 11}) + socket.emit('result', {success: true, suite: [], log: []}) + + // expected cleared last result (should not include the results from previous run) + expect(browser.lastResult.total).to.equal(11) + expect(browser.lastResult.success).to.equal(1) + expect(browser.lastResult.failed).to.equal(0) + expect(browser.lastResult.skipped).to.equal(0) + }) + + it('keeping multiple active sockets', () => { + // If there is a new connection (socket) for an already connected browser, + // we need to keep the old socket, in the case that the new socket will disconnect. + browser = new Browser('fake-id', 'Chrome 31.0', collection, emitter, socket, null, 10) + browser.init() + browser.execute() + + // A second connection... + var newSocket = mkSocket() + browser.reconnect(newSocket) + + // Disconnect the second connection... + browser.onDisconnect('socket.io-reason', newSocket) + expect(browser.state).to.equal(Browser.STATE_EXECUTING) + + // It should still be listening on the old socket. + socket.emit('result', {success: true, suite: [], log: []}) + expect(browser.lastResult.success).to.equal(1) + }) + + it('complete only once after reconnect on the same socket', () => { + // If there is a new connection on the same socket, + // we should emit complete message only once. + browser = new Browser('fake-id', 'Chrome 31.0', collection, emitter, socket, null, 10) + browser.onComplete = sinon.spy() + browser.init() + browser.execute() + + // A second connection... + browser.reconnect(socket) + + socket.emit('result', {success: true, suite: [], log: []}) + socket.emit('complete') + + expect(browser.onComplete.callCount).to.equal(1) + }) + + it('disconnect when no message during the run', () => { + var timer = createMockTimer() + browser = new Browser('fake-id', 'Chrome 31.0', collection, emitter, socket, timer, 10, 20) + browser.init() + browser.execute() + + var spyBrowserComplete = sinon.spy() + emitter.on('browser_complete', spyBrowserComplete) + + socket.emit('start', {total: 11}) + socket.emit('result', {success: true, suite: [], log: []}) + + timer.wind(20) + expect(browser.state).to.equal(Browser.STATE_DISCONNECTED) + expect(browser.disconnectsCount).to.equal(1) + expect(spyBrowserComplete).to.have.been.called + }) + }) +}) diff --git a/test/unit/browser_collection.spec.coffee b/test/unit/browser_collection.spec.coffee deleted file mode 100644 index ff2f4be05..000000000 --- a/test/unit/browser_collection.spec.coffee +++ /dev/null @@ -1,310 +0,0 @@ -#============================================================================ -# lib/browser_collection.js module -#============================================================================ -describe 'BrowserCollection', -> - e = require '../../lib/events' - Collection = require '../../lib/browser_collection' - Browser = require '../../lib/browser' - collection = emitter = null - - beforeEach -> - emitter = new e.EventEmitter - collection = new Collection emitter - - #========================================================================== - # Collection.add - #========================================================================== - describe 'add', -> - - it 'should add browser', -> - expect(collection.length).to.equal 0 - collection.add new Browser 'id' - expect(collection.length).to.equal 1 - - - it 'should fire "browsers_change" event', -> - spy = sinon.spy() - emitter.on 'browsers_change', spy - collection.add {} - expect(spy).to.have.been.called - - - #========================================================================== - # Collection.remove - #========================================================================== - describe 'remove', -> - - it 'should remove given browser', -> - browser = new Browser 'id' - collection.add browser - - expect(collection.length).to.equal 1 - expect(collection.remove browser).to.equal true - expect(collection.length).to.equal 0 - - - it 'should fire "browsers_change" event', -> - spy = sinon.spy() - browser = new Browser 'id' - collection.add browser - - emitter.on 'browsers_change', spy - collection.remove browser - expect(spy).to.have.been.called - - - it 'should return false if given browser does not exist within the collection', -> - spy = sinon.spy() - emitter.on 'browsers_change', spy - expect(collection.remove {}).to.equal false - expect(spy).not.to.have.been.called - - - #========================================================================== - # Collection.getById - #========================================================================== - describe 'getById', -> - - it 'should find the browser by id', -> - browser = new Browser 123 - collection.add browser - - expect(collection.getById 123).to.equal browser - - - it 'should return null if no browser with given id', -> - expect(collection.getById 123).to.equal null - - collection.add new Browser 456 - expect(collection.getById 123).to.equal null - - - #========================================================================== - # Collection.setAllToExecuting - #========================================================================== - describe 'setAllToExecuting', -> - browsers = null - - beforeEach -> - browsers = [new Browser, new Browser, new Browser] - browsers.forEach (browser) -> - collection.add browser - - - it 'should set all browsers state to executing', -> - collection.setAllToExecuting() - browsers.forEach (browser) -> - expect(browser.isReady()).to.equal false - expect(browser.state).to.equal Browser.STATE_EXECUTING - - - it 'should fire "browsers_change" event', -> - spy = sinon.spy() - emitter.on 'browsers_change', spy - collection.setAllToExecuting() - expect(spy).to.have.been.called - - - #========================================================================== - # Collection.areAllReady - #========================================================================== - describe 'areAllReady', -> - browsers = null - - beforeEach -> - browsers = [new Browser, new Browser, new Browser] - browsers.forEach (browser) -> - browser.state = Browser.STATE_READY - collection.add browser - - - it 'should return true if all browsers are ready', -> - expect(collection.areAllReady()).to.equal true - - - it 'should return false if at least one browser is not ready', -> - browsers[1].state = Browser.STATE_EXECUTING - expect(collection.areAllReady()).to.equal false - - - it 'should add all non-ready browsers into given array', -> - browsers[0].state = Browser.STATE_EXECUTING - browsers[1].state = Browser.STATE_EXECUTING_DISCONNECTED - nonReady = [] - collection.areAllReady nonReady - expect(nonReady).to.deep.equal [browsers[0], browsers[1]] - - - #========================================================================== - # Collection.serialize - #========================================================================== - describe 'serialize', -> - - it 'should return plain array with serialized browsers', -> - browsers = [new Browser('1'), new Browser('2')] - browsers[0].name = 'B 1.0' - browsers[1].name = 'B 2.0' - collection.add browsers[0] - collection.add browsers[1] - - expect(collection.serialize()).to.deep.equal [{id: '1', name: 'B 1.0', isReady: true}, - {id: '2', name: 'B 2.0', isReady: true}] - - - #========================================================================== - # Collection.getResults - #========================================================================== - describe 'getResults', -> - - it 'should return sum of all browser results', -> - browsers = [new Browser, new Browser] - collection.add browsers[0] - collection.add browsers[1] - browsers[0].lastResult.success = 2 - browsers[0].lastResult.failed = 3 - browsers[1].lastResult.success = 4 - browsers[1].lastResult.failed = 5 - - results = collection.getResults() - expect(results.success).to.equal 6 - expect(results.failed).to.equal 8 - - - it 'should compute disconnected true if any browser got disconnected', -> - browsers = [new Browser, new Browser] - collection.add browsers[0] - collection.add browsers[1] - - results = collection.getResults() - expect(results.disconnected).to.equal false - - browsers[0].lastResult.disconnected = true - results = collection.getResults() - expect(results.disconnected).to.equal true - - browsers[1].lastResult.disconnected = true - results = collection.getResults() - expect(results.disconnected).to.equal true - - browsers[0].lastResult.disconnected = false - results = collection.getResults() - expect(results.disconnected).to.equal true - - - it 'should compute error true if any browser had error', -> - browsers = [new Browser, new Browser] - collection.add browsers[0] - collection.add browsers[1] - - results = collection.getResults() - expect(results.error).to.equal false - - browsers[0].lastResult.error = true - results = collection.getResults() - expect(results.error).to.equal true - - browsers[1].lastResult.error = true - results = collection.getResults() - expect(results.error).to.equal true - - browsers[0].lastResult.error = false - results = collection.getResults() - expect(results.error).to.equal true - - - it 'should compute exitCode', -> - browsers = [new Browser, new Browser] - collection.add browser for browser in browsers - - browsers[0].lastResult.success = 2 - results = collection.getResults() - expect(results.exitCode).to.equal 0 - - browsers[0].lastResult.failed = 2 - results = collection.getResults() - expect(results.exitCode).to.equal 1 - - browsers[0].lastResult.failed = 0 - browsers[1].lastResult.error = true - results = collection.getResults() - expect(results.exitCode).to.equal 1 - - browsers[0].lastResult.disconnected = true - browsers[1].lastResult.error = false - results = collection.getResults() - expect(results.exitCode).to.equal 1 - - browsers[0].lastResult.disconnected = false - results = collection.getResults() - expect(results.exitCode).to.equal 0 - - - #========================================================================== - # Collection.clearResults - #========================================================================== - describe 'clearResults', -> - - it 'should clear all results', -> - # Date.now.returns 112233 - browsers = [new Browser, new Browser] - collection.add browsers[0] - collection.add browsers[1] - browsers[0].lastResult.sucess++ - browsers[0].lastResult.error = true - browsers[1].lastResult.failed++ - browsers[1].lastResult.skipped++ - browsers[1].lastResult.disconnected = true - - collection.clearResults() - browsers.forEach (browser) -> - expect(browser.lastResult.success).to.equal 0 - expect(browser.lastResult.failed).to.equal 0 - expect(browser.lastResult.skipped).to.equal 0 - expect(browser.lastResult.error).to.equal false - expect(browser.lastResult.disconnected).to.equal false - - - #========================================================================== - # Collection.clone - #========================================================================== - describe 'clone', -> - - it 'should create a shallow copy of the collection', -> - browsers = [new Browser, new Browser, new Browser] - collection.add browser for browser in browsers - - clone = collection.clone() - expect(clone.length).to.equal 3 - - clone.remove browsers[0] - expect(clone.length).to.equal 2 - expect(collection.length).to.equal 3 - - - #========================================================================== - # Collection.map - #========================================================================== - describe 'map', -> - - it 'should have map()', -> - browsers = [new Browser(1), new Browser(2), new Browser(3)] - collection.add browser for browser in browsers - - mappedIds = collection.map (browser) -> - browser.id - - expect(mappedIds).to.deep.equal [1, 2, 3] - - - #========================================================================== - # Collection.forEach - #========================================================================== - describe 'forEach', -> - - it 'should have forEach()', -> - browsers = [new Browser(0), new Browser(1), new Browser(2)] - collection.add browser for browser in browsers - - collection.forEach (browser, index) -> - expect(browser.id).to.equal index - diff --git a/test/unit/browser_collection.spec.js b/test/unit/browser_collection.spec.js new file mode 100644 index 000000000..1737e1132 --- /dev/null +++ b/test/unit/browser_collection.spec.js @@ -0,0 +1,285 @@ +describe('BrowserCollection', () => { + var emitter + var e = require('../../lib/events') + var Collection = require('../../lib/browser_collection') + var Browser = require('../../lib/browser') + var collection = emitter = null + + beforeEach(() => { + emitter = new e.EventEmitter() + collection = new Collection(emitter) + }) + + describe('add', () => { + it('should add browser', () => { + expect(collection.length).to.equal(0) + collection.add(new Browser('id')) + expect(collection.length).to.equal(1) + }) + + it('should fire "browsers_change" event', () => { + var spy = sinon.spy() + emitter.on('browsers_change', spy) + collection.add({}) + expect(spy).to.have.been.called + }) + }) + + describe('remove', () => { + it('should remove given browser', () => { + var browser = new Browser('id') + collection.add(browser) + + expect(collection.length).to.equal(1) + expect(collection.remove(browser)).to.equal(true) + expect(collection.length).to.equal(0) + }) + + it('should fire "browsers_change" event', () => { + var spy = sinon.spy() + var browser = new Browser('id') + collection.add(browser) + + emitter.on('browsers_change', spy) + collection.remove(browser) + expect(spy).to.have.been.called + }) + + it('should return false if given browser does not exist within the collection', () => { + var spy = sinon.spy() + emitter.on('browsers_change', spy) + expect(collection.remove({})).to.equal(false) + expect(spy).not.to.have.been.called + }) + }) + + describe('getById', () => { + it('should find the browser by id', () => { + var browser = new Browser(123) + collection.add(browser) + + expect(collection.getById(123)).to.equal(browser) + }) + + it('should return null if no browser with given id', () => { + expect(collection.getById(123)).to.equal(null) + + collection.add(new Browser(456)) + expect(collection.getById(123)).to.equal(null) + }) + }) + + describe('setAllToExecuting', () => { + var browsers = null + + beforeEach(() => { + browsers = [new Browser(), new Browser(), new Browser()] + browsers.forEach(browser => { + collection.add(browser) + }) + }) + + it('should set all browsers state to executing', () => { + collection.setAllToExecuting() + browsers.forEach(browser => { + expect(browser.isReady()).to.equal(false) + expect(browser.state).to.equal(Browser.STATE_EXECUTING) + }) + }) + + it('should fire "browsers_change" event', () => { + var spy = sinon.spy() + emitter.on('browsers_change', spy) + collection.setAllToExecuting() + expect(spy).to.have.been.called + }) + }) + + describe('areAllReady', () => { + var browsers = null + + beforeEach(() => { + browsers = [new Browser(), new Browser(), new Browser()] + browsers.forEach(browser => { + browser.state = Browser.STATE_READY + collection.add(browser) + }) + }) + + it('should return true if all browsers are ready', () => { + expect(collection.areAllReady()).to.equal(true) + }) + + it('should return false if at least one browser is not ready', () => { + browsers[1].state = Browser.STATE_EXECUTING + expect(collection.areAllReady()).to.equal(false) + }) + + it('should add all non-ready browsers into given array', () => { + browsers[0].state = Browser.STATE_EXECUTING + browsers[1].state = Browser.STATE_EXECUTING_DISCONNECTED + var nonReady = [] + collection.areAllReady(nonReady) + expect(nonReady).to.deep.equal([browsers[0], browsers[1]]) + }) + }) + + describe('serialize', () => { + it('should return plain array with serialized browsers', () => { + var browsers = [new Browser('1'), new Browser('2')] + browsers[0].name = 'B 1.0' + browsers[1].name = 'B 2.0' + collection.add(browsers[0]) + collection.add(browsers[1]) + + expect(collection.serialize()).to.deep.equal([ + {id: '1', name: 'B 1.0', isReady: true}, + {id: '2', name: 'B 2.0', isReady: true} + ]) + }) + }) + + describe('getResults', () => { + it('should return sum of all browser results', () => { + var browsers = [new Browser(), new Browser()] + collection.add(browsers[0]) + collection.add(browsers[1]) + browsers[0].lastResult.success = 2 + browsers[0].lastResult.failed = 3 + browsers[1].lastResult.success = 4 + browsers[1].lastResult.failed = 5 + + var results = collection.getResults() + expect(results.success).to.equal(6) + expect(results.failed).to.equal(8) + }) + + it('should compute disconnected true if any browser got disconnected', () => { + var browsers = [new Browser(), new Browser()] + collection.add(browsers[0]) + collection.add(browsers[1]) + + var results = collection.getResults() + expect(results.disconnected).to.equal(false) + + browsers[0].lastResult.disconnected = true + results = collection.getResults() + expect(results.disconnected).to.equal(true) + + browsers[1].lastResult.disconnected = true + results = collection.getResults() + expect(results.disconnected).to.equal(true) + + browsers[0].lastResult.disconnected = false + results = collection.getResults() + expect(results.disconnected).to.equal(true) + }) + + it('should compute error true if any browser had error', () => { + var browsers = [new Browser(), new Browser()] + collection.add(browsers[0]) + collection.add(browsers[1]) + + var results = collection.getResults() + expect(results.error).to.equal(false) + + browsers[0].lastResult.error = true + results = collection.getResults() + expect(results.error).to.equal(true) + + browsers[1].lastResult.error = true + results = collection.getResults() + expect(results.error).to.equal(true) + + browsers[0].lastResult.error = false + results = collection.getResults() + expect(results.error).to.equal(true) + }) + + it('should compute exitCode', () => { + var browsers = [new Browser(), new Browser()] + browsers.forEach(collection.add) + + browsers[0].lastResult.success = 2 + var results = collection.getResults() + expect(results.exitCode).to.equal(0) + + browsers[0].lastResult.failed = 2 + results = collection.getResults() + expect(results.exitCode).to.equal(1) + + browsers[0].lastResult.failed = 0 + browsers[1].lastResult.error = true + results = collection.getResults() + expect(results.exitCode).to.equal(1) + + browsers[0].lastResult.disconnected = true + browsers[1].lastResult.error = false + results = collection.getResults() + expect(results.exitCode).to.equal(1) + + browsers[0].lastResult.disconnected = false + results = collection.getResults() + expect(results.exitCode).to.equal(0) + }) + }) + + describe('clearResults', () => { + it('should clear all results', () => { + // Date.now.returns 112233 + var browsers = [new Browser(), new Browser()] + collection.add(browsers[0]) + collection.add(browsers[1]) + browsers[0].lastResult.sucess++ + browsers[0].lastResult.error = true + browsers[1].lastResult.failed++ + browsers[1].lastResult.skipped++ + browsers[1].lastResult.disconnected = true + + collection.clearResults() + browsers.forEach(browser => { + expect(browser.lastResult.success).to.equal(0) + expect(browser.lastResult.failed).to.equal(0) + expect(browser.lastResult.skipped).to.equal(0) + expect(browser.lastResult.error).to.equal(false) + expect(browser.lastResult.disconnected).to.equal(false) + }) + }) + }) + + describe('clone', () => { + it('should create a shallow copy of the collection', () => { + var browsers = [new Browser(), new Browser(), new Browser()] + browsers.forEach(collection.add) + + var clone = collection.clone() + expect(clone.length).to.equal(3) + + clone.remove(browsers[0]) + expect(clone.length).to.equal(2) + expect(collection.length).to.equal(3) + }) + }) + + describe('map', () => { + it('should have map()', () => { + var browsers = [new Browser(1), new Browser(2), new Browser(3)] + browsers.forEach(collection.add) + + var mappedIds = collection.map(browser => browser.id) + + expect(mappedIds).to.deep.equal([1, 2, 3]) + }) + }) + + describe('forEach', () => { + it('should have forEach()', () => { + var browsers = [new Browser(0), new Browser(1), new Browser(2)] + browsers.forEach(collection.add) + + collection.forEach(function (browser, index) { + expect(browser.id).to.equal(index) + }) + }) + }) +}) diff --git a/test/unit/browser_result.spec.coffee b/test/unit/browser_result.spec.coffee deleted file mode 100644 index c30f4a1ab..000000000 --- a/test/unit/browser_result.spec.coffee +++ /dev/null @@ -1,64 +0,0 @@ -#============================================================================ -# lib/browser_result.js module -#============================================================================ -describe 'BrowserResult', -> - Result = require '../../lib/browser_result' - result = null - - successResultFromBrowser = - success: true - skipped: false - time: 100 - failedResultFromBrowser = - success: false - skipped: false - time: 200 - skippedResultFromBrowser = - success: false - skipped: true - time: 0 - - - beforeEach -> - sinon.stub Date, 'now' - Date.now.returns 123 - result = new Result - - afterEach -> Date.now.restore() - - - it 'should compute totalTime', -> - Date.now.returns 223 - result.totalTimeEnd() - expect(result.totalTime).to.equal 223 - 123 - - - it 'should sum success/failed/skipped', -> - result.add successResultFromBrowser - expect(result.success).to.equal 1 - expect(result.failed).to.equal 0 - expect(result.skipped).to.equal 0 - - result.add failedResultFromBrowser - expect(result.success).to.equal 1 - expect(result.failed).to.equal 1 - expect(result.skipped).to.equal 0 - - result.add successResultFromBrowser - expect(result.success).to.equal 2 - expect(result.failed).to.equal 1 - expect(result.skipped).to.equal 0 - - result.add skippedResultFromBrowser - expect(result.success).to.equal 2 - expect(result.failed).to.equal 1 - expect(result.skipped).to.equal 1 - - - it 'should sum net time of all results', -> - result.add successResultFromBrowser - result.add failedResultFromBrowser - expect(result.netTime).to.equal 300 - - result.add successResultFromBrowser - expect(result.netTime).to.equal 400 diff --git a/test/unit/browser_result.spec.js b/test/unit/browser_result.spec.js new file mode 100644 index 000000000..9700ad113 --- /dev/null +++ b/test/unit/browser_result.spec.js @@ -0,0 +1,69 @@ +describe('BrowserResult', () => { + var Result = require('../../lib/browser_result') + var result = null + + var successResultFromBrowser = { + success: true, + skipped: false, + time: 100 + } + + var failedResultFromBrowser = { + success: false, + skipped: false, + time: 200 + } + + var skippedResultFromBrowser = { + success: false, + skipped: true, + time: 0 + } + + beforeEach(() => { + sinon.stub(Date, 'now') + Date.now.returns(123) + result = new Result() + }) + + afterEach(() => { + Date.now.restore() + }) + + it('should compute totalTime', () => { + Date.now.returns(223) + result.totalTimeEnd() + expect(result.totalTime).to.equal(223 - 123) + }) + + it('should sum success/failed/skipped', () => { + result.add(successResultFromBrowser) + expect(result.success).to.equal(1) + expect(result.failed).to.equal(0) + expect(result.skipped).to.equal(0) + + result.add(failedResultFromBrowser) + expect(result.success).to.equal(1) + expect(result.failed).to.equal(1) + expect(result.skipped).to.equal(0) + + result.add(successResultFromBrowser) + expect(result.success).to.equal(2) + expect(result.failed).to.equal(1) + expect(result.skipped).to.equal(0) + + result.add(skippedResultFromBrowser) + expect(result.success).to.equal(2) + expect(result.failed).to.equal(1) + expect(result.skipped).to.equal(1) + }) + + it('should sum net time of all results', () => { + result.add(successResultFromBrowser) + result.add(failedResultFromBrowser) + expect(result.netTime).to.equal(300) + + result.add(successResultFromBrowser) + expect(result.netTime).to.equal(400) + }) +}) diff --git a/test/unit/cli.spec.coffee b/test/unit/cli.spec.coffee deleted file mode 100644 index dc4413f76..000000000 --- a/test/unit/cli.spec.coffee +++ /dev/null @@ -1,177 +0,0 @@ -#============================================================================== -# lib/cli.js module -#============================================================================== -describe 'cli', -> - cli = require '../../lib/cli' - optimist = require 'optimist' - path = require 'path' - constant = require '../../lib/constants' - path = require 'path' - mocks = require 'mocks' - loadFile = mocks.loadFile - mockery = m = e = null - - fsMock = mocks.fs.create - cwd: - 'karma.conf.js': true - cwd2: - 'karma.conf.coffee': true - - currentCwd = null - - pathMock = - resolve: (p) -> path.resolve currentCwd, p - - setCWD = (cwd) -> - currentCwd = cwd - fsMock._setCWD cwd - - processArgs = (args, opts) -> - argv = optimist.parse(args) - e.processArgs argv, opts || {}, fsMock, pathMock - - beforeEach -> - setCWD '/' - mockery = {} - mockery.process = exit: sinon.spy() - mockery.console = error: sinon.spy() - - # load file under test - m = loadFile __dirname + '/../../lib/cli.js', mockery, { - global: {}, - console: mockery.console, - process: mockery.process, - require: (path) -> - if path.indexOf('./') is 0 - require '../../lib/' + path - else - require path - } - e = m.exports - - describe 'processArgs', -> - - it 'should override if multiple options given', -> - # optimist parses --port 123 --port 456 as port = [123, 456] which makes no sense - options = processArgs ['some.conf', '--port', '12', '--log-level', 'info', - '--port', '34', '--log-level', 'debug'] - - expect(options.port).to.equal 34 - expect(options.logLevel).to.equal 'DEBUG' - - - it 'should return camelCased options', -> - options = processArgs ['some.conf', '--port', '12', '--single-run'] - - expect(options.configFile).to.exist - expect(options.port).to.equal 12 - expect(options.singleRun).to.equal true - - - it 'should parse options without configFile and set default', -> - setCWD '/cwd' - options = processArgs ['--auto-watch', '--auto-watch-interval', '10'] - expect(path.resolve(options.configFile)).to.equal path.resolve('/cwd/karma.conf.js') - expect(options.autoWatch).to.equal true - expect(options.autoWatchInterval).to.equal 10 - - - it 'should set default karma.conf.coffee config file if exists', -> - setCWD '/cwd2' - options = processArgs ['--port', '10'] - - expect(path.resolve(options.configFile)).to.equal path.resolve('/cwd2/karma.conf.coffee') - - - it 'should not set default config if neither exists', -> - setCWD '/' - options = processArgs [] - - expect(options.configFile).to.equal null - - - it 'should parse auto-watch, colors, singleRun to boolean', -> - options = processArgs ['--auto-watch', 'false', '--colors', 'false', '--single-run', 'false'] - - expect(options.autoWatch).to.equal false - expect(options.colors).to.equal false - expect(options.singleRun).to.equal false - - options = processArgs ['--auto-watch', 'true', '--colors', 'true', '--single-run', 'true'] - - expect(options.autoWatch).to.equal true - expect(options.colors).to.equal true - expect(options.singleRun).to.equal true - - - it 'should replace log-level constants', -> - options = processArgs ['--log-level', 'debug'] - expect(options.logLevel).to.equal constant.LOG_DEBUG - - options = processArgs ['--log-level', 'error'] - expect(options.logLevel).to.equal constant.LOG_ERROR - - options = processArgs ['--log-level', 'warn'] - expect(options.logLevel).to.equal constant.LOG_WARN - - options = processArgs ['--log-level', 'foo'] - expect(mockery.process.exit).to.have.been.calledWith 1 - - options = processArgs ['--log-level'] - expect(mockery.process.exit).to.have.been.calledWith 1 - - - it 'should parse browsers into an array', -> - options = processArgs ['--browsers', 'Chrome,ChromeCanary,Firefox'] - expect(options.browsers).to.deep.equal ['Chrome', 'ChromeCanary', 'Firefox'] - - - it 'should resolve configFile to absolute path', -> - setCWD '/cwd' - options = processArgs ['some/config.js'] - expect(path.resolve(options.configFile)).to.equal path.resolve('/cwd/some/config.js') - - - it 'should parse report-slower-than to a number', -> - options = processArgs ['--report-slower-than', '2000'] - expect(options.reportSlowerThan).to.equal 2000 - - options = processArgs ['--no-report-slower-than'] - expect(options.reportSlowerThan).to.equal 0 - - - it 'should cast reporters to array', -> - options = processArgs ['--reporters', 'dots,junit'] - expect(options.reporters).to.deep.equal ['dots', 'junit'] - - options = processArgs ['--reporters', 'dots'] - expect(options.reporters).to.deep.equal ['dots'] - - - it 'should parse removed/added/changed files to array', -> - options = processArgs [ - '--removed-files', 'r1.js,r2.js', - '--changed-files', 'ch1.js,ch2.js', - '--added-files', 'a1.js,a2.js' - ] - - expect(options.removedFiles).to.deep.equal ['r1.js', 'r2.js'] - expect(options.addedFiles).to.deep.equal ['a1.js', 'a2.js'] - expect(options.changedFiles).to.deep.equal ['ch1.js', 'ch2.js'] - - - describe 'parseClientArgs', -> - it 'should return arguments after --', -> - args = cli.parseClientArgs ['node', 'karma.js', 'runArg', '--flag', '--', '--foo', '--bar', - 'baz'] - expect(args).to.deep.equal ['--foo', '--bar', 'baz'] - - it 'should return empty args if -- is not present', -> - args = cli.parseClientArgs ['node', 'karma.js', 'runArg', '--flag', '--foo', '--bar', 'baz'] - expect(args).to.deep.equal [] - - - describe 'argsBeforeDoubleDash', -> - it 'should return array of args that occur before --', -> - args = cli.argsBeforeDoubleDash ['aa', '--bb', 'value', '--', 'some', '--no-more'] - expect(args).to.deep.equal ['aa', '--bb', 'value'] diff --git a/test/unit/cli.spec.js b/test/unit/cli.spec.js new file mode 100644 index 000000000..93dbf23ed --- /dev/null +++ b/test/unit/cli.spec.js @@ -0,0 +1,186 @@ +import cli from '../../lib/cli' +import optimist from 'optimist' +import path from 'path' +import constant from '../../lib/constants' +import mocks from 'mocks' +var loadFile = mocks.loadFile + +describe('cli', () => { + var m + var e + var mockery + + var fsMock = mocks.fs.create({ + cwd: {'karma.conf.js': true}, + cwd2: {'karma.conf.coffee': true} + }) + + var currentCwd = null + + var pathMock = { + resolve (p) { + return path.resolve(currentCwd, p) + } + } + + var setCWD = cwd => { + currentCwd = cwd + fsMock._setCWD(cwd) + } + + var processArgs = (args, opts) => { + var argv = optimist.parse(args) + return e.processArgs(argv, opts || {}, fsMock, pathMock) + } + + beforeEach(() => { + setCWD('/') + mockery = {} + mockery.process = {exit: sinon.spy()} + mockery.console = {error: sinon.spy()} + + // load file under test + m = loadFile(__dirname + '/../../lib/cli.js', mockery, { + global: {}, + console: mockery.console, + process: mockery.process, + require (path) { + if (path.indexOf('./') === 0) { + return require('../../lib/' + path) + } else { + return require(path) + } + } + }) + e = m.exports + }) + + describe('processArgs', () => { + it('should override if multiple options given', () => { + // optimist parses --port 123 --port 456 as port = [123, 456] which makes no sense + var options = processArgs(['some.conf', '--port', '12', '--log-level', 'info', '--port', '34', '--log-level', 'debug']) + + expect(options.port).to.equal(34) + expect(options.logLevel).to.equal('DEBUG') + }) + + it('should return camelCased options', () => { + var options = processArgs(['some.conf', '--port', '12', '--single-run']) + + expect(options.configFile).to.exist + expect(options.port).to.equal(12) + expect(options.singleRun).to.equal(true) + }) + + it('should parse options without configFile and set default', () => { + setCWD('/cwd') + var options = processArgs(['--auto-watch', '--auto-watch-interval', '10']) + expect(path.resolve(options.configFile)).to.equal(path.resolve('/cwd/karma.conf.js')) + expect(options.autoWatch).to.equal(true) + expect(options.autoWatchInterval).to.equal(10) + }) + + it('should set default karma.conf.coffee config file if exists', () => { + setCWD('/cwd2') + var options = processArgs(['--port', '10']) + + expect(path.resolve(options.configFile)).to.equal(path.resolve('/cwd2/karma.conf.coffee')) + }) + + it('should not set default config if neither exists', () => { + setCWD('/') + var options = processArgs([]) + + expect(options.configFile).to.equal(null) + }) + + it('should parse auto-watch, colors, singleRun to boolean', () => { + var options = processArgs(['--auto-watch', 'false', '--colors', 'false', '--single-run', 'false']) + + expect(options.autoWatch).to.equal(false) + expect(options.colors).to.equal(false) + expect(options.singleRun).to.equal(false) + + options = processArgs(['--auto-watch', 'true', '--colors', 'true', '--single-run', 'true']) + + expect(options.autoWatch).to.equal(true) + expect(options.colors).to.equal(true) + expect(options.singleRun).to.equal(true) + }) + + it('should replace log-level constants', () => { + var options = processArgs(['--log-level', 'debug']) + expect(options.logLevel).to.equal(constant.LOG_DEBUG) + + options = processArgs(['--log-level', 'error']) + expect(options.logLevel).to.equal(constant.LOG_ERROR) + + options = processArgs(['--log-level', 'warn']) + expect(options.logLevel).to.equal(constant.LOG_WARN) + + options = processArgs(['--log-level', 'foo']) + expect(mockery.process.exit).to.have.been.calledWith(1) + + options = processArgs(['--log-level']) + expect(mockery.process.exit).to.have.been.calledWith(1) + }) + + it('should parse browsers into an array', () => { + var options = processArgs(['--browsers', 'Chrome,ChromeCanary,Firefox']) + expect(options.browsers).to.deep.equal(['Chrome', 'ChromeCanary', 'Firefox']) + }) + + it('should resolve configFile to absolute path', () => { + setCWD('/cwd') + var options = processArgs(['some/config.js']) + expect(path.resolve(options.configFile)).to.equal(path.resolve('/cwd/some/config.js')) + }) + + it('should parse report-slower-than to a number', () => { + var options = processArgs(['--report-slower-than', '2000']) + expect(options.reportSlowerThan).to.equal(2000) + + options = processArgs(['--no-report-slower-than']) + expect(options.reportSlowerThan).to.equal(0) + }) + + it('should cast reporters to array', () => { + var options = processArgs(['--reporters', 'dots,junit']) + expect(options.reporters).to.deep.equal(['dots', 'junit']) + + options = processArgs(['--reporters', 'dots']) + expect(options.reporters).to.deep.equal(['dots']) + }) + + it('should parse removed/added/changed files to array', () => { + var options = processArgs([ + '--removed-files', 'r1.js,r2.js', + '--changed-files', 'ch1.js,ch2.js', + '--added-files', 'a1.js,a2.js' + ]) + + expect(options.removedFiles).to.deep.equal(['r1.js', 'r2.js']) + expect(options.addedFiles).to.deep.equal(['a1.js', 'a2.js']) + expect(options.changedFiles).to.deep.equal(['ch1.js', 'ch2.js']) + }) + }) + + describe('parseClientArgs', () => { + it('should return arguments after --', () => { + var args = cli.parseClientArgs(['node', 'karma.js', 'runArg', '--flag', '--', '--foo', '--bar', 'baz']) + expect(args).to.deep.equal(['--foo', '--bar', 'baz']) + }) + + it('should return empty args if -- is not present', () => { + var args = cli.parseClientArgs(['node', 'karma.js', 'runArg', '--flag', '--foo', '--bar', 'baz']) + expect(args).to.deep.equal([]) + }) + }) + + describe('argsBeforeDoubleDash', () => { + it('should return array of args that occur before --', () => { + var args = cli.argsBeforeDoubleDash(['aa', '--bb', 'value', '--', 'some', '--no-more']) + expect(args).to.deep.equal(['aa', '--bb', 'value']) + }) + }) +}) diff --git a/test/unit/completion.spec.coffee b/test/unit/completion.spec.coffee deleted file mode 100644 index e5a822bcb..000000000 --- a/test/unit/completion.spec.coffee +++ /dev/null @@ -1,59 +0,0 @@ -#============================================================================== -# lib/completion.js module -#============================================================================== -describe 'completion', -> - c = require '../../lib/completion' - completion = null - - mockEnv = (line) -> - words = line.split ' ' - - words: words - count: words.length - last: words[words.length - 1] - prev: words[words.length - 2] - - beforeEach -> - sinon.stub console, 'log', (msg) -> completion.push msg - completion = [] - - describe 'opositeWord', -> - - it 'should handle --no-x args', -> - expect(c.opositeWord '--no-single-run').to.equal '--single-run' - - - it 'should handle --x args', -> - expect(c.opositeWord '--browsers').to.equal '--no-browsers' - - - it 'should ignore args without --', -> - expect(c.opositeWord 'start').to.equal null - - - describe 'sendCompletion', -> - - it 'should filter only words matching last typed partial', -> - c.sendCompletion ['start', 'init', 'run'], mockEnv 'in' - expect(completion).to.deep.equal ['init'] - - - it 'should filter out already used words/args', -> - c.sendCompletion ['--single-run', '--port', '--xxx'], mockEnv 'start --single-run ' - expect(completion).to.deep.equal ['--port', '--xxx'] - - - it 'should filter out already used oposite words', -> - c.sendCompletion ['--auto-watch', '--port'], mockEnv 'start --no-auto-watch ' - expect(completion).to.deep.equal ['--port'] - - - describe 'complete', -> - - it 'should complete the basic commands', -> - c.complete mockEnv '' - expect(completion).to.deep.equal ['start', 'init', 'run'] - - completion.length = 0 # reset - c.complete mockEnv 's' - expect(completion).to.deep.equal ['start'] diff --git a/test/unit/completion.spec.js b/test/unit/completion.spec.js new file mode 100644 index 000000000..c6c04f70f --- /dev/null +++ b/test/unit/completion.spec.js @@ -0,0 +1,63 @@ +var c = require('../../lib/completion') + +describe('completion', () => { + var completion + + function mockEnv (line) { + var words = line.split(' ') + + return { + words: words, + count: words.length, + last: words[words.length - 1], + prev: words[words.length - 2] + } + } + + beforeEach(() => { + sinon.stub(console, 'log', msg => completion.push(msg)) + completion = [] + }) + + describe('opositeWord', () => { + it('should handle --no-x args', () => { + expect(c.opositeWord('--no-single-run')).to.equal('--single-run') + }) + + it('should handle --x args', () => { + expect(c.opositeWord('--browsers')).to.equal('--no-browsers') + }) + + it('should ignore args without --', () => { + expect(c.opositeWord('start')).to.equal(null) + }) + }) + + describe('sendCompletion', () => { + it('should filter only words matching last typed partial', () => { + c.sendCompletion(['start', 'init', 'run'], mockEnv('in')) + expect(completion).to.deep.equal(['init']) + }) + + it('should filter out already used words/args', () => { + c.sendCompletion(['--single-run', '--port', '--xxx'], mockEnv('start --single-run ')) + expect(completion).to.deep.equal(['--port', '--xxx']) + }) + + it('should filter out already used oposite words', () => { + c.sendCompletion(['--auto-watch', '--port'], mockEnv('start --no-auto-watch ')) + expect(completion).to.deep.equal(['--port']) + }) + }) + + describe('complete', () => { + it('should complete the basic commands', () => { + c.complete(mockEnv('')) + expect(completion).to.deep.equal(['start', 'init', 'run']) + + completion.length = 0 // reset + c.complete(mockEnv('s')) + expect(completion).to.deep.equal(['start']) + }) + }) +}) diff --git a/test/unit/config.spec.coffee b/test/unit/config.spec.coffee deleted file mode 100644 index 182561293..000000000 --- a/test/unit/config.spec.coffee +++ /dev/null @@ -1,374 +0,0 @@ -#============================================================================== -# lib/config.js module -#============================================================================== -describe 'config', -> - loadFile = require('mocks').loadFile - mocks = m = e = null - path = require('path') - helper = require('../../lib/helper') - - resolveWinPath = (p) -> helper.normalizeWinPath(path.resolve(p)) - - normalizeConfigWithDefaults = (cfg) -> - cfg.urlRoot = '' if not cfg.urlRoot - cfg.files = [] if not cfg.files - cfg.exclude = [] if not cfg.exclude - cfg.junitReporter = {} if not cfg.junitReporter - cfg.coverageReporter = {} if not cfg.coverageReporter - cfg.plugins = [] if not cfg.plugins - m.normalizeConfig cfg - - # extract only pattern properties from list of pattern objects - patternsFrom = (list) -> - list.map (pattern) -> pattern.pattern - - wrapCfg = (cfg) -> - return (config) -> - config.set cfg - - beforeEach -> - mocks = {} - mocks.process = exit: sinon.spy() - mockConfigs = { - '/home/config1.js': wrapCfg({basePath: 'base', reporters: ['dots']}), - '/home/config2.js': wrapCfg({basePath: '/abs/base'}), - '/home/config3.js': wrapCfg({files: ['one.js', 'sub/two.js']}), - '/home/config4.js': wrapCfg({port: 123, autoWatch: true, basePath: '/abs/base'}), - '/home/config6.js': wrapCfg({reporters: 'junit'}), - '/home/config7.js': wrapCfg({browsers: ['Chrome', 'Firefox']}), - '/conf/invalid.js': () -> throw new SyntaxError('Unexpected token =') - '/conf/exclude.js': wrapCfg({exclude: ['one.js', 'sub/two.js']}), - '/conf/absolute.js': wrapCfg({files: ['http://some.com', 'https://more.org/file.js']}), - '/conf/both.js': wrapCfg({files: ['one.js', 'two.js'], exclude: ['third.js']}), - '/conf/coffee.coffee': wrapCfg({files: [ 'one.js', 'two.js']}), - } - - # load file under test - m = loadFile __dirname + '/../../lib/config.js', mocks, { - global: {}, - process: mocks.process, - require: (path) -> - if mockConfigs[path] - return mockConfigs[path] - if path.indexOf('./') is 0 - require '../../lib/' + path - else - require path - } - e = m.exports - - - #============================================================================ - # config.parseConfig() - # Should parse configuration file and do some basic processing as well - #============================================================================ - describe 'parseConfig', -> - logSpy = null - - beforeEach -> - logSpy = sinon.spy() - logger = require '../../lib/logger.js' - logger.create('config').on 'log', logSpy - - - it 'should resolve relative basePath to config directory', -> - config = e.parseConfig '/home/config1.js', {} - expect(config.basePath).to.equal resolveWinPath('/home/base') - - - it 'should keep absolute basePath', -> - config = e.parseConfig '/home/config2.js', {} - expect(config.basePath).to.equal resolveWinPath('/abs/base') - - - it 'should resolve all file patterns', -> - config = e.parseConfig '/home/config3.js', {} - actual = [resolveWinPath('/home/one.js'), resolveWinPath('/home/sub/two.js')] - expect(patternsFrom config.files).to.deep.equal actual - - - it 'should keep absolute url file patterns', -> - config = e.parseConfig '/conf/absolute.js', {} - expect(patternsFrom config.files).to.deep.equal [ - 'http://some.com' - 'https://more.org/file.js' - ] - - - it 'should resolve all exclude patterns', -> - config = e.parseConfig '/conf/exclude.js', {} - actual = [ - resolveWinPath('/conf/one.js') - resolveWinPath('/conf/sub/two.js') - resolveWinPath('/conf/exclude.js') - ] - - expect(config.exclude).to.deep.equal actual - - - it 'should log error and exit if file does not exist', -> - e.parseConfig '/conf/not-exist.js', {} - - expect(logSpy).to.have.been.called - event = logSpy.lastCall.args[0] - expect(event.level.toString()).to.be.equal 'ERROR' - expect(event.data).to.be.deep.equal ['File %s does not exist!', '/conf/not-exist.js'] - expect(mocks.process.exit).to.have.been.calledWith 1 - - - it 'should throw and log error if invalid file', -> - e.parseConfig '/conf/invalid.js', {} - - expect(logSpy).to.have.been.called - event = logSpy.lastCall.args[0] - expect(event.level.toString()).to.be.equal 'ERROR' - expect(event.data).to.be.deep.equal ["Error in config file!\n", - new SyntaxError('Unexpected token =')] - expect(mocks.process.exit).to.have.been.calledWith 1 - - - it 'should override config with given cli options', -> - config = e.parseConfig '/home/config4.js', {port: 456, autoWatch: false} - - expect(config.port).to.equal 456 - expect(config.autoWatch).to.equal false - expect(config.basePath).to.equal resolveWinPath('/abs/base') - - - it 'should override config with cli options, but not deep merge', -> - # regression https://github.com/karma-runner/karma/issues/283 - config = e.parseConfig '/home/config7.js', {browsers: ['Safari']} - - expect(config.browsers).to.deep.equal ['Safari'] - - - it 'should resolve files and excludes to overriden basePath from cli', -> - config = e.parseConfig '/conf/both.js', {port: 456, autoWatch: false, basePath: '/xxx'} - - expect(config.basePath).to.equal resolveWinPath('/xxx') - actual = [resolveWinPath('/xxx/one.js'), resolveWinPath('/xxx/two.js')] - expect(patternsFrom config.files).to.deep.equal actual - expect(config.exclude).to.deep.equal [ - resolveWinPath('/xxx/third.js') - resolveWinPath('/conf/both.js') - ] - - - it 'should normalize urlRoot config', -> - config = normalizeConfigWithDefaults {urlRoot: ''} - expect(config.urlRoot).to.equal '/' - - config = normalizeConfigWithDefaults {urlRoot: '/a/b'} - expect(config.urlRoot).to.equal '/a/b/' - - config = normalizeConfigWithDefaults {urlRoot: 'a/'} - expect(config.urlRoot).to.equal '/a/' - - config = normalizeConfigWithDefaults {urlRoot: 'some/thing'} - expect(config.urlRoot).to.equal '/some/thing/' - - - it 'should change autoWatch to false if singleRun', -> - # config4.js has autoWatch = true - config = m.parseConfig '/home/config4.js', {singleRun: true} - expect(config.autoWatch).to.equal false - - - it 'should normalize reporters to an array', -> - config = m.parseConfig '/home/config6.js', {} - expect(config.reporters).to.deep.equal ['junit'] - - - it 'should compile coffeescript config', -> - config = e.parseConfig '/conf/coffee.coffee', {} - expect(patternsFrom config.files).to.deep.equal [ - resolveWinPath('/conf/one.js') - resolveWinPath('/conf/two.js') - ] - - - it 'should set defaults with coffeescript', -> - config = e.parseConfig '/conf/coffee.coffee', {} - expect(config.autoWatch).to.equal true - - - it 'should not read config file, when null', -> - config = e.parseConfig null, {basePath: '/some'} - - expect(logSpy).not.to.have.been.called - expect(config.basePath).to.equal resolveWinPath('/some') # overriden by CLI - expect(config.urlRoot).to.equal '/' # default value - - - it 'should not read config file, when null but still resolve cli basePath', -> - config = e.parseConfig null, {basePath: './some' } - - expect(logSpy).not.to.have.been.called - expect(config.basePath).to.equal resolveWinPath('./some') - expect(config.urlRoot).to.equal '/' # default value - - it 'should default unset options in client config', -> - config = e.parseConfig null, {client: {args: ['--test']}} - - expect(config.client.useIframe).to.not.be.undefined - expect(config.client.captureConsole).to.not.be.undefined - - config = e.parseConfig null, {client: {useIframe: true}} - - expect(config.client.args).to.not.be.undefined - expect(config.client.captureConsole).to.not.be.undefined - - config = e.parseConfig null, {client: {captureConsole: true}} - - expect(config.client.useIframe).to.not.be.undefined - expect(config.client.args).to.not.be.undefined - - - describe 'normalizeConfig', -> - - it 'should convert patterns to objects and set defaults', -> - config = normalizeConfigWithDefaults - basePath: '/base' - files: ['a/*.js', {pattern: 'b.js', watched: false, included: false}, {pattern: 'c.js'}] - - expect(config.files.length).to.equal 3 - - file = config.files.shift() - expect(file.pattern).to.equal resolveWinPath '/base/a/*.js' - expect(file.included).to.equal true - expect(file.served).to.equal true - expect(file.watched).to.equal true - - file = config.files.shift() - expect(file.pattern).to.equal resolveWinPath '/base/b.js' - expect(file.included).to.equal false - expect(file.served).to.equal true - expect(file.watched).to.equal false - - file = config.files.shift() - expect(file.pattern).to.equal resolveWinPath '/base/c.js' - expect(file.included).to.equal true - expect(file.served).to.equal true - expect(file.watched).to.equal true - - - it 'should normalize preprocessors to an array', -> - config = normalizeConfigWithDefaults - basePath: '' - preprocessors: - '/*.coffee': 'coffee' - '/*.html': 'html2js' - - expect(config.preprocessors[resolveWinPath('/*.coffee')]).to.deep.equal ['coffee'] - expect(config.preprocessors[resolveWinPath('/*.html')]).to.deep.equal ['html2js'] - - - it 'should resolve relative preprocessor patterns', -> - config = normalizeConfigWithDefaults - basePath: '/some/base' - preprocessors: - '*.coffee': 'coffee' - '/**/*.html': 'html2js' - - expect(config.preprocessors).to.have.property resolveWinPath('/some/base/*.coffee') - expect(config.preprocessors).not.to.have.property resolveWinPath('*.coffee') - expect(config.preprocessors).to.have.property resolveWinPath('/**/*.html') - - - describe 'createPatternObject', -> - - it 'should parse string and set defaults', -> - pattern = m.createPatternObject 'some/**/*.js' - - expect(typeof pattern).to.equal 'object' - expect(pattern.pattern).to.equal 'some/**/*.js' - expect(pattern.watched).to.equal true - expect(pattern.included).to.equal true - expect(pattern.served).to.equal true - - - it 'should merge pattern object and set defaults', -> - pattern = m.createPatternObject {pattern: 'a.js', included: false, watched: false} - - expect(typeof pattern).to.equal 'object' - expect(pattern.pattern).to.equal 'a.js' - expect(pattern.watched).to.equal false - expect(pattern.included).to.equal false - expect(pattern.served).to.equal true - - - it 'should make urls not served neither watched', -> - pattern = m.createPatternObject 'http://some.url.com' - - expect(pattern.pattern).to.equal 'http://some.url.com' - expect(pattern.included).to.equal true - expect(pattern.watched).to.equal false - expect(pattern.served).to.equal false - - pattern = m.createPatternObject {pattern: 'https://some.other.com'} - - expect(pattern.pattern).to.equal 'https://some.other.com' - expect(pattern.included).to.equal true - expect(pattern.watched).to.equal false - expect(pattern.served).to.equal false - - - describe 'custom', -> - di = require 'di' - - forwardArgsFactory = (args) -> - args - - baseModule = - 'preprocessor:base': ['type', forwardArgsFactory] - 'launcher:base': ['type', forwardArgsFactory] - 'reporter:base': ['type', forwardArgsFactory] - - it 'should define a custom launcher', -> - config = normalizeConfigWithDefaults - customLaunchers: custom: - base: 'base' - first: 123 - whatever: 'aaa' - - injector = new di.Injector([baseModule].concat config.plugins) - injectedArgs = injector.get 'launcher:custom' - - expect(injectedArgs).to.be.defined - expect(injectedArgs.first).to.equal 123 - expect(injectedArgs.whatever).to.equal 'aaa' - - - it 'should define a custom preprocessor', -> - config = normalizeConfigWithDefaults - customPreprocessors: custom: - base: 'base' - second: 123 - whatever: 'bbb' - - injector = new di.Injector([baseModule].concat config.plugins) - injectedArgs = injector.get 'preprocessor:custom' - - expect(injectedArgs).to.be.defined - expect(injectedArgs.second).to.equal 123 - expect(injectedArgs.whatever).to.equal 'bbb' - - - it 'should define a custom reporter', -> - config = normalizeConfigWithDefaults - customReporters: custom: - base: 'base' - third: 123 - whatever: 'ccc' - - injector = new di.Injector([baseModule].concat config.plugins) - injectedArgs = injector.get 'reporter:custom' - - expect(injectedArgs).to.be.defined - expect(injectedArgs.third).to.equal 123 - expect(injectedArgs.whatever).to.equal 'ccc' - - - it 'should not create empty module', -> - config = normalizeConfigWithDefaults {} - expect(config.plugins).to.deep.equal [] diff --git a/test/unit/config.spec.js b/test/unit/config.spec.js new file mode 100644 index 000000000..04fe920e8 --- /dev/null +++ b/test/unit/config.spec.js @@ -0,0 +1,394 @@ +var loadFile = require('mocks').loadFile +import path from 'path' +var helper = require('../../lib/helper') +var logger = require('../../lib/logger.js') + +describe('config', () => { + var m + var e + var mocks + + var resolveWinPath = p => helper.normalizeWinPath(path.resolve(p)) + + var normalizeConfigWithDefaults = cfg => { + if (!cfg.urlRoot) cfg.urlRoot = '' + if (!cfg.files) cfg.files = [] + if (!cfg.exclude) cfg.exclude = [] + if (!cfg.junitReporter) cfg.junitReporter = {} + if (!cfg.coverageReporter) cfg.coverageReporter = {} + if (!cfg.plugins) cfg.plugins = [] + + return m.normalizeConfig(cfg) + } + + // extract only pattern properties from list of pattern objects + var patternsFrom = list => list.map(pattern => pattern.pattern) + + var wrapCfg = function (cfg) { + return config => config.set(cfg) + } + + beforeEach(() => { + mocks = {} + mocks.process = {exit: sinon.spy()} + var mockConfigs = { + '/home/config1.js': wrapCfg({basePath: 'base', reporters: ['dots']}), + '/home/config2.js': wrapCfg({basePath: '/abs/base'}), + '/home/config3.js': wrapCfg({files: ['one.js', 'sub/two.js']}), + '/home/config4.js': wrapCfg({port: 123, autoWatch: true, basePath: '/abs/base'}), + '/home/config6.js': wrapCfg({reporters: 'junit'}), + '/home/config7.js': wrapCfg({browsers: ['Chrome', 'Firefox']}), + '/conf/invalid.js': () => {throw new SyntaxError('Unexpected token =')}, + '/conf/exclude.js': wrapCfg({exclude: ['one.js', 'sub/two.js']}), + '/conf/absolute.js': wrapCfg({files: ['http://some.com', 'https://more.org/file.js']}), + '/conf/both.js': wrapCfg({files: ['one.js', 'two.js'], exclude: ['third.js']}), + '/conf/coffee.coffee': wrapCfg({files: ['one.js', 'two.js']}) + } + + // load file under test + m = loadFile(__dirname + '/../../lib/config.js', mocks, { + global: {}, + process: mocks.process, + require (path) { + if (mockConfigs[path]) { + return mockConfigs[path] + } + if (path.indexOf('./') === 0) { + return require('../../lib/' + path) + } else { + return require(path) + } + } + }) + e = m.exports + }) + + describe('parseConfig', () => { + var logSpy + + beforeEach(() => { + logSpy = sinon.spy() + logger.create('config').on('log', logSpy) + }) + + it('should resolve relative basePath to config directory', () => { + var config = e.parseConfig('/home/config1.js', {}) + expect(config.basePath).to.equal(resolveWinPath('/home/base')) + }) + + it('should keep absolute basePath', () => { + var config = e.parseConfig('/home/config2.js', {}) + expect(config.basePath).to.equal(resolveWinPath('/abs/base')) + }) + + it('should resolve all file patterns', () => { + var config = e.parseConfig('/home/config3.js', {}) + var actual = [resolveWinPath('/home/one.js'), resolveWinPath('/home/sub/two.js')] + expect(patternsFrom(config.files)).to.deep.equal(actual) + }) + + it('should keep absolute url file patterns', () => { + var config = e.parseConfig('/conf/absolute.js', {}) + expect(patternsFrom(config.files)).to.deep.equal([ + 'http://some.com', + 'https://more.org/file.js' + ]) + }) + + it('should resolve all exclude patterns', () => { + var config = e.parseConfig('/conf/exclude.js', {}) + var actual = [ + resolveWinPath('/conf/one.js'), + resolveWinPath('/conf/sub/two.js'), + resolveWinPath('/conf/exclude.js') + ] + + expect(config.exclude).to.deep.equal(actual) + }) + + it('should log error and exit if file does not exist', () => { + e.parseConfig('/conf/not-exist.js', {}) + + expect(logSpy).to.have.been.called + var event = logSpy.lastCall.args[0] + expect(event.level.toString()).to.be.equal('ERROR') + expect(event.data).to.be.deep.equal(['File %s does not exist!', '/conf/not-exist.js']) + expect(mocks.process.exit).to.have.been.calledWith(1) + }) + + it('should throw and log error if invalid file', () => { + e.parseConfig('/conf/invalid.js', {}) + + expect(logSpy).to.have.been.called + var event = logSpy.lastCall.args[0] + expect(event.level.toString()).to.be.equal('ERROR') + expect(event.data).to.be.deep.equal([ + 'Error in config file!\n', + new SyntaxError('Unexpected token =') + ]) + expect(mocks.process.exit).to.have.been.calledWith(1) + }) + + it('should override config with given cli options', () => { + var config = e.parseConfig('/home/config4.js', {port: 456, autoWatch: false}) + + expect(config.port).to.equal(456) + expect(config.autoWatch).to.equal(false) + expect(config.basePath).to.equal(resolveWinPath('/abs/base')) + }) + + it('should override config with cli options, but not deep merge', () => { + // regression https://github.com/karma-runner/karma/issues/283 + var config = e.parseConfig('/home/config7.js', {browsers: ['Safari']}) + + expect(config.browsers).to.deep.equal(['Safari']) + }) + + it('should resolve files and excludes to overriden basePath from cli', () => { + var config = e.parseConfig('/conf/both.js', {port: 456, autoWatch: false, basePath: '/xxx'}) + + expect(config.basePath).to.equal(resolveWinPath('/xxx')) + var actual = [resolveWinPath('/xxx/one.js'), resolveWinPath('/xxx/two.js')] + expect(patternsFrom(config.files)).to.deep.equal(actual) + expect(config.exclude).to.deep.equal([ + resolveWinPath('/xxx/third.js'), + resolveWinPath('/conf/both.js') + ]) + }) + + it('should normalize urlRoot config', () => { + var config = normalizeConfigWithDefaults({urlRoot: ''}) + expect(config.urlRoot).to.equal('/') + + config = normalizeConfigWithDefaults({urlRoot: '/a/b'}) + expect(config.urlRoot).to.equal('/a/b/') + + config = normalizeConfigWithDefaults({urlRoot: 'a/'}) + expect(config.urlRoot).to.equal('/a/') + + config = normalizeConfigWithDefaults({urlRoot: 'some/thing'}) + expect(config.urlRoot).to.equal('/some/thing/') + }) + + it('should change autoWatch to false if singleRun', () => { + // config4.js has autoWatch = true + var config = m.parseConfig('/home/config4.js', {singleRun: true}) + expect(config.autoWatch).to.equal(false) + }) + + it('should normalize reporters to an array', () => { + var config = m.parseConfig('/home/config6.js', {}) + expect(config.reporters).to.deep.equal(['junit']) + }) + + it('should compile coffeescript config', () => { + var config = e.parseConfig('/conf/coffee.coffee', {}) + expect(patternsFrom(config.files)).to.deep.equal([ + resolveWinPath('/conf/one.js'), + resolveWinPath('/conf/two.js') + ]) + }) + + it('should set defaults with coffeescript', () => { + var config = e.parseConfig('/conf/coffee.coffee', {}) + expect(config.autoWatch).to.equal(true) + }) + + it('should not read config file, when null', () => { + var config = e.parseConfig(null, {basePath: '/some'}) + + expect(logSpy).not.to.have.been.called + expect(config.basePath).to.equal(resolveWinPath('/some')) // overriden by CLI + expect(config.urlRoot).to.equal('/') + }) // default value + + it('should not read config file, when null but still resolve cli basePath', () => { + var config = e.parseConfig(null, {basePath: './some'}) + + expect(logSpy).not.to.have.been.called + expect(config.basePath).to.equal(resolveWinPath('./some')) + expect(config.urlRoot).to.equal('/') + }) // default value + + it('should default unset options in client config', () => { + var config = e.parseConfig(null, {client: {args: ['--test']}}) + + expect(config.client.useIframe).to.not.be.undefined + expect(config.client.captureConsole).to.not.be.undefined + + config = e.parseConfig(null, {client: {useIframe: true}}) + + expect(config.client.args).to.not.be.undefined + expect(config.client.captureConsole).to.not.be.undefined + + config = e.parseConfig(null, {client: {captureConsole: true}}) + + expect(config.client.useIframe).to.not.be.undefined + expect(config.client.args).to.not.be.undefined + }) + }) + + describe('normalizeConfig', () => { + it('should convert patterns to objects and set defaults', () => { + var config = normalizeConfigWithDefaults({ + basePath: '/base', + files: ['a/*.js', {pattern: 'b.js', watched: false, included: false}, {pattern: 'c.js'}] + }) + + expect(config.files.length).to.equal(3) + + var file = config.files.shift() + expect(file.pattern).to.equal(resolveWinPath('/base/a/*.js')) + expect(file.included).to.equal(true) + expect(file.served).to.equal(true) + expect(file.watched).to.equal(true) + + file = config.files.shift() + expect(file.pattern).to.equal(resolveWinPath('/base/b.js')) + expect(file.included).to.equal(false) + expect(file.served).to.equal(true) + expect(file.watched).to.equal(false) + + file = config.files.shift() + expect(file.pattern).to.equal(resolveWinPath('/base/c.js')) + expect(file.included).to.equal(true) + expect(file.served).to.equal(true) + expect(file.watched).to.equal(true) + }) + + it('should normalize preprocessors to an array', () => { + var config = normalizeConfigWithDefaults({ + basePath: '', + preprocessors: {'/*.coffee': 'coffee', + '/*.html': 'html2js'} + }) + + expect(config.preprocessors[resolveWinPath('/*.coffee')]).to.deep.equal(['coffee']) + expect(config.preprocessors[resolveWinPath('/*.html')]).to.deep.equal(['html2js']) + }) + + it('should resolve relative preprocessor patterns', () => { + var config = normalizeConfigWithDefaults({ + basePath: '/some/base', + preprocessors: {'*.coffee': 'coffee', + '/**/*.html': 'html2js'} + }) + + expect(config.preprocessors).to.have.property(resolveWinPath('/some/base/*.coffee')) + expect(config.preprocessors).not.to.have.property(resolveWinPath('*.coffee')) + expect(config.preprocessors).to.have.property(resolveWinPath('/**/*.html')) + }) + }) + + describe('createPatternObject', () => { + it('should parse string and set defaults', () => { + var pattern = m.createPatternObject('some/**/*.js') + + expect(typeof pattern).to.equal('object') + expect(pattern.pattern).to.equal('some/**/*.js') + expect(pattern.watched).to.equal(true) + expect(pattern.included).to.equal(true) + expect(pattern.served).to.equal(true) + }) + + it('should merge pattern object and set defaults', () => { + var pattern = m.createPatternObject({pattern: 'a.js', included: false, watched: false}) + + expect(typeof pattern).to.equal('object') + expect(pattern.pattern).to.equal('a.js') + expect(pattern.watched).to.equal(false) + expect(pattern.included).to.equal(false) + expect(pattern.served).to.equal(true) + }) + + it('should make urls not served neither watched', () => { + var pattern = m.createPatternObject('http://some.url.com') + + expect(pattern.pattern).to.equal('http://some.url.com') + expect(pattern.included).to.equal(true) + expect(pattern.watched).to.equal(false) + expect(pattern.served).to.equal(false) + + pattern = m.createPatternObject({pattern: 'https://some.other.com'}) + + expect(pattern.pattern).to.equal('https://some.other.com') + expect(pattern.included).to.equal(true) + expect(pattern.watched).to.equal(false) + expect(pattern.served).to.equal(false) + }) + }) + + describe('custom', () => { + var di = require('di') + + var forwardArgsFactory = args => args + + var baseModule = { + 'preprocessor:base': ['type', forwardArgsFactory], + 'launcher:base': ['type', forwardArgsFactory], + 'reporter:base': ['type', forwardArgsFactory] + } + + it('should define a custom launcher', () => { + var config = normalizeConfigWithDefaults({ + customLaunchers: { + custom: { + base: 'base', + first: 123, + whatever: 'aaa' + } + } + }) + + var injector = new di.Injector([baseModule].concat(config.plugins)) + var injectedArgs = injector.get('launcher:custom') + + expect(injectedArgs).to.be.defined + expect(injectedArgs.first).to.equal(123) + expect(injectedArgs.whatever).to.equal('aaa') + }) + + it('should define a custom preprocessor', () => { + var config = normalizeConfigWithDefaults({ + customPreprocessors: { + custom: { + base: 'base', + second: 123, + whatever: 'bbb' + } + } + }) + + var injector = new di.Injector([baseModule].concat(config.plugins)) + var injectedArgs = injector.get('preprocessor:custom') + + expect(injectedArgs).to.be.defined + expect(injectedArgs.second).to.equal(123) + expect(injectedArgs.whatever).to.equal('bbb') + }) + + it('should define a custom reporter', () => { + var config = normalizeConfigWithDefaults({ + customReporters: { + custom: { + base: 'base', + third: 123, + whatever: 'ccc' + } + } + }) + + var injector = new di.Injector([baseModule].concat(config.plugins)) + var injectedArgs = injector.get('reporter:custom') + + expect(injectedArgs).to.be.defined + expect(injectedArgs.third).to.equal(123) + expect(injectedArgs.whatever).to.equal('ccc') + }) + + it('should not create empty module', () => { + var config = normalizeConfigWithDefaults({}) + expect(config.plugins).to.deep.equal([]) + }) + }) +}) diff --git a/test/unit/emitter_wrapper.spec.coffee b/test/unit/emitter_wrapper.spec.coffee deleted file mode 100644 index fa9ae9579..000000000 --- a/test/unit/emitter_wrapper.spec.coffee +++ /dev/null @@ -1,57 +0,0 @@ -#============================================================================== -# lib/emitter_wrapper.js module -#============================================================================== -describe 'emitter_wrapper', -> - EmitterWrapper = require '../../lib/emitter_wrapper' - events = require 'events' - EventEmitter = events.EventEmitter - - emitter = null - wrapped = null - called = false - - beforeEach -> - emitter = new EventEmitter() - emitter.aMethod = (e) -> called = true - emitter.on 'anEvent', emitter.aMethod - wrapped = new EmitterWrapper(emitter) - - #=========================================================================== - # wrapper.addListener - #=========================================================================== - describe 'addListener', -> - aListener = (e) -> true - - it 'should add a listener to the wrapped emitter', -> - wrapped.addListener 'anEvent', aListener - expect(emitter.listeners('anEvent')).to.contain aListener - - it 'returns the wrapped emitter', -> - expect(wrapped.addListener 'anEvent', aListener).to.equal wrapped - - #=========================================================================== - # wrapper.removeAllListeners - #=========================================================================== - describe 'removeAllListeners', -> - aListener = (e) -> true - - beforeEach -> - wrapped.addListener 'anEvent', aListener - - it 'should remove listeners that were attached via the wrapper', -> - wrapped.removeAllListeners() - expect(emitter.listeners('anEvent')).not.to.contain aListener - - it 'should not remove listeners that were attached to the original emitter', -> - wrapped.removeAllListeners() - expect(emitter.listeners('anEvent')).to.contain emitter.aMethod - - it 'should remove only matching listeners when called with an event name', -> - anotherListener = (e) -> true - wrapped.addListener 'anotherEvent', anotherListener - wrapped.removeAllListeners('anEvent') - expect(emitter.listeners('anEvent')).not.to.contain aListener - expect(emitter.listeners('anotherEvent')).to.contain anotherListener - - it 'returns the wrapped emitter', -> - expect(wrapped.addListener 'anEvent', aListener).to.equal wrapped diff --git a/test/unit/emitter_wrapper.spec.js b/test/unit/emitter_wrapper.spec.js new file mode 100644 index 000000000..b5caddbf9 --- /dev/null +++ b/test/unit/emitter_wrapper.spec.js @@ -0,0 +1,57 @@ +import EmitterWrapper from '../../lib/emitter_wrapper' +import {EventEmitter} from 'events' + +describe('emitter_wrapper', () => { + var emitter + var wrapped + + beforeEach(() => { + emitter = new EventEmitter() + emitter.aMethod = e => true + emitter.on('anEvent', emitter.aMethod) + wrapped = new EmitterWrapper(emitter) + }) + + describe('addListener', () => { + var aListener = e => true + + it('should add a listener to the wrapped emitter', () => { + wrapped.addListener('anEvent', aListener) + expect(emitter.listeners('anEvent')).to.contain(aListener) + }) + + it('returns the wrapped emitter', () => { + expect(wrapped.addListener('anEvent', aListener)).to.equal(wrapped) + }) + }) + + describe('removeAllListeners', () => { + var aListener = e => true + + beforeEach(() => { + wrapped.addListener('anEvent', aListener) + }) + + it('should remove listeners that were attached via the wrapper', () => { + wrapped.removeAllListeners() + expect(emitter.listeners('anEvent')).not.to.contain(aListener) + }) + + it('should not remove listeners that were attached to the original emitter', () => { + wrapped.removeAllListeners() + expect(emitter.listeners('anEvent')).to.contain(emitter.aMethod) + }) + + it('should remove only matching listeners when called with an event name', () => { + var anotherListener = e => true + wrapped.addListener('anotherEvent', anotherListener) + wrapped.removeAllListeners('anEvent') + expect(emitter.listeners('anEvent')).not.to.contain(aListener) + expect(emitter.listeners('anotherEvent')).to.contain(anotherListener) + }) + + it('returns the wrapped emitter', () => { + expect(wrapped.addListener('anEvent', aListener)).to.equal(wrapped) + }) + }) +}) diff --git a/test/unit/events.spec.coffee b/test/unit/events.spec.coffee deleted file mode 100644 index d50005b7f..000000000 --- a/test/unit/events.spec.coffee +++ /dev/null @@ -1,180 +0,0 @@ -#============================================================================== -# lib/events.js module -#============================================================================== -describe 'events', -> - e = require '../../lib/events' - emitter = null - - beforeEach -> - emitter = new e.EventEmitter - - #============================================================================ - # events.EventEmitter - #============================================================================ - describe 'EventEmitter', -> - - it 'should emit events', -> - spy = sinon.spy() - - emitter.on 'abc', spy - emitter.emit 'abc' - expect(spy).to.have.been.called - - - #========================================================================== - # events.EventEmitter.bind() - #========================================================================== - describe 'bind', -> - object = null - - beforeEach -> - object = sinon.stub - onFoo: -> - onFooBar: -> - foo: -> - bar: -> - emitter.bind object - - - it 'should register all "on" methods to events', -> - emitter.emit 'foo' - expect(object.onFoo).to.have.been.called - - emitter.emit 'foo_bar' - expect(object.onFooBar).to.have.been.called - - expect(object.foo).not.to.have.been.called - expect(object.bar).not.to.have.been.called - - - it 'should bind methods to the owner object', -> - emitter.emit 'foo' - emitter.emit 'foo_bar' - - expect(object.onFoo).to.have.always.been.calledOn object - expect(object.onFooBar).to.have.always.been.calledOn object - expect(object.foo).not.to.have.been.called - expect(object.bar).not.to.have.been.called - - - #========================================================================== - # events.EventEmitter.emitAsync() - #========================================================================== - describe 'emitAsync', -> - object = null - - beforeEach -> - object = sinon.stub - onFoo: -> - onFooBar: -> - foo: -> - bar: -> - emitter.bind object - - - it 'should resolve the promise once all listeners are done', (done) -> - callbacks = [] - eventDone = sinon.spy() - - emitter.on 'a', (d) -> d() - emitter.on 'a', (d) -> callbacks.push d - emitter.on 'a', (d) -> callbacks.push d - - promise = emitter.emitAsync('a') - - expect(eventDone).not.to.have.been.called - callbacks.pop()() - expect(eventDone).not.to.have.been.called - callbacks.pop()() - - promise.then -> - eventDone() - expect(eventDone).to.have.been.called - done() - - - it 'should resolve asynchronously when no listener', (done) -> - spyDone = sinon.spy done - emitter.emitAsync('whatever').then spyDone - expect(spyDone).to.not.have.been.called - - - #============================================================================ - # events.bindAll - #============================================================================ - describe 'bindAll', -> - - it 'should take emitter as second argument', -> - object = sinon.stub onFoo: -> - - e.bindAll object, emitter - emitter.emit 'foo' - emitter.emit 'bar' - - expect(object.onFoo).to.have.been.called - - - it 'should append "context" to event arguments', -> - object = sinon.stub onFoo: -> - - e.bindAll object, emitter - emitter.emit 'foo', 'event-argument' - - expect(object.onFoo).to.have.been.calledWith 'event-argument', emitter - - - #============================================================================ - # events.bufferEvents - #============================================================================ - describe 'bufferEvents', -> - - it 'should reply all events', -> - spy = sinon.spy() - replyEvents = e.bufferEvents emitter, ['foo', 'bar'] - - emitter.emit 'foo', 'foo-1' - emitter.emit 'bar', 'bar-2' - emitter.emit 'foo', 'foo-3' - - emitter.on 'foo', spy - emitter.on 'bar', spy - - replyEvents() - expect(spy).to.have.been.calledThrice - expect(spy.firstCall).to.have.been.calledWith 'foo-1' - expect(spy.secondCall).to.have.been.calledWith 'bar-2' - expect(spy.thirdCall).to.have.been.calledWith 'foo-3' - - - it 'should not buffer after reply()', -> - spy = sinon.spy() - replyEvents = e.bufferEvents emitter, ['foo', 'bar'] - replyEvents() - - emitter.emit 'foo', 'foo-1' - emitter.emit 'bar', 'bar-2' - emitter.emit 'foo', 'foo-3' - - emitter.on 'foo', spy - emitter.on 'bar', spy - - replyEvents() - expect(spy).to.not.have.been.caleed - - - it 'should work with overriden "emit" method', -> - # This is to make sure it works with socket.io sockets, - # which overrides the emit() method to send the event through the wire, - # instead of local emit. - originalEmit = emitter.emit - emitter.emit = -> null - - spy = sinon.spy() - replyEvents = e.bufferEvents emitter, ['foo'] - - originalEmit.apply emitter, ['foo', 'whatever'] - - emitter.on 'foo', spy - - replyEvents() - expect(spy).to.have.been.calledWith 'whatever' diff --git a/test/unit/events.spec.js b/test/unit/events.spec.js new file mode 100644 index 000000000..6c254bdd0 --- /dev/null +++ b/test/unit/events.spec.js @@ -0,0 +1,171 @@ +var e = require('../../lib/events') + +describe('events', () => { + var emitter + + beforeEach(() => { + emitter = new e.EventEmitter() + }) + + describe('EventEmitter', () => { + it('should emit events', () => { + var spy = sinon.spy() + + emitter.on('abc', spy) + emitter.emit('abc') + expect(spy).to.have.been.called + }) + + describe('bind', () => { + var object = null + + beforeEach(() => { + object = sinon.stub({ + onFoo: () => {}, + onFooBar: () => {}, + foo: () => {}, + bar: () => {} + }) + emitter.bind(object) + }) + + it('should register all "on" methods to events', () => { + emitter.emit('foo') + expect(object.onFoo).to.have.been.called + + emitter.emit('foo_bar') + expect(object.onFooBar).to.have.been.called + + expect(object.foo).not.to.have.been.called + expect(object.bar).not.to.have.been.called + }) + + it('should bind methods to the owner object', () => { + emitter.emit('foo') + emitter.emit('foo_bar') + + expect(object.onFoo).to.have.always.been.calledOn(object) + expect(object.onFooBar).to.have.always.been.calledOn(object) + expect(object.foo).not.to.have.been.called + expect(object.bar).not.to.have.been.called + }) + }) + + describe('emitAsync', () => { + var object = null + + beforeEach(() => { + object = sinon.stub({ + onFoo: () => {}, + onFooBar: () => {}, + foo: () => {}, + bar: () => {} + }) + emitter.bind(object) + }) + + it('should resolve the promise once all listeners are done', done => { + var callbacks = [] + var eventDone = sinon.spy() + + emitter.on('a', d => d()) + emitter.on('a', d => callbacks.push(d)) + emitter.on('a', d => callbacks.push(d)) + + var promise = emitter.emitAsync('a') + + expect(eventDone).not.to.have.been.called + callbacks.pop()() + expect(eventDone).not.to.have.been.called + callbacks.pop()() + + promise.then(() => { + eventDone() + expect(eventDone).to.have.been.called + done() + }) + }) + + it('should resolve asynchronously when no listener', done => { + var spyDone = sinon.spy(done) + emitter.emitAsync('whatever').then(spyDone) + expect(spyDone).to.not.have.been.called + }) + }) + }) + + describe('bindAll', () => { + it('should take emitter as second argument', () => { + var object = sinon.stub({onFoo: () => {}}) + + e.bindAll(object, emitter) + emitter.emit('foo') + emitter.emit('bar') + + expect(object.onFoo).to.have.been.called + }) + + it('should append "context" to event arguments', () => { + var object = sinon.stub({onFoo: () => {}}) + + e.bindAll(object, emitter) + emitter.emit('foo', 'event-argument') + + expect(object.onFoo).to.have.been.calledWith('event-argument', emitter) + }) + }) + + describe('bufferEvents', () => { + it('should reply all events', () => { + var spy = sinon.spy() + var replyEvents = e.bufferEvents(emitter, ['foo', 'bar']) + + emitter.emit('foo', 'foo-1') + emitter.emit('bar', 'bar-2') + emitter.emit('foo', 'foo-3') + + emitter.on('foo', spy) + emitter.on('bar', spy) + + replyEvents() + expect(spy).to.have.been.calledThrice + expect(spy.firstCall).to.have.been.calledWith('foo-1') + expect(spy.secondCall).to.have.been.calledWith('bar-2') + expect(spy.thirdCall).to.have.been.calledWith('foo-3') + }) + + it('should not buffer after reply()', () => { + var spy = sinon.spy() + var replyEvents = e.bufferEvents(emitter, ['foo', 'bar']) + replyEvents() + + emitter.emit('foo', 'foo-1') + emitter.emit('bar', 'bar-2') + emitter.emit('foo', 'foo-3') + + emitter.on('foo', spy) + emitter.on('bar', spy) + + replyEvents() + expect(spy).to.not.have.been.caleed + }) + + it('should work with overriden "emit" method', () => { + // This is to make sure it works with socket.io sockets, + // which overrides the emit() method to send the event through the wire, + // instead of local emit. + var originalEmit = emitter.emit + emitter.emit = () => null + + var spy = sinon.spy() + var replyEvents = e.bufferEvents(emitter, ['foo']) + + originalEmit.apply(emitter, ['foo', 'whatever']) + + emitter.on('foo', spy) + + replyEvents() + expect(spy).to.have.been.calledWith('whatever') + }) + }) +}) diff --git a/test/unit/executor.spec.coffee b/test/unit/executor.spec.coffee deleted file mode 100644 index 564d244ad..000000000 --- a/test/unit/executor.spec.coffee +++ /dev/null @@ -1,47 +0,0 @@ -describe 'executor', -> - Browser = require '../../lib/browser' - BrowserCollection = require '../../lib/browser_collection' - EventEmitter = require('../../lib/events').EventEmitter - Executor = require '../../lib/executor' - - executor = emitter = capturedBrowsers = config = spy = null - - beforeEach -> - config = {client: {}} - emitter = new EventEmitter - capturedBrowsers = new BrowserCollection emitter - capturedBrowsers.add new Browser - executor = new Executor capturedBrowsers, config, emitter - executor.socketIoSockets = new EventEmitter - - spy = - onRunStart: -> null - onSocketsExecute: -> null - - sinon.spy spy, 'onRunStart' - sinon.spy spy, 'onSocketsExecute' - - emitter.on 'run_start', spy.onRunStart - executor.socketIoSockets.on 'execute', spy.onSocketsExecute - - - it 'should start the run and pass client config', -> - capturedBrowsers.areAllReady = -> true - - executor.schedule() - expect(spy.onRunStart).to.have.been.called - expect(spy.onSocketsExecute).to.have.been.calledWith config.client - - - it 'should wait for all browsers to finish', -> - capturedBrowsers.areAllReady = -> false - - # they are not ready yet - executor.schedule() - expect(spy.onRunStart).not.to.have.been.called - expect(spy.onSocketsExecute).not.to.have.been.called - - capturedBrowsers.areAllReady = -> true - emitter.emit 'run_complete' - expect(spy.onRunStart).to.have.been.called - expect(spy.onSocketsExecute).to.have.been.called diff --git a/test/unit/executor.spec.js b/test/unit/executor.spec.js new file mode 100644 index 000000000..21279aa18 --- /dev/null +++ b/test/unit/executor.spec.js @@ -0,0 +1,54 @@ +var Browser = require('../../lib/browser') +var BrowserCollection = require('../../lib/browser_collection') +var EventEmitter = require('../../lib/events').EventEmitter +var Executor = require('../../lib/executor') + +describe('executor', () => { + var emitter + var capturedBrowsers + var config + var spy + var executor + + beforeEach(() => { + config = {client: {}} + emitter = new EventEmitter() + capturedBrowsers = new BrowserCollection(emitter) + capturedBrowsers.add(new Browser()) + executor = new Executor(capturedBrowsers, config, emitter) + executor.socketIoSockets = new EventEmitter() + + spy = { + onRunStart: () => null, + onSocketsExecute: () => null + } + + sinon.spy(spy, 'onRunStart') + sinon.spy(spy, 'onSocketsExecute') + + emitter.on('run_start', spy.onRunStart) + executor.socketIoSockets.on('execute', spy.onSocketsExecute) + }) + + it('should start the run and pass client config', () => { + capturedBrowsers.areAllReady = () => true + + executor.schedule() + expect(spy.onRunStart).to.have.been.called + expect(spy.onSocketsExecute).to.have.been.calledWith(config.client) + }) + + it('should wait for all browsers to finish', () => { + capturedBrowsers.areAllReady = () => false + + // they are not ready yet + executor.schedule() + expect(spy.onRunStart).not.to.have.been.called + expect(spy.onSocketsExecute).not.to.have.been.called + + capturedBrowsers.areAllReady = () => true + emitter.emit('run_complete') + expect(spy.onRunStart).to.have.been.called + expect(spy.onSocketsExecute).to.have.been.called + }) +}) diff --git a/test/unit/file-list.spec.coffee b/test/unit/file-list.spec.coffee deleted file mode 100644 index 24322cf12..000000000 --- a/test/unit/file-list.spec.coffee +++ /dev/null @@ -1,639 +0,0 @@ -Promise = require 'bluebird' -EventEmitter = require('events').EventEmitter -mocks = require 'mocks' -proxyquire = require 'proxyquire' -helper = require '../../lib/helper' -_ = helper._ - -from = require 'core-js/library/fn/array/from' -config = require '../../lib/config' - -# create an array of pattern objects from given strings -patterns = (strings...) -> - new config.Pattern(str) for str in strings - -pathsFrom = (files) -> - _.pluck(from(files), 'path') - -findFile = (path, files) -> - from(files).find (file) -> file.path is path - -PATTERN_LIST = - '/some/*.js': ['/some/a.js', '/some/b.js'] - '*.txt': ['/c.txt', '/a.txt', '/b.txt'] - '/a.*': ['/a.txt'] - -MG = - statCache: - '/some/a.js': {mtime: new Date()} - '/some/b.js': {mtime: new Date()} - '/a.txt': {mtime: new Date()} - '/b.txt': {mtime: new Date()} - '/c.txt': {mtime: new Date()} - -mockFs = mocks.fs.create - some: - '0.js': mocks.fs.file '2012-04-04' - 'a.js': mocks.fs.file '2012-04-04' - 'b.js': mocks.fs.file '2012-05-05' - 'd.js': mocks.fs.file '2012-05-05' - folder: - 'x.js': mocks.fs.file 0 - 'a.txt': mocks.fs.file 0 - 'b.txt': mocks.fs.file 0 - 'c.txt': mocks.fs.file 0 - 'a.js': mocks.fs.file '2012-01-01' - -describe 'FileList', -> - List = list = emitter = preprocess = patternList = mg = modified = glob = null - - beforeEach -> - - describe 'files', -> - beforeEach -> - patternList = PATTERN_LIST - mg = MG - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - it 'returns a flat array of served files', -> - list = new List( - patterns('/some/*.js'), - [], - emitter, - preprocess - ) - - list.refresh().then -> - expect(list.files.served).to.have.length 2 - - it 'returns a unique set', -> - list = new List( - patterns('/a.*', '*.txt'), - [], - emitter, - preprocess - ) - - list.refresh().then -> - expect(list.files.served).to.have.length 3 - expect(pathsFrom list.files.served).to.contain '/a.txt', '/b.txt', '/c.txt' - - it 'returns only served files', -> - files = [ - new config.Pattern('/a.*', true) # served: true - new config.Pattern('/some/*.js', false) # served: false - ] - - list = new List(files, [], emitter, preprocess) - - list.refresh().then -> - expect(pathsFrom list.files.served).to.eql ['/a.txt'] - - it 'marks no cache files', -> - files = [ - new config.Pattern('/a.*') # nocach: false - new config.Pattern('/some/*.js', true, true, true, true) # nocache: true - ] - - list = new List(files, [], emitter, preprocess) - - list.refresh().then -> - expect(pathsFrom list.files.served).to.deep.equal [ - '/a.txt', - '/some/a.js', - '/some/b.js' - ] - expect(preprocess).to.have.been.calledOnce - expect(list.files.served[0].doNotCache).to.be.false - expect(list.files.served[1].doNotCache).to.be.true - expect(list.files.served[2].doNotCache).to.be.true - - it 'returns a flat array of included files', -> - files = [ - new config.Pattern('/a.*', true, false) # included: false - new config.Pattern('/some/*.js') # included: true - ] - - list = new List(files, [], emitter, preprocess) - - list.refresh().then -> - expect(pathsFrom list.files.included).not.to.contain '/a.txt' - expect(pathsFrom list.files.included).to.deep.equal [ - '/some/a.js' - '/some/b.js' - ] - - - - describe '_isExcluded', -> - beforeEach -> - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - it 'returns undefined when no match is found', -> - list = new List([], ['hello.js', 'world.js'], emitter, preprocess) - expect(list._isExcluded('hello.txt')).to.be.undefined - expect(list._isExcluded('/hello/world/i.js')).to.be.undefined - - it 'returns the first match if it finds one', -> - list = new List([], ['*.js', '**/*.js'], emitter, preprocess) - expect(list._isExcluded('world.js')).to.be.eql '*.js' - expect(list._isExcluded('/hello/world/i.js')).to.be.eql '**/*.js' - - - describe '_isIncluded', -> - beforeEach -> - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - it 'returns undefined when no match is found', -> - list = new List(patterns('*.js'), [], emitter, preprocess) - expect(list._isIncluded('hello.txt')).to.be.undefined - expect(list._isIncluded('/hello/world/i.js')).to.be.undefined - - it 'returns the first match if it finds one', -> - list = new List(patterns('*.js', '**/*.js'), [], emitter, preprocess) - expect(list._isIncluded('world.js').pattern).to.be.eql '*.js' - expect(list._isIncluded('/hello/world/i.js').pattern).to.be.eql '**/*.js' - - describe '_exists', -> - beforeEach -> - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - list = new List( - patterns('/some/*.js', '*.txt'), - [], - emitter, - preprocess - ) - - list.refresh() - - it 'returns false when no match is found', -> - expect(list._exists('/some/s.js')).to.be.false - expect(list._exists('/hello/world.ex')).to.be.false - - it 'returns true when a match is found', -> - expect(list._exists('/some/a.js')).to.be.true - expect(list._exists('/some/b.js')).to.be.true - - describe 'refresh', -> - beforeEach -> - patternList = _.cloneDeep(PATTERN_LIST) - mg = _.cloneDeep(MG) - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - list = new List( - patterns('/some/*.js', '*.txt'), - [], - emitter, - preprocess - ) - - it 'resolves patterns', -> - list.refresh().then (files) -> - expect(list.buckets.size).to.equal 2 - - first = pathsFrom list.buckets.get('/some/*.js') - second = pathsFrom list.buckets.get('*.txt') - - expect(first).to.contain '/some/a.js', '/some/b.js' - expect(second).to.contain '/a.txt', '/b.txt', '/c.txt' - - it 'cancels refreshs', -> - checkResult = (files) -> - expect(_.pluck(files.served, 'path')).to.contain '/some/a.js', '/some/b.js', '/some/c.js' - - p1 = list.refresh().then checkResult - patternList['/some/*.js'].push '/some/c.js' - mg.statCache['/some/c.js'] = {mtime: new Date()} - p2 = list.refresh().then checkResult - - Promise.all([p1, p2]) - - it 'sets the mtime for all files', -> - list.refresh().then (files) -> - bucket = list.buckets.get('/some/*.js') - - file1 = findFile '/some/a.js', bucket - file2 = findFile '/some/b.js', bucket - - expect(file1.mtime).to.be.eql mg.statCache['/some/a.js'].mtime - expect(file2.mtime).to.be.eql mg.statCache['/some/b.js'].mtime - - it 'sets the mtime for relative patterns', -> - list = new List( - patterns('/some/world/../*.js', '*.txt'), - [], - emitter, - preprocess - ) - - list.refresh().then (files) -> - bucket = list.buckets.get('/some/world/../*.js') - - file1 = findFile '/some/a.js', bucket - file2 = findFile '/some/b.js', bucket - - expect(file1.mtime).to.be.eql mg.statCache['/some/a.js'].mtime - expect(file2.mtime).to.be.eql mg.statCache['/some/b.js'].mtime - - it 'should sort files within buckets and keep order of patterns (buckets)', -> - # /a.* => /a.txt [MATCH in *.txt as well] - # /some/*.js => /some/a.js, /some/b.js [/some/b.js EXCLUDED] - # *.txt => /c.txt, a.txt, b.txt [UNSORTED] - list = new List( - patterns('/a.*', '/some/*.js', '*.txt'), - ['**/b.js'], - emitter, - preprocess - ) - - list.refresh().then (files) -> - expect(pathsFrom files.served).to.deep.equal [ - '/a.txt', - '/some/a.js', - '/b.txt', - '/c.txt' - ] - - it 'ingores excluded files', -> - list = new List( - patterns('*.txt'), - ['/a.*', '**/b.txt'], - emitter, - preprocess - ) - - list.refresh().then (files) -> - bucket = pathsFrom list.buckets.get('*.txt') - - expect(bucket).to.contain '/c.txt' - expect(bucket).not.to.contain '/a.txt' - expect(bucket).not.to.contain '/b.txt' - - it 'does not glob urls and sets the isUrl flag', -> - list = new List( - patterns('http://some.com'), - [], - emitter, - preprocess - ) - - list.refresh() - .then (files) -> - bucket = list.buckets.get('http://some.com') - file = findFile('http://some.com', bucket) - - expect(file).to.have.property 'isUrl', true - - it 'preprocesses all files', -> - list.refresh().then (files) -> - expect(preprocess.callCount).to.be.eql 5 - - it 'fails when a preprocessor fails', -> - preprocess = sinon.spy (file, next) -> - next new Error('failing'), null - - list = new List( - patterns('/some/*.js'), - [], - emitter, - preprocess - ) - - list.refresh().catch (err) -> - expect(err.message).to.be.eql 'failing' - - - describe 'reload', -> - beforeEach -> - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - list = new List( - patterns('/some/*.js', '*.txt'), - [], - emitter, - preprocess - ) - - it 'refreshes, even when a refresh is already happening', -> - sinon.spy(list, '_refresh') - - Promise.all([ - list.refresh() - list.reload(patterns('*.txt'), []) - ]) - .then -> - expect(list._refresh).to.have.been.calledTwice - - - describe 'addFile', -> - beforeEach -> - patternList = PATTERN_LIST - mg = MG - - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - list = new List( - patterns('/some/*.js', '*.txt'), - ['/secret/*.txt'], - emitter, - preprocess - ) - - it 'does not add excluded files', -> - list.refresh().then (before) -> - list.addFile('/secret/hello.txt').then (files) -> - expect(files.served).to.be.eql before.served - - it 'does not add already existing files', -> - list.refresh().then (before) -> - list.addFile('/some/a.js').then (files) -> - expect(files.served).to.be.eql before.served - - it 'does not add unmatching files', -> - list.refresh().then (before) -> - list.addFile('/some/a.ex').then (files) -> - expect(files.served).to.be.eql before.served - - it 'adds the file to the correct bucket', -> - list.refresh().then (before) -> - list.addFile('/some/d.js').then (files) -> - expect(pathsFrom files.served).to.contain '/some/d.js' - bucket = list.buckets.get('/some/*.js') - expect(pathsFrom bucket).to.contain '/some/d.js' - - it 'fires "file_list_modified"', -> - modified = sinon.stub() - emitter.on 'file_list_modified', modified - - list.refresh().then -> - expect(modified).to.have.been.calledOnce - modified.reset() - - list.addFile('/some/d.js').then -> - expect(modified).to.have.been.calledOnce - - - it 'ignores quick double "add"', -> - # On linux fs.watch (chokidar with usePolling: false) fires "add" event twice. - # This checks that we only stat and preprocess the file once. - - modified = sinon.stub() - emitter.on 'file_list_modified', modified - - list.refresh().then -> - expect(modified).to.have.been.calledOnce - modified.reset() - preprocess.reset() - sinon.spy mockFs, 'stat' - - Promise.all([ - list.addFile('/some/d.js') - list.addFile('/some/d.js') - ]).then -> - expect(modified).to.have.been.calledOnce - expect(preprocess).to.have.been.calledOnce - expect(mockFs.stat).to.have.been.calledOnce - - it 'sets the proper mtime of the new file', -> - list = new List(patterns('/a.*'), [], emitter, preprocess) - - list.refresh().then -> - list.addFile('/a.js').then (files) -> - expect(findFile('/a.js', files.served).mtime).to.eql new Date '2012-01-01' - - it 'preprocesses the added file', -> - # MATCH: /a.txt - list = new List(patterns('/a.*'), [], emitter, preprocess) - list.refresh().then (files) -> - preprocess.reset() - list.addFile('/a.js').then -> - expect(preprocess).to.have.been.calledOnce - expect(preprocess.args[0][0].originalPath).to.eql '/a.js' - - - describe 'changeFile', -> - beforeEach -> - patternList = PATTERN_LIST - mg = MG - - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - mockFs._touchFile '/some/a.js', '2012-04-04' - mockFs._touchFile '/some/b.js', '2012-05-05' - - modified = sinon.stub() - emitter.on 'file_list_modified', modified - - it 'updates mtime and fires "file_list_modified"', -> - # MATCH: /some/a.js, /some/b.js - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) - list.refresh().then (files) -> - mockFs._touchFile '/some/b.js', '2020-01-01' - modified.reset() - - list.changeFile('/some/b.js').then (files) -> - expect(modified).to.have.been.called - expect(findFile('/some/b.js', files.served).mtime).to.be.eql new Date '2020-01-01' - - it 'does not fire "file_list_modified" if no matching file is found', -> - # MATCH: /some/a.js - list = new List(patterns('/some/*.js', '/a.*'), ['/some/b.js'], emitter, preprocess) - - list.refresh().then (files) -> - mockFs._touchFile '/some/b.js', '2020-01-01' - modified.reset() - - list.changeFile('/some/b.js').then -> - expect(modified).to.not.have.been.called - - it 'does not fire "file_list_modified" if mtime has not changed', -> - # chokidar on fucking windows sometimes fires event multiple times - # MATCH: /some/a.js, /some/b.js, /a.txt - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) - - list.refresh().then (files) -> - # not touching the file, stat will return still the same - modified.reset() - list.changeFile('/some/b.js').then -> - expect(modified).not.to.have.been.called - - it 'preprocesses the changed file', -> - # MATCH: /some/a.js, /some/b.js - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) - - list.refresh().then (files) -> - preprocess.reset() - mockFs._touchFile '/some/a.js', '2020-01-01' - list.changeFile('/some/a.js').then -> - expect(preprocess).to.have.been.called - expect(preprocess.lastCall.args[0]).to.have.property 'path', '/some/a.js' - - - describe 'removeFile', -> - beforeEach -> - patternList = PATTERN_LIST - mg = MG - - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - modified = sinon.stub() - emitter.on 'file_list_modified', modified - - it 'removes the file from the list and fires "file_list_modified"', -> - # MATCH: /some/a.js, /some/b.js, /a.txt - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) - - list.refresh().then (files) -> - modified.reset() - list.removeFile('/some/a.js').then (files) -> - expect(pathsFrom files.served).to.be.eql [ - '/some/b.js', - '/a.txt' - ] - expect(modified).to.have.been.calledOnce - - it 'does not fire "file_list_modified" if the file is not in the list', -> - # MATCH: /some/a.js, /some/b.js, /a.txt - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) - - list.refresh().then (files) -> - modified.reset() - list.removeFile('/a.js').then -> - expect(modified).to.not.have.been.called - - describe 'batch interval', -> - clock = null - - beforeEach -> - patternList = PATTERN_LIST - mg = MG - - preprocess = sinon.spy((file, done) -> process.nextTick done) - emitter = new EventEmitter() - - glob = Glob: (pattern, opts) -> - {found: patternList[pattern], statCache: mg.statCache} - - modified = sinon.stub() - emitter.on 'file_list_modified', modified - - clock = sinon.useFakeTimers() - # This hack is needed to ensure lodash is using the fake timers - # from sinon - helper._ = _.runInContext() - List = proxyquire('../../lib/file-list', { - helper: helper - glob: glob - fs: mockFs - }) - - afterEach -> - clock.restore() - - it 'batches multiple changes within an interval', (done) -> - - # MATCH: /some/a.js, /some/b.js, /a.txt - list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess, 1000) - - list.refresh().then (files) -> - modified.reset() - mockFs._touchFile '/some/b.js', '2020-01-01' - list.changeFile '/some/b.js' - list.removeFile '/some/a.js' # /some/b.js, /a.txt - list.removeFile '/a.txt' # /some/b.js - list.addFile '/a.txt' # /some/b.js, /a.txt - list.addFile '/some/0.js' # /some/0.js, /some/b.js, /a.txt - - clock.tick(999) - expect(modified).to.not.have.been.called - emitter.once 'file_list_modified', (files) -> - expect(pathsFrom files.served).to.be.eql [ - '/some/0.js', - '/some/b.js', - '/a.txt' - ] - done() - - clock.tick(1001) - - it 'waits while file preprocessing, if the file was deleted and immediately added', (done) -> - list = new List(patterns('/a.*'), [], emitter, preprocess, 1000) - - list.refresh().then (files) -> - preprocess.reset() - - # Remove and then immediately add file to the bucket - list.removeFile '/a.txt' - list.addFile '/a.txt' - - clock.tick(1000) - - emitter.once 'file_list_modified', (files) -> - expect(preprocess).to.have.been.calledOnce - done() - - clock.tick(1001) diff --git a/test/unit/file-list.spec.js b/test/unit/file-list.spec.js new file mode 100644 index 000000000..172d744c2 --- /dev/null +++ b/test/unit/file-list.spec.js @@ -0,0 +1,723 @@ +import Promise from 'bluebird' +import {EventEmitter} from 'events' +import mocks from 'mocks' +import proxyquire from 'proxyquire' +var helper = require('../../lib/helper') +var _ = helper._ + +var from = require('core-js/library/fn/array/from') +var config = require('../../lib/config') + +// create an array of pattern objects from given strings +var patterns = (...strings) => strings.map(str => new config.Pattern(str)) + +function pathsFrom (files) { + return _.pluck(from(files), 'path') +} + +function findFile (path, files) { + return from(files).find(file => file.path === path) +} + +var PATTERN_LIST = { + '/some/*.js': ['/some/a.js', '/some/b.js'], + '*.txt': ['/c.txt', '/a.txt', '/b.txt'], + '/a.*': ['/a.txt'] +} + +var MG = { + statCache: { + '/some/a.js': {mtime: new Date()}, + '/some/b.js': {mtime: new Date()}, + '/a.txt': {mtime: new Date()}, + '/b.txt': {mtime: new Date()}, + '/c.txt': {mtime: new Date()} + } +} +var mockFs = mocks.fs.create({ + some: {'0.js': mocks.fs.file('2012-04-04'), + 'a.js': mocks.fs.file('2012-04-04'), + 'b.js': mocks.fs.file('2012-05-05'), + 'd.js': mocks.fs.file('2012-05-05')}, + folder: {'x.js': mocks.fs.file(0)}, + 'a.txt': mocks.fs.file(0), + 'b.txt': mocks.fs.file(0), + 'c.txt': mocks.fs.file(0), + 'a.js': mocks.fs.file('2012-01-01') +}) + +describe('FileList', () => { + var list + var emitter + var preprocess + var patternList + var mg + var modified + var glob + var List = list = emitter = preprocess = patternList = mg = modified = glob = null + + beforeEach(() => {}) + + describe('files', () => { + beforeEach(() => { + patternList = PATTERN_LIST + mg = MG + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + }) + + it('returns a flat array of served files', () => { + list = new List(patterns('/some/*.js'), [], emitter, preprocess) + + return list.refresh().then(() => { + expect(list.files.served).to.have.length(2) + }) + }) + + it('returns a unique set', () => { + list = new List(patterns('/a.*', '*.txt'), [], emitter, preprocess) + + return list.refresh().then(() => { + expect(list.files.served).to.have.length(3) + expect(pathsFrom(list.files.served)).to.contain('/a.txt', '/b.txt', '/c.txt') + }) + }) + + it('returns only served files', () => { + var files = [ + new config.Pattern('/a.*', true), // served: true + new config.Pattern('/some/*.js', false) // served: false + ] + + list = new List(files, [], emitter, preprocess) + + return list.refresh().then(() => { + expect(pathsFrom(list.files.served)).to.eql(['/a.txt']) + }) + }) + + it('marks no cache files', () => { + var files = [ + new config.Pattern('/a.*'), // nocach: false + new config.Pattern('/some/*.js', true, true, true, true) // nocache: true + ] + + list = new List(files, [], emitter, preprocess) + + return list.refresh().then(() => { + expect(pathsFrom(list.files.served)).to.deep.equal([ + '/a.txt', + '/some/a.js', + '/some/b.js' + ]) + expect(preprocess).to.have.been.calledOnce + expect(list.files.served[0].doNotCache).to.be.false + expect(list.files.served[1].doNotCache).to.be.true + expect(list.files.served[2].doNotCache).to.be.true + }) + }) + + it('returns a flat array of included files', () => { + var files = [ + new config.Pattern('/a.*', true, false), // included: false + new config.Pattern('/some/*.js') // included: true + ] + + list = new List(files, [], emitter, preprocess) + + return list.refresh().then(() => { + expect(pathsFrom(list.files.included)).not.to.contain('/a.txt') + expect(pathsFrom(list.files.included)).to.deep.equal([ + '/some/a.js', + '/some/b.js' + ]) + }) + }) + }) + + describe('_isExcluded', () => { + beforeEach(() => { + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + }) + + it('returns undefined when no match is found', () => { + list = new List([], ['hello.js', 'world.js'], emitter, preprocess) + expect(list._isExcluded('hello.txt')).to.be.undefined + expect(list._isExcluded('/hello/world/i.js')).to.be.undefined + }) + + it('returns the first match if it finds one', () => { + list = new List([], ['*.js', '**/*.js'], emitter, preprocess) + expect(list._isExcluded('world.js')).to.be.eql('*.js') + expect(list._isExcluded('/hello/world/i.js')).to.be.eql('**/*.js') + }) + }) + + describe('_isIncluded', () => { + beforeEach(() => { + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + }) + + it('returns undefined when no match is found', () => { + list = new List(patterns('*.js'), [], emitter, preprocess) + expect(list._isIncluded('hello.txt')).to.be.undefined + expect(list._isIncluded('/hello/world/i.js')).to.be.undefined + }) + + it('returns the first match if it finds one', () => { + list = new List(patterns('*.js', '**/*.js'), [], emitter, preprocess) + expect(list._isIncluded('world.js').pattern).to.be.eql('*.js') + expect(list._isIncluded('/hello/world/i.js').pattern).to.be.eql('**/*.js') + }) + }) + + describe('_exists', () => { + beforeEach(() => { + patternList = _.cloneDeep(PATTERN_LIST) + mg = _.cloneDeep(MG) + + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + + list = new List(patterns('/some/*.js', '*.txt'), [], emitter, preprocess) + + return list.refresh() + }) + + it('returns false when no match is found', () => { + expect(list._exists('/some/s.js')).to.be.false + expect(list._exists('/hello/world.ex')).to.be.false + }) + + it('returns true when a match is found', () => { + expect(list._exists('/some/a.js')).to.be.true + expect(list._exists('/some/b.js')).to.be.true + }) + }) + + describe('refresh', () => { + beforeEach(() => { + patternList = _.cloneDeep(PATTERN_LIST) + mg = _.cloneDeep(MG) + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + + list = new List(patterns('/some/*.js', '*.txt'), [], emitter, preprocess) + }) + + it('resolves patterns', () => { + return list.refresh().then(files => { + expect(list.buckets.size).to.equal(2) + + var first = pathsFrom(list.buckets.get('/some/*.js')) + var second = pathsFrom(list.buckets.get('*.txt')) + + expect(first).to.contain('/some/a.js', '/some/b.js') + expect(second).to.contain('/a.txt', '/b.txt', '/c.txt') + }) + }) + + it('cancels refreshs', () => { + var checkResult = files => { + expect(_.pluck(files.served, 'path')).to.contain('/some/a.js', '/some/b.js', '/some/c.js') + } + + var p1 = list.refresh().then(checkResult) + patternList['/some/*.js'].push('/some/c.js') + mg.statCache['/some/c.js'] = {mtime: new Date()} + var p2 = list.refresh().then(checkResult) + + return Promise.all([p1, p2]) + }) + + it('sets the mtime for all files', () => { + return list.refresh().then(files => { + var bucket = list.buckets.get('/some/*.js') + + var file1 = findFile('/some/a.js', bucket) + var file2 = findFile('/some/b.js', bucket) + + expect(file1.mtime).to.be.eql(mg.statCache['/some/a.js'].mtime) + expect(file2.mtime).to.be.eql(mg.statCache['/some/b.js'].mtime) + }) + }) + + it('sets the mtime for relative patterns', () => { + list = new List(patterns('/some/world/../*.js', '*.txt'), [], emitter, preprocess) + + return list.refresh().then(files => { + var bucket = list.buckets.get('/some/world/../*.js') + + var file1 = findFile('/some/a.js', bucket) + var file2 = findFile('/some/b.js', bucket) + + expect(file1.mtime).to.be.eql(mg.statCache['/some/a.js'].mtime) + expect(file2.mtime).to.be.eql(mg.statCache['/some/b.js'].mtime) + }) + }) + + it('should sort files within buckets and keep order of patterns (buckets)', () => { + // /a.* => /a.txt [MATCH in *.txt as well] + // /some/*.js => /some/a.js, /some/b.js [/some/b.js EXCLUDED] + // *.txt => /c.txt, a.txt, b.txt [UNSORTED] + list = new List(patterns('/a.*', '/some/*.js', '*.txt'), ['**/b.js'], emitter, preprocess) + + return list.refresh().then(files => { + expect(pathsFrom(files.served)).to.deep.equal([ + '/a.txt', + '/some/a.js', + '/b.txt', + '/c.txt' + ]) + }) + }) + + it('ingores excluded files', () => { + list = new List(patterns('*.txt'), ['/a.*', '**/b.txt'], emitter, preprocess) + + return list.refresh().then(files => { + var bucket = pathsFrom(list.buckets.get('*.txt')) + + expect(bucket).to.contain('/c.txt') + expect(bucket).not.to.contain('/a.txt') + expect(bucket).not.to.contain('/b.txt') + }) + }) + + it('does not glob urls and sets the isUrl flag', () => { + list = new List(patterns('http://some.com'), [], emitter, preprocess) + + return list.refresh() + .then(files => { + var bucket = list.buckets.get('http://some.com') + var file = findFile('http://some.com', bucket) + + expect(file).to.have.property('isUrl', true) + } + ) + }) + + it('preprocesses all files', () => { + return list.refresh().then(files => { + expect(preprocess.callCount).to.be.eql(5) + }) + }) + + it('fails when a preprocessor fails', () => { + preprocess = sinon.spy((file, next) => { + next(new Error('failing'), null) + }) + + list = new List(patterns('/some/*.js'), [], emitter, preprocess) + + return list.refresh().catch(err => { + expect(err.message).to.be.eql('failing') + }) + }) + }) + + describe('reload', () => { + beforeEach(() => { + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + list = new List(patterns('/some/*.js', '*.txt'), [], emitter, preprocess) + }) + + it('refreshes, even when a refresh is already happening', () => { + sinon.spy(list, '_refresh') + + return Promise.all([ + list.refresh(), + list.reload(patterns('*.txt'), []) + ]) + .then(() => { + expect(list._refresh).to.have.been.calledTwice + } + ) + }) + }) + + describe('addFile', () => { + beforeEach(() => { + patternList = PATTERN_LIST + mg = MG + + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + + list = new List(patterns('/some/*.js', '*.txt'), ['/secret/*.txt'], emitter, preprocess) + }) + + it('does not add excluded files', () => { + return list.refresh().then(before => { + return list.addFile('/secret/hello.txt').then(files => { + expect(files.served).to.be.eql(before.served) + }) + }) + }) + + it('does not add already existing files', () => { + return list.refresh().then(before => { + return list.addFile('/some/a.js').then(files => { + expect(files.served).to.be.eql(before.served) + }) + }) + }) + + it('does not add unmatching files', () => { + return list.refresh().then(before => { + return list.addFile('/some/a.ex').then(files => { + expect(files.served).to.be.eql(before.served) + }) + }) + }) + + it('adds the file to the correct bucket', () => { + return list.refresh().then(before => { + return list.addFile('/some/d.js').then(files => { + expect(pathsFrom(files.served)).to.contain('/some/d.js') + var bucket = list.buckets.get('/some/*.js') + expect(pathsFrom(bucket)).to.contain('/some/d.js') + }) + }) + }) + + it('fires "file_list_modified"', () => { + modified = sinon.stub() + emitter.on('file_list_modified', modified) + + return list.refresh().then(() => { + expect(modified).to.have.been.calledOnce + modified.reset() + + return list.addFile('/some/d.js').then(() => { + expect(modified).to.have.been.calledOnce + }) + }) + }) + + it('ignores quick double "add"', () => { + // On linux fs.watch (chokidar with usePolling: false) fires "add" event twice. + // This checks that we only stat and preprocess the file once. + + modified = sinon.stub() + emitter.on('file_list_modified', modified) + + return list.refresh().then(() => { + expect(modified).to.have.been.calledOnce + modified.reset() + preprocess.reset() + sinon.spy(mockFs, 'stat') + + return Promise.all([ + list.addFile('/some/d.js'), + list.addFile('/some/d.js') + ]).then(() => { + expect(modified).to.have.been.calledOnce + expect(preprocess).to.have.been.calledOnce + expect(mockFs.stat).to.have.been.calledOnce + } + ) + }) + }) + + it('sets the proper mtime of the new file', () => { + list = new List(patterns('/a.*'), [], emitter, preprocess) + + return list.refresh().then(() => { + return list.addFile('/a.js').then(files => { + expect(findFile('/a.js', files.served).mtime).to.eql(new Date('2012-01-01')) + }) + }) + }) + + it('preprocesses the added file', () => { + // MATCH: /a.txt + list = new List(patterns('/a.*'), [], emitter, preprocess) + return list.refresh().then(files => { + preprocess.reset() + return list.addFile('/a.js').then(() => { + expect(preprocess).to.have.been.calledOnce + expect(preprocess.args[0][0].originalPath).to.eql('/a.js') + }) + }) + }) + }) + + describe('changeFile', () => { + beforeEach(() => { + patternList = PATTERN_LIST + mg = MG + + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + + mockFs._touchFile('/some/a.js', '2012-04-04') + mockFs._touchFile('/some/b.js', '2012-05-05') + + modified = sinon.stub() + emitter.on('file_list_modified', modified) + }) + + it('updates mtime and fires "file_list_modified"', () => { + // MATCH: /some/a.js, /some/b.js + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) + return list.refresh().then(files => { + mockFs._touchFile('/some/b.js', '2020-01-01') + modified.reset() + + return list.changeFile('/some/b.js').then(files => { + expect(modified).to.have.been.called + expect(findFile('/some/b.js', files.served).mtime).to.be.eql(new Date('2020-01-01')) + }) + }) + }) + + it('does not fire "file_list_modified" if no matching file is found', () => { + // MATCH: /some/a.js + list = new List(patterns('/some/*.js', '/a.*'), ['/some/b.js'], emitter, preprocess) + + return list.refresh().then(files => { + mockFs._touchFile('/some/b.js', '2020-01-01') + modified.reset() + + return list.changeFile('/some/b.js').then(() => { + expect(modified).to.not.have.been.called + }) + }) + }) + + it('does not fire "file_list_modified" if mtime has not changed', () => { + // chokidar on fucking windows sometimes fires event multiple times + // MATCH: /some/a.js, /some/b.js, /a.txt + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) + + return list.refresh().then(files => { + // not touching the file, stat will return still the same + modified.reset() + return list.changeFile('/some/b.js').then(() => { + expect(modified).not.to.have.been.called + }) + }) + }) + + it('preprocesses the changed file', () => { + // MATCH: /some/a.js, /some/b.js + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) + + return list.refresh().then(files => { + preprocess.reset() + mockFs._touchFile('/some/a.js', '2020-01-01') + return list.changeFile('/some/a.js').then(() => { + expect(preprocess).to.have.been.called + expect(preprocess.lastCall.args[0]).to.have.property('path', '/some/a.js') + }) + }) + }) + }) + + describe('removeFile', () => { + beforeEach(() => { + patternList = PATTERN_LIST + mg = MG + + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + + modified = sinon.stub() + emitter.on('file_list_modified', modified) + }) + + it('removes the file from the list and fires "file_list_modified"', () => { + // MATCH: /some/a.js, /some/b.js, /a.txt + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) + + return list.refresh().then(files => { + modified.reset() + return list.removeFile('/some/a.js').then(files => { + expect(pathsFrom(files.served)).to.be.eql([ + '/some/b.js', + '/a.txt' + ]) + expect(modified).to.have.been.calledOnce + }) + }) + }) + + it('does not fire "file_list_modified" if the file is not in the list', () => { + // MATCH: /some/a.js, /some/b.js, /a.txt + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess) + + return list.refresh().then(files => { + modified.reset() + return list.removeFile('/a.js').then(() => { + expect(modified).to.not.have.been.called + }) + }) + }) + }) + + describe('batch interval', () => { + var clock = null + + beforeEach(() => { + patternList = PATTERN_LIST + mg = MG + + preprocess = sinon.spy((file, done) => process.nextTick(done)) + emitter = new EventEmitter() + + glob = { + Glob: (pattern, opts) => ({ + found: patternList[pattern], + statCache: mg.statCache + }) + } + + modified = sinon.stub() + emitter.on('file_list_modified', modified) + + clock = sinon.useFakeTimers() + // This hack is needed to ensure lodash is using the fake timers + // from sinon + helper._ = _.runInContext() + List = proxyquire('../../lib/file-list', { + helper: helper, + glob: glob, + fs: mockFs + }) + }) + + afterEach(() => { + clock.restore() + }) + + it('batches multiple changes within an interval', done => { + // MATCH: /some/a.js, /some/b.js, /a.txt + list = new List(patterns('/some/*.js', '/a.*'), [], emitter, preprocess, 1000) + + return list.refresh().then(files => { + modified.reset() + mockFs._touchFile('/some/b.js', '2020-01-01') + list.changeFile('/some/b.js') + list.removeFile('/some/a.js') // /some/b.js, /a.txt + list.removeFile('/a.txt') // /some/b.js + list.addFile('/a.txt') // /some/b.js, /a.txt + list.addFile('/some/0.js') // /some/0.js, /some/b.js, /a.txt + + clock.tick(999) + expect(modified).to.not.have.been.called + emitter.once('file_list_modified', files => { + expect(pathsFrom(files.served)).to.be.eql([ + '/some/0.js', + '/some/b.js', + '/a.txt' + ]) + done() + }) + + clock.tick(1001) + }) + }) + + it('waits while file preprocessing, if the file was deleted and immediately added', done => { + list = new List(patterns('/a.*'), [], emitter, preprocess, 1000) + + return list.refresh().then(files => { + preprocess.reset() + + // Remove and then immediately add file to the bucket + list.removeFile('/a.txt') + list.addFile('/a.txt') + + clock.tick(1000) + + emitter.once('file_list_modified', files => { + expect(preprocess).to.have.been.calledOnce + return done() + }) + + clock.tick(1001) + }) + }) + }) +}) diff --git a/test/unit/helper.spec.coffee b/test/unit/helper.spec.coffee deleted file mode 100644 index 1230e021b..000000000 --- a/test/unit/helper.spec.coffee +++ /dev/null @@ -1,242 +0,0 @@ -#============================================================================== -# lib/helper.js module -#============================================================================== -describe 'helper', -> - helper = require '../../lib/helper' - - #============================================================================== - # helper.browserFullNameToShort() - #============================================================================== - describe 'browserFullNameToShort', -> - - # helper function - expecting = (name) -> - expect helper.browserFullNameToShort name - - it 'should parse iOS', -> - expecting('Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 ' + - '(KHTML, like Gecko) Version/6.0 Mobile/10A403 Safari/8536.25'). - to.be.equal 'Mobile Safari 6.0.0 (iOS 6.0.0)' - - - it 'should parse Linux', -> - expecting('Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081216 ' + - 'Ubuntu/8.04 (hardy) Firefox/2.0.0.19'). - to.be.equal 'Firefox 2.0.0 (Ubuntu 8.04.0)' - - - it 'should degrade gracefully when OS not recognized', -> - expecting('Mozilla/5.0 (X11; U; FreeBSD; i386; en-US; rv:1.7) Gecko/20081216 ' + - 'Firefox/2.0.0.19'). - to.be.equal 'Firefox 2.0.0 (FreeBSD 0.0.0)' - - - it 'should parse Chrome', -> - expecting('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + - '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7'). - to.be.equal 'Chrome 16.0.912 (Mac OS X 10.6.8)' - - expecting('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.15 ' + - '(KHTML, like Gecko) Chrome/18.0.985.0 Safari/535.15'). - to.be.equal 'Chrome 18.0.985 (Mac OS X 10.6.8)' - - - it 'should parse Firefox', -> - expecting('Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 ' + - 'Firefox/7.0.1'). - to.be.equal 'Firefox 7.0.1 (Mac OS X 10.6.0)' - - - it 'should parse Opera', -> - expecting('Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.9.168 ' + - 'Version/11.52'). - to.be.equal 'Opera 11.52.0 (Mac OS X 10.6.8)' - - - it 'should parse Safari', -> - expecting('Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.52.7 ' + - '(KHTML, like Gecko) Version/5.1.2 Safari/534.52.7'). - to.be.equal 'Safari 5.1.2 (Mac OS X 10.6.8)' - - - it 'should parse IE7', -> - expecting('Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; ' + - '.NET CLR 2.0.50727; .NET4.0C; .NET4.0E)'). - to.be.equal 'IE 7.0.0 (Windows Vista 0.0.0)' - - - it 'should parse IE8', -> - expecting('Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; ' + - 'SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; ' + - '.NET4.0E; InfoPath.3)"'). - to.be.equal 'IE 8.0.0 (Windows 7 0.0.0)' - - - it 'should parse IE9', -> - expecting('Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; ' + - '.NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; '+ - 'Media Center PC 6.0)'). - to.be.equal 'IE 9.0.0 (Windows 7 0.0.0)' - - - it 'should parse IE10', -> - expecting('Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; ' + - '.NET4.0E; .NET4.0C)'). - to.be.equal 'IE 10.0.0 (Windows 8 0.0.0)' - - - it 'should parse PhantomJS', -> - expecting('Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) ' + - 'PhantomJS/1.6.0 Safari/534.34'). - to.be.equal 'PhantomJS 1.6.0 (Mac OS X 0.0.0)' - - - # Fix for #318 - it 'should parse old Android Browser', -> - expecting('Mozilla/5.0 (Linux; U; Android 4.2; en-us; sdk Build/JB_MR1) ' + - 'AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30'). - to.be.equal 'Android 4.2.0 (Android 4.2.0)' - - #============================================================================== - # helper.isDefined() - #============================================================================== - describe 'isDefined', -> - isDefined = helper.isDefined - - it 'should return true if defined', -> - expect(isDefined()).to.equal false - expect(isDefined undefined).to.equal false - - expect(isDefined false).to.equal true - expect(isDefined 0).to.equal true - expect(isDefined null).to.equal true - expect(isDefined '').to.equal true - - - #============================================================================== - # helper.camelToSnake() - #============================================================================== - describe 'camelToSnake', -> - camelToSnake = helper.camelToSnake - - it 'should convert camelCase string to snake_case', -> - expect(camelToSnake 'OneMoreThing' ).to.equal 'one_more_thing' - - - #============================================================================== - # helper.dashToCamel() - #============================================================================== - describe 'dashToCamel', -> - dashToCamel = helper.dashToCamel - - it 'should convert dash-case to camelCase', -> - expect(dashToCamel 'one-more-thing' ).to.equal 'oneMoreThing' - expect(dashToCamel 'one' ).to.equal 'one' - - - #============================================================================== - # helper.arrayRemove() - #============================================================================== - describe 'arrayRemove', -> - arrayRemove = helper.arrayRemove - - it 'should remove object from array', -> - a = 'one'; b = []; c = {}; d = -> null - collection = [a, b, c, d] - - expect(arrayRemove collection, b).to.equal true - expect(collection).to.deep.equal [a, c, d] - - expect(arrayRemove collection, {}).to.equal false - expect(collection).to.deep.equal [a, c, d] - - expect(arrayRemove collection, d).to.equal true - expect(collection).to.deep.equal [a, c] - - expect(arrayRemove collection, a).to.equal true - expect(collection).to.deep.equal [c] - - - #============================================================================== - # helper.merge() - #============================================================================== - describe 'merge', -> - - it 'should copy properties to first argument', -> - destination = {a: 1, b: 2} - result = helper.merge destination, {a: 4, c: 5} - - expect(destination.a).to.equal 1 - expect(result).to.deep.equal {a: 4, b: 2, c: 5} - - - #============================================================================== - # helper.isUrlAbsolute() - #============================================================================== - describe 'isUrlAbsolute', -> - - it 'should check http/https protocol', -> - expect(helper.isUrlAbsolute 'some/path/http.html').to.equal false - expect(helper.isUrlAbsolute '/some/more.py').to.equal false - expect(helper.isUrlAbsolute 'http://some.com/path').to.equal true - expect(helper.isUrlAbsolute 'https://more.org/some.js').to.equal true - - - #============================================================================== - # helper.formatTimeInterval() - #============================================================================== - describe 'formatTimeInterval', -> - - it 'should format into seconds', -> - expect(helper.formatTimeInterval 23000).to.equal '23 secs' - - - it 'should format into minutes', -> - expect(helper.formatTimeInterval 142000).to.equal '2 mins 22 secs' - - - it 'should handle singular minute or second', -> - expect(helper.formatTimeInterval 61000).to.equal '1 min 1 sec' - - - it 'should round to miliseconds', -> - expect(helper.formatTimeInterval 163017).to.equal '2 mins 43.017 secs' - - - #============================================================================== - # helper.mkdirIfNotExists() - #============================================================================== - describe 'mkdirIfNotExists', -> - - fsMock = require('mocks').fs - loadFile = require('mocks').loadFile - - done = null - - fs = fsMock.create - home: - 'some.js': fsMock.file() - - # load file under test - m = loadFile __dirname + '/../../lib/helper.js', {fs: fs, lodash: require 'lodash'} - mkdirIfNotExists = m.exports.mkdirIfNotExists - - - it 'should not do anything, if dir already exists', (done) -> - mkdirIfNotExists '/home', done - - - it 'should create directory if it does not exist', (done) -> - mkdirIfNotExists '/home/new', -> - stat = fs.statSync '/home/new' - expect(stat).to.exist - expect(stat.isDirectory()).to.equal true - done() - - - it 'should create even parent directories if it does not exist', (done) -> - mkdirIfNotExists '/home/new/parent/child', -> - stat = fs.statSync '/home/new/parent/child' - expect(stat).to.exist - expect(stat.isDirectory()).to.equal true - done() diff --git a/test/unit/helper.spec.js b/test/unit/helper.spec.js new file mode 100644 index 000000000..817ca7d6f --- /dev/null +++ b/test/unit/helper.spec.js @@ -0,0 +1,245 @@ +describe('helper', () => { + var helper = require('../../lib/helper') + + describe('browserFullNameToShort', () => { + // helper function + var expecting = name => expect(helper.browserFullNameToShort(name)) + + it('should parse iOS', () => { + expecting( + 'Mozilla/5.0 (iPhone; CPU iPhone OS 6_0 like Mac OS X) AppleWebKit/536.26 ' + + '(KHTML, like Gecko) Version/6.0 Mobile/10A403 Safari/8536.25' + ) + .to.be.equal('Mobile Safari 6.0.0 (iOS 6.0.0)') + }) + + it('should parse Linux', () => { + expecting( + 'Mozilla/5.0 (X11; U; Linux i686; en-US; rv:1.8.1.19) Gecko/20081216 ' + + 'Ubuntu/8.04 (hardy) Firefox/2.0.0.19' + ) + .to.be.equal('Firefox 2.0.0 (Ubuntu 8.04.0)') + }) + + it('should degrade gracefully when OS not recognized', () => { + expecting( + 'Mozilla/5.0 (X11; U; FreeBSD; i386; en-US; rv:1.7) Gecko/20081216 ' + + 'Firefox/2.0.0.19' + ).to.be.equal('Firefox 2.0.0 (FreeBSD 0.0.0)') + }) + + it('should parse Chrome', () => { + expecting( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.7 ' + + '(KHTML, like Gecko) Chrome/16.0.912.63 Safari/535.7' + ) + .to.be.equal('Chrome 16.0.912 (Mac OS X 10.6.8)') + + expecting( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/535.15 ' + + '(KHTML, like Gecko) Chrome/18.0.985.0 Safari/535.15' + ) + .to.be.equal('Chrome 18.0.985 (Mac OS X 10.6.8)') + }) + + it('should parse Firefox', () => { + expecting( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10.6; rv:7.0.1) Gecko/20100101 ' + + 'Firefox/7.0.1' + ) + .to.be.equal('Firefox 7.0.1 (Mac OS X 10.6.0)') + }) + + it('should parse Opera', () => { + expecting( + 'Opera/9.80 (Macintosh; Intel Mac OS X 10.6.8; U; en) Presto/2.9.168 ' + + 'Version/11.52' + ) + .to.be.equal('Opera 11.52.0 (Mac OS X 10.6.8)') + }) + + it('should parse Safari', () => { + expecting( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_6_8) AppleWebKit/534.52.7 ' + + '(KHTML, like Gecko) Version/5.1.2 Safari/534.52.7' + ) + .to.be.equal('Safari 5.1.2 (Mac OS X 10.6.8)') + }) + + it('should parse IE7', () => { + expecting( + 'Mozilla/4.0 (compatible; MSIE 7.0; Windows NT 6.0; WOW64; SLCC1; ' + + '.NET CLR 2.0.50727; .NET4.0C; .NET4.0E)' + ) + .to.be.equal('IE 7.0.0 (Windows Vista 0.0.0)') + }) + + it('should parse IE8', () => { + expecting( + 'Mozilla/4.0 (compatible; MSIE 8.0; Windows NT 6.1; WOW64; Trident/4.0; ' + + 'SLCC2; .NET CLR 2.0.50727; .NET CLR 3.5.30729; .NET CLR 3.0.30729; .NET4.0C; .NET4.0E; InfoPath.3)"' + ) + .to.be.equal('IE 8.0.0 (Windows 7 0.0.0)') + }) + + it('should parse IE9', () => { + expecting( + 'Mozilla/5.0 (compatible; MSIE 9.0; Windows NT 6.1; Win64; x64; Trident/5.0; ' + + '.NET CLR 2.0.50727; SLCC2; .NET CLR 3.5.30729; .NET CLR 3.0.30729; Media Center PC 6.0)' + ) + .to.be.equal('IE 9.0.0 (Windows 7 0.0.0)') + }) + + it('should parse IE10', () => { + expecting( + 'Mozilla/5.0 (compatible; MSIE 10.0; Windows NT 6.2; WOW64; Trident/6.0; ' + + '.NET4.0E; .NET4.0C)' + ) + .to.be.equal('IE 10.0.0 (Windows 8 0.0.0)') + }) + + it('should parse PhantomJS', () => { + expecting( + 'Mozilla/5.0 (Macintosh; Intel Mac OS X) AppleWebKit/534.34 (KHTML, like Gecko) ' + + 'PhantomJS/1.6.0 Safari/534.34' + ) + .to.be.equal('PhantomJS 1.6.0 (Mac OS X 0.0.0)') + }) + + // Fix for #318 + it('should parse old Android Browser', () => { + expecting( + 'Mozilla/5.0 (Linux; U; Android 4.2; en-us; sdk Build/JB_MR1) ' + + 'AppleWebKit/534.30 (KHTML, like Gecko) Version/4.0 Mobile Safari/534.30' + ) + .to.be.equal('Android 4.2.0 (Android 4.2.0)') + }) + }) + + describe('isDefined', () => { + var isDefined = helper.isDefined + + it('should return true if defined', () => { + expect(isDefined()).to.equal(false) + expect(isDefined(undefined)).to.equal(false) + + expect(isDefined(false)).to.equal(true) + expect(isDefined(0)).to.equal(true) + expect(isDefined(null)).to.equal(true) + expect(isDefined('')).to.equal(true) + }) + }) + + describe('camelToSnake', () => { + var camelToSnake = helper.camelToSnake + + it('should convert camelCase string to snake_case', () => { + expect(camelToSnake('OneMoreThing')).to.equal('one_more_thing') + }) + }) + + describe('dashToCamel', () => { + var dashToCamel = helper.dashToCamel + + it('should convert dash-case to camelCase', () => { + expect(dashToCamel('one-more-thing')).to.equal('oneMoreThing') + expect(dashToCamel('one')).to.equal('one') + }) + }) + + describe('arrayRemove', () => { + var arrayRemove = helper.arrayRemove + + it('should remove object from array', () => { + var a = 'one' + var b = [] + var c = {} + var d = () => null + var collection = [a, b, c, d] + + expect(arrayRemove(collection, b)).to.equal(true) + expect(collection).to.deep.equal([a, c, d]) + + expect(arrayRemove(collection, {})).to.equal(false) + expect(collection).to.deep.equal([a, c, d]) + + expect(arrayRemove(collection, d)).to.equal(true) + expect(collection).to.deep.equal([a, c]) + + expect(arrayRemove(collection, a)).to.equal(true) + expect(collection).to.deep.equal([c]) + }) + }) + + describe('merge', () => { + it('should copy properties to first argument', () => { + var destination = {a: 1, b: 2} + var result = helper.merge(destination, {a: 4, c: 5}) + + expect(destination.a).to.equal(1) + expect(result).to.deep.equal({a: 4, b: 2, c: 5}) + }) + }) + + describe('isUrlAbsolute', () => { + it('should check http/https protocol', () => { + expect(helper.isUrlAbsolute('some/path/http.html')).to.equal(false) + expect(helper.isUrlAbsolute('/some/more.py')).to.equal(false) + expect(helper.isUrlAbsolute('http://some.com/path')).to.equal(true) + expect(helper.isUrlAbsolute('https://more.org/some.js')).to.equal(true) + }) + }) + + describe('formatTimeInterval', () => { + it('should format into seconds', () => { + expect(helper.formatTimeInterval(23000)).to.equal('23 secs') + }) + + it('should format into minutes', () => { + expect(helper.formatTimeInterval(142000)).to.equal('2 mins 22 secs') + }) + + it('should handle singular minute or second', () => { + expect(helper.formatTimeInterval(61000)).to.equal('1 min 1 sec') + }) + + it('should round to miliseconds', () => { + expect(helper.formatTimeInterval(163017)).to.equal('2 mins 43.017 secs') + }) + }) + + describe('mkdirIfNotExists', () => { + var fsMock = require('mocks').fs + var loadFile = require('mocks').loadFile + + var fs = fsMock.create({ + home: {'some.js': fsMock.file()} + }) + + // load file under test + var m = loadFile(__dirname + '/../../lib/helper.js', {fs: fs, lodash: require('lodash')}) + var mkdirIfNotExists = m.exports.mkdirIfNotExists + + it('should not do anything, if dir already exists', done => { + mkdirIfNotExists('/home', done) + }) + + it('should create directory if it does not exist', done => { + mkdirIfNotExists('/home/new', () => { + var stat = fs.statSync('/home/new') + expect(stat).to.exist + expect(stat.isDirectory()).to.equal(true) + done() + }) + }) + + it('should create even parent directories if it does not exist', done => { + mkdirIfNotExists('/home/new/parent/child', () => { + var stat = fs.statSync('/home/new/parent/child') + expect(stat).to.exist + expect(stat.isDirectory()).to.equal(true) + done() + }) + }) + }) +}) diff --git a/test/unit/init.spec.coffee b/test/unit/init.spec.coffee deleted file mode 100644 index 91d057859..000000000 --- a/test/unit/init.spec.coffee +++ /dev/null @@ -1,276 +0,0 @@ -#============================================================================== -# lib/init.js module -#============================================================================== -describe 'init', -> - loadFile = require('mocks').loadFile - path = require 'path' - m = null - - beforeEach -> - m = loadFile __dirname + '/../../lib/init.js', {glob: require 'glob'} - sinon.stub m, 'installPackage' - - - describe 'getBasePath', -> - - # just for windows. - replace = (p) -> p.replace(/\//g, path.sep) - - it 'should be empty if config file in cwd', -> - expect(m.getBasePath 'some.conf', replace('/usr/local/whatever')).to.equal '' - - - it 'should handle leading "./', -> - expect(m.getBasePath replace('./some.conf'), replace('/usr/local/whatever')).to.equal '' - - - it 'should handle config file in subfolder', -> - # config /usr/local/sub/folder/file.conf - file = replace('sub/folder/file.conf') - expect(m.getBasePath file, replace('/usr/local')).to.equal replace('../..') - - - it 'should handle config in a parent path', -> - # config /home/file.js - basePath = m.getBasePath replace('../../../file.js'), replace('/home/vojta/tc/project') - expect(basePath).to.equal replace('vojta/tc/project') - - - it 'should handle config in parent subfolder', -> - # config /home/vojta/other/f.js - f = replace('../../other/f.js') - expect(m.getBasePath f, replace('/home/vojta/tc/prj')).to.equal replace('../tc/prj') - - - it 'should handle absolute paths', -> - basePath = m.getBasePath replace('/Users/vojta/karma/conf.js'), replace('/Users/vojta') - expect(basePath).to.equal replace('..') - - - describe 'processAnswers', -> - - answers = (obj = {}) -> - obj.files = obj.files or [] - obj.exclude = obj.exclude or [] - obj.browsers = obj.browsers or [] - obj - - - it 'should add requirejs and set files non-included if requirejs used', -> - processedAnswers = m.processAnswers answers { - requirejs: true, - includedFiles: ['test-main.js'], - files: ['*.js'] - } - - expect(processedAnswers.frameworks).to.contain 'requirejs' - expect(processedAnswers.files).to.deep.equal ['test-main.js'] - expect(processedAnswers.onlyServedFiles).to.deep.equal ['*.js'] - - - it 'should add coffee preprocessor', -> - processedAnswers = m.processAnswers answers { - files: ['src/*.coffee'] - } - - expect(processedAnswers.preprocessors).to.have.property '**/*.coffee' - expect(processedAnswers.preprocessors['**/*.coffee']).to.deep.equal ['coffee'] - - - describe 'scenario:', -> - vm = require 'vm' - - StateMachine = require '../../lib/init/state_machine' - JavaScriptFormatter = require('../../lib/init/formatters').JavaScript - DefaultKarmaConfig = require('../../lib/config').Config - - mockRli = - close: -> null - write: -> null - prompt: -> null - _deleteLineLeft: -> null - _deleteLineRight: -> null - - mockColors = - question: -> '' - - machine = formatter = null - - evaluateConfigCode = (code) -> - sandbox = {module: {}} - configModule = vm.runInNewContext code, sandbox - config = new DefaultKarmaConfig - sandbox.module.exports config - config - - - beforeEach -> - machine = new StateMachine mockRli, mockColors - formatter = new JavaScriptFormatter - - - it 'should generate working config', (done) -> - machine.process m.questions, (answers) -> - basePath = m.getBasePath '../karma.conf.js', path.normalize('/some/path') - processedAnswers = m.processAnswers answers, basePath - generatedConfigCode = formatter.generateConfigFile processedAnswers - config = evaluateConfigCode generatedConfigCode - - # expect correct configuration - expect(config.basePath).to.equal 'path' - expect(config.frameworks).to.deep.equal ['jasmine'] - expect(config.browsers).to.contain 'Chrome' - expect(config.browsers).to.contain 'Firefox' - expect(config.files).to.deep.equal ['src/app.js', 'src/**/*.js', 'test/**/*.js'] - expect(config.exclude).to.deep.equal ['src/config.js'] - expect(config.autoWatch).to.equal false - done() - - # frameworks - machine.onLine 'jasmine' - machine.onLine '' - - # requirejs - machine.onLine 'no' - - # browsers - machine.onLine 'Chrome' - machine.onLine 'Firefox' - machine.onLine '' - - # files - machine.onLine 'src/app.js' - machine.onLine 'src/**/*.js' - machine.onLine 'test/**/*.js' - machine.onLine '' - - # excludes - machine.onLine 'src/config.js' - machine.onLine '' - - # autoWatch - machine.onLine 'no' - - - it 'should generate config for requirejs', (done) -> - machine.process m.questions, (answers) -> - basePath = m.getBasePath '../karma.conf.js', '/some/path' - processedAnswers = m.processAnswers answers, basePath - generatedConfigCode = formatter.generateConfigFile processedAnswers - config = evaluateConfigCode generatedConfigCode - - # expect correct configuration - expect(config.frameworks).to.contain 'requirejs' - expect(config.files).to.contain 'test/main.js' - for pattern in config.files.slice(1) - expect(pattern.included).to.equal false - done() - - # frameworks - machine.onLine 'jasmine' - machine.onLine '' - - # requirejs - machine.onLine 'yes' - - # browsers - machine.onLine 'Chrome' - machine.onLine '' - - # files - machine.onLine 'src/**/*.js' - machine.onLine 'test/**/*.js' - machine.onLine '' - - # excludes - machine.onLine '' - machine.onLine '' - - # generate test-main - machine.onLine 'no' - - # included files - machine.onLine 'test/main.js' - machine.onLine '' - - # autoWatch - machine.onLine 'yes' - - - it 'should generate the test-main for requirejs', (done) -> - machine.process m.questions, (answers) -> - basePath = m.getBasePath '../karma.conf.js', '/some/path' - processedAnswers = m.processAnswers answers, basePath, 'test-main.js' - generatedConfigCode = formatter.generateConfigFile processedAnswers - config = evaluateConfigCode generatedConfigCode - - # expect correct processedAnswers - expect(processedAnswers.generateTestMain).to.be.ok - expect(processedAnswers.files).to.contain 'test-main.js' - - # expect correct configuration - expect(config.frameworks).to.contain 'requirejs' - for pattern in config.files.slice(1) - expect(pattern.included).to.equal false - done() - - # frameworks - machine.onLine 'jasmine' - machine.onLine '' - - # requirejs - machine.onLine 'yes' - - # browsers - machine.onLine 'Chrome' - machine.onLine '' - - # files - machine.onLine 'src/**/*.js' - machine.onLine 'test/**/*.js' - machine.onLine '' - - # excludes - machine.onLine '' - machine.onLine '' - - # generate test-main - machine.onLine 'yes' - - # autoWatch - machine.onLine 'yes' - - - it 'should add coffee preprocessor', (done) -> - machine.process m.questions, (answers) -> - basePath = m.getBasePath 'karma.conf.js', '/cwd' - processedAnswers = m.processAnswers answers, basePath - generatedConfigCode = formatter.generateConfigFile processedAnswers - config = evaluateConfigCode generatedConfigCode - - # expect correct configuration - expect(config.preprocessors).to.have.property '**/*.coffee' - expect(config.preprocessors['**/*.coffee']).to.deep.equal ['coffee'] - done() - - # frameworks - machine.onLine 'jasmine' - machine.onLine '' - - # requirejs - machine.onLine 'no' - - # browsers - machine.onLine 'Chrome' - machine.onLine '' - - # files - machine.onLine 'src/*.coffee' - machine.onLine 'src/**/*.js' - machine.onLine '' - - # excludes - machine.onLine '' - - # autoWatch - machine.onLine 'no' diff --git a/test/unit/init.spec.js b/test/unit/init.spec.js new file mode 100644 index 000000000..0ed9027ad --- /dev/null +++ b/test/unit/init.spec.js @@ -0,0 +1,288 @@ +import path from 'path' +describe('init', () => { + var loadFile = require('mocks').loadFile + var m = null + + beforeEach(() => { + m = loadFile(__dirname + '/../../lib/init.js', {glob: require('glob')}) + sinon.stub(m, 'installPackage') + }) + + describe('getBasePath', () => { + // just for windows. + var replace = p => p.replace(/\//g, path.sep) + + it('should be empty if config file in cwd', () => { + expect(m.getBasePath('some.conf', replace('/usr/local/whatever'))).to.equal('') + }) + + it('should handle leading "./', () => { + expect(m.getBasePath(replace('./some.conf'), replace('/usr/local/whatever'))).to.equal('') + }) + + it('should handle config file in subfolder', () => { + // config /usr/local/sub/folder/file.conf + var file = replace('sub/folder/file.conf') + expect(m.getBasePath(file, replace('/usr/local'))).to.equal(replace('../..')) + }) + + it('should handle config in a parent path', () => { + // config /home/file.js + var basePath = m.getBasePath(replace('../../../file.js'), replace('/home/vojta/tc/project')) + expect(basePath).to.equal(replace('vojta/tc/project')) + }) + + it('should handle config in parent subfolder', () => { + // config /home/vojta/other/f.js + var f = replace('../../other/f.js') + expect(m.getBasePath(f, replace('/home/vojta/tc/prj'))).to.equal(replace('../tc/prj')) + }) + + it('should handle absolute paths', () => { + var basePath = m.getBasePath(replace('/Users/vojta/karma/conf.js'), replace('/Users/vojta')) + expect(basePath).to.equal(replace('..')) + }) + }) + + describe('processAnswers', () => { + var answers = obj => { + obj = obj ? obj : {} + obj.files = obj.files || [] + obj.exclude = obj.exclude || [] + obj.browsers = obj.browsers || [] + return obj + } + + it('should add requirejs and set files non-included if requirejs used', () => { + var processedAnswers = m.processAnswers(answers({ + requirejs: true, + includedFiles: ['test-main.js'], + files: ['*.js'] + })) + + expect(processedAnswers.frameworks).to.contain('requirejs') + expect(processedAnswers.files).to.deep.equal(['test-main.js']) + expect(processedAnswers.onlyServedFiles).to.deep.equal(['*.js']) + }) + + it('should add coffee preprocessor', () => { + var processedAnswers = m.processAnswers(answers({ + files: ['src/*.coffee'] + })) + + expect(processedAnswers.preprocessors).to.have.property('**/*.coffee') + expect(processedAnswers.preprocessors['**/*.coffee']).to.deep.equal(['coffee']) + }) + }) + + describe('scenario:', () => { + var formatter + var vm = require('vm') + + var StateMachine = require('../../lib/init/state_machine') + var JavaScriptFormatter = require('../../lib/init/formatters').JavaScript + var DefaultKarmaConfig = require('../../lib/config').Config + + var mockRli = { + close: () => null, + write: () => null, + prompt: () => null, + _deleteLineLeft: () => null, + _deleteLineRight: () => null + } + + var mockColors = { + question: () => '' + } + + var machine = formatter = null + + var evaluateConfigCode = code => { + var sandbox = {module: {}} + vm.runInNewContext(code, sandbox) + var config = new DefaultKarmaConfig() + sandbox.module.exports(config) + return config + } + + beforeEach(() => { + machine = new StateMachine(mockRli, mockColors) + formatter = new JavaScriptFormatter() + }) + + it('should generate working config', done => { + machine.process(m.questions, answers => { + var basePath = m.getBasePath('../karma.conf.js', path.normalize('/some/path')) + var processedAnswers = m.processAnswers(answers, basePath) + var generatedConfigCode = formatter.generateConfigFile(processedAnswers) + var config = evaluateConfigCode(generatedConfigCode) + + // expect correct configuration + expect(config.basePath).to.equal('path') + expect(config.frameworks).to.deep.equal(['jasmine']) + expect(config.browsers).to.contain('Chrome') + expect(config.browsers).to.contain('Firefox') + expect(config.files).to.deep.equal(['src/app.js', 'src/**/*.js', 'test/**/*.js']) + expect(config.exclude).to.deep.equal(['src/config.js']) + expect(config.autoWatch).to.equal(false) + done() + }) + + // frameworks + machine.onLine('jasmine') + machine.onLine('') + + // requirejs + machine.onLine('no') + + // browsers + machine.onLine('Chrome') + machine.onLine('Firefox') + machine.onLine('') + + // files + machine.onLine('src/app.js') + machine.onLine('src/**/*.js') + machine.onLine('test/**/*.js') + machine.onLine('') + + // excludes + machine.onLine('src/config.js') + machine.onLine('') + + // autoWatch + machine.onLine('no') + }) + + it('should generate config for requirejs', done => { + machine.process(m.questions, answers => { + var basePath = m.getBasePath('../karma.conf.js', '/some/path') + var processedAnswers = m.processAnswers(answers, basePath) + var generatedConfigCode = formatter.generateConfigFile(processedAnswers) + var config = evaluateConfigCode(generatedConfigCode) + + // expect correct configuration + expect(config.frameworks).to.contain('requirejs') + expect(config.files).to.contain('test/main.js') + config.files.slice(1).forEach(pattern => { + expect(pattern.included).to.equal(false) + }) + + done() + }) + + // frameworks + machine.onLine('jasmine') + machine.onLine('') + + // requirejs + machine.onLine('yes') + + // browsers + machine.onLine('Chrome') + machine.onLine('') + + // files + machine.onLine('src/**/*.js') + machine.onLine('test/**/*.js') + machine.onLine('') + + // excludes + machine.onLine('') + machine.onLine('') + + // generate test-main + machine.onLine('no') + + // included files + machine.onLine('test/main.js') + machine.onLine('') + + // autoWatch + machine.onLine('yes') + }) + + it('should generate the test-main for requirejs', done => { + machine.process(m.questions, answers => { + var basePath = m.getBasePath('../karma.conf.js', '/some/path') + var processedAnswers = m.processAnswers(answers, basePath, 'test-main.js') + var generatedConfigCode = formatter.generateConfigFile(processedAnswers) + var config = evaluateConfigCode(generatedConfigCode) + + // expect correct processedAnswers + expect(processedAnswers.generateTestMain).to.be.ok + expect(processedAnswers.files).to.contain('test-main.js') + + // expect correct configuration + expect(config.frameworks).to.contain('requirejs') + config.files.slice(1).forEach(pattern => { + expect(pattern.included).to.equal(false) + }) + + done() + }) + + // frameworks + machine.onLine('jasmine') + machine.onLine('') + + // requirejs + machine.onLine('yes') + + // browsers + machine.onLine('Chrome') + machine.onLine('') + + // files + machine.onLine('src/**/*.js') + machine.onLine('test/**/*.js') + machine.onLine('') + + // excludes + machine.onLine('') + machine.onLine('') + + // generate test-main + machine.onLine('yes') + + // autoWatch + machine.onLine('yes') + }) + + it('should add coffee preprocessor', done => { + machine.process(m.questions, answers => { + var basePath = m.getBasePath('karma.conf.js', '/cwd') + var processedAnswers = m.processAnswers(answers, basePath) + var generatedConfigCode = formatter.generateConfigFile(processedAnswers) + var config = evaluateConfigCode(generatedConfigCode) + + // expect correct configuration + expect(config.preprocessors).to.have.property('**/*.coffee') + expect(config.preprocessors['**/*.coffee']).to.deep.equal(['coffee']) + done() + }) + + // frameworks + machine.onLine('jasmine') + machine.onLine('') + + // requirejs + machine.onLine('no') + + // browsers + machine.onLine('Chrome') + machine.onLine('') + + // files + machine.onLine('src/*.coffee') + machine.onLine('src/**/*.js') + machine.onLine('') + + // excludes + machine.onLine('') + + // autoWatch + machine.onLine('no') + }) + }) +}) diff --git a/test/unit/init/formatters.spec.coffee b/test/unit/init/formatters.spec.coffee deleted file mode 100644 index 034f36403..000000000 --- a/test/unit/init/formatters.spec.coffee +++ /dev/null @@ -1,64 +0,0 @@ -#============================================================================== -# lib/init/formatters.js module -#============================================================================== -describe 'init/formatters', -> - f = require '../../../lib/init/formatters' - formatter = null - - describe 'JavaScript', -> - - beforeEach -> - formatter = new f.JavaScript - - describe 'formatAnswers', -> - createAnswers = (ans = {}) -> - ans.frameworks = ans.frameworks or [] - ans.files = ans.files or [] - ans.onlyServedFiles = ans.onlyServedFiles or [] - ans.exclude = ans.exclude or [] - ans.browsers = ans.browsers or [] - ans.preprocessors = ans.preprocessors or {} - ans - - it 'should format FRAMEWORKS', -> - replacements = formatter.formatAnswers createAnswers {frameworks: ['jasmine', 'requirejs']} - expect(replacements.FRAMEWORKS).to.equal "'jasmine', 'requirejs'" - - - it 'should format FILES', -> - replacements = formatter.formatAnswers createAnswers() - expect(replacements.FILES).to.equal '' - - replacements = formatter.formatAnswers createAnswers {files: ['*.js', 'other/file.js']} - expect(replacements.FILES).to.equal "\n '*.js',\n 'other/file.js'" - - - it 'should format BROWSERS', -> - replacements = formatter.formatAnswers createAnswers {browsers: ['Chrome', 'Firefox']} - expect(replacements.BROWSERS).to.equal "'Chrome', 'Firefox'" - - - it 'should format AUTO_WATCH', -> - replacements = formatter.formatAnswers createAnswers {autoWatch: true} - expect(replacements.AUTO_WATCH).to.equal 'true' - - replacements = formatter.formatAnswers createAnswers {autoWatch: false} - expect(replacements.AUTO_WATCH).to.equal 'false' - - - it 'should format onlyServedFiles', -> - replacements = formatter.formatAnswers createAnswers { - files: ['test-main.js'] - onlyServedFiles: ['src/*.js'] - } - - expect(replacements.FILES).to.equal "\n 'test-main.js',\n" + - " {pattern: 'src/*.js', included: false}" - - - it 'should format PREPROCESSORS', -> - replacements = formatter.formatAnswers createAnswers {preprocessors: '*.coffee': ['coffee']} - - expect(replacements.PREPROCESSORS).to.equal "{\n" + - " '*.coffee': ['coffee']\n" + - " }" diff --git a/test/unit/init/formatters.spec.js b/test/unit/init/formatters.spec.js new file mode 100644 index 000000000..a311132f6 --- /dev/null +++ b/test/unit/init/formatters.spec.js @@ -0,0 +1,69 @@ +import formatters from '../../../lib/init/formatters' +describe('init/formatters', () => { + var formatter + + describe('JavaScript', () => { + beforeEach(() => { + formatter = new formatters.JavaScript() + }) + + describe('formatAnswers', () => { + var createAnswers = function (ans = {}) { + ans.frameworks = ans.frameworks || [] + ans.files = ans.files || [] + ans.onlyServedFiles = ans.onlyServedFiles || [] + ans.exclude = ans.exclude || [] + ans.browsers = ans.browsers || [] + ans.preprocessors = ans.preprocessors || {} + return ans + } + + it('should format FRAMEWORKS', () => { + var replacements = formatter.formatAnswers(createAnswers({frameworks: ['jasmine', 'requirejs']})) + expect(replacements.FRAMEWORKS).to.equal("'jasmine', 'requirejs'") + }) + + it('should format FILES', () => { + var replacements = formatter.formatAnswers(createAnswers()) + expect(replacements.FILES).to.equal('') + + replacements = formatter.formatAnswers(createAnswers({files: ['*.js', 'other/file.js']})) + expect(replacements.FILES).to.equal( + "\n '*.js',\n 'other/file.js'" + ) + }) + + it('should format BROWSERS', () => { + var replacements = formatter.formatAnswers(createAnswers({browsers: ['Chrome', 'Firefox']})) + expect(replacements.BROWSERS).to.equal("'Chrome', 'Firefox'") + }) + + it('should format AUTO_WATCH', () => { + var replacements = formatter.formatAnswers(createAnswers({autoWatch: true})) + expect(replacements.AUTO_WATCH).to.equal('true') + + replacements = formatter.formatAnswers(createAnswers({autoWatch: false})) + expect(replacements.AUTO_WATCH).to.equal('false') + }) + + it('should format onlyServedFiles', () => { + var replacements = formatter.formatAnswers(createAnswers({ + files: ['test-main.js'], + onlyServedFiles: ['src/*.js'] + })) + + expect(replacements.FILES).to.equal( + "\n 'test-main.js',\n {pattern: 'src/*.js', included: false}" + ) + }) + + it('should format PREPROCESSORS', () => { + var replacements = formatter.formatAnswers(createAnswers({preprocessors: {'*.coffee': ['coffee']}})) + + expect(replacements.PREPROCESSORS).to.equal( + "{\n '*.coffee': ['coffee']\n }" + ) + }) + }) + }) +}) diff --git a/test/unit/init/state_machine.spec.coffee b/test/unit/init/state_machine.spec.coffee deleted file mode 100644 index 8c484ff57..000000000 --- a/test/unit/init/state_machine.spec.coffee +++ /dev/null @@ -1,148 +0,0 @@ -#============================================================================== -# lib/init/state_machine.js module -#============================================================================== -describe 'init/StateMachine', -> - StateMachine = require '../../../lib/init/state_machine' - machine = done = null - - mockRli = - close: -> null - write: -> null - prompt: -> null - _deleteLineLeft: -> null - _deleteLineRight: -> null - - mockColors = - question: -> '' - - - beforeEach -> - machine = new StateMachine mockRli, mockColors - done = sinon.spy() - - it 'should go through all the questions', -> - questions = [ - {id: 'framework', options: ['jasmine', 'mocha']} - {id: 'other'} - ] - - done = sinon.spy (answers) -> - expect(answers.framework).to.equal 'jasmine' - expect(answers.other).to.equal 'abc' - - machine.process questions, done - machine.onLine 'jasmine' - machine.onLine 'abc' - expect(done).to.have.been.called - - - it 'should allow multiple answers', -> - questions = [ - {id: 'browsers', multiple: true} - ] - - done = sinon.spy (answers) -> - expect(answers.browsers).to.deep.equal ['Chrome', 'Safari'] - - machine.process questions, done - machine.onLine 'Chrome' - machine.onLine 'Safari' - machine.onLine '' - expect(done).to.have.been.called - - - it 'should treat spaces as confirmation of multiple answers', -> - questions = [ - {id: 'browsers', multiple: true} - ] - - done = sinon.spy (answers) -> - expect(answers.browsers).to.deep.equal ['Chrome'] - - machine.process questions, done - machine.onLine 'Chrome' - machine.onLine ' ' - expect(done).to.have.been.called - - - it 'should always return array for multiple', -> - questions = [ - {id: 'empty', multiple: true} - ] - - done = sinon.spy (answers) -> - expect(answers.empty).to.deep.equal [] - - machine.process questions, done - machine.onLine '' - expect(done).to.have.been.called - - - it 'should validate answers', -> - validator = sinon.spy() - questions = [ - {id: 'validated', validate: validator} - ] - - machine.process questions, done - machine.onLine 'something' - - expect(done).to.have.been.called - expect(validator).to.have.been.calledWith 'something' - - - it 'should allow conditional answers', -> - ifTrue = sinon.spy (answers) -> - answers.first is 'true' - ifFalse = sinon.spy (answers) -> - answers.first is 'false' - - done = sinon.spy (answers) -> - expect(answers.first).to.equal 'true' - expect(answers.onlyIfTrue).to.equal 'something' - expect(answers.onlyIfFalse).to.not.exist - - questions = [ - {id: 'first'} - {id: 'onlyIfTrue', condition: ifTrue} - {id: 'onlyIfFalse', condition: ifFalse} - ] - - machine.process questions, done - machine.onLine 'true' - machine.onLine 'something' - - expect(done).to.have.been.called - - - it 'should parse booleans', -> - done = sinon.spy (answers) -> - expect(answers.yes).to.equal true - expect(answers.no).to.equal false - - questions = [ - {id: 'yes', options: ['yes', 'no'], boolean: true} - {id: 'no', options: ['yes', 'no'], boolean: true} - ] - - machine.process questions, done - machine.onLine 'yes' - machine.onLine 'no' - - expect(done).to.have.been.called - - - it 'should parse booleans before validation', -> - validator = sinon.spy (value) -> - expect(typeof value).to.equal 'boolean' - - questions = [ - {id: 'what', options: ['yes', 'no'], boolean: true, validate: validator} - {id: 'really', options: ['yes', 'no'], boolean: true, validate: validator} - ] - - machine.process questions, done - machine.onLine 'yes' - machine.onLine 'no' - - expect(validator).to.have.been.calledTwice diff --git a/test/unit/init/state_machine.spec.js b/test/unit/init/state_machine.spec.js new file mode 100644 index 000000000..a4142d5b8 --- /dev/null +++ b/test/unit/init/state_machine.spec.js @@ -0,0 +1,160 @@ +import StateMachine from '../../../lib/init/state_machine' + +describe('init/StateMachine', () => { + var done + var machine + + var mockRli = { + close: () => null, + write: () => null, + prompt: () => null, + _deleteLineLeft: () => null, + _deleteLineRight: () => null + } + + var mockColors = { + question: () => '' + } + + beforeEach(() => { + machine = new StateMachine(mockRli, mockColors) + done = sinon.spy() + }) + + it('should go through all the questions', () => { + var questions = [ + {id: 'framework', options: ['jasmine', 'mocha']}, + {id: 'other'} + ] + + done = sinon.spy(answers => { + expect(answers.framework).to.equal('jasmine') + expect(answers.other).to.equal('abc') + }) + + machine.process(questions, done) + machine.onLine('jasmine') + machine.onLine('abc') + expect(done).to.have.been.called + }) + + it('should allow multiple answers', () => { + var questions = [ + {id: 'browsers', multiple: true} + ] + + done = sinon.spy(answers => { + expect(answers.browsers).to.deep.equal(['Chrome', 'Safari']) + }) + + machine.process(questions, done) + machine.onLine('Chrome') + machine.onLine('Safari') + machine.onLine('') + expect(done).to.have.been.called + }) + + it('should treat spaces as confirmation of multiple answers', () => { + var questions = [ + {id: 'browsers', multiple: true} + ] + + done = sinon.spy(answers => { + expect(answers.browsers).to.deep.equal(['Chrome']) + }) + + machine.process(questions, done) + machine.onLine('Chrome') + machine.onLine(' ') + expect(done).to.have.been.called + }) + + it('should always return array for multiple', () => { + var questions = [ + {id: 'empty', multiple: true} + ] + + done = sinon.spy(answers => { + expect(answers.empty).to.deep.equal([]) + }) + + machine.process(questions, done) + machine.onLine('') + expect(done).to.have.been.called + }) + + it('should validate answers', () => { + var validator = sinon.spy() + var questions = [ + {id: 'validated', validate: validator} + ] + + machine.process(questions, done) + machine.onLine('something') + + expect(done).to.have.been.called + expect(validator).to.have.been.calledWith('something') + }) + + it('should allow conditional answers', () => { + var ifTrue = sinon.spy(answers => { + return answers.first === 'true' + }) + var ifFalse = sinon.spy(answers => { + return answers.first === 'false' + }) + + done = sinon.spy(answers => { + expect(answers.first).to.equal('true') + expect(answers.onlyIfTrue).to.equal('something') + expect(answers.onlyIfFalse).to.not.exist + }) + + var questions = [ + {id: 'first'}, + {id: 'onlyIfTrue', condition: ifTrue}, + {id: 'onlyIfFalse', condition: ifFalse} + ] + + machine.process(questions, done) + machine.onLine('true') + machine.onLine('something') + + expect(done).to.have.been.called + }) + + it('should parse booleans', () => { + done = sinon.spy(answers => { + expect(answers.yes).to.equal(true) + expect(answers.no).to.equal(false) + }) + + var questions = [ + {id: 'yes', options: ['yes', 'no'], boolean: true}, + {id: 'no', options: ['yes', 'no'], boolean: true} + ] + + machine.process(questions, done) + machine.onLine('yes') + machine.onLine('no') + + expect(done).to.have.been.called + }) + + it('should parse booleans before validation', () => { + var validator = sinon.spy(value => { + expect(typeof value).to.equal('boolean') + }) + + var questions = [ + {id: 'what', options: ['yes', 'no'], boolean: true, validate: validator}, + {id: 'really', options: ['yes', 'no'], boolean: true, validate: validator} + ] + + machine.process(questions, done) + machine.onLine('yes') + machine.onLine('no') + + expect(validator).to.have.been.calledTwice + }) +}) diff --git a/test/unit/launcher.spec.coffee b/test/unit/launcher.spec.coffee deleted file mode 100644 index 1859decab..000000000 --- a/test/unit/launcher.spec.coffee +++ /dev/null @@ -1,204 +0,0 @@ -#============================================================================== -# lib/launcher.js module -#============================================================================== -describe 'launcher', -> - Promise = require 'bluebird' - di = require 'di' - events = require '../../lib/events' - logger = require '../../lib/logger' - launcher = require '../../lib/launcher' - createMockTimer = require './mocks/timer' - - # mock out id generator - lastGeneratedId = null - launcher.Launcher.generateId = -> - ++lastGeneratedId - - # promise mock - stubPromise = (obj, method, stubAction) -> - promise = new Promise((resolve) -> - obj[method].resolve = resolve - ) - sinon.stub obj, method, -> - stubAction() if stubAction - promise - - - class FakeBrowser - constructor: (@id, @name, baseBrowserDecorator) -> - baseBrowserDecorator @ - FakeBrowser._instances.push @ - sinon.stub @, 'start', -> @state = @STATE_BEING_CAPTURED - stubPromise @, 'forceKill' - sinon.stub @, 'restart' - - class ScriptBrowser - constructor: (@id, @name, baseBrowserDecorator) -> - baseBrowserDecorator @ - ScriptBrowser._instances.push @ - sinon.stub @, 'start', -> @state = @STATE_BEING_CAPTURED - stubPromise @, 'forceKill' - sinon.stub @, 'restart' - - - beforeEach -> - lastGeneratedId = 0 - FakeBrowser._instances = [] - ScriptBrowser._instances = [] - - - #============================================================================ - # launcher.Launcher - #============================================================================ - describe 'Launcher', -> - l = emitter = null - - beforeEach -> - emitter = new events.EventEmitter() - injector = new di.Injector [{ - 'launcher:Fake': ['type', FakeBrowser] - 'launcher:Script': ['type', ScriptBrowser] - 'emitter': ['value', emitter] - 'config': ['value', {captureTimeout: 0}] - 'timer': ['factory', createMockTimer] - }] - l = new launcher.Launcher emitter, injector - - describe 'launch', -> - - it 'should inject and start all browsers', -> - l.launch ['Fake'], 'localhost', 1234, '/root/' - - browser = FakeBrowser._instances.pop() - expect(browser.start).to.have.been.calledWith 'http://localhost:1234/root/' - expect(browser.id).to.equal lastGeneratedId - expect(browser.name).to.equal 'Fake' - - - it 'should allow launching a script', -> - l.launch ['/usr/local/bin/special-browser'], 'localhost', 1234, '/' - - script = ScriptBrowser._instances.pop() - expect(script.start).to.have.been.calledWith 'http://localhost:1234/' - expect(script.name).to.equal '/usr/local/bin/special-browser' - - - it 'should use the non default host', -> - l.launch ['Fake'], 'whatever', 1234, '/root/' - - browser = FakeBrowser._instances.pop() - expect(browser.start).to.have.been.calledWith 'http://whatever:1234/root/' - - - describe 'restart', -> - it 'should restart the browser', -> - l.launch ['Fake'], 'localhost', 1234, '/root/' - browser = FakeBrowser._instances.pop() - - returnedValue = l.restart lastGeneratedId - expect(returnedValue).to.equal true - expect(browser.restart).to.have.been.called - - - it 'should return false if the browser was not launched by launcher (manual)', -> - l.launch [], 'localhost', 1234, '/' - expect(l.restart 'manual-id').to.equal false - - - describe 'kill', -> - it 'should kill browser with given id', (done) -> - killSpy = sinon.spy() - - l.launch ['Fake'] - browser = FakeBrowser._instances.pop() - - l.kill browser.id, done - expect(browser.forceKill).to.have.been.called - - browser.forceKill.resolve() - - - it 'should return false if browser does not exist, but still resolve the callback', (done) -> - l.launch ['Fake'] - browser = FakeBrowser._instances.pop() - - returnedValue = l.kill 'weird-id', done - expect(returnedValue).to.equal false - expect(browser.forceKill).not.to.have.been.called - - - it 'should not require a callback', (done) -> - l.launch ['Fake'] - browser = FakeBrowser._instances.pop() - - l.kill 'weird-id' - process.nextTick done - - - describe 'killAll', -> - - it 'should kill all running processe', -> - l.launch ['Fake', 'Fake'], 'localhost', 1234 - l.killAll() - - browser = FakeBrowser._instances.pop() - expect(browser.forceKill).to.have.been.called - - browser = FakeBrowser._instances.pop() - expect(browser.forceKill).to.have.been.called - - - it 'should call callback when all processes killed', -> - exitSpy = sinon.spy() - - l.launch ['Fake', 'Fake'], 'localhost', 1234 - l.killAll exitSpy - - expect(exitSpy).not.to.have.been.called - - # finish the first browser - browser = FakeBrowser._instances.pop() - browser.forceKill.resolve() - - scheduleNextTick -> - expect(exitSpy).not.to.have.been.called - - scheduleNextTick -> - # finish the second browser - browser = FakeBrowser._instances.pop() - browser.forceKill.resolve() - - scheduleNextTick -> - expect(exitSpy).to.have.been.called - - - it 'should call callback even if no browsers lanunched', (done) -> - l.killAll done - - - describe 'areAllCaptured', -> - - it 'should return true if only if all browsers captured', -> - l.launch ['Fake', 'Fake'], 'localhost', 1234 - - expect(l.areAllCaptured()).to.equal false - - l.markCaptured 1 - expect(l.areAllCaptured()).to.equal false - - l.markCaptured 2 - expect(l.areAllCaptured()).to.equal true - - - describe 'onExit', -> - - it 'should kill all browsers', (done) -> - l.launch ['Fake', 'Fake'], 'localhost', 1234, '/', 0, 1 - - emitter.emitAsync('exit').then done - - browser = FakeBrowser._instances.pop() - browser.forceKill.resolve() - - browser = FakeBrowser._instances.pop() - browser.forceKill.resolve() diff --git a/test/unit/launcher.spec.js b/test/unit/launcher.spec.js new file mode 100644 index 000000000..206eb22ea --- /dev/null +++ b/test/unit/launcher.spec.js @@ -0,0 +1,224 @@ +import Promise from 'bluebird' +import di from 'di' +import events from '../../lib/events' +import launcher from '../../lib/launcher' +import createMockTimer from './mocks/timer' + +// promise mock +var stubPromise = (obj, method, stubAction) => { + var promise = new Promise(resolve => { + obj[method].resolve = resolve + }) + + sinon.stub(obj, method, () => { + if (stubAction) stubAction() + + return promise + }) +} + +class FakeBrowser { + constructor (id, name, baseBrowserDecorator) { + this.id = id + this.name = name + baseBrowserDecorator(this) + FakeBrowser._instances.push(this) + sinon.stub(this, 'start', () => { + this.state = this.STATE_BEING_CAPTURED + return this.state + }) + stubPromise(this, 'forceKill') + sinon.stub(this, 'restart') + } +} + +class ScriptBrowser { + constructor (id, name, baseBrowserDecorator) { + this.id = id + this.name = name + baseBrowserDecorator(this) + ScriptBrowser._instances.push(this) + sinon.stub(this, 'start', () => { + this.state = this.STATE_BEING_CAPTURED + }) + stubPromise(this, 'forceKill') + sinon.stub(this, 'restart') + } +} + +describe('launcher', () => { + // mock out id generator + var lastGeneratedId = null + launcher.Launcher.generateId = () => { + return ++lastGeneratedId + } + + beforeEach(() => { + lastGeneratedId = 0 + FakeBrowser._instances = [] + ScriptBrowser._instances = [] + }) + + // ============================================================================ + // launcher.Launcher + // ============================================================================ + describe('Launcher', () => { + var emitter + var l = emitter = null + + beforeEach(() => { + emitter = new events.EventEmitter() + var injector = new di.Injector([{ + 'launcher:Fake': ['type', FakeBrowser], + 'launcher:Script': ['type', ScriptBrowser], + 'emitter': ['value', emitter], + 'config': ['value', {captureTimeout: 0}], + 'timer': ['factory', createMockTimer] + }]) + l = new launcher.Launcher(emitter, injector) + }) + + describe('launch', () => { + it('should inject and start all browsers', () => { + l.launch(['Fake'], 'localhost', 1234, '/root/') + + var browser = FakeBrowser._instances.pop() + expect(browser.start).to.have.been.calledWith('http://localhost:1234/root/') + expect(browser.id).to.equal(lastGeneratedId) + expect(browser.name).to.equal('Fake') + }) + + it('should allow launching a script', () => { + l.launch(['/usr/local/bin/special-browser'], 'localhost', 1234, '/') + + var script = ScriptBrowser._instances.pop() + expect(script.start).to.have.been.calledWith('http://localhost:1234/') + expect(script.name).to.equal('/usr/local/bin/special-browser') + }) + + it('should use the non default host', () => { + l.launch(['Fake'], 'whatever', 1234, '/root/') + + var browser = FakeBrowser._instances.pop() + expect(browser.start).to.have.been.calledWith('http://whatever:1234/root/') + }) + }) + + describe('restart', () => { + it('should restart the browser', () => { + l.launch(['Fake'], 'localhost', 1234, '/root/') + var browser = FakeBrowser._instances.pop() + + var returnedValue = l.restart(lastGeneratedId) + expect(returnedValue).to.equal(true) + expect(browser.restart).to.have.been.called + }) + + it('should return false if the browser was not launched by launcher (manual)', () => { + l.launch([], 'localhost', 1234, '/') + expect(l.restart('manual-id')).to.equal(false) + }) + }) + + describe('kill', () => { + it('should kill browser with given id', done => { + l.launch(['Fake']) + var browser = FakeBrowser._instances.pop() + + l.kill(browser.id, done) + expect(browser.forceKill).to.have.been.called + + browser.forceKill.resolve() + }) + + it('should return false if browser does not exist, but still resolve the callback', done => { + l.launch(['Fake']) + var browser = FakeBrowser._instances.pop() + + var returnedValue = l.kill('weird-id', done) + expect(returnedValue).to.equal(false) + expect(browser.forceKill).not.to.have.been.called + }) + + it('should not require a callback', done => { + l.launch(['Fake']) + FakeBrowser._instances.pop() + + l.kill('weird-id') + process.nextTick(done) + }) + }) + + describe('killAll', () => { + it('should kill all running processe', () => { + l.launch(['Fake', 'Fake'], 'localhost', 1234) + l.killAll() + + var browser = FakeBrowser._instances.pop() + expect(browser.forceKill).to.have.been.called + + browser = FakeBrowser._instances.pop() + expect(browser.forceKill).to.have.been.called + }) + + it('should call callback when all processes killed', () => { + var exitSpy = sinon.spy() + + l.launch(['Fake', 'Fake'], 'localhost', 1234) + l.killAll(exitSpy) + + expect(exitSpy).not.to.have.been.called + + // finish the first browser + var browser = FakeBrowser._instances.pop() + browser.forceKill.resolve() + + scheduleNextTick(() => { + expect(exitSpy).not.to.have.been.called + }) + + scheduleNextTick(() => { + // finish the second browser + browser = FakeBrowser._instances.pop() + browser.forceKill.resolve() + }) + + scheduleNextTick(() => { + expect(exitSpy).to.have.been.called + }) + }) + + it('should call callback even if no browsers lanunched', done => { + l.killAll(done) + }) + }) + + describe('areAllCaptured', () => { + it('should return true if only if all browsers captured', () => { + l.launch(['Fake', 'Fake'], 'localhost', 1234) + + expect(l.areAllCaptured()).to.equal(false) + + l.markCaptured(1) + expect(l.areAllCaptured()).to.equal(false) + + l.markCaptured(2) + expect(l.areAllCaptured()).to.equal(true) + }) + }) + + describe('onExit', () => { + it('should kill all browsers', done => { + l.launch(['Fake', 'Fake'], 'localhost', 1234, '/', 0, 1) + + emitter.emitAsync('exit').then(done) + + var browser = FakeBrowser._instances.pop() + browser.forceKill.resolve() + + browser = FakeBrowser._instances.pop() + browser.forceKill.resolve() + }) + }) + }) +}) diff --git a/test/unit/launchers/base.spec.coffee b/test/unit/launchers/base.spec.coffee deleted file mode 100644 index 375e0729c..000000000 --- a/test/unit/launchers/base.spec.coffee +++ /dev/null @@ -1,239 +0,0 @@ -describe 'launchers/base.js', -> - _ = require('../../../lib/helper')._ - BaseLauncher = require '../../../lib/launchers/base' - EventEmitter = require('../../../lib/events').EventEmitter - launcher = emitter = null - - beforeEach -> - emitter = new EventEmitter - launcher = new BaseLauncher 'fake-id', emitter - - it 'should manage state', -> - launcher.start 'http://localhost:9876/' - expect(launcher.state).to.equal launcher.STATE_BEING_CAPTURED - - launcher.markCaptured() - expect(launcher.state).to.equal launcher.STATE_CAPTURED - expect(launcher.isCaptured()).to.equal true - - - describe 'start', -> - - it 'should fire "start" event and pass url with id', -> - spyOnStart = sinon.spy() - launcher.on 'start', spyOnStart - launcher.start 'http://localhost:9876/' - - expect(spyOnStart).to.have.been.calledWith 'http://localhost:9876/?id=fake-id' - - - describe 'restart', -> - - it 'should kill running browser and start with previous url', (done) -> - spyOnStart = sinon.spy() - spyOnKill = sinon.spy() - launcher.on 'start', spyOnStart - launcher.on 'kill', spyOnKill - - launcher.start 'http://host:9988/' - spyOnStart.reset() - - launcher.restart() - expect(spyOnKill).to.have.been.called - expect(spyOnStart).to.not.have.been.called - - # the process (or whatever it is) actually finished - launcher._done() - spyOnKill.callArg 0 - - _.defer -> - expect(spyOnStart).to.have.been.calledWith 'http://host:9988/?id=fake-id' - done() - - - it 'should start when already finished (crashed)', (done) -> - spyOnStart = sinon.spy() - spyOnKill = sinon.spy() - spyOnDone = sinon.spy() - launcher.on 'start', spyOnStart - launcher.on 'kill', spyOnKill - - launcher.on 'done', -> launcher.restart() - launcher.on 'done', spyOnDone - - - launcher.start 'http://host:9988/' - spyOnStart.reset() - - # simulate crash - # the first onDone will restart - launcher._done 'crashed' - - _.defer -> - expect(spyOnKill).to.not.have.been.called - expect(spyOnStart).to.have.been.called - expect(spyOnDone).to.have.been.called - expect(spyOnDone).to.have.been.calledBefore spyOnStart - done() - - - it 'should not restart when being force killed', (done) -> - spyOnStart = sinon.spy() - spyOnKill = sinon.spy() - launcher.on 'start', spyOnStart - launcher.on 'kill', spyOnKill - - - launcher.start 'http://host:9988/' - spyOnStart.reset() - - onceKilled = launcher.forceKill() - - launcher.restart() - - # the process (or whatever it is) actually finished - launcher._done() - spyOnKill.callArg 0 - - onceKilled.done -> - expect(spyOnStart).to.not.have.been.called - done() - - - describe 'kill', -> - - it 'should manage state', (done) -> - onceKilled = launcher.kill() - expect(launcher.state).to.equal launcher.STATE_BEING_KILLED - - onceKilled.done -> - expect(launcher.state).to.equal launcher.STATE_FINISHED - done() - - it 'should fire "kill" and wait for all listeners to finish', (done) -> - spyOnKill1 = sinon.spy() - spyOnKill2 = sinon.spy() - spyKillDone = sinon.spy done - - launcher.on 'kill', spyOnKill1 - launcher.on 'kill', spyOnKill2 - - launcher.start 'http://localhost:9876/' - launcher.kill().then spyKillDone - expect(spyOnKill1).to.have.been.called - expect(spyOnKill2).to.have.been.called - expect(spyKillDone).to.not.have.been.called - - spyOnKill1.callArg 0 # the first listener is done - expect(spyKillDone).to.not.have.been.called - - spyOnKill2.callArg 0 # the second listener is done - - - it 'should not fire "kill" if already killed', (done) -> - spyOnKill = sinon.spy() - launcher.on 'kill', spyOnKill - - launcher.start 'http://localhost:9876/' - launcher.kill().then -> - spyOnKill.reset() - launcher.kill().then -> - expect(spyOnKill).to.not.have.been.called - done() - - spyOnKill.callArg 0 - - - it 'should not fire "kill" if already being killed, but wait for all listeners', (done) -> - spyOnKill = sinon.spy() - launcher.on 'kill', spyOnKill - - expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce = -> - expect(spyOnKill).to.have.been.called - expect(spyOnKill.callCount).to.equal 1 - expect(spyOnKill.finished).to.equal true - expect(launcher.state).to.equal launcher.STATE_FINISHED - - launcher.start 'http://localhost:9876/' - firstKilling = launcher.kill().then -> - expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce() - - secondKilling = launcher.kill().then -> - expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce() - - expect(launcher.state).to.equal launcher.STATE_BEING_KILLED - - _.defer -> - spyOnKill.finished = true - spyOnKill.callArg 0 - - # finish the test once everything is done - firstKilling.done -> secondKilling.done -> done() - - - it 'should not kill already crashed browser', (done) -> - spyOnKill = sinon.spy((killDone) -> killDone()) - launcher.on 'kill', spyOnKill - - launcher._done 'crash' - launcher.kill().done -> - expect(spyOnKill).to.not.have.been.called - done() - - - describe 'forceKill', -> - - it 'should cancel restart', (done) -> - spyOnStart = sinon.spy() - launcher.on 'start', spyOnStart - - launcher.start 'http://localhost:9876/' - spyOnStart.reset() - launcher.restart() - - launcher.forceKill().done -> - expect(launcher.state).to.equal launcher.STATE_FINISHED - expect(spyOnStart).to.not.have.been.called - done() - - - it 'should not fire "browser_process_failure" even if browser crashes', (done) -> - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - launcher.on 'kill', (killDone) -> - _.defer -> - launcher._done 'crashed' - killDone() - - launcher.start 'http://localhost:9876/' - launcher.forceKill().done -> - expect(spyOnBrowserProcessFailure).to.not.have.been.called - done() - - - describe 'markCaptured', -> - - it 'should not mark capture when killing', -> - launcher.kill() - launcher.markCaptured() - expect(launcher.state).to.not.equal launcher.STATE_CAPTURED - - - describe '_done', -> - - it 'should emit "browser_process_failure" if there is an error', -> - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - launcher._done 'crashed' - expect(spyOnBrowserProcessFailure).to.have.been.called - expect(spyOnBrowserProcessFailure).to.have.been.calledWith launcher - - - it 'should not emit "browser_process_failure" when no error happend', -> - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - launcher._done() - expect(spyOnBrowserProcessFailure).not.to.have.been.called diff --git a/test/unit/launchers/base.spec.js b/test/unit/launchers/base.spec.js new file mode 100644 index 000000000..427b15b53 --- /dev/null +++ b/test/unit/launchers/base.spec.js @@ -0,0 +1,258 @@ +var _ = require('../../../lib/helper')._ +import BaseLauncher from '../../../lib/launchers/base' +import {EventEmitter} from '../../../lib/events' + +describe('launchers/base.js', () => { + var emitter + var launcher + + beforeEach(() => { + emitter = new EventEmitter() + launcher = new BaseLauncher('fake-id', emitter) + }) + + it('should manage state', () => { + launcher.start('http://localhost:9876/') + expect(launcher.state).to.equal(launcher.STATE_BEING_CAPTURED) + + launcher.markCaptured() + expect(launcher.state).to.equal(launcher.STATE_CAPTURED) + expect(launcher.isCaptured()).to.equal(true) + }) + + describe('start', () => { + it('should fire "start" event and pass url with id', () => { + var spyOnStart = sinon.spy() + launcher.on('start', spyOnStart) + launcher.start('http://localhost:9876/') + + expect(spyOnStart).to.have.been.calledWith('http://localhost:9876/?id=fake-id') + }) + }) + + describe('restart', () => { + it('should kill running browser and start with previous url', done => { + var spyOnStart = sinon.spy() + var spyOnKill = sinon.spy() + launcher.on('start', spyOnStart) + launcher.on('kill', spyOnKill) + + launcher.start('http://host:9988/') + spyOnStart.reset() + + launcher.restart() + expect(spyOnKill).to.have.been.called + expect(spyOnStart).to.not.have.been.called + + // the process (or whatever it is) actually finished + launcher._done() + spyOnKill.callArg(0) + + _.defer(() => { + expect(spyOnStart).to.have.been.calledWith('http://host:9988/?id=fake-id') + done() + }) + }) + + it('should start when already finished (crashed)', done => { + var spyOnStart = sinon.spy() + var spyOnKill = sinon.spy() + var spyOnDone = sinon.spy() + launcher.on('start', spyOnStart) + launcher.on('kill', spyOnKill) + + launcher.on('done', () => launcher.restart()) + launcher.on('done', spyOnDone) + + launcher.start('http://host:9988/') + spyOnStart.reset() + + // simulate crash + // the first onDone will restart + launcher._done('crashed') + + _.defer(() => { + expect(spyOnKill).to.not.have.been.called + expect(spyOnStart).to.have.been.called + expect(spyOnDone).to.have.been.called + expect(spyOnDone).to.have.been.calledBefore(spyOnStart) + done() + }) + }) + + it('should not restart when being force killed', done => { + var spyOnStart = sinon.spy() + var spyOnKill = sinon.spy() + launcher.on('start', spyOnStart) + launcher.on('kill', spyOnKill) + + launcher.start('http://host:9988/') + spyOnStart.reset() + + var onceKilled = launcher.forceKill() + + launcher.restart() + + // the process (or whatever it is) actually finished + launcher._done() + spyOnKill.callArg(0) + + onceKilled.done(() => { + expect(spyOnStart).to.not.have.been.called + done() + }) + }) + }) + + describe('kill', () => { + it('should manage state', done => { + var onceKilled = launcher.kill() + expect(launcher.state).to.equal(launcher.STATE_BEING_KILLED) + + onceKilled.done(() => { + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + done() + }) + }) + + it('should fire "kill" and wait for all listeners to finish', done => { + var spyOnKill1 = sinon.spy() + var spyOnKill2 = sinon.spy() + var spyKillDone = sinon.spy(done) + + launcher.on('kill', spyOnKill1) + launcher.on('kill', spyOnKill2) + + launcher.start('http://localhost:9876/') + launcher.kill().then(spyKillDone) + expect(spyOnKill1).to.have.been.called + expect(spyOnKill2).to.have.been.called + expect(spyKillDone).to.not.have.been.called + + spyOnKill1.callArg(0) // the first listener is done + expect(spyKillDone).to.not.have.been.called + + spyOnKill2.callArg(0) + }) // the second listener is done + + it('should not fire "kill" if already killed', done => { + var spyOnKill = sinon.spy() + launcher.on('kill', spyOnKill) + + launcher.start('http://localhost:9876/') + launcher.kill().then(() => { + spyOnKill.reset() + launcher.kill().then(() => { + expect(spyOnKill).to.not.have.been.called + done() + }) + }) + + spyOnKill.callArg(0) + }) + + it('should not fire "kill" if already being killed, but wait for all listeners', done => { + var spyOnKill = sinon.spy() + launcher.on('kill', spyOnKill) + + var expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce = () => { + expect(spyOnKill).to.have.been.called + expect(spyOnKill.callCount).to.equal(1) + expect(spyOnKill.finished).to.equal(true) + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + } + + launcher.start('http://localhost:9876/') + var firstKilling = launcher.kill().then(() => { + expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce() + }) + + var secondKilling = launcher.kill().then(() => { + expectOnKillListenerIsAlreadyFinishedAndHasBeenOnlyCalledOnce() + }) + + expect(launcher.state).to.equal(launcher.STATE_BEING_KILLED) + + _.defer(() => { + spyOnKill.finished = true + spyOnKill.callArg(0) + }) + + // finish the test once everything is done + firstKilling.done(() => secondKilling.done(() => done())) + }) + + it('should not kill already crashed browser', done => { + var spyOnKill = sinon.spy(killDone => killDone()) + launcher.on('kill', spyOnKill) + + launcher._done('crash') + launcher.kill().done(() => { + expect(spyOnKill).to.not.have.been.called + done() + }) + }) + }) + + describe('forceKill', () => { + it('should cancel restart', done => { + var spyOnStart = sinon.spy() + launcher.on('start', spyOnStart) + + launcher.start('http://localhost:9876/') + spyOnStart.reset() + launcher.restart() + + launcher.forceKill().done(() => { + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + expect(spyOnStart).to.not.have.been.called + done() + }) + }) + + it('should not fire "browser_process_failure" even if browser crashes', done => { + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + launcher.on('kill', killDone => { + _.defer(() => { + launcher._done('crashed') + killDone() + }) + }) + + launcher.start('http://localhost:9876/') + launcher.forceKill().done(() => { + expect(spyOnBrowserProcessFailure).to.not.have.been.called + done() + }) + }) + }) + + describe('markCaptured', () => { + it('should not mark capture when killing', () => { + launcher.kill() + launcher.markCaptured() + expect(launcher.state).to.not.equal(launcher.STATE_CAPTURED) + }) + }) + + describe('_done', () => { + it('should emit "browser_process_failure" if there is an error', () => { + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + launcher._done('crashed') + expect(spyOnBrowserProcessFailure).to.have.been.called + expect(spyOnBrowserProcessFailure).to.have.been.calledWith(launcher) + }) + + it('should not emit "browser_process_failure" when no error happend', () => { + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + launcher._done() + expect(spyOnBrowserProcessFailure).not.to.have.been.called + }) + }) +}) diff --git a/test/unit/launchers/capture_timeout.spec.coffee b/test/unit/launchers/capture_timeout.spec.coffee deleted file mode 100644 index 7c2fff81d..000000000 --- a/test/unit/launchers/capture_timeout.spec.coffee +++ /dev/null @@ -1,54 +0,0 @@ -describe 'launchers/capture_timeout.js', -> - BaseLauncher = require '../../../lib/launchers/base' - CaptureTimeoutLauncher = require '../../../lib/launchers/capture_timeout' - createMockTimer = require '../mocks/timer' - launcher = timer = null - - beforeEach -> - timer = createMockTimer() - launcher = new BaseLauncher 'fake-id' - - sinon.spy launcher, 'kill' - - - it 'should kill if not captured in captureTimeout', -> - CaptureTimeoutLauncher.call launcher, timer, 10 - - launcher.start() - timer.wind 20 - expect(launcher.kill).to.have.been.called - - - it 'should not kill if browser got captured', -> - CaptureTimeoutLauncher.call launcher, timer, 10 - - launcher.start() - launcher.markCaptured() - timer.wind 20 - expect(launcher.kill).not.to.have.been.called - - - it 'should not do anything if captureTimeout = 0', -> - CaptureTimeoutLauncher.call launcher, timer, 0 - - launcher.start() - timer.wind 20 - expect(launcher.kill).not.to.have.been.called - - - it 'should clear timeout between restarts', (done) -> - CaptureTimeoutLauncher.call launcher, timer, 10 - - # simulate process finished - launcher.on 'kill', (onKillDone) -> - launcher._done() - onKillDone() - - launcher.start() - timer.wind 8 - launcher.kill().done -> - launcher.kill.reset() - launcher.start() - timer.wind 8 - expect(launcher.kill).not.to.have.been.called - done() diff --git a/test/unit/launchers/capture_timeout.spec.js b/test/unit/launchers/capture_timeout.spec.js new file mode 100644 index 000000000..6b5c41a99 --- /dev/null +++ b/test/unit/launchers/capture_timeout.spec.js @@ -0,0 +1,60 @@ +import BaseLauncher from '../../../lib/launchers/base' +import CaptureTimeoutLauncher from '../../../lib/launchers/capture_timeout' +import createMockTimer from '../mocks/timer' + +describe('launchers/capture_timeout.js', () => { + var timer + var launcher + + beforeEach(() => { + timer = createMockTimer() + launcher = new BaseLauncher('fake-id') + + sinon.spy(launcher, 'kill') + }) + + it('should kill if not captured in captureTimeout', () => { + CaptureTimeoutLauncher.call(launcher, timer, 10) + + launcher.start() + timer.wind(20) + expect(launcher.kill).to.have.been.called + }) + + it('should not kill if browser got captured', () => { + CaptureTimeoutLauncher.call(launcher, timer, 10) + + launcher.start() + launcher.markCaptured() + timer.wind(20) + expect(launcher.kill).not.to.have.been.called + }) + + it('should not do anything if captureTimeout = 0', () => { + CaptureTimeoutLauncher.call(launcher, timer, 0) + + launcher.start() + timer.wind(20) + expect(launcher.kill).not.to.have.been.called + }) + + it('should clear timeout between restarts', done => { + CaptureTimeoutLauncher.call(launcher, timer, 10) + + // simulate process finished + launcher.on('kill', onKillDone => { + launcher._done() + onKillDone() + }) + + launcher.start() + timer.wind(8) + launcher.kill().done(() => { + launcher.kill.reset() + launcher.start() + timer.wind(8) + expect(launcher.kill).not.to.have.been.called + done() + }) + }) +}) diff --git a/test/unit/launchers/process.spec.coffee b/test/unit/launchers/process.spec.coffee deleted file mode 100644 index d0d232d36..000000000 --- a/test/unit/launchers/process.spec.coffee +++ /dev/null @@ -1,225 +0,0 @@ -describe 'launchers/process.js', -> - path = require 'path' - _ = require('../../../lib/helper')._ - BaseLauncher = require '../../../lib/launchers/base' - RetryLauncher = require '../../../lib/launchers/retry' - CaptureTimeoutLauncher = require '../../../lib/launchers/capture_timeout' - ProcessLauncher = require '../../../lib/launchers/process' - EventEmitter = require('../../../lib/events').EventEmitter - createMockTimer = require '../mocks/timer' - launcher = timer = emitter = mockSpawn = mockTempDir = null - - BROWSER_PATH = path.normalize '/usr/bin/browser' - - beforeEach -> - emitter = new EventEmitter - launcher = new BaseLauncher 'fake-id', emitter - - mockSpawn = sinon.spy (cmd, args) -> - process = new EventEmitter - process.stderr = new EventEmitter - process.kill = sinon.spy() - process.exitCode = null - mockSpawn._processes.push process - process - mockSpawn._processes = [] - - mockTempDir = - getPath: (suffix) -> '/temp' + suffix - create: sinon.spy() - remove: sinon.spy() - - it 'should create a temp directory', -> - ProcessLauncher.call launcher, mockSpawn, mockTempDir - launcher._getCommand = -> null - - launcher.start 'http://host:9988/' - expect(launcher._tempDir).to.equal '/temp/karma-fake-id' - expect(mockTempDir.create).to.have.been.calledWith '/temp/karma-fake-id' - - - it 'should remove the temp directory', (done) -> - ProcessLauncher.call launcher, mockSpawn, mockTempDir - launcher._getCommand = -> null - - launcher.start 'http://host:9988/' - launcher.kill() - - _.defer -> - expect(mockTempDir.remove).to.have.been.called - expect(mockTempDir.remove.args[0][0]).to.equal '/temp/karma-fake-id' - done() - - - describe '_normalizeCommand', -> - it 'should remove quotes from the cmd', -> - ProcessLauncher.call launcher, null, mockTempDir - - expect(launcher._normalizeCommand '"/bin/brow ser"').to.equal path.normalize('/bin/brow ser') - expect(launcher._normalizeCommand '\'/bin/brow ser\'').to.equal - path.normalize('/bin/brow ser') - expect(launcher._normalizeCommand '`/bin/brow ser`').to.equal path.normalize('/bin/brow ser') - - - describe 'with RetryLauncher', -> - it 'should handle spawn ENOENT error and not even retry', (done) -> - ProcessLauncher.call launcher, mockSpawn, mockTempDir - RetryLauncher.call launcher, 2 - launcher._getCommand = -> BROWSER_PATH - - failureSpy = sinon.spy() - emitter.on 'browser_process_failure', failureSpy - - launcher.start 'http://host:9876/' - mockSpawn._processes[0].emit 'error', {code: 'ENOENT'} - mockSpawn._processes[0].emit 'exit', 1 - mockTempDir.remove.callArg 1 - - _.defer -> - expect(launcher.state).to.equal launcher.STATE_FINISHED - expect(failureSpy).to.have.been.called - done() - - - # higher level tests with Retry and CaptureTimeout launchers - describe 'flow', -> - mockTimer = failureSpy = null - - beforeEach -> - mockTimer = createMockTimer() - CaptureTimeoutLauncher.call launcher, mockTimer, 100 - ProcessLauncher.call launcher, mockSpawn, mockTempDir, mockTimer - RetryLauncher.call launcher, 2 - - launcher._getCommand = -> BROWSER_PATH - - failureSpy = sinon.spy() - emitter.on 'browser_process_failure', failureSpy - - - # the most common scenario, when everything works fine - it 'start -> capture -> kill', (done) -> - # start the browser - launcher.start 'http://localhost/' - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - - # mark captured - launcher.markCaptured() - - # kill it - killingLauncher = launcher.kill() - expect(launcher.state).to.equal launcher.STATE_BEING_KILLED - expect(mockSpawn._processes[0].kill).to.have.been.called - - # process exits - mockSpawn._processes[0].emit 'exit', 0 - mockTempDir.remove.callArg 1 - - killingLauncher.done -> - expect(launcher.state).to.equal launcher.STATE_FINISHED - done() - - - # when the browser fails to get captured in given timeout, it should restart - it 'start -> timeout -> restart', (done) -> - # start - launcher.start 'http://localhost/' - - # expect starting the process - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - - # timeout - mockTimer.wind 101 - - # expect killing browser - expect(browserProcess.kill).to.have.been.called - browserProcess.emit 'exit', 0 - mockTempDir.remove.callArg 1 - mockSpawn.reset() - - _.defer -> _.delay -> - # expect re-starting - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - expect(failureSpy).not.to.have.been.called - done() - , 100 - - it 'start -> timeout -> 3xrestart -> failure', (done) -> - # start - launcher.start 'http://localhost/' - - # expect starting - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - mockSpawn.reset() - - # timeout - first time - mockTimer.wind 101 - - # expect killing browser - expect(browserProcess.kill).to.have.been.called - browserProcess.emit 'exit', 0 - mockTempDir.remove.callArg 1 - mockTempDir.remove.reset() - - _.defer -> - # expect re-starting - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - expect(failureSpy).not.to.have.been.called - mockSpawn.reset() - - # timeout - second time - mockTimer.wind 101 - - # expect killing browser - expect(browserProcess.kill).to.have.been.called - browserProcess.emit 'exit', 0 - mockTempDir.remove.callArg 1 - mockTempDir.remove.reset() - - _.defer -> - # expect re-starting - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - expect(failureSpy).not.to.have.been.called - mockSpawn.reset() - - # timeout - third time - mockTimer.wind 201 - - # expect killing browser - expect(browserProcess.kill).to.have.been.called - browserProcess.emit 'exit', 0 - mockTempDir.remove.callArg 1 - mockTempDir.remove.reset() - - _.defer -> - expect(mockSpawn).to.not.have.been.called - expect(failureSpy).to.have.been.called - done() - - - # when the browser fails to start, it should restart - it 'start -> crash -> restart', (done) -> - # start - launcher.start 'http://localhost/' - - # expect starting the process - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - mockSpawn.reset() - - # crash - browserProcess.emit 'exit', 1 - mockTempDir.remove.callArg 1 - mockTempDir.remove.reset() - - _.defer -> - # expect re-starting - expect(mockSpawn).to.have.been.calledWith BROWSER_PATH, ['http://localhost/?id=fake-id'] - browserProcess = mockSpawn._processes.shift() - - expect(failureSpy).not.to.have.been.called - done() diff --git a/test/unit/launchers/process.spec.js b/test/unit/launchers/process.spec.js new file mode 100644 index 000000000..9c5e90456 --- /dev/null +++ b/test/unit/launchers/process.spec.js @@ -0,0 +1,247 @@ +import path from 'path' +var _ = require('../../../lib/helper')._ +import BaseLauncher from '../../../lib/launchers/base' +import RetryLauncher from '../../../lib/launchers/retry' +import CaptureTimeoutLauncher from '../../../lib/launchers/capture_timeout' +import ProcessLauncher from '../../../lib/launchers/process' +import {EventEmitter} from '../../../lib/events' +import createMockTimer from '../mocks/timer' + +describe('launchers/process.js', () => { + var emitter + var mockSpawn + var mockTempDir + var launcher + + var BROWSER_PATH = path.normalize('/usr/bin/browser') + + beforeEach(() => { + emitter = new EventEmitter() + launcher = new BaseLauncher('fake-id', emitter) + + mockSpawn = sinon.spy(function (cmd, args) { + var process = new EventEmitter() + process.stderr = new EventEmitter() + process.kill = sinon.spy() + process.exitCode = null + mockSpawn._processes.push(process) + return process + }) + + mockSpawn._processes = [] + + mockTempDir = { + getPath: suffix => `/temp${suffix}`, + create: sinon.spy(), + remove: sinon.spy() + } + }) + + it('should create a temp directory', () => { + ProcessLauncher.call(launcher, mockSpawn, mockTempDir) + launcher._getCommand = () => null + + launcher.start('http://host:9988/') + expect(launcher._tempDir).to.equal('/temp/karma-fake-id') + expect(mockTempDir.create).to.have.been.calledWith('/temp/karma-fake-id') + }) + + it('should remove the temp directory', (done) => { + ProcessLauncher.call(launcher, mockSpawn, mockTempDir) + launcher._getCommand = () => null + + launcher.start('http://host:9988/') + launcher.kill() + + _.defer(() => { + expect(mockTempDir.remove).to.have.been.called + expect(mockTempDir.remove.args[0][0]).to.equal('/temp/karma-fake-id') + done() + }) + }) + + describe('_normalizeCommand', () => { + it('should remove quotes from the cmd', () => { + ProcessLauncher.call(launcher, null, mockTempDir) + + expect(launcher._normalizeCommand('"/bin/brow ser"')).to.equal(path.normalize('/bin/brow ser')) + expect(launcher._normalizeCommand("'/bin/brow ser'")).to.equal + path.normalize('/bin/brow ser') + expect(launcher._normalizeCommand('`/bin/brow ser`')).to.equal(path.normalize('/bin/brow ser')) + }) + }) + + describe('with RetryLauncher', () => { + it('should handle spawn ENOENT error and not even retry', (done) => { + ProcessLauncher.call(launcher, mockSpawn, mockTempDir) + RetryLauncher.call(launcher, 2) + launcher._getCommand = () => BROWSER_PATH + + var failureSpy = sinon.spy() + emitter.on('browser_process_failure', failureSpy) + + launcher.start('http://host:9876/') + mockSpawn._processes[0].emit('error', {code: 'ENOENT'}) + mockSpawn._processes[0].emit('exit', 1) + mockTempDir.remove.callArg(1) + + _.defer(() => { + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + expect(failureSpy).to.have.been.called + done() + }) + }) + }) + + // higher level tests with Retry and CaptureTimeout launchers + describe('flow', () => { + var failureSpy + var mockTimer = failureSpy = null + + beforeEach(() => { + mockTimer = createMockTimer() + CaptureTimeoutLauncher.call(launcher, mockTimer, 100) + ProcessLauncher.call(launcher, mockSpawn, mockTempDir, mockTimer) + RetryLauncher.call(launcher, 2) + + launcher._getCommand = () => BROWSER_PATH + + failureSpy = sinon.spy() + emitter.on('browser_process_failure', failureSpy) + }) + + // the most common scenario, when everything works fine + it('start -> capture -> kill', (done) => { + // start the browser + launcher.start('http://localhost/') + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + + // mark captured + launcher.markCaptured() + + // kill it + var killingLauncher = launcher.kill() + expect(launcher.state).to.equal(launcher.STATE_BEING_KILLED) + expect(mockSpawn._processes[0].kill).to.have.been.called + + // process exits + mockSpawn._processes[0].emit('exit', 0) + mockTempDir.remove.callArg(1) + + killingLauncher.done(() => { + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + done() + }) + }) + + // when the browser fails to get captured in given timeout, it should restart + it('start -> timeout -> restart', (done) => { + // start + launcher.start('http://localhost/') + + // expect starting the process + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + var browserProcess = mockSpawn._processes.shift() + + // timeout + mockTimer.wind(101) + + // expect killing browser + expect(browserProcess.kill).to.have.been.called + browserProcess.emit('exit', 0) + mockTempDir.remove.callArg(1) + mockSpawn.reset() + + _.defer(() => _.delay(() => { + // expect re-starting + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + expect(failureSpy).not.to.have.been.called + done() + }, 100)) + }) + + it('start -> timeout -> 3xrestart -> failure', (done) => { + // start + launcher.start('http://localhost/') + + // expect starting + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + var browserProcess = mockSpawn._processes.shift() + mockSpawn.reset() + + // timeout - first time + mockTimer.wind(101) + + // expect killing browser + expect(browserProcess.kill).to.have.been.called + browserProcess.emit('exit', 0) + mockTempDir.remove.callArg(1) + mockTempDir.remove.reset() + + _.defer(() => { + // expect re-starting + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + browserProcess = mockSpawn._processes.shift() + expect(failureSpy).not.to.have.been.called + mockSpawn.reset() + + // timeout - second time + mockTimer.wind(101) + + // expect killing browser + expect(browserProcess.kill).to.have.been.called + browserProcess.emit('exit', 0) + mockTempDir.remove.callArg(1) + mockTempDir.remove.reset() + + _.defer(() => { + // expect re-starting + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + browserProcess = mockSpawn._processes.shift() + expect(failureSpy).not.to.have.been.called + mockSpawn.reset() + + // timeout - third time + mockTimer.wind(201) + + // expect killing browser + expect(browserProcess.kill).to.have.been.called + browserProcess.emit('exit', 0) + mockTempDir.remove.callArg(1) + mockTempDir.remove.reset() + + _.defer(() => { + expect(mockSpawn).to.not.have.been.called + expect(failureSpy).to.have.been.called + done() + }) + }) + }) + }) + + // when the browser fails to start, it should restart + it('start -> crash -> restart', (done) => { + // start + launcher.start('http://localhost/') + + // expect starting the process + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + var browserProcess = mockSpawn._processes.shift() + mockSpawn.reset() + + // crash + browserProcess.emit('exit', 1) + mockTempDir.remove.callArg(1) + mockTempDir.remove.reset() + + _.defer(() => { + // expect re-starting + expect(mockSpawn).to.have.been.calledWith(BROWSER_PATH, ['http://localhost/?id=fake-id']) + browserProcess = mockSpawn._processes.shift() + + expect(failureSpy).not.to.have.been.called + done() + }) + }) + }) +}) diff --git a/test/unit/launchers/retry.spec.coffee b/test/unit/launchers/retry.spec.coffee deleted file mode 100644 index 90fd0843d..000000000 --- a/test/unit/launchers/retry.spec.coffee +++ /dev/null @@ -1,83 +0,0 @@ -describe 'launchers/retry.js', -> - _ = require('../../../lib/helper')._ - BaseLauncher = require '../../../lib/launchers/base' - RetryLauncher = require '../../../lib/launchers/retry' - EventEmitter = require('../../../lib/events').EventEmitter - createMockTimer = require '../mocks/timer' - launcher = timer = emitter = null - - beforeEach -> - timer = createMockTimer() - emitter = new EventEmitter - launcher = new BaseLauncher 'fake-id', emitter - - - it 'should restart if browser crashed', (done) -> - RetryLauncher.call launcher, 2 - - launcher.start 'http://localhost:9876' - - sinon.spy launcher, 'start' - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - # simulate crash - launcher._done 'crash' - - _.defer -> - expect(launcher.start).to.have.been.called - expect(spyOnBrowserProcessFailure).not.to.have.been.called - done() - - - it 'should eventually fail with "browser_process_failure"', (done) -> - RetryLauncher.call launcher, 2 - - launcher.start 'http://localhost:9876' - - sinon.spy launcher, 'start' - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - # simulate first crash - launcher._done 'crash' - - _.defer -> - expect(launcher.start).to.have.been.called - expect(spyOnBrowserProcessFailure).not.to.have.been.called - launcher.start.reset() - - # simulate second crash - launcher._done 'crash' - - _.defer -> - expect(launcher.start).to.have.been.called - expect(spyOnBrowserProcessFailure).not.to.have.been.called - launcher.start.reset() - - # simulate third crash - launcher._done 'crash' - - _.defer -> - expect(launcher.start).not.to.have.been.called - expect(spyOnBrowserProcessFailure).to.have.been.called - done() - - - it 'should not restart if killed normally', (done) -> - RetryLauncher.call launcher, 2 - - launcher.start 'http://localhost:9876' - - sinon.spy launcher, 'start' - spyOnBrowserProcessFailure = sinon.spy() - emitter.on 'browser_process_failure', spyOnBrowserProcessFailure - - # process just exited normally - launcher._done() - - _.defer -> - expect(launcher.start).not.to.have.been.called - expect(spyOnBrowserProcessFailure).not.to.have.been.called - expect(launcher.state).to.equal launcher.STATE_FINISHED - done() diff --git a/test/unit/launchers/retry.spec.js b/test/unit/launchers/retry.spec.js new file mode 100644 index 000000000..b7cb609a1 --- /dev/null +++ b/test/unit/launchers/retry.spec.js @@ -0,0 +1,90 @@ +var _ = require('../../../lib/helper')._ +import BaseLauncher from '../../../lib/launchers/base' +import RetryLauncher from '../../../lib/launchers/retry' +import {EventEmitter} from '../../../lib/events' + +describe('launchers/retry.js', () => { + var emitter + var launcher + + beforeEach(() => { + emitter = new EventEmitter() + launcher = new BaseLauncher('fake-id', emitter) + }) + + it('should restart if browser crashed', (done) => { + RetryLauncher.call(launcher, 2) + + launcher.start('http://localhost:9876') + + sinon.spy(launcher, 'start') + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + // simulate crash + launcher._done('crash') + + _.defer(() => { + expect(launcher.start).to.have.been.called + expect(spyOnBrowserProcessFailure).not.to.have.been.called + done() + }) + }) + + it('should eventually fail with "browser_process_failure"', (done) => { + RetryLauncher.call(launcher, 2) + + launcher.start('http://localhost:9876') + + sinon.spy(launcher, 'start') + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + // simulate first crash + launcher._done('crash') + + _.defer(() => { + expect(launcher.start).to.have.been.called + expect(spyOnBrowserProcessFailure).not.to.have.been.called + launcher.start.reset() + + // simulate second crash + launcher._done('crash') + + _.defer(() => { + expect(launcher.start).to.have.been.called + expect(spyOnBrowserProcessFailure).not.to.have.been.called + launcher.start.reset() + + // simulate third crash + launcher._done('crash') + + _.defer(() => { + expect(launcher.start).not.to.have.been.called + expect(spyOnBrowserProcessFailure).to.have.been.called + done() + }) + }) + }) + }) + + it('should not restart if killed normally', (done) => { + RetryLauncher.call(launcher, 2) + + launcher.start('http://localhost:9876') + + sinon.spy(launcher, 'start') + var spyOnBrowserProcessFailure = sinon.spy() + emitter.on('browser_process_failure', spyOnBrowserProcessFailure) + + // process just exited normally + launcher._done() + + _.defer(() => { + expect(launcher.start).not.to.have.been.called + expect(spyOnBrowserProcessFailure).not.to.have.been.called + expect(launcher.state).to.equal(launcher.STATE_FINISHED) + done() + }) + }) +}) diff --git a/test/unit/logger.spec.coffee b/test/unit/logger.spec.coffee deleted file mode 100644 index 9b91e5763..000000000 --- a/test/unit/logger.spec.coffee +++ /dev/null @@ -1,22 +0,0 @@ -#============================================================================== -# lib/logger.js module -#============================================================================== - -describe 'logger', -> - loadFile = require('mocks').loadFile - logSpy = m = null - beforeEach -> - logSpy = sinon.spy() - m = loadFile __dirname + '/../../lib/logger.js' - - #============================================================================ - # setup() - #============================================================================ - describe 'setup', -> - it 'should allow for configuration via setup() using an array', -> - m.setup 'INFO', true, [ - type: 'file' - filename: 'test/unit/test.log' - ] - - expect(m.log4js.appenders).to.have.keys ['console', 'file'] diff --git a/test/unit/logger.spec.js b/test/unit/logger.spec.js new file mode 100644 index 000000000..a4516b96e --- /dev/null +++ b/test/unit/logger.spec.js @@ -0,0 +1,20 @@ +import {loadFile} from 'mocks' + +describe('logger', () => { + var m + + beforeEach(() => { + m = loadFile(__dirname + '/../../lib/logger.js') + }) + + describe('setup', () => { + it('should allow for configuration via setup() using an array', () => { + m.setup('INFO', true, [{ + type: 'file', + filename: 'test/unit/test.log' + }]) + + expect(m.log4js.appenders).to.have.keys(['console', 'file']) + }) + }) +}) diff --git a/test/unit/middleware/karma.spec.coffee b/test/unit/middleware/karma.spec.coffee deleted file mode 100644 index 029796e0f..000000000 --- a/test/unit/middleware/karma.spec.coffee +++ /dev/null @@ -1,369 +0,0 @@ -describe 'middleware.karma', -> - helper = require '../../../lib/helper' - constants = require '../../../lib/constants' - - mocks = require 'mocks' - HttpResponseMock = mocks.http.ServerResponse - HttpRequestMock = mocks.http.ServerRequest - - File = require('../../../lib/file') - Url = require('../../../lib/url') - - MockFile = (path, sha) -> - File.call @, path - @sha = sha or 'sha-default' - - fsMock = mocks.fs.create - karma: - static: - 'client.html': mocks.fs.file(0, 'CLIENT HTML\n%X_UA_COMPATIBLE%%X_UA_COMPATIBLE_URL%') - 'context.html': mocks.fs.file(0, 'CONTEXT\n%SCRIPTS%') - 'debug.html': mocks.fs.file(0, 'DEBUG\n%SCRIPTS%\n%X_UA_COMPATIBLE%') - 'karma.js': mocks.fs.file(0, 'root: %KARMA_URL_ROOT%, v: %KARMA_VERSION%') - - createServeFile = require('../../../lib/middleware/common').createServeFile - createKarmaMiddleware = require('../../../lib/middleware/karma').create - - handler = serveFile = filesDeferred = nextSpy = response = null - - beforeEach -> - clientConfig = foo: 'bar' - nextSpy = sinon.spy() - response = new HttpResponseMock - filesDeferred = helper.defer() - serveFile = createServeFile fsMock, '/karma/static' - handler = createKarmaMiddleware filesDeferred.promise, serveFile, - '/base/path', '/__karma__/', clientConfig - - # helpers - includedFiles = (files) -> - filesDeferred.resolve {included: files, served: []} - - servedFiles = (files) -> - filesDeferred.resolve {included: [], served: files} - - normalizedHttpRequest = (urlPath) -> - req = new HttpRequestMock(urlPath) - req.normalizedUrl = req.url - return req - - callHandlerWith = (urlPath, next) -> - promise = handler normalizedHttpRequest(urlPath), response, next or nextSpy - if promise and promise.done then promise.done() - - - it 'should redirect urlRoot without trailing slash', (done) -> - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 301, 'MOVED PERMANENTLY' - expect(response._headers['Location']).to.equal '/__karma__/' - done() - - callHandlerWith '/__karma__' - - - it 'should not serve outside of urlRoot', -> - handler normalizedHttpRequest('/'), null, nextSpy - expect(nextSpy).to.have.been.called - nextSpy.reset() - - handler normalizedHttpRequest('/client.html'), null, nextSpy - expect(nextSpy).to.have.been.called - nextSpy.reset() - - handler normalizedHttpRequest('/debug.html'), null, nextSpy - expect(nextSpy).to.have.been.called - nextSpy.reset() - - handler normalizedHttpRequest('/context.html'), null, nextSpy - expect(nextSpy).to.have.been.called - - - it 'should serve client.html', (done) -> - handler = createKarmaMiddleware null, serveFile, '/base', '/' - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CLIENT HTML' - done() - - callHandlerWith '/' - - - it 'should serve /?id=xxx', (done) -> - handler = createKarmaMiddleware null, serveFile, '/base', '/' - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CLIENT HTML' - done() - - callHandlerWith '/?id=123' - - it 'should serve /?x-ua-compatible with replaced values', (done) -> - handler = createKarmaMiddleware null, serveFile, '/base', '/' - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, - 'CLIENT HTML\n' + - '?x-ua-compatible=xxx%3Dyyy' - done() - - callHandlerWith '/?x-ua-compatible=xxx%3Dyyy' - - it 'should serve debug.html/?x-ua-compatible with replaced values', (done) -> - includedFiles [] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, - 'DEBUG\n\n' - done() - - callHandlerWith '/__karma__/debug.html?x-ua-compatible=xxx%3Dyyy' - - it 'should serve karma.js with version and urlRoot variables', (done) -> - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'root: /__karma__/, v: ' + constants.VERSION - expect(response._headers['Content-Type']).to.equal 'application/javascript' - done() - - callHandlerWith '/__karma__/karma.js' - - - it 'should serve context.html with replaced script tags', (done) -> - includedFiles [ - new MockFile('/first.js', 'sha123') - new MockFile('/second.dart', 'sha456') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CONTEXT\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/context.html' - - - it 'should serve context.html with replaced link tags', (done) -> - includedFiles [ - new MockFile('/first.css', 'sha007') - new MockFile('/second.html', 'sha678') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CONTEXT\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/context.html' - - it 'should serve context.html with the correct path for the script tags', (done) -> - includedFiles [ - new MockFile('/some/abc/a.js', 'sha') - new MockFile('/base/path/b.js', 'shaaa') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CONTEXT\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/context.html' - - - it 'should serve context.html with the correct path for link tags', (done) -> - includedFiles [ - new MockFile('/some/abc/a.css', 'sha1') - new MockFile('/base/path/b.css', 'sha2') - new MockFile('/some/abc/c.html', 'sha3') - new MockFile('/base/path/d.html', 'sha4') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'CONTEXT\n' + - '\n' + - '\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/context.html' - - it 'should serve context.json with the correct paths for all files', (done) -> - includedFiles [ - new MockFile('/some/abc/a.css', 'sha1') - new MockFile('/base/path/b.css', 'sha2') - new MockFile('/some/abc/c.html', 'sha3') - new MockFile('/base/path/d.html', 'sha4') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, JSON.stringify { - files: [ - '/__karma__/absolute/some/abc/a.css?sha1', - '/__karma__/base/b.css?sha2', - '/__karma__/absolute/some/abc/c.html?sha3', - '/__karma__/base/d.html?sha4' - ] - } - done() - - callHandlerWith '/__karma__/context.json' - - - it 'should not change urls', (done) -> - includedFiles [ - new Url('http://some.url.com/whatever') - ] - - response.once 'end', -> - expect(response).to.beServedAs 200, 'CONTEXT\n' + - '' - done() - - callHandlerWith '/__karma__/context.html' - - - it 'should send non-caching headers for context.html', (done) -> - ZERO_DATE = (new Date 0).toString() - - includedFiles [] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response._headers['Cache-Control']).to.equal 'no-cache' - # idiotic IE8 needs more - expect(response._headers['Pragma']).to.equal 'no-cache' - expect(response._headers['Expires']).to.equal ZERO_DATE - done() - - callHandlerWith '/__karma__/context.html' - - - it 'should inline mappings with all served files', (done) -> - fsMock._touchFile '/karma/static/context.html', 0, '%MAPPINGS%' - servedFiles [ - new MockFile('/some/abc/a.js', 'sha_a') - new MockFile('/base/path/b.js', 'sha_b') - new MockFile('\\windows\\path\\uuu\\c.js', 'sha_c') - ] - - response.once 'end', -> - expect(response).to.beServedAs 200, 'window.__karma__.files = {\n' + - " '/__karma__/absolute/some/abc/a.js': 'sha_a',\n" + - " '/__karma__/base/b.js': 'sha_b',\n" + - " '/__karma__/absolute\\\\windows\\\\path\\\\uuu\\\\c.js': 'sha_c'\n" + - "};\n" - done() - - callHandlerWith '/__karma__/context.html' - - - it 'should serve debug.html with replaced script tags without timestamps', (done) -> - includedFiles [ - new MockFile('/first.js') - new MockFile('/base/path/b.js') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'DEBUG\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/debug.html' - - - it 'should serve debug.html with replaced link tags without timestamps', (done) -> - includedFiles [ - new MockFile('/first.css') - new MockFile('/base/path/b.css') - new MockFile('/second.html') - new MockFile('/base/path/d.html') - ] - - response.once 'end', -> - expect(nextSpy).not.to.have.been.called - expect(response).to.beServedAs 200, 'DEBUG\n' + - '\n' + - '\n' + - '\n' + - '' - done() - - callHandlerWith '/__karma__/debug.html' - - - it 'should inline client config to debug.html', (done) -> - includedFiles [ - new MockFile('/first.js') - ] - fsMock._touchFile '/karma/static/debug.html', 1, '%CLIENT_CONFIG%' - - response.once 'end', -> - expect(response).to.beServedAs 200, 'window.__karma__.config = {"foo":"bar"};\n' - done() - - callHandlerWith '/__karma__/debug.html' - - - it 'should not serve other files even if they are in urlRoot', (done) -> - includedFiles [] - - callHandlerWith '/__karma__/something/else.js', -> - expect(response).to.beNotServed() - done() - - - - - - - - # it 'should invoke custom handler', (done) -> - # response.once 'end', -> - # expect(nextSpy).not.to.have.been.called - # expect(response.statusCode).to.equal 200 - # expect(response._content.toString()).to.equal 'Hello World' - # done() - - # customHandler = - # urlRegex: /\/test/, - # handler: (request, response, staticFolder, adapterFolder, baseFolder, urlRoot) -> - # response.end 'Hello World' - - # karmaSrcHandler = m.createKarmaSourceHandler promiseContainer, staticFolderPath, - # adapterFolderPath, baseFolder, '/_karma_/', [customHandler], [] - # karmaSrcHandler new httpMock.ServerRequest('/_karma_/test'), response, nextSpy - - - # it 'should set custom script type', (done) -> - # mocks.fs._touchFile '/karma/static/context.html', 0, 'CONTEXT\n%SCRIPTS%' - # includedFiles [{path: 'http://some.url.com/whatever.blah', isUrl: true}] - - # response.once 'end', -> - # expect(response._content.toString()).to.equal 'CONTEXT\n' + - # '' - # expect(response.statusCode).to.equal 200 - # done() - - # customScriptType = - # extension: 'blah', - # contentType: 'application/blah' - - # karmaSrcHandler = m.createKarmaSourceHandler promiseContainer, staticFolderPath, - # adapterFolderPath, baseFolder, '/_karma_/', [], [customScriptType] - - # karmaSrcHandler new httpMock.ServerRequest('/_karma_/context.html'), response, nextSpy diff --git a/test/unit/middleware/karma.spec.js b/test/unit/middleware/karma.spec.js new file mode 100644 index 000000000..841fe8a4f --- /dev/null +++ b/test/unit/middleware/karma.spec.js @@ -0,0 +1,339 @@ +import helper from '../../../lib/helper' +import constants from '../../../lib/constants' +import File from '../../../lib/file' +import Url from '../../../lib/url' +import mocks from 'mocks' + +var HttpResponseMock = mocks.http.ServerResponse +var HttpRequestMock = mocks.http.ServerRequest + +describe('middleware.karma', () => { + var serveFile + var filesDeferred + var nextSpy + var response + + var MockFile = function (path, sha) { + File.call(this, path) + this.sha = sha || 'sha-default' + } + + var fsMock = mocks.fs.create({ + karma: { + static: { + 'client.html': mocks.fs.file(0, 'CLIENT HTML\n%X_UA_COMPATIBLE%%X_UA_COMPATIBLE_URL%'), + 'context.html': mocks.fs.file(0, 'CONTEXT\n%SCRIPTS%'), + 'debug.html': mocks.fs.file(0, 'DEBUG\n%SCRIPTS%\n%X_UA_COMPATIBLE%'), + 'karma.js': mocks.fs.file(0, 'root: %KARMA_URL_ROOT%, v: %KARMA_VERSION%') + } + } + }) + + var createServeFile = require('../../../lib/middleware/common').createServeFile + var createKarmaMiddleware = require('../../../lib/middleware/karma').create + + var handler = serveFile = filesDeferred = nextSpy = response = null + + beforeEach(() => { + var clientConfig = {foo: 'bar'} + nextSpy = sinon.spy() + response = new HttpResponseMock() + filesDeferred = helper.defer() + serveFile = createServeFile(fsMock, '/karma/static') + handler = createKarmaMiddleware(filesDeferred.promise, serveFile, '/base/path', '/__karma__/', clientConfig) + }) + + // helpers + var includedFiles = (files) => { + return filesDeferred.resolve({included: files, served: []}) + } + + var servedFiles = (files) => { + return filesDeferred.resolve({included: [], served: files}) + } + + var normalizedHttpRequest = (urlPath) => { + var req = new HttpRequestMock(urlPath) + req.normalizedUrl = req.url + return req + } + + var callHandlerWith = function (urlPath, next) { + var promise = handler(normalizedHttpRequest(urlPath), response, next || nextSpy) + if (promise && promise.done) promise.done() + } + + it('should redirect urlRoot without trailing slash', (done) => { + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(301, 'MOVED PERMANENTLY') + expect(response._headers['Location']).to.equal('/__karma__/') + done() + }) + + callHandlerWith('/__karma__') + }) + + it('should not serve outside of urlRoot', () => { + handler(normalizedHttpRequest('/'), null, nextSpy) + expect(nextSpy).to.have.been.called + nextSpy.reset() + + handler(normalizedHttpRequest('/client.html'), null, nextSpy) + expect(nextSpy).to.have.been.called + nextSpy.reset() + + handler(normalizedHttpRequest('/debug.html'), null, nextSpy) + expect(nextSpy).to.have.been.called + nextSpy.reset() + + handler(normalizedHttpRequest('/context.html'), null, nextSpy) + expect(nextSpy).to.have.been.called + }) + + it('should serve client.html', (done) => { + handler = createKarmaMiddleware(null, serveFile, '/base', '/') + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CLIENT HTML') + done() + }) + + callHandlerWith('/') + }) + + it('should serve /?id=xxx', (done) => { + handler = createKarmaMiddleware(null, serveFile, '/base', '/') + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CLIENT HTML') + done() + }) + + callHandlerWith('/?id=123') + }) + + it('should serve /?x-ua-compatible with replaced values', (done) => { + handler = createKarmaMiddleware(null, serveFile, '/base', '/') + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CLIENT HTML\n?x-ua-compatible=xxx%3Dyyy') + done() + }) + + callHandlerWith('/?x-ua-compatible=xxx%3Dyyy') + }) + + it('should serve debug.html/?x-ua-compatible with replaced values', (done) => { + includedFiles([]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'DEBUG\n\n') + done() + }) + + callHandlerWith('/__karma__/debug.html?x-ua-compatible=xxx%3Dyyy') + }) + + it('should serve karma.js with version and urlRoot variables', (done) => { + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'root: /__karma__/, v: ' + constants.VERSION) + expect(response._headers['Content-Type']).to.equal('application/javascript') + done() + }) + + callHandlerWith('/__karma__/karma.js') + }) + + it('should serve context.html with replaced script tags', (done) => { + includedFiles([ + new MockFile('/first.js', 'sha123'), + new MockFile('/second.dart', 'sha456') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CONTEXT\n\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should serve context.html with replaced link tags', (done) => { + includedFiles([ + new MockFile('/first.css', 'sha007'), + new MockFile('/second.html', 'sha678') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CONTEXT\n\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should serve context.html with the correct path for the script tags', (done) => { + includedFiles([ + new MockFile('/some/abc/a.js', 'sha'), + new MockFile('/base/path/b.js', 'shaaa') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CONTEXT\n\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should serve context.html with the correct path for link tags', (done) => { + includedFiles([ + new MockFile('/some/abc/a.css', 'sha1'), + new MockFile('/base/path/b.css', 'sha2'), + new MockFile('/some/abc/c.html', 'sha3'), + new MockFile('/base/path/d.html', 'sha4') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'CONTEXT\n\n\n\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should serve context.json with the correct paths for all files', (done) => { + includedFiles([ + new MockFile('/some/abc/a.css', 'sha1'), + new MockFile('/base/path/b.css', 'sha2'), + new MockFile('/some/abc/c.html', 'sha3'), + new MockFile('/base/path/d.html', 'sha4') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, JSON.stringify({ + files: [ + '/__karma__/absolute/some/abc/a.css?sha1', + '/__karma__/base/b.css?sha2', + '/__karma__/absolute/some/abc/c.html?sha3', + '/__karma__/base/d.html?sha4' + ] + })) + done() + }) + + callHandlerWith('/__karma__/context.json') + }) + + it('should not change urls', (done) => { + includedFiles([ + new Url('http://some.url.com/whatever') + ]) + + response.once('end', () => { + expect(response).to.beServedAs(200, 'CONTEXT\n') + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should send non-caching headers for context.html', (done) => { + var ZERO_DATE = (new Date(0)).toString() + + includedFiles([]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response._headers['Cache-Control']).to.equal('no-cache') + // idiotic IE8 needs more + expect(response._headers['Pragma']).to.equal('no-cache') + expect(response._headers['Expires']).to.equal(ZERO_DATE) + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should inline mappings with all served files', (done) => { + fsMock._touchFile('/karma/static/context.html', 0, '%MAPPINGS%') + servedFiles([ + new MockFile('/some/abc/a.js', 'sha_a'), + new MockFile('/base/path/b.js', 'sha_b'), + new MockFile('\\windows\\path\\uuu\\c.js', 'sha_c') + ]) + + response.once('end', () => { + expect(response).to.beServedAs(200, "window.__karma__.files = {\n '/__karma__/absolute/some/abc/a.js': 'sha_a',\n '/__karma__/base/b.js': 'sha_b',\n '/__karma__/absolute\\\\windows\\\\path\\\\uuu\\\\c.js': 'sha_c'\n};\n") + done() + }) + + callHandlerWith('/__karma__/context.html') + }) + + it('should serve debug.html with replaced script tags without timestamps', (done) => { + includedFiles([ + new MockFile('/first.js'), + new MockFile('/base/path/b.js') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'DEBUG\n\n') + done() + }) + + callHandlerWith('/__karma__/debug.html') + }) + + it('should serve debug.html with replaced link tags without timestamps', (done) => { + includedFiles([ + new MockFile('/first.css'), + new MockFile('/base/path/b.css'), + new MockFile('/second.html'), + new MockFile('/base/path/d.html') + ]) + + response.once('end', () => { + expect(nextSpy).not.to.have.been.called + expect(response).to.beServedAs(200, 'DEBUG\n\n\n\n') + done() + }) + + callHandlerWith('/__karma__/debug.html') + }) + + it('should inline client config to debug.html', (done) => { + includedFiles([ + new MockFile('/first.js') + ]) + fsMock._touchFile('/karma/static/debug.html', 1, '%CLIENT_CONFIG%') + + response.once('end', () => { + expect(response).to.beServedAs(200, 'window.__karma__.config = {"foo":"bar"};\n') + done() + }) + + callHandlerWith('/__karma__/debug.html') + }) + + it('should not serve other files even if they are in urlRoot', (done) => { + includedFiles([]) + + callHandlerWith('/__karma__/something/else.js', () => { + expect(response).to.beNotServed() + done() + }) + }) +}) diff --git a/test/unit/middleware/proxy.spec.coffee b/test/unit/middleware/proxy.spec.coffee deleted file mode 100644 index 5c9e80ea3..000000000 --- a/test/unit/middleware/proxy.spec.coffee +++ /dev/null @@ -1,263 +0,0 @@ -#============================================================================== -# lib/proxy.js module -#============================================================================== -describe 'middleware.proxy', -> - httpMock = require('mocks').http - loadFile = require('mocks').loadFile - - actualOptions = requestedUrl = response = nextSpy = type = null - - m = loadFile __dirname + '/../../../lib/middleware/proxy.js' - - mockProxies = [{ - path: '/proxy', - baseUrl: '', - host: 'localhost', - port: '9000', - proxy: { - web: (req, res) -> - type = 'web' - requestedUrl = req.url - res.writeHead 200 - res.end 'DONE' - ws: (req, socket, head) -> - type = 'ws' - requestedUrl = req.url - } - }, { - path: '/static', - baseUrl: '', - host: 'gstatic.com', - port: '80', - proxy: { - web: (req, res) -> - type = 'web' - requestedUrl = req.url - res.writeHead 200 - res.end 'DONE' - ws: (req, socket, head) -> - type = 'ws' - requestedUrl = req.url - } - }, { - path: '/sub/some', - baseUrl: '/something', - host: 'gstatic.com', - port: '80', - proxy: { - web: (req, res) -> - type = 'web' - requestedUrl = req.url - res.writeHead 200 - res.end 'DONE' - ws: (req, socket, head) -> - type = 'ws' - requestedUrl = req.url - } - }, { - path: '/sub', - baseUrl: '', - host: 'localhost', - port: '9000', - proxy: { - web: (req, res) -> - type = 'web' - requestedUrl = req.url - res.writeHead 200 - res.end 'DONE' - ws: (req, socket, head) -> - type = 'ws' - requestedUrl = req.url - } - }] - - beforeEach -> - actualOptions = {} - requestedUrl = '' - type = '' - response = new httpMock.ServerResponse - nextSpy = sinon.spy() - - - it 'should proxy requests', (done) -> - proxy = m.createProxyHandler mockProxies, true, '/', {} - proxy new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy - - expect(nextSpy).not.to.have.been.called - expect(requestedUrl).to.equal '/test.html' - expect(type).to.equal 'web' - done() - - it 'should proxy websocket requests', (done) -> - proxy = m.createProxyHandler mockProxies, true, '/', {} - proxy.upgrade new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy - - expect(nextSpy).not.to.have.been.called - expect(requestedUrl).to.equal '/test.html' - expect(type).to.equal 'ws' - done() - - it 'should support multiple proxies', -> - proxy = m.createProxyHandler mockProxies, true, '/', {} - proxy new httpMock.ServerRequest('/static/test.html'), response, nextSpy - - expect(nextSpy).not.to.have.been.called - expect(requestedUrl).to.equal '/test.html' - expect(type).to.equal 'web' - - it 'should handle nested proxies', -> - proxy = m.createProxyHandler mockProxies, true, '/', {} - proxy new httpMock.ServerRequest('/sub/some/Test.html'), response, nextSpy - - expect(nextSpy).not.to.have.been.called - expect(requestedUrl).to.equal '/something/Test.html' - expect(type).to.equal 'web' - - - it 'should call next handler if the path is not proxied', -> - proxy = m.createProxyHandler mockProxies, true, '/', {} - proxy new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy - - expect(nextSpy).to.have.been.called - - - it 'should call next handler if no proxy defined', -> - proxy = m.createProxyHandler {}, true, '/', {} - proxy new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy - - expect(nextSpy).to.have.been.called - - - it 'should parse a simple proxy config', -> - proxy = {'/base/': 'http://localhost:8000/'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '8000', - baseUrl: '/', - path: '/base/', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should set defualt http port', -> - proxy = {'/base/': 'http://localhost/'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '80', - baseUrl: '/', - path: '/base/', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should set defualt https port', -> - proxy = {'/base/': 'https://localhost/'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '443', - baseUrl: '/', - path: '/base/', - https: true - } - expect(parsedProxyConfig[0].proxy).to.exist - - - it 'should handle proxy configs with paths', -> - proxy = {'/base': 'http://localhost:8000/proxy'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '8000', - baseUrl: '/proxy', - path: '/base', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should determine protocol', -> - proxy = {'/base':'https://localhost:8000'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '8000', - baseUrl: '', - path: '/base', - https: true - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should handle proxy configs with only basepaths', -> - proxy = {'/base': '/proxy/test'} - config = {port: 9877, hostname: 'localhost'} - parsedProxyConfig = m.parseProxyConfig proxy, config - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: 9877, - baseUrl: '/proxy/test', - path: '/base', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should normalize proxy url with only basepaths', -> - proxy = {'/base/': '/proxy/test'} - config = {port: 9877, hostname: 'localhost'} - parsedProxyConfig = m.parseProxyConfig proxy, config - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: 9877, - baseUrl: '/proxy/test/', - path: '/base/', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should normalize proxy url', -> - proxy = {'/base/': 'http://localhost:8000/proxy/test'} - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 1 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'localhost', - port: '8000', - baseUrl: '/proxy/test/', - path: '/base/', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - - it 'should parse nested proxy config', -> - proxy = { - '/sub': 'http://localhost:9000' - '/sub/some': 'http://gstatic.com/something' - } - parsedProxyConfig = m.parseProxyConfig proxy, {} - expect(parsedProxyConfig).to.have.length 2 - expect(parsedProxyConfig[0]).to.containSubset { - host: 'gstatic.com', - port: '80', - baseUrl: '/something', - path: '/sub/some', - https: false - } - expect(parsedProxyConfig[0].proxy).to.exist - expect(parsedProxyConfig[1]).to.containSubset { - host: 'localhost', - port: '9000', - baseUrl: '', - path: '/sub', - https: false - } - expect(parsedProxyConfig[1].proxy).to.exist - - it 'should handle empty proxy config', -> - expect(m.parseProxyConfig {}).to.deep.equal([]) diff --git a/test/unit/middleware/proxy.spec.js b/test/unit/middleware/proxy.spec.js new file mode 100644 index 000000000..d44961128 --- /dev/null +++ b/test/unit/middleware/proxy.spec.js @@ -0,0 +1,283 @@ +var httpMock = require('mocks').http +var loadFile = require('mocks').loadFile + +describe('middleware.proxy', () => { + var requestedUrl + var response + var nextSpy + var type + + var m = loadFile(__dirname + '/../../../lib/middleware/proxy.js') + + var mockProxies = [{ + path: '/proxy', + baseUrl: '', + host: 'localhost', + port: '9000', + proxy: { + web: function (req, res) { + type = 'web' + requestedUrl = req.url + res.writeHead(200) + res.end('DONE') + }, + ws: function (req, socket, head) { + type = 'ws' + requestedUrl = req.url + } + } + }, { + path: '/static', + baseUrl: '', + host: 'gstatic.com', + port: '80', + proxy: { + web: function (req, res) { + type = 'web' + requestedUrl = req.url + res.writeHead(200) + res.end('DONE') + }, + ws: function (req, socket, head) { + type = 'ws' + requestedUrl = req.url + } + } + }, { + path: '/sub/some', + baseUrl: '/something', + host: 'gstatic.com', + port: '80', + proxy: { + web: function (req, res) { + type = 'web' + requestedUrl = req.url + res.writeHead(200) + res.end('DONE') + }, + ws: function (req, socket, head) { + type = 'ws' + requestedUrl = req.url + } + } + }, { + path: '/sub', + baseUrl: '', + host: 'localhost', + port: '9000', + proxy: { + web: function (req, res) { + type = 'web' + requestedUrl = req.url + res.writeHead(200) + res.end('DONE') + }, + ws: function (req, socket, head) { + type = 'ws' + requestedUrl = req.url + } + } + }] + + beforeEach(() => { + requestedUrl = '' + type = '' + response = new httpMock.ServerResponse() + nextSpy = sinon.spy() + }) + + it('should proxy requests', (done) => { + var proxy = m.createProxyHandler(mockProxies, true, '/', {}) + proxy(new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy) + + expect(nextSpy).not.to.have.been.called + expect(requestedUrl).to.equal('/test.html') + expect(type).to.equal('web') + done() + }) + + it('should proxy websocket requests', (done) => { + var proxy = m.createProxyHandler(mockProxies, true, '/', {}) + proxy.upgrade(new httpMock.ServerRequest('/proxy/test.html'), response, nextSpy) + + expect(nextSpy).not.to.have.been.called + expect(requestedUrl).to.equal('/test.html') + expect(type).to.equal('ws') + done() + }) + + it('should support multiple proxies', () => { + var proxy = m.createProxyHandler(mockProxies, true, '/', {}) + proxy(new httpMock.ServerRequest('/static/test.html'), response, nextSpy) + + expect(nextSpy).not.to.have.been.called + expect(requestedUrl).to.equal('/test.html') + expect(type).to.equal('web') + }) + + it('should handle nested proxies', () => { + var proxy = m.createProxyHandler(mockProxies, true, '/', {}) + proxy(new httpMock.ServerRequest('/sub/some/Test.html'), response, nextSpy) + + expect(nextSpy).not.to.have.been.called + expect(requestedUrl).to.equal('/something/Test.html') + expect(type).to.equal('web') + }) + + it('should call next handler if the path is not proxied', () => { + var proxy = m.createProxyHandler(mockProxies, true, '/', {}) + proxy(new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy) + + expect(nextSpy).to.have.been.called + }) + + it('should call next handler if no proxy defined', () => { + var proxy = m.createProxyHandler({}, true, '/', {}) + proxy(new httpMock.ServerRequest('/non/proxy/test.html'), response, nextSpy) + + expect(nextSpy).to.have.been.called + }) + + it('should parse a simple proxy config', () => { + var proxy = {'/base/': 'http://localhost:8000/'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '8000', + baseUrl: '/', + path: '/base/', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should set defualt http port', () => { + var proxy = {'/base/': 'http://localhost/'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '80', + baseUrl: '/', + path: '/base/', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should set defualt https port', () => { + var proxy = {'/base/': 'https://localhost/'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '443', + baseUrl: '/', + path: '/base/', + https: true + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should handle proxy configs with paths', () => { + var proxy = {'/base': 'http://localhost:8000/proxy'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '8000', + baseUrl: '/proxy', + path: '/base', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should determine protocol', () => { + var proxy = {'/base': 'https://localhost:8000'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '8000', + baseUrl: '', + path: '/base', + https: true + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should handle proxy configs with only basepaths', () => { + var proxy = {'/base': '/proxy/test'} + var config = {port: 9877, hostname: 'localhost'} + var parsedProxyConfig = m.parseProxyConfig(proxy, config) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: 9877, + baseUrl: '/proxy/test', + path: '/base', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should normalize proxy url with only basepaths', () => { + var proxy = {'/base/': '/proxy/test'} + var config = {port: 9877, hostname: 'localhost'} + var parsedProxyConfig = m.parseProxyConfig(proxy, config) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: 9877, + baseUrl: '/proxy/test/', + path: '/base/', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should normalize proxy url', () => { + var proxy = {'/base/': 'http://localhost:8000/proxy/test'} + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(1) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'localhost', + port: '8000', + baseUrl: '/proxy/test/', + path: '/base/', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + }) + + it('should parse nested proxy config', () => { + var proxy = { + '/sub': 'http://localhost:9000', + '/sub/some': 'http://gstatic.com/something' + } + var parsedProxyConfig = m.parseProxyConfig(proxy, {}) + expect(parsedProxyConfig).to.have.length(2) + expect(parsedProxyConfig[0]).to.containSubset({ + host: 'gstatic.com', + port: '80', + baseUrl: '/something', + path: '/sub/some', + https: false + }) + expect(parsedProxyConfig[0].proxy).to.exist + expect(parsedProxyConfig[1]).to.containSubset({ + host: 'localhost', + port: '9000', + baseUrl: '', + path: '/sub', + https: false + }) + expect(parsedProxyConfig[1].proxy).to.exist + }) + + it('should handle empty proxy config', () => { + expect(m.parseProxyConfig({})).to.deep.equal([]) + }) +}) diff --git a/test/unit/middleware/runner.spec.coffee b/test/unit/middleware/runner.spec.coffee deleted file mode 100644 index 6b851aeb3..000000000 --- a/test/unit/middleware/runner.spec.coffee +++ /dev/null @@ -1,192 +0,0 @@ -describe 'middleware.runner', -> - - mocks = require 'mocks' - HttpResponseMock = mocks.http.ServerResponse - HttpRequestMock = mocks.http.ServerRequest - - path = require('path') - EventEmitter = require('events').EventEmitter - Browser = require '../../../lib/browser' - BrowserCollection = require '../../../lib/browser_collection' - MultReporter = require('../../../lib/reporters/multi') - createRunnerMiddleware = require('../../../lib/middleware/runner').create - Promise = require('bluebird') - - handler = nextSpy = response = mockReporter = capturedBrowsers = emitter = config = null - fileListMock = executor = null - - beforeEach -> - mockReporter = - adapters: [] - write: (msg) -> @adapters.forEach (adapter) -> adapter msg - - executor = - schedule: -> emitter.emit 'run_start' - - emitter = new EventEmitter - capturedBrowsers = new BrowserCollection emitter - fileListMock = - refresh: -> Promise.resolve(null) - addFile: -> null - removeFile: -> null - changeFile: -> null - - nextSpy = sinon.spy() - response = new HttpResponseMock - config = {client: {}, basePath: '/'} - - handler = createRunnerMiddleware emitter, fileListMock, capturedBrowsers, - new MultReporter([mockReporter]), executor, 'localhost', 8877, '/', config - - - it 'should trigger test run and stream the reporter', (done) -> - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - - response.once 'end', -> - expect(nextSpy).to.not.have.been.called - expect(response).to.beServedAs 200, 'result\x1FEXIT0' - done() - - handler new HttpRequestMock('/__run__'), response, nextSpy - - # Wrap this in a setTimeout so the fileListPromise has time to resolve. - setTimeout( -> - mockReporter.write 'result' - emitter.emit 'run_complete', capturedBrowsers, {exitCode: 0} - , 2) - - - it 'should not run if there is no browser captured', (done) -> - sinon.stub fileListMock, 'refresh' - - response.once 'end', -> - expect(nextSpy).to.not.have.been.called - expect(response).to.beServedAs 200, 'No captured browser, open http://localhost:8877/\n' - expect(fileListMock.refresh).not.to.have.been.called - done() - - handler new HttpRequestMock('/__run__'), response, nextSpy - - - it 'should parse body and set client.args', (done) -> - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - - emitter.once 'run_start', -> - expect(config.client.args).to.deep.equal ['arg1', 'arg2'] - done() - - RAW_MESSAGE = '{"args": ["arg1", "arg2"]}' - - request = new HttpRequestMock '/__run__', { - 'content-type': 'application/json' - 'content-length': RAW_MESSAGE.length - } - - handler request, response, nextSpy - - request.emit 'data', RAW_MESSAGE - request.emit 'end' - - - it 'should refresh explicit files if specified', (done) -> - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - sinon.stub fileListMock, 'refresh' - sinon.stub fileListMock, 'addFile' - sinon.stub fileListMock, 'changeFile' - sinon.stub fileListMock, 'removeFile' - - RAW_MESSAGE = JSON.stringify - addedFiles: ['/new.js'] - removedFiles: ['/foo.js', '/bar.js'] - changedFiles: ['/changed.js'] - - request = new HttpRequestMock '/__run__', { - 'content-type': 'application/json' - 'content-length': RAW_MESSAGE.length - } - - handler request, response, nextSpy - - request.emit 'data', RAW_MESSAGE - request.emit 'end' - - process.nextTick -> - expect(fileListMock.refresh).not.to.have.been.called - expect(fileListMock.addFile).to.have.been.calledWith path.resolve('/new.js') - expect(fileListMock.removeFile).to.have.been.calledWith path.resolve('/foo.js') - expect(fileListMock.removeFile).to.have.been.calledWith path.resolve('/bar.js') - expect(fileListMock.changeFile).to.have.been.calledWith path.resolve('/changed.js') - done() - - it 'should schedule execution if no refresh', (done) -> - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - - sinon.stub fileListMock, 'refresh' - sinon.stub executor, 'schedule' - - RAW_MESSAGE = JSON.stringify {refresh: false} - - request = new HttpRequestMock '/__run__', { - 'content-type': 'application/json' - 'content-length': RAW_MESSAGE.length - } - - handler request, response, nextSpy - - request.emit 'data', RAW_MESSAGE - request.emit 'end' - - process.nextTick -> - expect(fileListMock.refresh).not.to.have.been.called - expect(executor.schedule).to.have.been.called - done() - - it 'should wait for refresh to finish if applicable before scheduling execution', (done) -> - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - - resolve = null - fileListPromise = new Promise (_resolve, _reject) -> - resolve = _resolve - sinon.stub(fileListMock, 'refresh').returns fileListPromise - sinon.stub executor, 'schedule' - - request = new HttpRequestMock '/__run__' - handler request, response, nextSpy - - process.nextTick -> - expect(fileListMock.refresh).to.have.been.called - expect(executor.schedule).to.not.have.been.called - - # Now try resolving the promise - resolve() - setTimeout(-> - expect(executor.schedule).to.have.been.called - done() - , 2) - - it 'should not schedule execution if refreshing and autoWatch', (done) -> - config.autoWatch = true - - capturedBrowsers.add new Browser - sinon.stub capturedBrowsers, 'areAllReady', -> true - - sinon.stub(fileListMock, 'refresh').returns Promise.resolve(null) - sinon.stub executor, 'schedule' - - handler new HttpRequestMock('/__run__'), response, nextSpy - - process.nextTick -> - expect(fileListMock.refresh).to.have.been.called - expect(executor.schedule).not.to.have.been.called - done() - - - it 'should ignore other urls', (done) -> - handler new HttpRequestMock('/something'), response, -> - expect(response).to.beNotServed() - done() diff --git a/test/unit/middleware/runner.spec.js b/test/unit/middleware/runner.spec.js new file mode 100644 index 000000000..d469d5e54 --- /dev/null +++ b/test/unit/middleware/runner.spec.js @@ -0,0 +1,218 @@ +import path from 'path' +import {EventEmitter} from 'events' +import mocks from 'mocks' +import {Promise} from 'bluebird' +import _ from 'lodash' +import Browser from '../../../lib/browser' +import BrowserCollection from '../../../lib/browser_collection' +import MultReporter from '../../../lib/reporters/multi' +var createRunnerMiddleware = require('../../../lib/middleware/runner').create +var HttpResponseMock = mocks.http.ServerResponse +var HttpRequestMock = mocks.http.ServerRequest + +describe('middleware.runner', () => { + var nextSpy + var response + var mockReporter + var capturedBrowsers + var emitter + var config + var executor + var handler + var fileListMock + + beforeEach(() => { + mockReporter = { + adapters: [], + write (msg) { + return this.adapters.forEach(adapter => adapter(msg)) + } + } + + executor = { + schedule: () => emitter.emit('run_start') + } + + emitter = new EventEmitter() + capturedBrowsers = new BrowserCollection(emitter) + fileListMock = { + refresh: () => Promise.resolve(), + addFile: () => null, + removeFile: () => null, + changeFile: () => null + } + + nextSpy = sinon.spy() + response = new HttpResponseMock() + config = {client: {}, basePath: '/'} + + handler = createRunnerMiddleware(emitter, fileListMock, capturedBrowsers, + new MultReporter([mockReporter]), executor, 'localhost', 8877, '/', config) + }) + + it('should trigger test run and stream the reporter', (done) => { + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + + response.once('end', () => { + expect(nextSpy).to.not.have.been.called + expect(response).to.beServedAs(200, 'result\x1FEXIT0') + done() + }) + + handler(new HttpRequestMock('/__run__'), response, nextSpy) + + // Wrap this in a setTimeout so the fileListPromise has time to resolve. + _.delay(() => { + mockReporter.write('result') + emitter.emit('run_complete', capturedBrowsers, {exitCode: 0}) + }) + }) + + it('should not run if there is no browser captured', (done) => { + sinon.stub(fileListMock, 'refresh') + + response.once('end', () => { + expect(nextSpy).to.not.have.been.called + expect(response).to.beServedAs(200, 'No captured browser, open http://localhost:8877/\n') + expect(fileListMock.refresh).not.to.have.been.called + done() + }) + + handler(new HttpRequestMock('/__run__'), response, nextSpy) + }) + + it('should parse body and set client.args', (done) => { + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + + emitter.once('run_start', () => { + expect(config.client.args).to.deep.equal(['arg1', 'arg2']) + done() + }) + + var RAW_MESSAGE = '{"args": ["arg1", "arg2"]}' + + var request = new HttpRequestMock('/__run__', { + 'content-type': 'application/json', + 'content-length': RAW_MESSAGE.length + }) + + handler(request, response, nextSpy) + + request.emit('data', RAW_MESSAGE) + request.emit('end') + }) + + it('should refresh explicit files if specified', (done) => { + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + sinon.stub(fileListMock, 'refresh') + sinon.stub(fileListMock, 'addFile') + sinon.stub(fileListMock, 'changeFile') + sinon.stub(fileListMock, 'removeFile') + + var RAW_MESSAGE = JSON.stringify({ + addedFiles: ['/new.js'], + removedFiles: ['/foo.js', '/bar.js'], + changedFiles: ['/changed.js'] + }) + + var request = new HttpRequestMock('/__run__', { + 'content-type': 'application/json', + 'content-length': RAW_MESSAGE.length + }) + + handler(request, response, nextSpy) + + request.emit('data', RAW_MESSAGE) + request.emit('end') + + process.nextTick(() => { + expect(fileListMock.refresh).not.to.have.been.called + expect(fileListMock.addFile).to.have.been.calledWith(path.resolve('/new.js')) + expect(fileListMock.removeFile).to.have.been.calledWith(path.resolve('/foo.js')) + expect(fileListMock.removeFile).to.have.been.calledWith(path.resolve('/bar.js')) + expect(fileListMock.changeFile).to.have.been.calledWith(path.resolve('/changed.js')) + done() + }) + }) + + it('should wait for refresh to finish if applicable before scheduling execution', (done) => { + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + + var resolve = null + var fileListPromise = new Promise((_resolve, _reject) => { + resolve = _resolve + }) + sinon.stub(fileListMock, 'refresh').returns(fileListPromise) + sinon.stub(executor, 'schedule') + + var request = new HttpRequestMock('/__run__') + handler(request, response, nextSpy) + + process.nextTick(() => { + expect(fileListMock.refresh).to.have.been.called + expect(executor.schedule).to.not.have.been.called + + // Now try resolving the promise + resolve() + setTimeout(() => { + expect(executor.schedule).to.have.been.called + done() + }, 2) + }) + }) + + it('should schedule execution if no refresh', (done) => { + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + + sinon.spy(fileListMock, 'refresh') + sinon.stub(executor, 'schedule') + + var RAW_MESSAGE = JSON.stringify({refresh: false}) + + var request = new HttpRequestMock('/__run__', { + 'content-type': 'application/json', + 'content-length': RAW_MESSAGE.length + }) + + handler(request, response, nextSpy) + + request.emit('data', RAW_MESSAGE) + request.emit('end') + + process.nextTick(() => { + expect(fileListMock.refresh).not.to.have.been.called + expect(executor.schedule).to.have.been.called + done() + }) + }) + + it('should not schedule execution if refreshing and autoWatch', (done) => { + config.autoWatch = true + + capturedBrowsers.add(new Browser()) + sinon.stub(capturedBrowsers, 'areAllReady', () => true) + + sinon.spy(fileListMock, 'refresh') + sinon.stub(executor, 'schedule') + + handler(new HttpRequestMock('/__run__'), response, nextSpy) + + process.nextTick(() => { + expect(fileListMock.refresh).to.have.been.called + expect(executor.schedule).not.to.have.been.called + done() + }) + }) + + it('should ignore other urls', (done) => { + handler(new HttpRequestMock('/something'), response, () => { + expect(response).to.beNotServed() + done() + }) + }) +}) diff --git a/test/unit/middleware/source_files.spec.coffee b/test/unit/middleware/source_files.spec.coffee deleted file mode 100644 index 56c01f8c5..000000000 --- a/test/unit/middleware/source_files.spec.coffee +++ /dev/null @@ -1,184 +0,0 @@ -http = require 'http' -mocks = require 'mocks' -request = require 'supertest-as-promised' -helper = require '../../../lib/helper' -File = require('../../../lib/file') -Url = require('../../../lib/url') -createServeFile = require('../../../lib/middleware/common').createServeFile -createSourceFilesMiddleware = require('../../../lib/middleware/source_files').create - -describe 'middleware.source_files', -> - server = next = files = null - - fsMock = mocks.fs.create - base: - path: - 'a.js': mocks.fs.file(0, 'js-src-a') - 'index.html': mocks.fs.file(0, '') - src: - 'some.js': mocks.fs.file(0, 'js-source') - 'utf8ášč': - 'some.js': mocks.fs.file(0, 'utf8-file') - - serveFile = createServeFile fsMock, null - - createServer = (f, s, basePath) -> - handler = createSourceFilesMiddleware f.promise, s, basePath - http.createServer (req, res) -> - next = sinon.spy (err) -> - if err - res.statusCode = err.status || 500 - res.end err.message - else - res.statusCode = 200 - res.end JSON.stringify(req.body) - - handler req, res, next - - beforeEach -> - files = helper.defer() - server = createServer files, serveFile, '/base/path' - - afterEach -> - next.reset() - - # helpers - includedFiles = (list) -> - files.resolve {included: list, served: []} - - servedFiles = (list) -> - files.resolve {included: [], served: list} - - it 'should serve absolute js source files ignoring timestamp', () -> - servedFiles [ - new File('/src/some.js') - ] - - request(server) - .get('/absolute/src/some.js?123345') - .expect(200, 'js-source') - - it 'should serve js source files from base folder ignoring timestamp', () -> - servedFiles [ - new File('/base/path/a.js') - ] - - request(server) - .get('/base/a.js?123345') - .expect(200, 'js-src-a') - .then(() -> - expect(next).not.to.have.been.called - ) - - it 'should send strict caching headers for js source files with sha', () -> - servedFiles [ - new File('/src/some.js') - ] - - request(server) - .get('/absolute/src/some.js?df43b8acf136389a8dd989bda397d1c9b4e048be') - .expect('Cache-Control', 'public, max-age=31536000') - .expect(200) - .then(() -> - expect(next).not.to.have.been.called - ) - - it 'should send strict caching headers for js source files with sha (in basePath)', () -> - servedFiles [ - new File('/base/path/a.js') - ] - - request(server) - .get('/base/a.js?df43b8acf136389a8dd989bda397d1c9b4e048be') - .expect('Cache-Control', 'public, max-age=31536000') - .expect(200) - - it 'should send no-caching headers for js source files without timestamps', () -> - ZERO_DATE = (new Date 0).toString() - - servedFiles [ - new File('/src/some.js') - ] - - request(server) - .get('/absolute/src/some.js') - .expect('Cache-Control', 'no-cache') - # idiotic IE8 needs more - .expect('Pragma', 'no-cache') - .expect('Expires', ZERO_DATE) - .expect(200) - .then(() -> - expect(next).not.to.have.been.called - ) - - it 'should not serve files that are not in served', () -> - servedFiles [] - - request(server) - .get('/absolute/non-existing.html') - .expect(200, '') - - it 'should serve 404 if file is served but does not exist', () -> - servedFiles [ - new File('/non-existing.js') - ] - - request(server) - .get('/absolute/non-existing.js') - .expect(404, 'NOT FOUND') - - - it 'should serve js source file from base path containing utf8 chars', () -> - servedFiles [ - new File('/utf8ášč/some.js') - ] - - server = createServer files, serveFile, '/utf8ášč' - - request(server) - .get('/base/some.js') - .expect(200, 'utf8-file') - .then(() -> - expect(next).not.to.have.been.called - ) - - it 'should set content-type headers', () -> - servedFiles [ - new File('/base/path/index.html') - ] - - request(server) - .get('/base/index.html') - .expect('Content-Type', 'text/html') - .expect(200) - - it 'should use cached content if available', () -> - cachedFile = new File('/some/file.js') - cachedFile.content = 'cached-content' - - servedFiles [ - cachedFile - ] - - request(server) - .get('/absolute/some/file.js') - .expect(200, 'cached-content') - .then(() -> - expect(next).not.to.have.been.called - ) - - it 'should not use cached content if doNotCache is set', () -> - cachedFile = new File('/src/some.js') - cachedFile.content = 'cached-content' - cachedFile.doNotCache = true - - servedFiles [ - cachedFile - ] - - request(server) - .get('/absolute/src/some.js') - .expect(200, 'js-source') - .then(() -> - expect(next).not.to.have.been.called - ) diff --git a/test/unit/middleware/source_files.spec.js b/test/unit/middleware/source_files.spec.js new file mode 100644 index 000000000..acf675f0c --- /dev/null +++ b/test/unit/middleware/source_files.spec.js @@ -0,0 +1,211 @@ +import http from 'http' +import mocks from 'mocks' +import request from 'supertest-as-promised' +import helper from '../../../lib/helper' +import File from '../../../lib/file' +import {createServeFile} from '../../../lib/middleware/common' +var createSourceFilesMiddleware = require('../../../lib/middleware/source_files').create + +describe('middleware.source_files', function () { + var next + var files + var server = next = files = null + + var fsMock = mocks.fs.create({ + base: { + path: { + 'a.js': mocks.fs.file(0, 'js-src-a'), + 'index.html': mocks.fs.file(0, '') + } + }, + src: { + 'some.js': mocks.fs.file(0, 'js-source') + }, + 'utf8ášč': { + 'some.js': mocks.fs.file(0, 'utf8-file') + } + }) + + var serveFile = createServeFile(fsMock, null) + + var createServer = function (f, s, basePath) { + var handler = createSourceFilesMiddleware(f.promise, s, basePath) + return http.createServer(function (req, res) { + next = sinon.spy(function (err) { + if (err) { + res.statusCode = err.status || 500 + return res.end(err.message) + } else { + res.statusCode = 200 + return res.end(JSON.stringify(req.body)) + } + }) + + return handler(req, res, next) + }) + } + + beforeEach(function () { + files = helper.defer() + server = createServer(files, serveFile, '/base/path') + return server + }) + + afterEach(function () { + return next.reset() + }) + + var servedFiles = function (list) { + return files.resolve({included: [], served: list}) + } + + it('should serve absolute js source files ignoring timestamp', function () { + servedFiles([ + new File('/src/some.js') + ]) + + return request(server) + .get('/absolute/src/some.js?123345') + .expect(200, 'js-source') + }) + + it('should serve js source files from base folder ignoring timestamp', function () { + servedFiles([ + new File('/base/path/a.js') + ]) + + return request(server) + .get('/base/a.js?123345') + .expect(200, 'js-src-a') + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) + + it('should send strict caching headers for js source files with sha', function () { + servedFiles([ + new File('/src/some.js') + ]) + + return request(server) + .get('/absolute/src/some.js?df43b8acf136389a8dd989bda397d1c9b4e048be') + .expect('Cache-Control', 'public, max-age=31536000') + .expect(200) + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) + + it('should send strict caching headers for js source files with sha (in basePath)', function () { + servedFiles([ + new File('/base/path/a.js') + ]) + + return request(server) + .get('/base/a.js?df43b8acf136389a8dd989bda397d1c9b4e048be') + .expect('Cache-Control', 'public, max-age=31536000') + .expect(200) + }) + + it('should send no-caching headers for js source files without timestamps', function () { + var ZERO_DATE = (new Date(0)).toString() + + servedFiles([ + new File('/src/some.js') + ]) + + return request(server) + .get('/absolute/src/some.js') + .expect('Cache-Control', 'no-cache') + // idiotic IE8 needs more + .expect('Pragma', 'no-cache') + .expect('Expires', ZERO_DATE) + .expect(200) + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) + + it('should not serve files that are not in served', function () { + servedFiles([]) + + return request(server) + .get('/absolute/non-existing.html') + .expect(200, '') + }) + + it('should serve 404 if file is served but does not exist', function () { + servedFiles([ + new File('/non-existing.js') + ]) + + return request(server) + .get('/absolute/non-existing.js') + .expect(404, 'NOT FOUND') + }) + + it('should serve js source file from base path containing utf8 chars', function () { + servedFiles([ + new File('/utf8ášč/some.js') + ]) + + server = createServer(files, serveFile, '/utf8ášč') + + return request(server) + .get('/base/some.js') + .expect(200, 'utf8-file') + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) + + it('should set content-type headers', function () { + servedFiles([ + new File('/base/path/index.html') + ]) + + return request(server) + .get('/base/index.html') + .expect('Content-Type', 'text/html') + .expect(200) + }) + + it('should use cached content if available', function () { + var cachedFile = new File('/some/file.js') + cachedFile.content = 'cached-content' + + servedFiles([ + cachedFile + ]) + + return request(server) + .get('/absolute/some/file.js') + .expect(200, 'cached-content') + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) + + return it('should not use cached content if doNotCache is set', function () { + var cachedFile = new File('/src/some.js') + cachedFile.content = 'cached-content' + cachedFile.doNotCache = true + + servedFiles([ + cachedFile + ]) + + return request(server) + .get('/absolute/src/some.js') + .expect(200, 'js-source') + .then(function () { + return expect(next).not.to.have.been.called + } + ) + }) +}) diff --git a/test/unit/middleware/strip_host.spec.coffee b/test/unit/middleware/strip_host.spec.coffee deleted file mode 100644 index 6d961fccb..000000000 --- a/test/unit/middleware/strip_host.spec.coffee +++ /dev/null @@ -1,68 +0,0 @@ -describe 'middleware.strip_host', -> - mocks = require 'mocks' - HttpResponseMock = mocks.http.ServerResponse - HttpRequestMock = mocks.http.ServerRequest - - File = require('../../../lib/file') - Url = require('../../../lib/url') - - fsMock = mocks.fs.create - base: - path: - 'a.js': mocks.fs.file(0, 'js-src-a') - 'index.html': mocks.fs.file(0, '') - src: - 'some.js': mocks.fs.file(0, 'js-source') - 'utf8ášč': - 'some.js': mocks.fs.file(0, 'utf8-file') - - - serveFile = require('../../../lib/middleware/common').createServeFile fsMock, null - createStripHostMiddleware = require('../../../lib/middleware/strip_host').create - - handler = filesDeferred = nextSpy = response = null - - beforeEach -> - nextSpy = sinon.spy() - request = null - handler = createStripHostMiddleware null, null, '/base/path' - - it 'should strip request with IP number', (done) -> - request = new HttpRequestMock('http://192.12.31.100/base/a.js?123345') - handler request, null, nextSpy - - expect(request.normalizedUrl).to.equal '/base/a.js?123345' - expect(nextSpy).to.have.been.called - done() - - it 'should strip request with absoluteURI', (done) -> - request = new HttpRequestMock('http://localhost/base/a.js?123345') - handler request, null, nextSpy - - expect(request.normalizedUrl).to.equal '/base/a.js?123345' - expect(nextSpy).to.have.been.called - done() - - it 'should strip request with absoluteURI and port', (done) -> - request = new HttpRequestMock('http://localhost:9876/base/a.js?123345') - handler request, null, nextSpy - - expect(request.normalizedUrl).to.equal '/base/a.js?123345' - expect(nextSpy).to.have.been.called - done() - - it 'should strip request with absoluteURI over HTTPS', (done) -> - request = new HttpRequestMock('https://karma-runner.github.io/base/a.js?123345') - handler request, null, nextSpy - - expect(request.normalizedUrl).to.equal '/base/a.js?123345' - expect(nextSpy).to.have.been.called - done() - - it 'should return same url as passed one', (done) -> - request = new HttpRequestMock('/base/b.js?123345') - handler request, null, nextSpy - - expect(request.normalizedUrl).to.equal '/base/b.js?123345' - expect(nextSpy).to.have.been.called - done() diff --git a/test/unit/middleware/strip_host.spec.js b/test/unit/middleware/strip_host.spec.js new file mode 100644 index 000000000..39681533a --- /dev/null +++ b/test/unit/middleware/strip_host.spec.js @@ -0,0 +1,61 @@ +import mocks from 'mocks' + +describe('middleware.strip_host', function () { + var nextSpy + var HttpRequestMock = mocks.http.ServerRequest + + var createStripHostMiddleware = require('../../../lib/middleware/strip_host').create + + var handler = nextSpy = null + + beforeEach(function () { + nextSpy = sinon.spy() + handler = createStripHostMiddleware(null, null, '/base/path') + return handler + }) + + it('should strip request with IP number', function (done) { + var request = new HttpRequestMock('http://192.12.31.100/base/a.js?123345') + handler(request, null, nextSpy) + + expect(request.normalizedUrl).to.equal('/base/a.js?123345') + expect(nextSpy).to.have.been.called + return done() + }) + + it('should strip request with absoluteURI', function (done) { + var request = new HttpRequestMock('http://localhost/base/a.js?123345') + handler(request, null, nextSpy) + + expect(request.normalizedUrl).to.equal('/base/a.js?123345') + expect(nextSpy).to.have.been.called + return done() + }) + + it('should strip request with absoluteURI and port', function (done) { + var request = new HttpRequestMock('http://localhost:9876/base/a.js?123345') + handler(request, null, nextSpy) + + expect(request.normalizedUrl).to.equal('/base/a.js?123345') + expect(nextSpy).to.have.been.called + return done() + }) + + it('should strip request with absoluteURI over HTTPS', function (done) { + var request = new HttpRequestMock('https://karma-runner.github.io/base/a.js?123345') + handler(request, null, nextSpy) + + expect(request.normalizedUrl).to.equal('/base/a.js?123345') + expect(nextSpy).to.have.been.called + return done() + }) + + return it('should return same url as passed one', function (done) { + var request = new HttpRequestMock('/base/b.js?123345') + handler(request, null, nextSpy) + + expect(request.normalizedUrl).to.equal('/base/b.js?123345') + expect(nextSpy).to.have.been.called + return done() + }) +}) diff --git a/test/unit/mocha-globals.coffee b/test/unit/mocha-globals.coffee deleted file mode 100644 index e089906f7..000000000 --- a/test/unit/mocha-globals.coffee +++ /dev/null @@ -1,77 +0,0 @@ -require 'coffee-errors' - -sinon = require 'sinon' -chai = require 'chai' -logger = require '../../lib/logger' - -require('bluebird').longStackTraces() - -# publish globals that all specs can use -global.expect = chai.expect -global.should = chai.should() -global.sinon = sinon - -# chai plugins -chai.use(require 'chai-as-promised') -chai.use(require 'sinon-chai') -chai.use(require 'chai-subset') - -beforeEach -> - global.sinon = sinon.sandbox.create() - - # set logger to log INFO, but do not append to console - # so that we can assert logs by logger.on('info', ...) - logger.setup 'INFO', false, [] - -afterEach -> - global.sinon.restore() - - - -# TODO(vojta): move to helpers or something -chai.use (chai, utils) -> - chai.Assertion.addMethod 'beServedAs', (expectedStatus, expectedBody) -> - response = utils.flag @, 'object' - - @assert response._status is expectedStatus, - "expected response status '#{response._status}' to be '#{expectedStatus}'" - @assert response._body is expectedBody, - "expected response body '#{response._body}' to be '#{expectedBody}'" - - chai.Assertion.addMethod 'beNotServed', -> - response = utils.flag @, 'object' - - @assert response._status is null, - "expected response status to not be set, it was '#{response._status}'" - @assert response._body is null, - "expected response body to not be set, it was '#{response._body}'" - - -# TODO(vojta): move it somewhere ;-) -nextTickQueue = [] -nextTickCallback = -> - if not nextTickQueue.length then throw new Error 'Nothing scheduled!' - nextTickQueue.shift()() - if nextTickQueue.length then process.nextTick nextTickCallback - -global.scheduleNextTick = (action) -> - nextTickQueue.push action - if nextTickQueue.length is 1 then process.nextTick nextTickCallback - -nextQueue = [] -nextCallback = -> - if not nextQueue.length then throw new Error 'Nothing scheduled!' - nextQueue.shift()() - -global.scheduleNextTick = (action) -> - nextTickQueue.push action - if nextTickQueue.length is 1 then process.nextTick nextTickCallback - -global.scheduleNext = (action) -> - nextQueue.push action - -global.next = nextCallback - -beforeEach -> - nextTickQueue.length = 0 - nextQueue.length = 0 diff --git a/test/unit/mocha-globals.js b/test/unit/mocha-globals.js new file mode 100644 index 000000000..b61c09596 --- /dev/null +++ b/test/unit/mocha-globals.js @@ -0,0 +1,84 @@ +var sinon = require('sinon') +var chai = require('chai') +var logger = require('../../lib/logger') + +require('bluebird').longStackTraces() + +// publish globals that all specs can use +global.expect = chai.expect +global.should = chai.should() +global.sinon = sinon + +// chai plugins +chai.use(require('chai-as-promised')) +chai.use(require('sinon-chai')) +chai.use(require('chai-subset')) + +beforeEach(() => { + global.sinon = sinon.sandbox.create() + + // set logger to log INFO, but do not append to console + // so that we can assert logs by logger.on('info', ...) + logger.setup('INFO', false, []) +}) + +afterEach(() => { + global.sinon.restore() +}) + +// TODO(vojta): move to helpers or something +chai.use((chai, utils) => { + chai.Assertion.addMethod('beServedAs', function (expectedStatus, expectedBody) { + var response = utils.flag(this, 'object') + + this.assert(response._status === expectedStatus, + `expected response status '#{response._status}' to be '#{expectedStatus}'`) + this.assert(response._body === expectedBody, + `expected response body '#{response._body}' to be '#{exp)ectedBody}'`) + }) + + chai.Assertion.addMethod('beNotServed', function () { + var response = utils.flag(this, 'object') + + this.assert(response._status === null, + `expected response status to not be set, it was '#{response._status}'`) + this.assert(response._body === null, + `expected response body to not be set, it was '#{response._body}'`) + + }) +}) + +// TODO(vojta): move it somewhere ;-) +var nextTickQueue = [] +var nextTickCallback = () => { + if (!nextTickQueue.length) throw new Error('Nothing scheduled!') + nextTickQueue.shift()() + + if (nextTickQueue.length) process.nextTick(nextTickCallback) +} +global.scheduleNextTick = action => { + nextTickQueue.push(action) + + if (nextTickQueue.length === 1) process.nextTick(nextTickCallback) +} +var nextQueue = [] +var nextCallback = () => { + // if not nextQueue.length then throw new Error 'Nothing scheduled!' + nextQueue.shift()() +} + +global.scheduleNextTick = action => { + nextTickQueue.push(action) + + if (nextTickQueue.length === 1) process.nextTick(nextTickCallback) +} +global.scheduleNext = action => { + nextQueue.push(action) +} + +global.next = nextCallback + +beforeEach(() => { + nextTickQueue.length = 0 + nextQueue.length = 0 +}) diff --git a/test/unit/mocks/timer.js b/test/unit/mocks/timer.js index 8abb35e9b..943201fd3 100644 --- a/test/unit/mocks/timer.js +++ b/test/unit/mocks/timer.js @@ -1,4 +1,4 @@ -var Timer = require('timer-shim').Timer +import {Timer} from 'timer-shim' module.exports = function () { var timer = new Timer() diff --git a/test/unit/preprocessor.spec.coffee b/test/unit/preprocessor.spec.coffee deleted file mode 100644 index d5d96c310..000000000 --- a/test/unit/preprocessor.spec.coffee +++ /dev/null @@ -1,216 +0,0 @@ -#============================================================================== -# lib/preprocessor.js module -#============================================================================== -describe 'preprocessor', -> - mocks = require 'mocks' - di = require 'di' - - m = pp = mockFs = null - - beforeEach -> - mockFs = mocks.fs.create - some: - 'a.js': mocks.fs.file 0, 'content' - 'b.js': mocks.fs.file 0, 'content' - 'a.txt': mocks.fs.file 0, 'some-text' - 'photo.png': mocks.fs.file 0, 'binary' - 'CAM_PHOTO.JPG': mocks.fs.file 0, 'binary' - - mocks_ = - 'graceful-fs': mockFs - minimatch: require 'minimatch' - - m = mocks.loadFile __dirname + '/../../lib/preprocessor.js', mocks_ - - - it 'should preprocess matching file', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - file.path = file.path + '-preprocessed' - done null, 'new-content' - - injector = new di.Injector [{'preprocessor:fake': ['factory', -> fakePreprocessor]}] - pp = m.createPreprocessor {'**/*.js': ['fake']}, null, injector - - file = {originalPath: '/some/a.js', path: 'path'} - - pp file, -> - expect(fakePreprocessor).to.have.been.called - expect(file.path).to.equal 'path-preprocessed' - expect(file.content).to.equal 'new-content' - done() - - - it 'should check patterns after creation when invoked', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - file.path = file.path + '-preprocessed' - done null, 'new-content' - - injector = new di.Injector [{'preprocessor:fake': ['factory', -> fakePreprocessor]}] - config = {'**/*.txt': ['fake']} - pp = m.createPreprocessor config, null, injector - - file = {originalPath: '/some/a.js', path: 'path'} - - config['**/*.js'] = ['fake'] - - pp file, -> - expect(fakePreprocessor).to.have.been.called - expect(file.path).to.equal 'path-preprocessed' - expect(file.content).to.equal 'new-content' - done() - - - it 'should ignore not matching file', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - done null, '' - - injector = new di.Injector [{'preprocessor:fake': ['factory', -> fakePreprocessor]}] - pp = m.createPreprocessor {'**/*.js': ['fake']}, null, injector - - file = {originalPath: '/some/a.txt', path: 'path'} - - pp file, -> - expect(fakePreprocessor).to.not.have.been.called - done() - - - it 'should apply all preprocessors', (done) -> - fakePreprocessor1 = sinon.spy (content, file, done) -> - file.path = file.path + '-p1' - done null, content + '-c1' - - fakePreprocessor2 = sinon.spy (content, file, done) -> - file.path = file.path + '-p2' - done content + '-c2' - - injector = new di.Injector [{ - 'preprocessor:fake1': ['factory', -> fakePreprocessor1] - 'preprocessor:fake2': ['factory', -> fakePreprocessor2] - }] - - pp = m.createPreprocessor {'**/*.js': ['fake1', 'fake2']}, null, injector - - file = {originalPath: '/some/a.js', path: 'path'} - - pp file, -> - expect(fakePreprocessor1).to.have.been.calledOnce - expect(fakePreprocessor2).to.have.been.calledOnce - expect(file.path).to.equal 'path-p1-p2' - expect(file.content).to.equal 'content-c1-c2' - done() - - - it 'should compute SHA', (done) -> - pp = m.createPreprocessor {}, null, new di.Injector([]) - file = {originalPath: '/some/a.js', path: 'path'} - - pp file, -> - expect(file.sha).to.exist - expect(file.sha.length).to.equal 40 - previousSHA = file.sha - - pp file, -> - expect(file.sha).to.equal previousSHA - mockFs._touchFile '/some/a.js', null, 'new-content' - - pp file, -> - expect(file.sha.length).to.equal 40 - expect(file.sha).not.to.equal previousSHA - done() - - it 'should compute SHA from content returned by a processor', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - done null, content + '-processed' - - injector = new di.Injector [{ - 'preprocessor:fake': ['factory', -> fakePreprocessor] - }] - - pp = m.createPreprocessor {'**/a.js': ['fake']}, null, injector - - fileProcess = {originalPath: '/some/a.js', path: 'path'} - fileSkip = {originalPath: '/some/b.js', path: 'path'} - - pp fileProcess, -> - pp fileSkip, -> - expect(fileProcess.sha).to.exist - expect(fileProcess.sha.length).to.equal 40 - expect(fileSkip.sha).to.exist - expect(fileSkip.sha.length).to.equal 40 - expect(fileProcess.sha).not.to.equal fileSkip.sha - done() - - - it 'should return error if any preprocessor fails', (done) -> - failingPreprocessor = sinon.spy (content, file, done) -> - done new Error('Some error'), null - - injector = new di.Injector [{ - 'preprocessor:failing': ['factory', -> failingPreprocessor] - }] - - pp = m.createPreprocessor {'**/*.js': ['failing']}, null, injector - - file = {originalPath: '/some/a.js', path: 'path'} - - pp file, (err) -> - expect(err).to.exist - done() - - - it 'should stop preprocessing after an error', (done) -> - failingPreprocessor = sinon.spy (content, file, done) -> - done new Error('Some error'), null - - fakePreprocessor = sinon.spy (content, file, done) -> - done null, content - - - injector = new di.Injector [{ - 'preprocessor:failing': ['factory', -> failingPreprocessor] - 'preprocessor:fake': ['factory', -> fakePreprocessor] - }] - - pp = m.createPreprocessor {'**/*.js': ['failing', 'fake']}, null, injector - - file = {originalPath: '/some/a.js', path: 'path'} - - pp file, (err) -> - expect(fakePreprocessor).not.to.have.been.called - done() - - - it 'should not preprocess binary files', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - done null, content - - injector = new di.Injector [{ - 'preprocessor:fake': ['factory', -> fakePreprocessor] - }] - - pp = m.createPreprocessor {'**/*': ['fake']}, null, injector - - file = {originalPath: '/some/photo.png', path: 'path'} - - pp file, (err) -> - expect(fakePreprocessor).not.to.have.been.called - expect(file.content).to.be.an.instanceof Buffer - done() - - - it 'should not preprocess binary files with capital letters in extension', (done) -> - fakePreprocessor = sinon.spy (content, file, done) -> - done null, content - - injector = new di.Injector [{ - 'preprocessor:fake': ['factory', -> fakePreprocessor] - }] - - pp = m.createPreprocessor {'**/*': ['fake']}, null, injector - - file = {originalPath: '/some/CAM_PHOTO.JPG', path: 'path'} - - pp file, (err) -> - expect(fakePreprocessor).not.to.have.been.called - expect(file.content).to.be.an.instanceof Buffer - done() diff --git a/test/unit/preprocessor.spec.js b/test/unit/preprocessor.spec.js new file mode 100644 index 000000000..f8c918cf8 --- /dev/null +++ b/test/unit/preprocessor.spec.js @@ -0,0 +1,248 @@ +import mocks from 'mocks' +import di from 'di' + +describe('preprocessor', () => { + var pp + var m + var mockFs + + beforeEach(() => { + mockFs = mocks.fs.create({ + some: { + 'a.js': mocks.fs.file(0, 'content'), + 'b.js': mocks.fs.file(0, 'content'), + 'a.txt': mocks.fs.file(0, 'some-text'), + 'photo.png': mocks.fs.file(0, 'binary'), + 'CAM_PHOTO.JPG': mocks.fs.file(0, 'binary') + } + }) + + var mocks_ = { + 'graceful-fs': mockFs, + minimatch: require('minimatch') + } + + m = mocks.loadFile(__dirname + '/../../lib/preprocessor.js', mocks_) + }) + + it('should preprocess matching file', done => { + var fakePreprocessor = sinon.spy((content, file, done) => { + file.path = file.path + '-preprocessed' + done(null, 'new-content') + }) + + var injector = new di.Injector([{'preprocessor:fake': ['factory', () => fakePreprocessor]}]) + pp = m.createPreprocessor({'**/*.js': ['fake']}, null, injector) + + var file = {originalPath: '/some/a.js', path: 'path'} + + pp(file, () => { + expect(fakePreprocessor).to.have.been.called + expect(file.path).to.equal('path-preprocessed') + expect(file.content).to.equal('new-content') + done() + }) + }) + + it('should check patterns after creation when invoked', done => { + var fakePreprocessor = sinon.spy((content, file, done) => { + file.path = file.path + '-preprocessed' + done(null, 'new-content') + }) + + var injector = new di.Injector([{'preprocessor:fake': ['factory', () => fakePreprocessor]}]) + var config = {'**/*.txt': ['fake']} + pp = m.createPreprocessor(config, null, injector) + + var file = {originalPath: '/some/a.js', path: 'path'} + + config['**/*.js'] = ['fake'] + + pp(file, () => { + expect(fakePreprocessor).to.have.been.called + expect(file.path).to.equal('path-preprocessed') + expect(file.content).to.equal('new-content') + done() + }) + }) + + it('should ignore not matching file', done => { + var fakePreprocessor = sinon.spy((content, file, done) => { + done(null, '') + }) + + var injector = new di.Injector([{'preprocessor:fake': ['factory', () => fakePreprocessor]}]) + pp = m.createPreprocessor({'**/*.js': ['fake']}, null, injector) + + var file = {originalPath: '/some/a.txt', path: 'path'} + + pp(file, () => { + expect(fakePreprocessor).to.not.have.been.called + done() + }) + }) + + it('should apply all preprocessors', done => { + var fakePreprocessor1 = sinon.spy((content, file, done) => { + file.path = file.path + '-p1' + done(null, content + '-c1') + }) + + var fakePreprocessor2 = sinon.spy((content, file, done) => { + file.path = file.path + '-p2' + done(content + '-c2') + }) + + var injector = new di.Injector([{ + 'preprocessor:fake1': ['factory', () => fakePreprocessor1], + 'preprocessor:fake2': ['factory', () => fakePreprocessor2] + }]) + + pp = m.createPreprocessor({'**/*.js': ['fake1', 'fake2']}, null, injector) + + var file = {originalPath: '/some/a.js', path: 'path'} + + pp(file, () => { + expect(fakePreprocessor1).to.have.been.calledOnce + expect(fakePreprocessor2).to.have.been.calledOnce + expect(file.path).to.equal('path-p1-p2') + expect(file.content).to.equal('content-c1-c2') + done() + }) + }) + + it('should compute SHA', done => { + pp = m.createPreprocessor({}, null, new di.Injector([])) + var file = {originalPath: '/some/a.js', path: 'path'} + + pp(file, () => { + expect(file.sha).to.exist + expect(file.sha.length).to.equal(40) + var previousSHA = file.sha + + pp(file, () => { + expect(file.sha).to.equal(previousSHA) + mockFs._touchFile('/some/a.js', null, 'new-content') + + pp(file, () => { + expect(file.sha.length).to.equal(40) + expect(file.sha).not.to.equal(previousSHA) + done() + }) + }) + }) + }) + + it('should compute SHA from content returned by a processor', done => { + var fakePreprocessor = sinon.spy((content, file, done) => { + done(null, content + '-processed') + }) + + var injector = new di.Injector([{ + 'preprocessor:fake': ['factory', () => fakePreprocessor] + }]) + + pp = m.createPreprocessor({'**/a.js': ['fake']}, null, injector) + + var fileProcess = {originalPath: '/some/a.js', path: 'path'} + var fileSkip = {originalPath: '/some/b.js', path: 'path'} + + pp(fileProcess, () => { + pp(fileSkip, () => { + expect(fileProcess.sha).to.exist + expect(fileProcess.sha.length).to.equal(40) + expect(fileSkip.sha).to.exist + expect(fileSkip.sha.length).to.equal(40) + expect(fileProcess.sha).not.to.equal(fileSkip.sha) + done() + }) + }) + }) + + it('should return error if any preprocessor fails', done => { + var failingPreprocessor = sinon.spy((content, file, done) => { + done(new Error('Some error'), null) + }) + + var injector = new di.Injector([{ + 'preprocessor:failing': ['factory', () => failingPreprocessor] + }]) + + pp = m.createPreprocessor({'**/*.js': ['failing']}, null, injector) + + var file = {originalPath: '/some/a.js', path: 'path'} + + pp(file, err => { + expect(err).to.exist + done() + }) + }) + + it('should stop preprocessing after an error', done => { + var failingPreprocessor = sinon.spy((content, file, done) => { + done(new Error('Some error'), null) + }) + + var fakePreprocessor = sinon.spy((content, file, done) => { + done(null, content) + }) + + var injector = new di.Injector([{ + 'preprocessor:failing': ['factory', () => failingPreprocessor], + 'preprocessor:fake': ['factory', () => fakePreprocessor] + }]) + + pp = m.createPreprocessor({'**/*.js': ['failing', 'fake']}, null, injector) + + var file = {originalPath: '/some/a.js', path: 'path'} + + pp(file, () => { + expect(fakePreprocessor).not.to.have.been.called + done() + }) + }) + + it('should not preprocess binary files', done => { + var fakePreprocessor = sinon.spy((content, file, done) => { + done(null, content) + }) + + var injector = new di.Injector([{ + 'preprocessor:fake': ['factory', () => fakePreprocessor] + }]) + + pp = m.createPreprocessor({'**/*': ['fake']}, null, injector) + + var file = {originalPath: '/some/photo.png', path: 'path'} + + pp(file, err => { + if (err) throw err + + expect(fakePreprocessor).not.to.have.been.called + expect(file.content).to.be.an.instanceof(Buffer) + done() + }) + }) + + it('should not preprocess binary files with capital letters in extension', (done) => { + var fakePreprocessor = sinon.spy((content, file, done) => { + done(null, content) + }) + + var injector = new di.Injector([{ + 'preprocessor:fake': ['factory', () => fakePreprocessor] + }]) + + pp = m.createPreprocessor({'**/*': ['fake']}, null, injector) + + var file = {originalPath: '/some/CAM_PHOTO.JPG', path: 'path'} + + pp(file, err => { + if (err) throw err + + expect(fakePreprocessor).not.to.have.been.called + expect(file.content).to.be.an.instanceof(Buffer) + done() + }) + }) +}) diff --git a/test/unit/reporter.spec.coffee b/test/unit/reporter.spec.coffee deleted file mode 100644 index 7d05746b8..000000000 --- a/test/unit/reporter.spec.coffee +++ /dev/null @@ -1,138 +0,0 @@ -#============================================================================== -# lib/reporter.js module -#============================================================================== -describe 'reporter', -> - EventEmitter = require('events').EventEmitter - File = require('../../lib/file') - loadFile = require('mocks').loadFile - _ = require('../../lib/helper')._ - m = null - - beforeEach -> - m = loadFile __dirname + '/../../lib/reporter.js' - - - #============================================================================== - # formatError() [PRIVATE] - #============================================================================== - describe 'formatError', -> - formatError = emitter = null - - beforeEach -> - emitter = new EventEmitter - formatError = m.createErrorFormatter '', emitter - - - it 'should indent', -> - expect(formatError 'Something', '\t').to.equal '\tSomething\n' - - - it 'should handle empty message', -> - expect(formatError null).to.equal '\n' - - - it 'should remove domain from files', -> - expect(formatError 'file http://localhost:8080/base/usr/a.js and ' + - 'http://127.0.0.1:8080/base/home/b.js'). - to.be.equal 'file /usr/a.js and /home/b.js\n' - - - # TODO(vojta): enable once we serve source under urlRoot - it.skip 'should handle non default karma service folders', -> - formatError = m.createErrorFormatter '', '/_karma_/' - expect(formatError 'file http://localhost:8080/_karma_/base/usr/a.js and ' + - 'http://127.0.0.1:8080/_karma_/base/home/b.js'). - to.be.equal 'file /usr/a.js and /home/b.js\n' - - - it 'should remove shas', -> - ERROR = 'file ' + - 'http://localhost:8080/base/usr/file.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9 ' + - 'and ' + - 'http://127.0.0.1:8080/absolute/home/file.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9' - expect(formatError ERROR).to.be.equal 'file /usr/file.js and /home/file.js\n' - - - it 'should indent all lines', -> - expect(formatError 'first\nsecond\nthird', '\t').to.equal '\tfirst\n\tsecond\n\tthird\n' - - - it 'should restore base paths', -> - formatError = m.createErrorFormatter '/some/base', emitter - expect(formatError 'at http://localhost:123/base/a.js?123').to.equal 'at /some/base/a.js\n' - - - it 'should restore absolute paths', -> - ERROR = 'at http://local:1233/absolute/usr/path.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9' - expect(formatError ERROR).to.equal 'at /usr/path.js\n' - - - it 'should preserve line numbers', -> - ERROR = 'at http://local:1233/absolute/usr/path.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9:2' - expect(formatError ERROR).to.equal 'at /usr/path.js:2\n' - - - describe 'source maps', -> - - class MockSourceMapConsumer - constructor: (sourceMap) -> - @source = sourceMap.content.replace 'SOURCE MAP ', '/original/' - originalPositionFor: (position) -> - if position.line == 0 - throw new TypeError('Line must be greater than or equal to 1, got 0') - - source: @source - line: position.line + 2 - column: position.column + 2 - - it 'should rewrite stack traces', (done) -> - formatError = m.createErrorFormatter '/some/base', emitter, MockSourceMapConsumer - servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')] - servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'} - servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'} - - emitter.emit 'file_list_modified', served: servedFiles - - _.defer -> _.delay -> - ERROR = 'at http://localhost:123/base/b.js:2:6' - expect(formatError ERROR).to.equal 'at /some/base/b.js:2:6 <- /original/b.js:4:8\n' - done() - , 100 - - it 'should fall back to non-source-map format if originalPositionFor throws', (done) -> - formatError = m.createErrorFormatter '/some/base', emitter, MockSourceMapConsumer - servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')] - servedFiles[0].sourceMap = 'SOURCE MAP a.js' - servedFiles[1].sourceMap = 'SOURCE MAP b.js' - - emitter.emit 'file_list_modified', served: servedFiles - - _.defer -> - ERROR = 'at http://localhost:123/base/b.js:0:0' - expect(formatError ERROR).to.equal 'at /some/base/b.js\n' - done() - - describe 'Windows', -> - formatError = null - servedFiles = null - - beforeEach -> - formatError = m.createErrorFormatter '/some/base', emitter, MockSourceMapConsumer - servedFiles = [new File('C:/a/b/c.js')] - servedFiles[0].sourceMap = {content: 'SOURCE MAP b.js'} - - it 'should correct rewrite stack traces without sha', (done) -> - emitter.emit 'file_list_modified', served: servedFiles - - _.defer -> - ERROR = 'at http://localhost:123/absoluteC:/a/b/c.js:2:6' - expect(formatError ERROR).to.equal 'at C:/a/b/c.js:2:6 <- /original/b.js:4:8\n' - done() - - it 'should correct rewrite stack traces with sha', (done) -> - emitter.emit 'file_list_modified', served: servedFiles - - _.defer -> - ERROR = 'at http://localhost:123/absoluteC:/a/b/c.js?da39a3ee5e6:2:6' - expect(formatError ERROR).to.equal 'at C:/a/b/c.js:2:6 <- /original/b.js:4:8\n' - done() diff --git a/test/unit/reporter.spec.js b/test/unit/reporter.spec.js new file mode 100644 index 000000000..2d0130731 --- /dev/null +++ b/test/unit/reporter.spec.js @@ -0,0 +1,148 @@ +import {EventEmitter} from 'events' +import File from '../../lib/file' +import {loadFile} from 'mocks' +var _ = require('../../lib/helper')._ + +describe('reporter', () => { + var m + + beforeEach(() => { + m = loadFile(__dirname + '/../../lib/reporter.js') + }) + + // ============================================================================== + // formatError() [PRIVATE] + // ============================================================================== + describe('formatError', () => { + var emitter + var formatError = emitter = null + + beforeEach(() => { + emitter = new EventEmitter() + formatError = m.createErrorFormatter('', emitter) + }) + + it('should indent', () => { + expect(formatError('Something', '\t')).to.equal('\tSomething\n') + }) + + it('should handle empty message', () => { + expect(formatError(null)).to.equal('\n') + }) + + it('should remove domain from files', () => { + expect(formatError('file http://localhost:8080/base/usr/a.js and http://127.0.0.1:8080/base/home/b.js')).to.be.equal('file /usr/a.js and /home/b.js\n') + }) + + // TODO(vojta): enable once we serve source under urlRoot + it.skip('should handle non default karma service folders', () => { + formatError = m.createErrorFormatter('', '/_karma_/') + expect(formatError('file http://localhost:8080/_karma_/base/usr/a.js and http://127.0.0.1:8080/_karma_/base/home/b.js')).to.be.equal('file /usr/a.js and /home/b.js\n') + }) + + it('should remove shas', () => { + var ERROR = 'file http://localhost:8080/base/usr/file.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9 and http://127.0.0.1:8080/absolute/home/file.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9' + expect(formatError(ERROR)).to.be.equal('file /usr/file.js and /home/file.js\n') + }) + + it('should indent all lines', () => { + expect(formatError('first\nsecond\nthird', '\t')).to.equal('\tfirst\n\tsecond\n\tthird\n') + }) + + it('should restore base paths', () => { + formatError = m.createErrorFormatter('/some/base', emitter) + expect(formatError('at http://localhost:123/base/a.js?123')).to.equal('at /some/base/a.js\n') + }) + + it('should restore absolute paths', () => { + var ERROR = 'at http://local:1233/absolute/usr/path.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9' + expect(formatError(ERROR)).to.equal('at /usr/path.js\n') + }) + + it('should preserve line numbers', () => { + var ERROR = 'at http://local:1233/absolute/usr/path.js?6e31cb249ee5b32d91f37ea516ca0f84bddc5aa9:2' + expect(formatError(ERROR)).to.equal('at /usr/path.js:2\n') + }) + + describe('source maps', () => { + class MockSourceMapConsumer { + constructor (sourceMap) { + this.source = sourceMap.content.replace('SOURCE MAP ', '/original/') + } + + originalPositionFor (position) { + if (position.line === 0) { + throw new TypeError('Line must be greater than or equal to 1, got 0') + } + + return { + source: this.source, + line: position.line + 2, + column: position.column + 2 + } + } + } + + it('should rewrite stack traces', done => { + formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer) + var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')] + servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'} + servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'} + + emitter.emit('file_list_modified', {served: servedFiles}) + + _.defer(() => _.delay(() => { + var ERROR = 'at http://localhost:123/base/b.js:2:6' + expect(formatError(ERROR)).to.equal('at /some/base/b.js:2:6 <- /original/b.js:4:8\n') + done() + }, 100)) + }) + + it('should fall back to non-source-map format if originalPositionFor throws', done => { + formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer) + var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')] + servedFiles[0].sourceMap = 'SOURCE MAP a.js' + servedFiles[1].sourceMap = 'SOURCE MAP b.js' + + emitter.emit('file_list_modified', {served: servedFiles}) + + _.defer(() => { + var ERROR = 'at http://localhost:123/base/b.js:0:0' + expect(formatError(ERROR)).to.equal('at /some/base/b.js\n') + done() + }) + }) + + describe('Windows', () => { + formatError = null + var servedFiles = null + + beforeEach(() => { + formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer) + servedFiles = [new File('C:/a/b/c.js')] + servedFiles[0].sourceMap = {content: 'SOURCE MAP b.js'} + }) + + it('should correct rewrite stack traces without sha', done => { + emitter.emit('file_list_modified', {served: servedFiles}) + + _.defer(() => { + var ERROR = 'at http://localhost:123/absoluteC:/a/b/c.js:2:6' + expect(formatError(ERROR)).to.equal('at C:/a/b/c.js:2:6 <- /original/b.js:4:8\n') + done() + }) + }) + + it('should correct rewrite stack traces with sha', done => { + emitter.emit('file_list_modified', {served: servedFiles}) + + _.defer(() => { + var ERROR = 'at http://localhost:123/absoluteC:/a/b/c.js?da39a3ee5e6:2:6' + expect(formatError(ERROR)).to.equal('at C:/a/b/c.js:2:6 <- /original/b.js:4:8\n') + done() + }) + }) + }) + }) + }) +}) diff --git a/test/unit/reporters/base.spec.coffee b/test/unit/reporters/base.spec.coffee deleted file mode 100644 index 5ee9821c7..000000000 --- a/test/unit/reporters/base.spec.coffee +++ /dev/null @@ -1,49 +0,0 @@ -#============================================================================== -# lib/reporters/Base.js module -#============================================================================== -describe 'reporter', -> - loadFile = require('mocks').loadFile - m = null - - beforeEach -> - m = loadFile __dirname + '/../../../lib/reporters/base.js' - - describe 'Progress', -> - adapter = reporter = null - - beforeEach -> - - adapter = sinon.spy() - reporter = new m.BaseReporter null, null, adapter - - - it 'should write to all registered adapters', -> - anotherAdapter = sinon.spy() - reporter.adapters.push anotherAdapter - - reporter.write 'some' - expect(adapter).to.have.been.calledWith 'some' - expect(anotherAdapter).to.have.been.calledWith 'some' - - - it 'should format', -> - reporter.write 'Success: %d Failure: %d', 10, 20 - - expect(adapter).to.have.been.calledWith 'Success: 10 Failure: 20' - - it 'should format log messages correctly for single browser', -> - writeSpy = sinon.spy reporter, 'writeCommonMsg' - - reporter._browsers = ['Chrome'] - reporter.onBrowserLog 'Chrome', 'Message', 'LOG' - - expect(writeSpy).to.have.been.calledWith 'LOG: Message\n' - - it 'should format log messages correctly for multi browsers', -> - writeSpy = sinon.spy reporter, 'writeCommonMsg' - - reporter._browsers = ['Chrome', 'Firefox'] - reporter.onBrowserLog 'Chrome', 'Message', 'LOG' - - expect(writeSpy).to.have.been.calledWith 'Chrome LOG: Message\n' - diff --git a/test/unit/reporters/base.spec.js b/test/unit/reporters/base.spec.js new file mode 100644 index 000000000..0eee0cd20 --- /dev/null +++ b/test/unit/reporters/base.spec.js @@ -0,0 +1,56 @@ +// ============================================================================== +// lib/reporters/Base.js module +// ============================================================================== +describe('reporter', function () { + var loadFile = require('mocks').loadFile + var m = null + + beforeEach(function () { + m = loadFile(__dirname + '/../../../lib/reporters/base.js') + return m + }) + + return describe('Progress', function () { + var reporter + var adapter = reporter = null + + beforeEach(function () { + adapter = sinon.spy() + reporter = new m.BaseReporter(null, null, adapter) + return reporter + }) + + it('should write to all registered adapters', function () { + var anotherAdapter = sinon.spy() + reporter.adapters.push(anotherAdapter) + + reporter.write('some') + expect(adapter).to.have.been.calledWith('some') + return expect(anotherAdapter).to.have.been.calledWith('some') + }) + + it('should format', function () { + reporter.write('Success: %d Failure: %d', 10, 20) + + return expect(adapter).to.have.been.calledWith('Success: 10 Failure: 20') + }) + + it('should format log messages correctly for single browser', function () { + var writeSpy = sinon.spy(reporter, 'writeCommonMsg') + + reporter._browsers = ['Chrome'] + reporter.onBrowserLog('Chrome', 'Message', 'LOG') + + return expect(writeSpy).to.have.been.calledWith('LOG: Message\n') + }) + + return it('should format log messages correctly for multi browsers', function () { + var writeSpy = sinon.spy(reporter, 'writeCommonMsg') + + reporter._browsers = ['Chrome', 'Firefox'] + reporter.onBrowserLog('Chrome', 'Message', 'LOG') + + return expect(writeSpy).to.have.been.calledWith('Chrome LOG: Message\n') + }) + }) +}) diff --git a/test/unit/runner.spec.coffee b/test/unit/runner.spec.coffee deleted file mode 100644 index 97a3ad855..000000000 --- a/test/unit/runner.spec.coffee +++ /dev/null @@ -1,49 +0,0 @@ -#============================================================================== -# lib/runner.js module -#============================================================================== -describe 'runner', -> - loadFile = require('mocks').loadFile - constant = require '../../lib/constants' - m = null - - beforeEach -> - m = loadFile __dirname + '/../../lib/runner.js' - - #============================================================================ - # runner.parseExitCode - #============================================================================ - describe 'parseExitCode', -> - EXIT = constant.EXIT_CODE - - it 'should return 0 exit code if present in the buffer', -> - expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '0').to.equal 0 - - - it 'should null the exit code part of the buffer', -> - buffer = new Buffer 'some' + EXIT + '1' - m.parseExitCode buffer - - expect(buffer.toString()).to.equal 'some\0\0\0\0\0\0' - - - it 'should not touch buffer without exit code and return default', -> - msg = 'some nice \n messgae {}' - buffer = new Buffer msg - code = m.parseExitCode buffer, 10 - - expect(buffer.toString()).to.equal msg - expect(code).to.equal 10 - - - it 'should not slice buffer if smaller than exit code msg', -> - # regression - fakeBuffer = {length: 1, slice: -> null} - sinon.stub fakeBuffer, 'slice' - - code = m.parseExitCode fakeBuffer, 10 - expect(fakeBuffer.slice).not.to.have.been.called - - - it 'should parse any single digit exit code', -> - expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '1').to.equal 1 - expect(m.parseExitCode new Buffer 'something\nfake' + EXIT + '7').to.equal 7 diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js new file mode 100644 index 000000000..5d5c7a785 --- /dev/null +++ b/test/unit/runner.spec.js @@ -0,0 +1,48 @@ +import {loadFile} from 'mocks' +import constant from '../../lib/constants' + +describe('runner', () => { + var m + + beforeEach(() => { + m = loadFile(__dirname + '/../../lib/runner.js') + }) + + describe('parseExitCode', () => { + var EXIT = constant.EXIT_CODE + + it('should return 0 exit code if present in the buffer', () => { + expect(m.parseExitCode(new Buffer(`something\nfake${EXIT}0`))).to.equal(0) + }) + + it('should null the exit code part of the buffer', () => { + var buffer = new Buffer(`some${EXIT}1`) + m.parseExitCode(buffer) + + expect(buffer.toString()).to.equal('some\0\0\0\0\0\0') + }) + + it('should not touch buffer without exit code and return default', () => { + var msg = 'some nice \n messgae {}' + var buffer = new Buffer(msg) + var code = m.parseExitCode(buffer, 10) + + expect(buffer.toString()).to.equal(msg) + expect(code).to.equal(10) + }) + + it('should not slice buffer if smaller than exit code msg', () => { + // regression + var fakeBuffer = {length: 1, slice: () => null} + sinon.stub(fakeBuffer, 'slice') + + m.parseExitCode(fakeBuffer, 10) + expect(fakeBuffer.slice).not.to.have.been.called + }) + + it('should parse any single digit exit code', () => { + expect(m.parseExitCode(new Buffer(`something\nfake${EXIT}1`))).to.equal(1) + expect(m.parseExitCode(new Buffer(`something\nfake${EXIT}7`))).to.equal(7) + }) + }) +}) diff --git a/test/unit/server.spec.coffee b/test/unit/server.spec.coffee deleted file mode 100644 index 3fffb003a..000000000 --- a/test/unit/server.spec.coffee +++ /dev/null @@ -1,167 +0,0 @@ -#============================================================================== -# lib/server.js module -#============================================================================== - -Server = require('../../lib/server') -BrowserCollection = require('../../lib/browser_collection') - -describe 'server', -> - - server = mockConfig = browserCollection = injector = webServerOnError = null - fileListOnResolve = fileListOnReject = mockLauncher = null - mockFileList = mockWebServer = mockSocketServer = mockExecutor = doneSpy = null - - beforeEach -> - browserCollection = new BrowserCollection - doneSpy = sinon.spy() - - fileListOnResolve = fileListOnReject = null - - doneSpy = sinon.spy() - - mockConfig = - frameworks: [] - port: 9876 - autoWatch: true - hostname: 'localhost' - urlRoot: '/' - browsers: ['fake'] - singleRun: true - logLevel: 'OFF' - browserDisconnectTolerance: 0 - - server = new Server(mockConfig, doneSpy) - - sinon.stub(server._injector, 'invoke').returns([]) - - mockExecutor = - schedule: -> - - mockFileList = - refresh: sinon.spy( -> then: (onResolve, onReject) -> - fileListOnResolve = onResolve - fileListOnReject = onReject - ) - - mockLauncher = - launch: -> - markCaptured: -> - areAllCaptured: -> false - restart: -> true - kill: -> true - - mockSocketServer = - flashPolicyServer: - close: -> - sockets: - sockets: {} - on: -> - emit: -> - - mockWebServer = - on: (name, handler) -> - if name == 'error' - webServerOnError = handler - listen: sinon.spy((port, callback) -> callback && callback()) - removeAllListeners: -> - close: -> - - webServerOnError = null - - - - #============================================================================ - # server._start() - #============================================================================ - describe '_start', -> - it 'should start the web server after all files have been preprocessed successfully', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - expect(mockFileList.refresh).to.have.been.called - expect(fileListOnResolve).not.to.be.null - expect(mockWebServer.listen).not.to.have.been.called - expect(server._injector.invoke).not.to.have.been.calledWith mockLauncher.launch, mockLauncher - - fileListOnResolve() - - expect(mockWebServer.listen).to.have.been.called - expect(server._injector.invoke).to.have.been.calledWith mockLauncher.launch, mockLauncher - - it 'should start the web server after all files have been preprocessed with an error', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - expect(mockFileList.refresh).to.have.been.called - expect(fileListOnReject).not.to.be.null - expect(mockWebServer.listen).not.to.have.been.called - expect(server._injector.invoke).not.to.have.been.calledWith mockLauncher.launch, mockLauncher - - fileListOnReject() - - expect(mockWebServer.listen).to.have.been.called - expect(server._injector.invoke).to.have.been.calledWith mockLauncher.launch, mockLauncher - - it 'should launch browsers after the web server has started', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - expect(mockWebServer.listen).not.to.have.been.called - expect(server._injector.invoke).not.to.have.been.calledWith mockLauncher.launch, mockLauncher - - fileListOnResolve() - - expect(mockWebServer.listen).to.have.been.called - expect(server._injector.invoke).to.have.been.calledWith mockLauncher.launch, mockLauncher - - it 'should try next port if already in use', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - expect(mockWebServer.listen).not.to.have.been.called - expect(webServerOnError).not.to.be.null - - expect(mockConfig.port).to.be.equal 9876 - - fileListOnResolve() - - expect(mockWebServer.listen).to.have.been.calledWith(9876) - - webServerOnError({ code: 'EADDRINUSE'}) - - expect(mockWebServer.listen).to.have.been.calledWith(9877) - expect(mockConfig.port).to.be.equal 9877 - - it 'should emit a browsers_ready event once all the browsers are captured', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - browsersReady = sinon.spy() - server.on('browsers_ready', browsersReady) - - mockLauncher.areAllCaptured = -> false - fileListOnResolve() - expect(browsersReady).not.to.have.been.called - - mockLauncher.areAllCaptured = -> true - server.emit('browser_register', {}) - expect(browsersReady).to.have.been.called - - it 'should emit a browser_register event for each browser added', -> - server._start(mockConfig, mockLauncher, null, mockFileList, - mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) - - browsersReady = sinon.spy() - server.on('browsers_ready', browsersReady) - - mockLauncher.areAllCaptured = -> false - fileListOnResolve() - expect(browsersReady).not.to.have.been.called - - mockLauncher.areAllCaptured = -> true - server.emit('browser_register', {}) - expect(browsersReady).to.have.been.called - - describe.skip 'singleRun', -> - it 'should run tests when all browsers captured', -> - it 'should run tests when first browser captured if no browser configured', -> diff --git a/test/unit/server.spec.js b/test/unit/server.spec.js new file mode 100644 index 000000000..e537aedcf --- /dev/null +++ b/test/unit/server.spec.js @@ -0,0 +1,187 @@ +import Server from '../../lib/server' +import BrowserCollection from '../../lib/browser_collection' + +describe('server', () => { + var mockConfig + var browserCollection + var webServerOnError + var fileListOnReject + var mockLauncher + var mockWebServer + var mockSocketServer + var mockExecutor + var doneSpy + var server = mockConfig = browserCollection = webServerOnError = null + var fileListOnResolve = fileListOnReject = mockLauncher = null + var mockFileList = mockWebServer = mockSocketServer = mockExecutor = doneSpy = null + + beforeEach(() => { + browserCollection = new BrowserCollection() + doneSpy = sinon.spy() + + fileListOnResolve = fileListOnReject = null + + doneSpy = sinon.spy() + + mockConfig = + {frameworks: [], + port: 9876, + autoWatch: true, + hostname: 'localhost', + urlRoot: '/', + browsers: ['fake'], + singleRun: true, + logLevel: 'OFF', + browserDisconnectTolerance: 0} + + server = new Server(mockConfig, doneSpy) + + sinon.stub(server._injector, 'invoke').returns([]) + + mockExecutor = + {schedule: () => {}} + + mockFileList = { + refresh: sinon.spy(() => { + return { + then (onResolve, onReject) { + fileListOnResolve = onResolve + fileListOnReject = onReject + } + } + }) + } + + mockLauncher = { + launch: () => {}, + markCaptured: () => {}, + areAllCaptured: () => false, + restart: () => true, + kill: () => true + } + + mockSocketServer = { + flashPolicyServer: { + close: () => {} + }, + sockets: { + sockets: {}, + on: () => {}, + emit: () => {} + } + } + + mockWebServer = { + on (name, handler) { + if (name === 'error') { + webServerOnError = handler + } + }, + listen: sinon.spy((port, callback) => { + callback && callback() + }), + removeAllListeners: () => {}, + close: () => {} + } + + webServerOnError = null + }) + + // ============================================================================ + // server._start() + // ============================================================================ + describe('_start', () => { + it('should start the web server after all files have been preprocessed successfully', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + expect(mockFileList.refresh).to.have.been.called + expect(fileListOnResolve).not.to.be.null + expect(mockWebServer.listen).not.to.have.been.called + expect(server._injector.invoke).not.to.have.been.calledWith(mockLauncher.launch, mockLauncher) + + fileListOnResolve() + + expect(mockWebServer.listen).to.have.been.called + expect(server._injector.invoke).to.have.been.calledWith(mockLauncher.launch, mockLauncher) + }) + + it('should start the web server after all files have been preprocessed with an error', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + expect(mockFileList.refresh).to.have.been.called + expect(fileListOnReject).not.to.be.null + expect(mockWebServer.listen).not.to.have.been.called + expect(server._injector.invoke).not.to.have.been.calledWith(mockLauncher.launch, mockLauncher) + + fileListOnReject() + + expect(mockWebServer.listen).to.have.been.called + expect(server._injector.invoke).to.have.been.calledWith(mockLauncher.launch, mockLauncher) + }) + + it('should launch browsers after the web server has started', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + expect(mockWebServer.listen).not.to.have.been.called + expect(server._injector.invoke).not.to.have.been.calledWith(mockLauncher.launch, mockLauncher) + + fileListOnResolve() + + expect(mockWebServer.listen).to.have.been.called + expect(server._injector.invoke).to.have.been.calledWith(mockLauncher.launch, mockLauncher) + }) + + it('should try next port if already in use', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + expect(mockWebServer.listen).not.to.have.been.called + expect(webServerOnError).not.to.be.null + + expect(mockConfig.port).to.be.equal(9876) + + fileListOnResolve() + + expect(mockWebServer.listen).to.have.been.calledWith(9876) + + webServerOnError({code: 'EADDRINUSE'}) + + expect(mockWebServer.listen).to.have.been.calledWith(9877) + expect(mockConfig.port).to.be.equal(9877) + }) + + it('should emit a browsers_ready event once all the browsers are captured', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + var browsersReady = sinon.spy() + server.on('browsers_ready', browsersReady) + + mockLauncher.areAllCaptured = () => false + fileListOnResolve() + expect(browsersReady).not.to.have.been.called + + mockLauncher.areAllCaptured = () => true + server.emit('browser_register', {}) + expect(browsersReady).to.have.been.called + }) + + it('should emit a browser_register event for each browser added', () => { + server._start(mockConfig, mockLauncher, null, mockFileList, mockWebServer, browserCollection, mockSocketServer, mockExecutor, doneSpy) + + var browsersReady = sinon.spy() + server.on('browsers_ready', browsersReady) + + mockLauncher.areAllCaptured = () => false + fileListOnResolve() + expect(browsersReady).not.to.have.been.called + + mockLauncher.areAllCaptured = () => true + server.emit('browser_register', {}) + expect(browsersReady).to.have.been.called + }) + }) + + describe.skip('singleRun', () => { + it('should run tests when all browsers captured', () => {}) + it('should run tests when first browser captured if no browser configured', () => {}) + }) +}) diff --git a/test/unit/watcher.spec.coffee b/test/unit/watcher.spec.coffee deleted file mode 100644 index 866454975..000000000 --- a/test/unit/watcher.spec.coffee +++ /dev/null @@ -1,145 +0,0 @@ -#============================================================================== -# lib/watcher.js module -#============================================================================== -describe 'watcher', -> - mocks = require 'mocks' - config = require '../../lib/config' - path = require 'path' - m = null - - beforeEach -> - mocks_ = chokidar: mocks.chokidar - m = mocks.loadFile __dirname + '/../../lib/watcher.js', mocks_ - - #============================================================================ - # baseDirFromPattern() [PRIVATE] - #============================================================================ - describe 'baseDirFromPattern', -> - - it 'should return parent directory without start', -> - expect(m.baseDirFromPattern '/some/path/**/more.js').to.equal '/some/path' - expect(m.baseDirFromPattern '/some/p*/file.js').to.equal '/some' - - - it 'should remove part with !(x)', -> - expect(m.baseDirFromPattern '/some/p/!(a|b).js').to.equal '/some/p' - expect(m.baseDirFromPattern '/some/p!(c|b)*.js').to.equal '/some' - - - it 'should remove part with +(x)', -> - expect(m.baseDirFromPattern '/some/p/+(a|b).js').to.equal '/some/p' - expect(m.baseDirFromPattern '/some/p+(c|bb).js').to.equal '/some' - - - it 'should remove part with (x)?', -> - expect(m.baseDirFromPattern '/some/p/(a|b)?.js').to.equal '/some/p' - expect(m.baseDirFromPattern '/some/p(c|b)?.js').to.equal '/some' - - - it 'should allow paths with parentheses', -> - expect(m.baseDirFromPattern '/some/x (a|b)/a.js').to.equal '/some/x (a|b)/a.js' - expect(m.baseDirFromPattern '/some/p(c|b)/*.js').to.equal '/some/p(c|b)' - - - it 'should ignore exact files', -> - expect(m.baseDirFromPattern '/usr/local/bin.js').to.equal '/usr/local/bin.js' - - - #============================================================================== - # watchPatterns() [PRIVATE] - #============================================================================== - describe 'watchPatterns', -> - chokidarWatcher = null - - beforeEach -> - chokidarWatcher = new mocks.chokidar.FSWatcher - - it 'should watch all the patterns', -> - m.watchPatterns ['/some/*.js', '/a/*'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some', '/a'] - - - it 'should expand braces and watch all the patterns', -> - m.watchPatterns ['/some/{a,b}/*.js', '/a/*'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some/a', '/some/b', '/a'] - - - it 'should not watch the same path twice', -> - m.watchPatterns ['/some/a*.js', '/some/*.txt'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some'] - - - it 'should not watch the same path twice when using braces', -> - m.watchPatterns ['/some/*.{js,txt}'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some'] - - - it 'should not watch subpaths that are already watched', -> - m.watchPatterns ['/some/sub/*.js', '/some/a*.*'].map(path.normalize), chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal [path.normalize('/some')] - - - it 'should watch a file matching subpath directory', -> - # regression #521 - m.watchPatterns ['/some/test-file.js', '/some/test/**'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some/test-file.js', '/some/test'] - - - it 'should watch files matching a subpath directory with braces', -> - m.watchPatterns ['/some/{a,b}/test.js'], chokidarWatcher - expect(chokidarWatcher.watchedPaths_).to.deep.equal ['/some/a/test.js', '/some/b/test.js'] - - - describe 'getWatchedPatterns', -> - - it 'should return list of watched patterns (strings)', -> - watchedPatterns = m.getWatchedPatterns [ - config.createPatternObject('/watched.js') - config.createPatternObject(pattern: 'non/*.js', watched: false) - ] - expect(watchedPatterns).to.deep.equal ['/watched.js'] - - - #============================================================================ - # ignore() [PRIVATE] - #============================================================================ - describe 'ignore', -> - FILE_STAT = - isDirectory: -> false - isFile: -> true - DIRECTORY_STAT = - isDirectory: -> true - isFile: -> false - - it 'should ignore all files', -> - ignore = m.createIgnore ['**/*'], ['**/*'] - expect(ignore '/some/files/deep/nested.js', FILE_STAT).to.equal true - expect(ignore '/some/files', FILE_STAT).to.equal true - - - it 'should ignore .# files', -> - ignore = m.createIgnore ['**/*'], ['**/.#*'] - expect(ignore '/some/files/deep/nested.js', FILE_STAT).to.equal false - expect(ignore '/some/files', FILE_STAT).to.equal false - expect(ignore '/some/files/deep/.npm', FILE_STAT).to.equal false - expect(ignore '.#files.js', FILE_STAT).to.equal true - expect(ignore '/some/files/deeper/nested/.#files.js', FILE_STAT).to.equal true - - - it 'should ignore files that do not match any pattern', -> - ignore = m.createIgnore ['/some/*.js'], [] - expect(ignore '/a.js', FILE_STAT).to.equal true - expect(ignore '/some.js', FILE_STAT).to.equal true - expect(ignore '/some/a.js', FILE_STAT).to.equal false - - - it 'should not ignore directories', -> - ignore = m.createIgnore ['**/*'], ['**/*'] - expect(ignore '/some/dir', DIRECTORY_STAT).to.equal false - - - it 'should not ignore items without stat', -> - # before we know whether it's a directory or file, we can't ignore - ignore = m.createIgnore ['**/*'], ['**/*'] - expect(ignore '/some.js', undefined).to.equal false - expect(ignore '/what/ever', undefined).to.equal false diff --git a/test/unit/watcher.spec.js b/test/unit/watcher.spec.js new file mode 100644 index 000000000..125a67574 --- /dev/null +++ b/test/unit/watcher.spec.js @@ -0,0 +1,145 @@ +import mocks from 'mocks' +import path from 'path' + +describe('watcher', () => { + + var config = require('../../lib/config') + + var m = null + + beforeEach(() => { + var mocks_ = {chokidar: mocks.chokidar} + m = mocks.loadFile(__dirname + '/../../lib/watcher.js', mocks_) + }) + + describe('baseDirFromPattern', () => { + it('should return parent directory without start', () => { + expect(m.baseDirFromPattern('/some/path/**/more.js')).to.equal('/some/path') + expect(m.baseDirFromPattern('/some/p*/file.js')).to.equal('/some') + }) + + it('should remove part with !(x)', () => { + expect(m.baseDirFromPattern('/some/p/!(a|b).js')).to.equal('/some/p') + expect(m.baseDirFromPattern('/some/p!(c|b)*.js')).to.equal('/some') + }) + + it('should remove part with +(x)', () => { + expect(m.baseDirFromPattern('/some/p/+(a|b).js')).to.equal('/some/p') + expect(m.baseDirFromPattern('/some/p+(c|bb).js')).to.equal('/some') + }) + + it('should remove part with (x)?', () => { + expect(m.baseDirFromPattern('/some/p/(a|b)?.js')).to.equal('/some/p') + expect(m.baseDirFromPattern('/some/p(c|b)?.js')).to.equal('/some') + }) + + it('should allow paths with parentheses', () => { + expect(m.baseDirFromPattern('/some/x (a|b)/a.js')).to.equal('/some/x (a|b)/a.js') + expect(m.baseDirFromPattern('/some/p(c|b)/*.js')).to.equal('/some/p(c|b)') + }) + + it('should ignore exact files', () => { + expect(m.baseDirFromPattern('/usr/local/bin.js')).to.equal('/usr/local/bin.js') + }) + }) + + describe('watchPatterns', () => { + var chokidarWatcher = null + + beforeEach(() => { + chokidarWatcher = new mocks.chokidar.FSWatcher() + }) + + it('should watch all the patterns', () => { + m.watchPatterns(['/some/*.js', '/a/*'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some', '/a']) + }) + + it('should expand braces and watch all the patterns', () => { + m.watchPatterns(['/some/{a,b}/*.js', '/a/*'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some/a', '/some/b', '/a']) + }) + + it('should not watch the same path twice', () => { + m.watchPatterns(['/some/a*.js', '/some/*.txt'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some']) + }) + + it('should not watch the same path twice when using braces', () => { + m.watchPatterns(['/some/*.{js,txt}'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some']) + }) + + it('should not watch subpaths that are already watched', () => { + m.watchPatterns(['/some/sub/*.js', '/some/a*.*'].map(path.normalize), chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal([path.normalize('/some')]) + }) + + it('should watch a file matching subpath directory', () => { + // regression #521 + m.watchPatterns(['/some/test-file.js', '/some/test/**'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some/test-file.js', '/some/test']) + }) + + it('should watch files matching a subpath directory with braces', () => { + m.watchPatterns(['/some/{a,b}/test.js'], chokidarWatcher) + expect(chokidarWatcher.watchedPaths_).to.deep.equal(['/some/a/test.js', '/some/b/test.js']) + }) + }) + + describe('getWatchedPatterns', () => { + it('should return list of watched patterns (strings)', () => { + var watchedPatterns = m.getWatchedPatterns([ + config.createPatternObject('/watched.js'), + config.createPatternObject({pattern: 'non/*.js', watched: false}) + ]) + expect(watchedPatterns).to.deep.equal(['/watched.js']) + }) + }) + + describe('ignore', () => { + var FILE_STAT = { + isDirectory: () => false, + isFile: () => true + } + + var DIRECTORY_STAT = { + isDirectory: () => true, + isFile: () => false + } + + it('should ignore all files', () => { + var ignore = m.createIgnore(['**/*'], ['**/*']) + expect(ignore('/some/files/deep/nested.js', FILE_STAT)).to.equal(true) + expect(ignore('/some/files', FILE_STAT)).to.equal(true) + }) + + it('should ignore .# files', () => { + var ignore = m.createIgnore(['**/*'], ['**/.#*']) + expect(ignore('/some/files/deep/nested.js', FILE_STAT)).to.equal(false) + expect(ignore('/some/files', FILE_STAT)).to.equal(false) + expect(ignore('/some/files/deep/.npm', FILE_STAT)).to.equal(false) + expect(ignore('.#files.js', FILE_STAT)).to.equal(true) + expect(ignore('/some/files/deeper/nested/.#files.js', FILE_STAT)).to.equal(true) + }) + + it('should ignore files that do not match any pattern', () => { + var ignore = m.createIgnore(['/some/*.js'], []) + expect(ignore('/a.js', FILE_STAT)).to.equal(true) + expect(ignore('/some.js', FILE_STAT)).to.equal(true) + expect(ignore('/some/a.js', FILE_STAT)).to.equal(false) + }) + + it('should not ignore directories', () => { + var ignore = m.createIgnore(['**/*'], ['**/*']) + expect(ignore('/some/dir', DIRECTORY_STAT)).to.equal(false) + }) + + it('should not ignore items without stat', () => { + // before we know whether it's a directory or file, we can't ignore + var ignore = m.createIgnore(['**/*'], ['**/*']) + expect(ignore('/some.js', undefined)).to.equal(false) + expect(ignore('/what/ever', undefined)).to.equal(false) + }) + }) +}) diff --git a/test/unit/web-server.spec.coffee b/test/unit/web-server.spec.coffee deleted file mode 100644 index b4e1e015b..000000000 --- a/test/unit/web-server.spec.coffee +++ /dev/null @@ -1,93 +0,0 @@ -require 'core-js' -request = require 'supertest-as-promised' -di = require 'di' -Promise = require 'bluebird' -mocks = require 'mocks' -_ = require('../../lib/helper')._ - -describe 'web-server', -> - - File = require('../../lib/file') - - EventEmitter = require('events').EventEmitter - - _mocks = {} - _globals = {__dirname: '/karma/lib'} - - _mocks.fs = mocks.fs.create - karma: - static: - 'client.html': mocks.fs.file(0, 'CLIENT HTML') - base: - path: - 'one.js': mocks.fs.file(0, 'js-source') - 'new.js': mocks.fs.file(0, 'new-js-source') - - # NOTE(vojta): only loading once, to speed things up - # this relies on the fact that none of these tests mutate fs - m = mocks.loadFile __dirname + '/../../lib/web-server.js', _mocks, _globals - - customFileHandlers = server = emitter = null - - servedFiles = (files) -> - emitter.emit 'file_list_modified', {included: [], served: files} - - beforeEach -> - customFileHandlers = [] - emitter = new EventEmitter - - injector = new di.Injector [{ - config: ['value', {basePath: '/base/path', urlRoot: '/'}] - customFileHandlers: ['value', customFileHandlers], - emitter: ['value', emitter], - fileList: ['value', null], - capturedBrowsers: ['value', null], - reporter: ['value', null], - executor: ['value', null] - }] - - server = injector.invoke m.createWebServer - - it 'should serve client.html', () -> - servedFiles new Set() - - request(server) - .get('/') - .expect(200, 'CLIENT HTML') - - it 'should serve source files', () -> - servedFiles new Set([new File '/base/path/one.js']) - - request(server) - .get('/base/one.js') - .expect(200, 'js-source') - - it 'should serve updated source files on file_list_modified', () -> - servedFiles new Set([new File '/base/path/one.js']) - servedFiles new Set([new File '/base/path/new.js']) - - request(server) - .get('/base/new.js') - .expect(200, 'new-js-source') - - it 'should load custom handlers', () -> - servedFiles new Set() - - # TODO(vojta): change this, only keeping because karma-dart is relying on it - customFileHandlers.push { - urlRegex: /\/some\/weird/ - handler: (request, response, staticFolder, adapterFolder, baseFolder, urlRoot) -> - response.writeHead 222 - response.end 'CONTENT' - } - - request(server) - .get('/some/weird/url') - .expect(222, 'CONTENT') - - it 'should serve 404 for non-existing files', () -> - servedFiles new Set() - - request(server) - .get('/non/existing.html') - .expect(404) diff --git a/test/unit/web-server.spec.js b/test/unit/web-server.spec.js new file mode 100644 index 000000000..38bac6bcf --- /dev/null +++ b/test/unit/web-server.spec.js @@ -0,0 +1,106 @@ +require('core-js') +import {EventEmitter} from 'events' +import request from 'supertest-as-promised' +import di from 'di' +import mocks from 'mocks' + +describe('web-server', () => { + var server + var emitter + var File = require('../../lib/file') + + var _mocks = {} + var _globals = {__dirname: '/karma/lib'} + + _mocks.fs = mocks.fs.create({ + karma: { + static: { + 'client.html': mocks.fs.file(0, 'CLIENT HTML') + } + }, + base: { + path: { + 'one.js': mocks.fs.file(0, 'js-source'), + 'new.js': mocks.fs.file(0, 'new-js-source') + } + } + }) + + // NOTE(vojta): only loading once, to speed things up + // this relies on the fact that none of these tests mutate fs + var m = mocks.loadFile(__dirname + '/../../lib/web-server.js', _mocks, _globals) + + var customFileHandlers = server = emitter = null + + var servedFiles = (files) => { + emitter.emit('file_list_modified', {included: [], served: files}) + } + + beforeEach(() => { + customFileHandlers = [] + emitter = new EventEmitter() + + var injector = new di.Injector([{ + config: ['value', {basePath: '/base/path', urlRoot: '/'}], + customFileHandlers: ['value', customFileHandlers], + emitter: ['value', emitter], + fileList: ['value', null], + capturedBrowsers: ['value', null], + reporter: ['value', null], + executor: ['value', null], + proxies: ['value', null] + }]) + + server = injector.invoke(m.createWebServer) + }) + + it('should serve client.html', () => { + servedFiles(new Set()) + + return request(server) + .get('/') + .expect(200, 'CLIENT HTML') + }) + + it('should serve source files', () => { + servedFiles(new Set([new File('/base/path/one.js')])) + + return request(server) + .get('/base/one.js') + .expect(200, 'js-source') + }) + + it('should serve updated source files on file_list_modified', () => { + servedFiles(new Set([new File('/base/path/one.js')])) + servedFiles(new Set([new File('/base/path/new.js')])) + + return request(server) + .get('/base/new.js') + .expect(200, 'new-js-source') + }) + + it('should load custom handlers', () => { + servedFiles(new Set()) + + // TODO(vojta): change this, only keeping because karma-dart is relying on it + customFileHandlers.push({ + urlRegex: /\/some\/weird/, + handler (request, response, staticFolder, adapterFolder, baseFolder, urlRoot) { + response.writeHead(222) + response.end('CONTENT') + } + }) + + return request(server) + .get('/some/weird/url') + .expect(222, 'CONTENT') + }) + + it('should serve 404 for non-existing files', () => { + servedFiles(new Set()) + + return request(server) + .get('/non/existing.html') + .expect(404) + }) +})