mirror of https://github.com/lukechilds/ava.git
Mark Wubben
9 years ago
committed by
Sindre Sorhus
13 changed files with 1204 additions and 106 deletions
@ -0,0 +1,203 @@ |
|||
'use strict'; |
|||
|
|||
var AvaError = require('./ava-error'); |
|||
var debug = require('debug')('ava:watcher'); |
|||
var defaultIgnore = require('ignore-by-default').directories(); |
|||
var multimatch = require('multimatch'); |
|||
var nodePath = require('path'); |
|||
var Promise = require('bluebird'); |
|||
|
|||
function requireChokidar() { |
|||
try { |
|||
return require('chokidar'); |
|||
} catch (err) { |
|||
throw new AvaError('The optional dependency chokidar failed to install and is required for --watch. Chokidar is likely not supported on your platform.'); |
|||
} |
|||
} |
|||
|
|||
function rethrowAsync(err) { |
|||
// Don't swallow exceptions. Note that any expected error should already have
|
|||
// been logged.
|
|||
setImmediate(function () { |
|||
throw err; |
|||
}); |
|||
} |
|||
|
|||
function getChokidarPatterns(sources, initialFiles) { |
|||
var paths = []; |
|||
var ignored = []; |
|||
|
|||
sources.forEach(function (pattern) { |
|||
if (pattern[0] === '!') { |
|||
ignored.push(pattern.slice(1)); |
|||
} else { |
|||
paths.push(pattern); |
|||
} |
|||
}); |
|||
|
|||
if (paths.length === 0) { |
|||
paths = ['package.json', '**/*.js']; |
|||
} |
|||
paths = paths.concat(initialFiles); |
|||
|
|||
if (ignored.length === 0) { |
|||
ignored = defaultIgnore; |
|||
} |
|||
|
|||
return {paths: paths, ignored: ignored}; |
|||
} |
|||
|
|||
exports.start = function (logger, api, sources, stdin) { |
|||
var isTest = makeTestMatcher(api.files, api.excludePatterns); |
|||
|
|||
var patterns = getChokidarPatterns(sources, api.files); |
|||
var watcher = requireChokidar().watch(patterns.paths, { |
|||
ignored: patterns.ignored, |
|||
ignoreInitial: true |
|||
}); |
|||
|
|||
var busy = api.run().then(function () { |
|||
logger.finish(); |
|||
}).catch(rethrowAsync); |
|||
|
|||
var dirtyStates = {}; |
|||
watcher.on('all', function (event, path) { |
|||
if (event === 'add' || event === 'change' || event === 'unlink') { |
|||
debug('Detected %s of %s', event, path); |
|||
dirtyStates[path] = event; |
|||
debounce(); |
|||
} |
|||
}); |
|||
|
|||
var debouncing = null; |
|||
var debounceAgain = false; |
|||
function debounce() { |
|||
if (debouncing) { |
|||
debounceAgain = true; |
|||
return; |
|||
} |
|||
|
|||
var timer = debouncing = setTimeout(function () { |
|||
busy.then(function () { |
|||
// Do nothing if debouncing was canceled while waiting for the busy
|
|||
// promise to fulfil.
|
|||
if (debouncing !== timer) { |
|||
return; |
|||
} |
|||
|
|||
if (debounceAgain) { |
|||
debouncing = null; |
|||
debounceAgain = false; |
|||
debounce(); |
|||
} else { |
|||
busy = runAfterChanges(logger, api, isTest, dirtyStates); |
|||
dirtyStates = {}; |
|||
debouncing = null; |
|||
debounceAgain = false; |
|||
} |
|||
}); |
|||
}, 10); |
|||
} |
|||
|
|||
function cancelDebounce() { |
|||
if (debouncing) { |
|||
clearTimeout(debouncing); |
|||
debouncing = null; |
|||
debounceAgain = false; |
|||
} |
|||
} |
|||
|
|||
stdin.resume(); |
|||
stdin.setEncoding('utf8'); |
|||
stdin.on('data', function (data) { |
|||
data = data.trim().toLowerCase(); |
|||
if (data !== 'rs') { |
|||
return; |
|||
} |
|||
|
|||
// Cancel the debouncer, it might rerun specific tests whereas *all* tests
|
|||
// need to be rerun.
|
|||
cancelDebounce(); |
|||
busy.then(function () { |
|||
// Cancel the debouncer again, it might have restarted while waiting for
|
|||
// the busy promise to fulfil.
|
|||
cancelDebounce(); |
|||
busy = runAfterChanges(logger, api, isTest, {}); |
|||
}); |
|||
}); |
|||
}; |
|||
|
|||
function makeTestMatcher(files, excludePatterns) { |
|||
var initialPatterns = files.concat(excludePatterns); |
|||
return function (path) { |
|||
// Like in api.js, tests must be .js files and not start with _
|
|||
if (nodePath.extname(path) !== '.js' || nodePath.basename(path)[0] === '_') { |
|||
return false; |
|||
} |
|||
|
|||
// Check if the entire path matches a pattern.
|
|||
if (multimatch(path, initialPatterns).length === 1) { |
|||
return true; |
|||
} |
|||
|
|||
// Check if the path contains any directory components.
|
|||
var dirname = nodePath.dirname(path); |
|||
if (dirname === '.') { |
|||
return false; |
|||
} |
|||
|
|||
// Compute all possible subpaths. Note that the dirname is assumed to be
|
|||
// relative to the working directory, without a leading `./`.
|
|||
var subpaths = dirname.split(nodePath.sep).reduce(function (subpaths, component) { |
|||
var parent = subpaths[subpaths.length - 1]; |
|||
if (parent) { |
|||
subpaths.push(nodePath.join(parent, component)); |
|||
} else { |
|||
subpaths.push(component); |
|||
} |
|||
return subpaths; |
|||
}, []); |
|||
|
|||
// Check if any of the possible subpaths match a pattern. If so, generate a
|
|||
// new pattern with **/*.js.
|
|||
var recursivePatterns = subpaths.filter(function (subpath) { |
|||
return multimatch(subpath, initialPatterns).length === 1; |
|||
}).map(function (subpath) { |
|||
return nodePath.join(subpath, '**', '*.js'); |
|||
}); |
|||
|
|||
// See if the entire path matches any of the subpaths patterns, taking the
|
|||
// excludePatterns into account. This mimicks the behavior in api.js
|
|||
return multimatch(path, recursivePatterns.concat(excludePatterns)).length === 1; |
|||
}; |
|||
} |
|||
|
|||
function runAfterChanges(logger, api, isTest, dirtyStates) { |
|||
var dirtyPaths = Object.keys(dirtyStates); |
|||
var dirtyTests = dirtyPaths.filter(isTest); |
|||
var addedOrChangedTests = dirtyTests.filter(function (path) { |
|||
return dirtyStates[path] !== 'unlink'; |
|||
}); |
|||
var unlinkedTests = dirtyTests.filter(function (path) { |
|||
return dirtyStates[path] === 'unlink'; |
|||
}); |
|||
|
|||
// No need to rerun tests if the only change is that tests were deleted.
|
|||
if (dirtyPaths.length > 0 && unlinkedTests.length === dirtyPaths.length) { |
|||
return Promise.resolve(); |
|||
} |
|||
|
|||
return new Promise(function (resolve) { |
|||
logger.reset(); |
|||
|
|||
// Run any new or changed tests, unless non-test files were changed too.
|
|||
// In that case rerun the entire test suite.
|
|||
if (dirtyPaths.length > 0 && dirtyTests.length === dirtyPaths.length) { |
|||
resolve(api.run(addedOrChangedTests)); |
|||
} else { |
|||
resolve(api.run()); |
|||
} |
|||
}).then(function () { |
|||
logger.finish(); |
|||
}).catch(rethrowAsync); |
|||
} |
@ -0,0 +1,644 @@ |
|||
'use strict'; |
|||
|
|||
var Promise = require('bluebird'); |
|||
var EventEmitter = require('events').EventEmitter; |
|||
var defaultIgnore = require('ignore-by-default').directories(); |
|||
var lolex = require('lolex'); |
|||
var path = require('path'); |
|||
var proxyquire = require('proxyquire'); |
|||
var sinon = require('sinon'); |
|||
var PassThrough = require('stream').PassThrough; |
|||
var test = require('tap').test; |
|||
|
|||
var setImmediate = require('../lib/globals').setImmediate; |
|||
|
|||
test('chokidar is not installed', function (t) { |
|||
t.plan(2); |
|||
|
|||
var subject = proxyquire.noCallThru().load('../lib/watcher', { |
|||
chokidar: null |
|||
}); |
|||
|
|||
try { |
|||
subject.start({}, {files: [], excludePatterns: []}, []); |
|||
} catch (err) { |
|||
t.is(err.name, 'AvaError'); |
|||
t.is(err.message, 'The optional dependency chokidar failed to install and is required for --watch. Chokidar is likely not supported on your platform.'); |
|||
} |
|||
}); |
|||
|
|||
test('chokidar is installed', function (_t) { |
|||
var chokidar = { |
|||
watch: sinon.stub() |
|||
}; |
|||
|
|||
var debug = sinon.spy(); |
|||
|
|||
var logger = { |
|||
finish: sinon.spy(), |
|||
reset: sinon.spy() |
|||
}; |
|||
|
|||
var api = { |
|||
run: sinon.stub() |
|||
}; |
|||
|
|||
var subject = proxyquire.noCallThru().load('../lib/watcher', { |
|||
chokidar: chokidar, |
|||
debug: function (name) { |
|||
return function () { |
|||
var args = [name]; |
|||
args.push.apply(args, arguments); |
|||
debug.apply(null, args); |
|||
}; |
|||
} |
|||
}); |
|||
|
|||
var clock; |
|||
var emitter; |
|||
var stdin; |
|||
_t.beforeEach(function (done) { |
|||
if (clock) { |
|||
clock.uninstall(); |
|||
} |
|||
clock = lolex.install(0, ['setImmediate', 'setTimeout', 'clearTimeout']); |
|||
|
|||
emitter = new EventEmitter(); |
|||
chokidar.watch.reset(); |
|||
chokidar.watch.returns(emitter); |
|||
|
|||
debug.reset(); |
|||
|
|||
logger.finish.reset(); |
|||
logger.reset.reset(); |
|||
|
|||
api.run.reset(); |
|||
api.run.returns(new Promise(function () {})); |
|||
api.files = [ |
|||
'test.js', |
|||
'test-*.js', |
|||
'test' |
|||
]; |
|||
api.excludePatterns = [ |
|||
'!**/node_modules/**', |
|||
'!**/fixtures/**', |
|||
'!**/helpers/**' |
|||
]; |
|||
|
|||
stdin = new PassThrough(); |
|||
stdin.pause(); |
|||
|
|||
done(); |
|||
}); |
|||
|
|||
var start = function (sources) { |
|||
subject.start(logger, api, sources || [], stdin); |
|||
}; |
|||
|
|||
var add = function (path) { |
|||
emitter.emit('all', 'add', path || 'source.js'); |
|||
}; |
|||
var change = function (path) { |
|||
emitter.emit('all', 'change', path || 'source.js'); |
|||
}; |
|||
var unlink = function (path) { |
|||
emitter.emit('all', 'unlink', path || 'source.js'); |
|||
}; |
|||
|
|||
var delay = function () { |
|||
return new Promise(function (now) { |
|||
setImmediate(now); |
|||
}); |
|||
}; |
|||
|
|||
// Advance the clock to get past the debounce timeout, then wait for a promise
|
|||
// to be resolved to get past the busy.then() delay.
|
|||
var debounce = function (times) { |
|||
times = times >= 0 ? times : 1; |
|||
clock.next(); |
|||
return delay().then(function () { |
|||
if (times > 1) { |
|||
return debounce(times - 1); |
|||
} |
|||
}); |
|||
}; |
|||
|
|||
var pending = []; |
|||
var test = function (name, fn) { |
|||
pending.push(_t.test(name, fn)); |
|||
}; |
|||
|
|||
test('watches for default source file changes, as well as test files', function (t) { |
|||
t.plan(2); |
|||
start(); |
|||
|
|||
t.ok(chokidar.watch.calledOnce); |
|||
t.same(chokidar.watch.firstCall.args, [ |
|||
['package.json', '**/*.js'].concat(api.files), |
|||
{ |
|||
ignored: defaultIgnore, |
|||
ignoreInitial: true |
|||
} |
|||
]); |
|||
}); |
|||
|
|||
test('watched source files are configurable', function (t) { |
|||
t.plan(2); |
|||
start(['foo.js', '!bar.js', 'baz.js', '!qux.js']); |
|||
|
|||
t.ok(chokidar.watch.calledOnce); |
|||
t.same(chokidar.watch.firstCall.args, [ |
|||
['foo.js', 'baz.js'].concat(api.files), |
|||
{ |
|||
ignored: ['bar.js', 'qux.js'], |
|||
ignoreInitial: true |
|||
} |
|||
]); |
|||
}); |
|||
|
|||
test('default set of ignored files if configured sources does not contain exclusion patterns', function (t) { |
|||
t.plan(2); |
|||
start(['foo.js', 'baz.js']); |
|||
|
|||
t.ok(chokidar.watch.calledOnce); |
|||
t.same(chokidar.watch.firstCall.args, [ |
|||
['foo.js', 'baz.js'].concat(api.files), |
|||
{ |
|||
ignored: defaultIgnore, |
|||
ignoreInitial: true |
|||
} |
|||
]); |
|||
}); |
|||
|
|||
test('starts running the initial tests', function (t) { |
|||
t.plan(4); |
|||
|
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
start(); |
|||
t.ok(api.run.calledOnce); |
|||
t.same(api.run.firstCall.args, []); |
|||
|
|||
// finish is only called after the run promise fulfils.
|
|||
t.ok(logger.finish.notCalled); |
|||
done(); |
|||
return delay().then(function () { |
|||
t.ok(logger.finish.calledOnce); |
|||
}); |
|||
}); |
|||
|
|||
[ |
|||
{label: 'is added', fire: add, event: 'add'}, |
|||
{label: 'changes', fire: change, event: 'change'}, |
|||
{label: 'is removed', fire: unlink, event: 'unlink'} |
|||
].forEach(function (variant) { |
|||
test('logs a debug message when a file is ' + variant.label, function (t) { |
|||
t.plan(2); |
|||
start(); |
|||
|
|||
variant.fire('file.js'); |
|||
t.ok(debug.calledOnce); |
|||
t.same(debug.firstCall.args, ['ava:watcher', 'Detected %s of %s', variant.event, 'file.js']); |
|||
}); |
|||
}); |
|||
|
|||
[ |
|||
{label: 'is added', fire: add}, |
|||
{label: 'changes', fire: change}, |
|||
{label: 'is removed', fire: unlink} |
|||
].forEach(function (variant) { |
|||
test('reruns initial tests when a source file ' + variant.label, function (t) { |
|||
t.plan(6); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
// reset isn't called in the initial run.
|
|||
t.ok(logger.reset.notCalled); |
|||
|
|||
variant.fire(); |
|||
return debounce().then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// reset is called before the second run.
|
|||
t.ok(logger.reset.calledBefore(api.run.secondCall)); |
|||
// no explicit files are provided.
|
|||
t.same(api.run.secondCall.args, []); |
|||
|
|||
// finish is only called after the run promise fulfils.
|
|||
t.ok(logger.finish.calledOnce); |
|||
done(); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(logger.finish.calledTwice); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
test('debounces by 10ms', function (t) { |
|||
t.plan(1); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
change(); |
|||
var before = clock.now; |
|||
return debounce().then(function () { |
|||
t.is(clock.now - before, 10); |
|||
}); |
|||
}); |
|||
|
|||
test('debounces again if changes occur in the interval', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
change(); |
|||
change(); |
|||
|
|||
var before = clock.now; |
|||
return debounce(2).then(function () { |
|||
t.is(clock.now - before, 2 * 10); |
|||
change(); |
|||
return debounce(); |
|||
}).then(function () { |
|||
t.is(clock.now - before, 3 * 10); |
|||
}); |
|||
}); |
|||
|
|||
test('only reruns tests once the initial run has finished', function (t) { |
|||
t.plan(2); |
|||
|
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
start(); |
|||
|
|||
change(); |
|||
clock.next(); |
|||
return delay().then(function () { |
|||
t.ok(api.run.calledOnce); |
|||
|
|||
done(); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
}); |
|||
}); |
|||
|
|||
test('only reruns tests once the previous run has finished', function (t) { |
|||
t.plan(3); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
change(); |
|||
return debounce().then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
|
|||
change(); |
|||
clock.next(); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
|
|||
done(); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(api.run.calledThrice); |
|||
}); |
|||
}); |
|||
|
|||
[ |
|||
{label: 'is added', fire: add}, |
|||
{label: 'changes', fire: change} |
|||
].forEach(function (variant) { |
|||
test('(re)runs a test file when it ' + variant.label, function (t) { |
|||
t.plan(6); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
// reset isn't called in the initial run.
|
|||
t.ok(logger.reset.notCalled); |
|||
|
|||
variant.fire('test.js'); |
|||
return debounce().then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// reset is called before the second run.
|
|||
t.ok(logger.reset.calledBefore(api.run.secondCall)); |
|||
// the test.js file is provided
|
|||
t.same(api.run.secondCall.args, [['test.js']]); |
|||
|
|||
// finish is only called after the run promise fulfils.
|
|||
t.ok(logger.finish.calledOnce); |
|||
done(); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(logger.finish.calledTwice); |
|||
}); |
|||
}); |
|||
}); |
|||
|
|||
test('(re)runs several test files when they are added or changed', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('test-one.js'); |
|||
change('test-two.js'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// the test files are provided
|
|||
t.same(api.run.secondCall.args, [['test-one.js', 'test-two.js']]); |
|||
}); |
|||
}); |
|||
|
|||
test('reruns initial tests if both source and test files are added or changed', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('test.js'); |
|||
unlink('source.js'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// no explicit files are provided.
|
|||
t.same(api.run.secondCall.args, []); |
|||
}); |
|||
}); |
|||
|
|||
test('does nothing if tests are deleted', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
unlink('test.js'); |
|||
return debounce().then(function () { |
|||
t.ok(logger.reset.notCalled); |
|||
t.ok(api.run.calledOnce); |
|||
}); |
|||
}); |
|||
|
|||
test('determines whether changed files are tests based on the initial files patterns', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['foo-{bar,baz}.js']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('foo-bar.js'); |
|||
add('foo-baz.js'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
t.same(api.run.secondCall.args, [['foo-bar.js', 'foo-baz.js']]); |
|||
}); |
|||
}); |
|||
|
|||
test('initial exclude patterns override whether something is a test file', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['foo-{bar,baz}.js']; |
|||
api.excludePatterns = ['!*bar*']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('foo-bar.js'); |
|||
add('foo-baz.js'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// foo-bar.js is excluded from being a test file, thus the initial tests
|
|||
// are run.
|
|||
t.same(api.run.secondCall.args, []); |
|||
}); |
|||
}); |
|||
|
|||
test('test files must end in .js', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['foo.bar']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('foo.bar'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// foo.bar cannot be a test file, thus the initial tests are run.
|
|||
t.same(api.run.secondCall.args, []); |
|||
}); |
|||
}); |
|||
|
|||
test('test files must not start with an underscore', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['_foo.bar']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add('_foo.bar'); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// _foo.bar cannot be a test file, thus the initial tests are run.
|
|||
t.same(api.run.secondCall.args, []); |
|||
}); |
|||
}); |
|||
|
|||
test('files patterns may match directories', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['dir', 'dir2/*/dir3']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add(path.join('dir', 'foo.js')); |
|||
add(path.join('dir2', 'foo', 'dir3', 'bar.js')); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
t.same(api.run.secondCall.args, [[path.join('dir', 'foo.js'), path.join('dir2', 'foo', 'dir3', 'bar.js')]]); |
|||
}); |
|||
}); |
|||
|
|||
test('exclude patterns override directory matches', function (t) { |
|||
t.plan(2); |
|||
|
|||
api.files = ['dir']; |
|||
api.excludePatterns = ['!**/exclude/**']; |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
add(path.join('dir', 'exclude', 'foo.js')); |
|||
return debounce(2).then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
// dir/exclude/foo.js is excluded from being a test file, thus the initial
|
|||
// tests are run.
|
|||
t.same(api.run.secondCall.args, []); |
|||
}); |
|||
}); |
|||
|
|||
test('reruns initial tests when "rs" is entered on stdin', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
stdin.write('rs\n'); |
|||
return delay().then(function () { |
|||
t.ok(api.run.calledTwice); |
|||
|
|||
stdin.write('\trs \n'); |
|||
return delay(); |
|||
}).then(function () { |
|||
t.ok(api.run.calledThrice); |
|||
}); |
|||
}); |
|||
|
|||
test('entering "rs" on stdin cancels any debouncing', function (t) { |
|||
t.plan(7); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
var before = clock.now; |
|||
var done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
add(); |
|||
stdin.write('rs\n'); |
|||
return delay().then(function () { |
|||
// Processing "rs" caused a new run.
|
|||
t.ok(api.run.calledTwice); |
|||
|
|||
// Try to advance the clock. This is *after* "rs" was processed. The
|
|||
// debounce timeout should have been canceled, so the clock can't have
|
|||
// advanced.
|
|||
clock.next(); |
|||
t.is(before, clock.now); |
|||
|
|||
add(); |
|||
// Advance clock *before* "rs" is received. Note that the previous run
|
|||
// hasn't finished yet.
|
|||
clock.next(); |
|||
stdin.write('rs\n'); |
|||
|
|||
return delay(); |
|||
}).then(function () { |
|||
// No new runs yet.
|
|||
t.ok(api.run.calledTwice); |
|||
// Though the clock has advanced.
|
|||
t.is(clock.now - before, 10); |
|||
before = clock.now; |
|||
|
|||
var previous = done; |
|||
api.run.returns(new Promise(function (resolve) { |
|||
done = resolve; |
|||
})); |
|||
|
|||
// Finish the previous run.
|
|||
previous(); |
|||
|
|||
return delay(); |
|||
}).then(function () { |
|||
// There's only one new run.
|
|||
t.ok(api.run.calledThrice); |
|||
|
|||
stdin.write('rs\n'); |
|||
return delay(); |
|||
}).then(function () { |
|||
add(); |
|||
|
|||
// Finish the previous run. This should cause a new run due to the "rs"
|
|||
// input.
|
|||
done(); |
|||
|
|||
return delay(); |
|||
}).then(function () { |
|||
// Again there's only one new run.
|
|||
t.is(api.run.callCount, 4); |
|||
|
|||
// Try to advance the clock. This is *after* "rs" was processed. The
|
|||
// debounce timeout should have been canceled, so the clock can't have
|
|||
// advanced.
|
|||
clock.next(); |
|||
t.is(before, clock.now); |
|||
}); |
|||
}); |
|||
|
|||
test('does nothing if anything other than "rs" is entered on stdin', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
stdin.write('foo\n'); |
|||
return debounce().then(function () { |
|||
t.ok(logger.reset.notCalled); |
|||
t.ok(api.run.calledOnce); |
|||
}); |
|||
}); |
|||
|
|||
test('ignores unexpected events from chokidar', function (t) { |
|||
t.plan(2); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
emitter.emit('all', 'foo'); |
|||
return debounce().then(function () { |
|||
t.ok(logger.reset.notCalled); |
|||
t.ok(api.run.calledOnce); |
|||
}); |
|||
}); |
|||
|
|||
test('initial run rejects', function (t) { |
|||
t.plan(1); |
|||
var expected = new Error(); |
|||
api.run.returns(Promise.reject(expected)); |
|||
start(); |
|||
|
|||
return delay().then(function () { |
|||
// The error is rethrown asynchronously, using setImmediate. The clock has
|
|||
// faked setTimeout, so if we call clock.next() it'll invoke and rethrow
|
|||
// the error, which can then be caught here.
|
|||
try { |
|||
clock.next(); |
|||
} catch (err) { |
|||
t.is(err, expected); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
test('subsequent run rejects', function (t) { |
|||
t.plan(1); |
|||
api.run.returns(Promise.resolve()); |
|||
start(); |
|||
|
|||
var expected = new Error(); |
|||
api.run.returns(Promise.reject(expected)); |
|||
|
|||
add(); |
|||
return debounce().then(function () { |
|||
// The error is rethrown asynchronously, using setImmediate. The clock has
|
|||
// faked setTimeout, so if we call clock.next() it'll invoke and rethrow
|
|||
// the error, which can then be caught here.
|
|||
try { |
|||
clock.next(); |
|||
} catch (err) { |
|||
t.is(err, expected); |
|||
} |
|||
}); |
|||
}); |
|||
|
|||
return Promise.all(pending); |
|||
}); |
Loading…
Reference in new issue