diff --git a/CHANGELOG.md b/CHANGELOG.md index d73cc749b..f7b8e13a8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,17 @@ + +## [2.0.4](https://github.com/karma-runner/karma/compare/v2.0.3...v2.0.4) (2018-06-21) + + +### Bug Fixes + +* **deps:** remove babel-core and babel call in wallaby. ([#3044](https://github.com/karma-runner/karma/issues/3044)) ([7da8ca0](https://github.com/karma-runner/karma/commit/7da8ca0)) +* **events:** bind emitters with for..in. ([#3059](https://github.com/karma-runner/karma/issues/3059)) ([b99f03f](https://github.com/karma-runner/karma/commit/b99f03f)), closes [#3057](https://github.com/karma-runner/karma/issues/3057) +* **launcher:** Only markCaptured browsers that are launched. ([#3047](https://github.com/karma-runner/karma/issues/3047)) ([f8f3ebc](https://github.com/karma-runner/karma/commit/f8f3ebc)) +* **server:** actually call stert(). ([#3062](https://github.com/karma-runner/karma/issues/3062)) ([40d836a](https://github.com/karma-runner/karma/commit/40d836a)) +* **server:** Resurrect static function Server.start() lost in 2.0.3 ([#3055](https://github.com/karma-runner/karma/issues/3055)) ([c88ebc6](https://github.com/karma-runner/karma/commit/c88ebc6)) + + + ## [2.0.3](https://github.com/karma-runner/karma/compare/v0.12.16...v2.0.3) (2018-06-15) diff --git a/lib/events.js b/lib/events.js index 29951849b..6d8280bfc 100644 --- a/lib/events.js +++ b/lib/events.js @@ -33,13 +33,14 @@ function bufferEvents (emitter, eventsToBuffer) { class KarmaEventEmitter extends EventEmitter { bind (object) { - Object.keys(object).forEach((method) => { + for (const method in object) { if (method.startsWith('on') && helper.isFunction(object[method])) { this.on(helper.camelToSnake(method.substr(2)), function () { + // We do not use an arrow function here, to supply the caller as this. object[method].apply(object, Array.from(arguments).concat(this)) }) } - }) + } } emitAsync (name) { diff --git a/lib/file-list.js b/lib/file-list.js index 13129cd32..887a9d71e 100644 --- a/lib/file-list.js +++ b/lib/file-list.js @@ -1,8 +1,5 @@ 'use strict' -// Dependencies -// ------------ - const Promise = require('bluebird') const mm = require('minimatch') const Glob = require('glob').Glob @@ -16,127 +13,56 @@ const helper = require('./helper') const log = require('./logger').create('watcher') const createPatternObject = require('./config').createPatternObject -// Constants -// --------- - -const GLOB_OPTS = { - cwd: '/', - follow: true, - nodir: true, - sync: true -} - -// Helper Functions -// ---------------- - -const byPath = (a, b) => { +function byPath (a, b) { if (a.path > b.path) return 1 if (a.path < b.path) return -1 return 0 } -/** - * The List is an object for tracking all files that karma knows about - * currently. - */ class FileList { - /** - * @param {Array} patterns - * @param {Array} excludes - * @param {EventEmitter} emitter - * @param {Function} preprocess - * @param {number} autoWatchBatchDelay - */ constructor (patterns, excludes, emitter, preprocess, autoWatchBatchDelay) { - // Store options - this._patterns = patterns - this._excludes = excludes + this._patterns = patterns || [] + this._excludes = excludes || [] this._emitter = emitter this._preprocess = Promise.promisify(preprocess) - this._autoWatchBatchDelay = autoWatchBatchDelay - // The actual list of files this.buckets = new Map() - // Internal tracker if we are refreshing. - // When a refresh is triggered this gets set - // to the promise that `this._refresh` returns. - // So we know we are refreshing when this promise - // is still pending, and we are done when it's either - // resolved or rejected. this._refreshing = Promise.resolve() - // Emit the `file_list_modified` event. - // This function is debounced to the value of `autoWatchBatchDelay` - // to avoid reloading while files are still being modified. const emit = () => { this._emitter.emit('file_list_modified', this.files) } - const debouncedEmit = _.debounce(emit, this._autoWatchBatchDelay) + const debouncedEmit = _.debounce(emit, autoWatchBatchDelay) this._emitModified = (immediate) => { immediate ? emit() : debouncedEmit() } } - // Private Interface - // ----------------- - - // Is the given path matched by any exclusion filter - // - // path - String - // - // Returns `undefined` if no match, otherwise the matching - // pattern. - _isExcluded (path) { - return _.find(this._excludes, (pattern) => mm(path, pattern)) + _findExcluded (path) { + return this._excludes.find((pattern) => mm(path, pattern)) } - // Find the matching include pattern for the given path. - // - // path - String - // - // Returns the match or `undefined` if none found. - _isIncluded (path) { - return _.find(this._patterns, (pattern) => mm(path, pattern.pattern)) + _findIncluded (path) { + return this._patterns.find((pattern) => mm(path, pattern.pattern)) } - // Find the given path in the bucket corresponding - // to the given pattern. - // - // path - String - // pattern - Object - // - // Returns a File or undefined _findFile (path, pattern) { if (!path || !pattern) return - if (!this.buckets.has(pattern.pattern)) return - - return _.find(Array.from(this.buckets.get(pattern.pattern)), (file) => { - return file.originalPath === path - }) + return this._getFilesByPattern(pattern.pattern).find((file) => file.originalPath === path) } - // Is the given path already in the files list. - // - // path - String - // - // Returns a boolean. _exists (path) { - const patterns = this._patterns.filter((pattern) => mm(path, pattern.pattern)) - - return !!_.find(patterns, (pattern) => this._findFile(path, pattern)) + return !!this._patterns.find((pattern) => mm(path, pattern.pattern) && this._findFile(path, pattern)) } - // Check if we are currently refreshing - _isRefreshing () { - return this._refreshing.isPending() + _getFilesByPattern (pattern) { + return this.buckets.get(pattern) || [] } - // Do the actual work of refreshing _refresh () { - const buckets = this.buckets const matchedFiles = new Set() let promise @@ -145,21 +71,20 @@ class FileList { const type = patternObject.type if (helper.isUrlAbsolute(pattern)) { - buckets.set(pattern, new Set([new Url(pattern, type)])) + this.buckets.set(pattern, [new Url(pattern, type)]) return Promise.resolve() } - const mg = new Glob(pathLib.normalize(pattern), GLOB_OPTS) + const mg = new Glob(pathLib.normalize(pattern), { cwd: '/', follow: true, nodir: true, sync: true }) const files = mg.found - buckets.set(pattern, new Set()) - if (_.isEmpty(files)) { + this.buckets.set(pattern, []) log.warn('Pattern "%s" does not match any file.', pattern) return } return Promise.map(files, (path) => { - if (this._isExcluded(path)) { + if (this._findExcluded(path)) { log.debug('Excluded file "%s"', path) return Promise.resolve() } @@ -170,27 +95,20 @@ class FileList { matchedFiles.add(path) - const mtime = mg.statCache[path].mtime - const doNotCache = patternObject.nocache - const type = patternObject.type - const file = new File(path, mtime, doNotCache, type) - + const file = new File(path, mg.statCache[path].mtime, patternObject.nocache, type) if (file.doNotCache) { log.debug('Not preprocessing "%s" due to nocache', pattern) return Promise.resolve(file) } - return this._preprocess(file).then(() => { - return file - }) + return this._preprocess(file).then(() => file) }) .then((files) => { files = _.compact(files) + this.buckets.set(pattern, files) if (_.isEmpty(files)) { log.warn('All files matched by "%s" were excluded or matched by prior matchers.', pattern) - } else { - buckets.set(pattern, new Set(files)) } }) }) @@ -198,7 +116,6 @@ class FileList { if (this._refreshing !== promise) { return this._refreshing } - this.buckets = buckets this._emitModified(true) return this.files }) @@ -206,25 +123,10 @@ class FileList { return promise } - // Public Interface - // ---------------- - get files () { - const uniqueFlat = (list) => { - return _.uniq(_.flatten(list), 'path') - } - - const expandPattern = (p) => { - return Array.from(this.buckets.get(p.pattern) || []).sort(byPath) - } - - const served = this._patterns.filter((pattern) => { - return pattern.served - }) - .map(expandPattern) - - const lookup = {} + const served = [] const included = {} + const lookup = {} this._patterns.forEach((p) => { // This needs to be here sadly, as plugins are modifiying // the _patterns directly resulting in elements not being @@ -233,10 +135,16 @@ class FileList { p = createPatternObject(p) } - const bucket = expandPattern(p) - bucket.forEach((file) => { + const files = this._getFilesByPattern(p.pattern) + files.sort(byPath) + if (p.served) { + served.push.apply(served, files) // TODO: replace with served.push(...files) after remove Node 4 support + } + + files.forEach((file) => { const other = lookup[file.path] if (other && other.compare(p) < 0) return + lookup[file.path] = p if (p.included) { included[file.path] = file @@ -247,55 +155,31 @@ class FileList { }) return { - served: uniqueFlat(served), + served: _.uniq(served, 'path'), included: _.values(included) } } - // Reglob all patterns to update the list. - // - // Returns a promise that is resolved when the refresh - // is completed. refresh () { this._refreshing = this._refresh() return this._refreshing } - // Set new patterns and excludes and update - // the list accordingly - // - // patterns - Array, the new patterns. - // excludes - Array, the new exclude patterns. - // - // Returns a promise that is resolved when the refresh - // is completed. reload (patterns, excludes) { - this._patterns = patterns - this._excludes = excludes + this._patterns = patterns || [] + this._excludes = excludes || [] - // Wait until the current refresh is done and then do a - // refresh to ensure a refresh actually happens return this.refresh() } - // Add a new file from the list. - // This is called by the watcher - // - // path - String, the path of the file to update. - // - // Returns a promise that is resolved when the update - // is completed. addFile (path) { - // Ensure we are not adding a file that should be excluded - const excluded = this._isExcluded(path) + const excluded = this._findExcluded(path) if (excluded) { log.debug('Add file "%s" ignored. Excluded by "%s".', path, excluded) - return Promise.resolve(this.files) } - const pattern = this._isIncluded(path) - + const pattern = this._findIncluded(path) if (!pattern) { log.debug('Add file "%s" ignored. Does not match any pattern.', path) return Promise.resolve(this.files) @@ -307,15 +191,16 @@ class FileList { } const file = new File(path) - this.buckets.get(pattern.pattern).add(file) + this._getFilesByPattern(pattern.pattern).push(file) return Promise.all([ fs.statAsync(path), this._refreshing - ]).spread((stat) => { - file.mtime = stat.mtime - return this._preprocess(file) - }) + ]) + .spread((stat) => { + file.mtime = stat.mtime + return this._preprocess(file) + }) .then(() => { log.info('Added file "%s".', path) this._emitModified() @@ -323,18 +208,11 @@ class FileList { }) } - // Update the `mtime` of a file. - // This is called by the watcher - // - // path - String, the path of the file to update. - // - // Returns a promise that is resolved when the update - // is completed. changeFile (path) { - const pattern = this._isIncluded(path) + const pattern = this._findIncluded(path) const file = this._findFile(path, pattern) - if (!pattern || !file) { + if (!file) { log.debug('Changed file "%s" ignored. Does not match any file in the list.', path) return Promise.resolve(this.files) } @@ -353,42 +231,31 @@ class FileList { this._emitModified() return this.files }) - .catch(Promise.CancellationError, () => { - return this.files - }) + .catch(Promise.CancellationError, () => this.files) } - // Remove a file from the list. - // This is called by the watcher - // - // path - String, the path of the file to update. - // - // Returns a promise that is resolved when the update - // is completed. removeFile (path) { return Promise.try(() => { - const pattern = this._isIncluded(path) + const pattern = this._findIncluded(path) const file = this._findFile(path, pattern) - if (!pattern || !file) { + if (file) { + helper.arrayRemove(this._getFilesByPattern(pattern.pattern), file) + log.info('Removed file "%s".', path) + + this._emitModified() + } else { log.debug('Removed file "%s" ignored. Does not match any file in the list.', path) - return this.files } - - this.buckets.get(pattern.pattern).delete(file) - - log.info('Removed file "%s".', path) - this._emitModified() return this.files }) } } -FileList.factory = function (patterns, excludes, emitter, preprocess, autoWatchBatchDelay) { - return new FileList(patterns, excludes, emitter, preprocess, autoWatchBatchDelay) +FileList.factory = function (config, emitter, preprocess) { + return new FileList(config.files, config.exclude, emitter, preprocess, config.autoWatchBatchDelay) } -FileList.factory.$inject = ['config.files', 'config.exclude', 'emitter', 'preprocess', - 'config.autoWatchBatchDelay'] +FileList.factory.$inject = ['config', 'emitter', 'preprocess'] module.exports = FileList diff --git a/lib/launcher.js b/lib/launcher.js index f0fde7054..80d5d1d30 100644 --- a/lib/launcher.js +++ b/lib/launcher.js @@ -173,8 +173,10 @@ function Launcher (server, emitter, injector) { this.markCaptured = (id) => { const browser = getBrowserById(id) - browser.markCaptured() - log.debug(`${browser.name} (id ${browser.id}) captured in ${(Date.now() - lastStartTime) / 1000} secs`) + if (browser) { + browser.markCaptured() + log.debug(`${browser.name} (id ${browser.id}) captured in ${(Date.now() - lastStartTime) / 1000} secs`) + } } emitter.on('exit', this.killAll) diff --git a/lib/server.js b/lib/server.js index 05af65c0c..cbad577ed 100644 --- a/lib/server.js +++ b/lib/server.js @@ -373,6 +373,11 @@ class Server extends KarmaEventEmitter { }) child.unref() } + + static start (cliOptions, done) { + console.warn('Deprecated static method to be removed in v3.0') + return new Server(cliOptions, done).start() + } } Server.prototype._start.$inject = ['config', 'launcher', 'preprocess', 'fileList', 'capturedBrowsers', 'executor', 'done'] diff --git a/lib/temp_dir.js b/lib/temp_dir.js index 6543c2407..225059bbf 100644 --- a/lib/temp_dir.js +++ b/lib/temp_dir.js @@ -1,30 +1,31 @@ -var path = require('path') -var fs = require('graceful-fs') -var os = require('os') -var rimraf = require('rimraf') -var log = require('./logger').create('temp-dir') +'use strict' -var TEMP_DIR = os.tmpdir() +const path = require('path') +const fs = require('graceful-fs') +const rimraf = require('rimraf') +const log = require('./logger').create('temp-dir') + +const TEMP_DIR = require('os').tmpdir() module.exports = { - getPath: function (suffix) { + getPath (suffix) { return path.normalize(TEMP_DIR + suffix) }, - create: function (path) { - log.debug('Creating temp dir at %s', path) + create (path) { + log.debug(`Creating temp dir at ${path}`) try { fs.mkdirSync(path) } catch (e) { - log.warn('Failed to create a temp dir at %s', path) + log.warn(`Failed to create a temp dir at ${path}`) } return path }, - remove: function (path, done) { - log.debug('Cleaning temp dir %s', path) + remove (path, done) { + log.debug(`Cleaning temp dir ${path}`) rimraf(path, done) } } diff --git a/package.json b/package.json index 00347a84d..e111f79b3 100644 --- a/package.json +++ b/package.json @@ -30,17 +30,17 @@ "Maksim Ryzhikov ", "johnjbarton ", "Christian Budde Christensen ", - "Yaroslav Admin ", "ukasz Usarz ", - "Wesley Cho ", + "Yaroslav Admin ", "taichi ", + "Wesley Cho ", "Liam Newman ", "Todd Wolfson ", "Michał Gołębiowski-Owczarek ", "Mark Trostler ", + "lukasz ", "Ciro Nunes ", "Pawel Kozlowski ", - "lukasz ", "Shyam Seshadri ", "Christian Budde Christensen ", "Robo ", @@ -384,7 +384,6 @@ }, "devDependencies": { "LiveScript": "^1.3.0", - "babel-core": "^6.26.0", "browserify": "^14.5.0", "chai": "^4.1.2", "chai-as-promised": "^7.1.1", @@ -458,7 +457,7 @@ "engines": { "node": ">= 4" }, - "version": "2.0.3", + "version": "2.0.4", "license": "MIT", "scripts": { "lint": "eslint . --ext js --ignore-pattern *.tpl.js", diff --git a/test/unit/events.spec.js b/test/unit/events.spec.js index 70defe502..673b97e3f 100644 --- a/test/unit/events.spec.js +++ b/test/unit/events.spec.js @@ -20,34 +20,48 @@ describe('events', () => { var object = null beforeEach(() => { - object = sinon.stub({ + // Note: es6 class instances have non-enumerable prototype properties. + function FB () {}; + FB.prototype = { + onPrototypeBar () {} + } + object = new FB() + Object.assign(object, { onFoo: () => {}, onFooBar: () => {}, - foo: () => {}, - bar: () => {} + foo: () => {} }) + emitter.bind(object) }) it('should register all "on" methods to events', () => { + sinon.spy(object, 'onFoo') emitter.emit('foo') expect(object.onFoo).to.have.been.called + sinon.spy(object, 'onFooBar') emitter.emit('foo_bar') expect(object.onFooBar).to.have.been.called + sinon.spy(object, 'onPrototypeBar') + emitter.emit('prototype_bar') + expect(object.onPrototypeBar).to.have.been.called + + sinon.spy(object, 'foo') expect(object.foo).not.to.have.been.called - expect(object.bar).not.to.have.been.called }) it('should bind methods to the owner object', () => { + sinon.spy(object, 'foo') + sinon.spy(object, 'onFoo') + sinon.spy(object, 'onFooBar') 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 }) }) diff --git a/test/unit/file-list.spec.js b/test/unit/file-list.spec.js index 42a61a6e1..b4a99d3b6 100644 --- a/test/unit/file-list.spec.js +++ b/test/unit/file-list.spec.js @@ -11,15 +11,15 @@ const helper = require('../../lib/helper') const config = require('../../lib/config') // create an array of pattern objects from given strings -const patterns = function () { +function patterns () { return Array.from(arguments).map((str) => new config.Pattern(str)) } -const pathsFrom = (files) => { - return _.map(Array.from(files), 'path') +function pathsFrom (files) { + return Array.from(files).map((file) => file.path) } -const findFile = (path, files) => { +function findFile (path, files) { return Array.from(files).find((file) => file.path === path) } @@ -159,7 +159,7 @@ describe('FileList', () => { }) }) - describe('_isExcluded', () => { + describe('_findExcluded', () => { beforeEach(() => { preprocess = sinon.spy((file, done) => process.nextTick(done)) emitter = new EventEmitter() @@ -167,18 +167,18 @@ describe('FileList', () => { 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 + expect(list._findExcluded('hello.txt')).to.be.undefined + expect(list._findExcluded('/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') + expect(list._findExcluded('world.js')).to.be.eql('*.js') + expect(list._findExcluded('/hello/world/i.js')).to.be.eql('**/*.js') }) }) - describe('_isIncluded', () => { + describe('_findIncluded', () => { beforeEach(() => { preprocess = sinon.spy((file, done) => process.nextTick(done)) emitter = new EventEmitter() @@ -186,14 +186,14 @@ describe('FileList', () => { 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 + expect(list._findIncluded('hello.txt')).to.be.undefined + expect(list._findIncluded('/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') + expect(list._findIncluded('world.js').pattern).to.be.eql('*.js') + expect(list._findIncluded('/hello/world/i.js').pattern).to.be.eql('**/*.js') }) }) @@ -288,7 +288,7 @@ describe('FileList', () => { it('cancels refreshs', () => { const checkResult = (files) => { - expect(_.map(files.served, 'path')).to.contain('/some/a.js', '/some/b.js', '/some/c.js') + expect(pathsFrom(files.served)).to.contain('/some/a.js', '/some/b.js', '/some/c.js') } const p1 = list.refresh().then(checkResult) diff --git a/test/unit/launcher.spec.js b/test/unit/launcher.spec.js index d00ffa684..129773295 100644 --- a/test/unit/launcher.spec.js +++ b/test/unit/launcher.spec.js @@ -306,6 +306,12 @@ describe('launcher', () => { }) }) + describe('markCaptured', () => { + it('should not fail if an un-launched browser attaches', () => { + expect(() => l.markCaptured('not-a-thing')).to.not.throw() + }) + }) + describe('onExit', () => { it('should kill all browsers', (done) => { l.launch(['Fake', 'Fake'], 1) diff --git a/wallaby.js b/wallaby.js index e9b96d5db..cc45815b7 100644 --- a/wallaby.js +++ b/wallaby.js @@ -1,4 +1,3 @@ -const babel = require('babel-core') module.exports = function (wallaby) { return { @@ -32,12 +31,6 @@ module.exports = function (wallaby) { 'test/unit/**/*.spec.js' ], - compilers: { - '**/*.js': wallaby.compilers.babel({ - babel: babel - }) - }, - bootstrap: function (w) { var path = require('path') var mocha = w.testFramework