diff --git a/lib/logger.js b/lib/logger.js index e9847a573..17c29df17 100644 --- a/lib/logger.js +++ b/lib/logger.js @@ -50,6 +50,23 @@ Logger.prototype.finish = function (runStatus) { this.write(this.reporter.finish(runStatus), runStatus); }; +Logger.prototype.section = function () { + if (!this.reporter.section) { + return; + } + + this.write(this.reporter.section()); +}; + +Logger.prototype.clear = function () { + if (!this.reporter.clear) { + return false; + } + + this.write(this.reporter.clear()); + return true; +}; + Logger.prototype.write = function (str, runStatus) { if (typeof str === 'undefined') { return; diff --git a/lib/reporters/mini.js b/lib/reporters/mini.js index 288ec76bb..dd301bfef 100644 --- a/lib/reporters/mini.js +++ b/lib/reporters/mini.js @@ -7,6 +7,7 @@ var spinners = require('cli-spinners'); var chalk = require('chalk'); var cliTruncate = require('cli-truncate'); var cross = require('figures').cross; +var repeating = require('repeating'); var colors = require('../colors'); chalk.enabled = true; @@ -113,32 +114,26 @@ MiniReporter.prototype.unhandledError = function (err) { } }; -MiniReporter.prototype.reportCounts = function () { - var status = ''; +MiniReporter.prototype.reportCounts = function (time) { + var lines = [ + this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '', + this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '', + this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '', + this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : '' + ].filter(Boolean); - if (this.passCount > 0) { - status += '\n ' + colors.pass(this.passCount, 'passed'); + if (time && lines.length > 0) { + lines[0] += ' ' + time; } - if (this.failCount > 0) { - status += '\n ' + colors.error(this.failCount, 'failed'); - } - - if (this.skipCount > 0) { - status += '\n ' + colors.skip(this.skipCount, 'skipped'); - } - - if (this.todoCount > 0) { - status += '\n ' + colors.todo(this.todoCount, 'todo'); - } - - return status; + return lines.join(''); }; MiniReporter.prototype.finish = function (runStatus) { this.clearInterval(); - var status = this.reportCounts(); + var time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']'); + var status = this.reportCounts(time); if (this.rejectionCount > 0) { status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount)); @@ -162,12 +157,12 @@ MiniReporter.prototype.finish = function (runStatus) { var description; if (test.error) { - description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack); + description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack).trimRight(); } else { description = JSON.stringify(test); } - status += '\n\n ' + colors.error(i + '.', title) + '\n'; + status += '\n\n\n ' + colors.error(i + '.', title) + '\n'; status += colors.stack(description); }); } @@ -181,22 +176,26 @@ MiniReporter.prototype.finish = function (runStatus) { i++; if (err.type === 'exception' && err.name === 'AvaError') { - status += '\n\n ' + colors.error(cross + ' ' + err.message) + '\n'; + status += '\n\n\n ' + colors.error(cross + ' ' + err.message); } else { var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception'; - var description = err.stack ? err.stack : JSON.stringify(err); + var description = err.stack ? err.stack.trimRight() : JSON.stringify(err); - status += '\n\n ' + colors.error(i + '.', title) + '\n'; - status += ' ' + colors.stack(description); + status += '\n\n\n ' + colors.error(i + '.', title) + '\n'; + status += ' ' + colors.stack(description); } }); } - if (this.failCount === 0 && this.rejectionCount === 0 && this.exceptionCount === 0) { - status += '\n'; - } + return status + '\n'; +}; + +MiniReporter.prototype.section = function () { + return '\n' + chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80)); +}; - return status; +MiniReporter.prototype.clear = function () { + return ''; }; MiniReporter.prototype.write = function (str) { diff --git a/lib/reporters/verbose.js b/lib/reporters/verbose.js index 8befa84ff..4f84d4fbd 100644 --- a/lib/reporters/verbose.js +++ b/lib/reporters/verbose.js @@ -1,7 +1,9 @@ 'use strict'; var prettyMs = require('pretty-ms'); var figures = require('figures'); +var chalk = require('chalk'); var plur = require('plur'); +var repeating = require('repeating'); var colors = require('../colors'); Object.keys(colors).forEach(function (key) { @@ -68,31 +70,22 @@ VerboseReporter.prototype.unhandledError = function (err) { VerboseReporter.prototype.finish = function (runStatus) { var output = '\n'; - if (runStatus.failCount > 0) { - output += ' ' + colors.error(runStatus.failCount, plur('test', runStatus.failCount), 'failed') + '\n'; - } else { - output += ' ' + colors.pass(runStatus.passCount, plur('test', runStatus.passCount), 'passed') + '\n'; - } - - if (runStatus.skipCount > 0) { - output += ' ' + colors.skip(runStatus.skipCount, plur('test', runStatus.skipCount), 'skipped') + '\n'; - } - - if (runStatus.todoCount > 0) { - output += ' ' + colors.todo(runStatus.todoCount, plur('test', runStatus.todoCount), 'todo') + '\n'; - } - - if (runStatus.rejectionCount > 0) { - output += ' ' + colors.error(runStatus.rejectionCount, 'unhandled', plur('rejection', runStatus.rejectionCount)) + '\n'; - } - - if (runStatus.exceptionCount > 0) { - output += ' ' + colors.error(runStatus.exceptionCount, 'uncaught', plur('exception', runStatus.exceptionCount)) + '\n'; + var lines = [ + runStatus.failCount > 0 ? + ' ' + colors.error(runStatus.failCount, plur('test', runStatus.failCount), 'failed') : + ' ' + colors.pass(runStatus.passCount, plur('test', runStatus.passCount), 'passed'), + runStatus.skipCount > 0 ? ' ' + colors.skip(runStatus.skipCount, plur('test', runStatus.skipCount), 'skipped') : '', + runStatus.todoCount > 0 ? ' ' + colors.todo(runStatus.todoCount, plur('test', runStatus.todoCount), 'todo') : '', + runStatus.rejectionCount > 0 ? ' ' + colors.error(runStatus.rejectionCount, 'unhandled', plur('rejection', runStatus.rejectionCount)) : '', + runStatus.exceptionCount > 0 ? ' ' + colors.error(runStatus.exceptionCount, 'uncaught', plur('exception', runStatus.exceptionCount)) : '' + ].filter(Boolean); + + if (lines.length > 0) { + lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']'); + output += lines.join('\n'); } if (runStatus.failCount > 0) { - output += '\n'; - var i = 0; runStatus.tests.forEach(function (test) { @@ -102,12 +95,17 @@ VerboseReporter.prototype.finish = function (runStatus) { i++; - output += ' ' + colors.error(i + '.', test.title) + '\n'; - output += ' ' + colors.stack(test.error.stack) + '\n'; + output += '\n\n\n ' + colors.error(i + '.', test.title) + '\n'; + var stack = test.error.stack ? test.error.stack.trimRight() : ''; + output += ' ' + colors.stack(stack); }); } - return output; + return output + '\n'; +}; + +VerboseReporter.prototype.section = function () { + return chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80)); }; VerboseReporter.prototype.write = function (str) { diff --git a/lib/watcher.js b/lib/watcher.js index dd4f74c7c..851b09263 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -37,8 +37,23 @@ function Watcher(logger, api, files, sources) { this.debouncer = new Debouncer(this); this.isTest = makeTestMatcher(files, AvaFiles.defaultExcludePatterns()); + + var isFirstRun = true; + this.clearLogOnNextRun = true; this.run = function (specificFiles) { - logger.reset(); + if (isFirstRun) { + isFirstRun = false; + } else { + var cleared = this.clearLogOnNextRun && logger.clear(); + if (!cleared) { + logger.reset(); + logger.section(); + } + this.clearLogOnNextRun = true; + + logger.reset(); + logger.start(); + } var runOnlyExclusive = false; @@ -57,10 +72,12 @@ function Watcher(logger, api, files, sources) { } } + var self = this; this.busy = api.run(specificFiles || files, { runOnlyExclusive: runOnlyExclusive }).then(function (runStatus) { logger.finish(runStatus); + self.clearLogOnNextRun = self.clearLogOnNextRun && runStatus.failCount === 0; }, rethrowAsync); }; @@ -176,6 +193,7 @@ Watcher.prototype.observeStdin = function (stdin) { // Cancel the debouncer again, it might have restarted while waiting for // the busy promise to fulfil. self.debouncer.cancel(); + self.clearLogOnNextRun = false; self.rerunAll(); }); }); diff --git a/package.json b/package.json index 674f5ae7d..2c60525e3 100644 --- a/package.json +++ b/package.json @@ -129,6 +129,7 @@ "power-assert-formatter": "^1.3.0", "power-assert-renderers": "^0.1.0", "pretty-ms": "^2.0.0", + "repeating": "^2.0.0", "require-precompiled": "^0.1.0", "resolve-cwd": "^1.0.0", "set-immediate-shim": "^1.0.1", diff --git a/test/cli.js b/test/cli.js index 4bb65e37b..c8e4146ff 100644 --- a/test/cli.js +++ b/test/cli.js @@ -235,7 +235,7 @@ if (hasChokidar) { t.is(err.code, 1); t.match(stderr, 'The TAP reporter is not available when using watch mode.'); t.end(); - }).stderr.pipe(process.stderr); + }); }); ['--watch', '-w'].forEach(function (watchFlag) { @@ -245,7 +245,7 @@ if (hasChokidar) { t.is(err.code, 1); t.match(stderr, 'The TAP reporter is not available when using watch mode.'); t.end(); - }).stderr.pipe(process.stderr); + }); }); }); }); diff --git a/test/helper/compare-line-output.js b/test/helper/compare-line-output.js new file mode 100644 index 000000000..f1ec00814 --- /dev/null +++ b/test/helper/compare-line-output.js @@ -0,0 +1,27 @@ +'use strict'; +var SKIP_UNTIL_EMPTY_LINE = {}; + +function compareLineOutput(t, actual, lineExpectations) { + var actualLines = actual.split('\n'); + + var expectationIndex = 0; + var lineIndex = 0; + while (lineIndex < actualLines.length && expectationIndex < lineExpectations.length) { + var line = actualLines[lineIndex++]; + var expected = lineExpectations[expectationIndex++]; + if (expected === SKIP_UNTIL_EMPTY_LINE) { + lineIndex = actualLines.indexOf('', lineIndex); + continue; + } + + if (typeof expected === 'string') { + // Assertion titles use 1-based line indexes + t.is(line, expected, 'line ' + lineIndex + ' ≪' + line + '≫ is ≪' + expected + '≫'); + } else { + t.match(line, expected, 'line ' + lineIndex + ' ≪' + line + '≫ matches ' + expected); + } + } +} + +module.exports = compareLineOutput; +compareLineOutput.SKIP_UNTIL_EMPTY_LINE = SKIP_UNTIL_EMPTY_LINE; diff --git a/test/logger.js b/test/logger.js index 9e382c5e4..cda48325a 100644 --- a/test/logger.js +++ b/test/logger.js @@ -45,6 +45,56 @@ test('only write if reset is supported by reporter', function (t) { t.end(); }); +test('only call section if supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.section = undefined; + logger.section(); + t.end(); +}); + +test('only write if section is supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.section = undefined; + logger.write = t.fail; + logger.section(); + t.end(); +}); + +test('only call clear if supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.clear = undefined; + logger.clear(); + t.end(); +}); + +test('only write if clear is supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.clear = undefined; + logger.write = t.fail; + logger.clear(); + t.end(); +}); + +test('return false if clear is not supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.clear = undefined; + t.false(logger.clear()); + t.end(); +}); + +test('return true if clear is supported by reporter', function (t) { + var tapReporter = tap(); + var logger = new Logger(tapReporter); + tapReporter.clear = function () {}; + t.true(logger.clear()); + t.end(); +}); + test('writes the reporter reset result', function (t) { var tapReporter = tap(); var logger = new Logger(tapReporter); diff --git a/test/reporters/mini.js b/test/reporters/mini.js index 507cfe5c2..7edf35896 100644 --- a/test/reporters/mini.js +++ b/test/reporters/mini.js @@ -2,9 +2,13 @@ var chalk = require('chalk'); var test = require('tap').test; var cross = require('figures').cross; +var lolex = require('lolex'); +var repeating = require('repeating'); var AvaError = require('../../lib/ava-error'); var _miniReporter = require('../../lib/reporters/mini'); var beautifyStack = require('../../lib/beautify-stack'); +var colors = require('../../lib/colors'); +var compareLineOutput = require('../helper/compare-line-output'); chalk.enabled = true; @@ -13,6 +17,7 @@ var graySpinner = chalk.gray.dim('⠋'); // Needed because tap doesn't emulate a tty environment and thus this is // undefined, making `cli-truncate` append '...' to test titles process.stdout.columns = 5000; +var fullWidthLine = chalk.gray.dim(repeating('\u2500', 5000)); function miniReporter() { var reporter = _miniReporter(); @@ -24,6 +29,9 @@ function miniReporter() { process.stderr.setMaxListeners(50); +lolex.install(new Date('2014-11-19T00:19:12+0700').getTime(), ['Date']); +var time = ' ' + chalk.grey.dim('[17:19:12]'); + test('start', function (t) { var reporter = _miniReporter(); @@ -151,7 +159,7 @@ test('results with passing tests', function (t) { var actualOutput = reporter.finish(); var expectedOutput = [ - '\n ' + chalk.green('1 passed'), + '\n ' + chalk.green('1 passed') + time, '' ].join('\n'); @@ -167,7 +175,7 @@ test('results with skipped tests', function (t) { var actualOutput = reporter.finish(); var expectedOutput = [ - '\n ' + chalk.yellow('1 skipped'), + '\n ' + chalk.yellow('1 skipped') + time, '' ].join('\n'); @@ -183,7 +191,7 @@ test('results with todo tests', function (t) { var actualOutput = reporter.finish(); var expectedOutput = [ - '\n ' + chalk.blue('1 todo'), + '\n ' + chalk.blue('1 todo') + time, '' ].join('\n'); @@ -199,7 +207,7 @@ test('results with passing skipped tests', function (t) { var output = reporter.finish().split('\n'); t.is(output[0], ''); - t.is(output[1], ' ' + chalk.green('1 passed')); + t.is(output[1], ' ' + chalk.green('1 passed') + time); t.is(output[2], ' ' + chalk.yellow('1 skipped')); t.is(output[3], ''); t.end(); @@ -210,23 +218,33 @@ test('results with passing tests and rejections', function (t) { reporter.passCount = 1; reporter.rejectionCount = 1; - var err = new Error('failure'); - err.type = 'rejection'; - err.stack = beautifyStack(err.stack); + var err1 = new Error('failure one'); + err1.type = 'rejection'; + err1.stack = beautifyStack(err1.stack); + var err2 = new Error('failure two'); + err2.type = 'rejection'; + err2.stack = 'stack line with trailing whitespace\t\n'; var runStatus = { - errors: [err] + errors: [err1, err2] }; - var output = reporter.finish(runStatus).split('\n'); - - t.is(output[0], ''); - t.is(output[1], ' ' + chalk.green('1 passed')); - t.is(output[2], ' ' + chalk.red('1 rejection')); - t.is(output[3], ''); - t.is(output[4], ' ' + chalk.red('1. Unhandled Rejection')); - t.match(output[5], /Error: failure/); - t.match(output[6], /test\/reporters\/mini\.js/); + var output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + chalk.green('1 passed') + time, + ' ' + chalk.red('1 rejection'), + '', + '', + ' ' + chalk.red('1. Unhandled Rejection'), + /Error: failure/, + /test\/reporters\/mini\.js/, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + ' ' + chalk.red('2. Unhandled Rejection'), + ' ' + colors.stack('stack line with trailing whitespace') + ]); t.end(); }); @@ -246,17 +264,21 @@ test('results with passing tests and exceptions', function (t) { errors: [err, avaErr] }; - var output = reporter.finish(runStatus).split('\n'); - - t.is(output[0], ''); - t.is(output[1], ' ' + chalk.green('1 passed')); - t.is(output[2], ' ' + chalk.red('2 exceptions')); - t.is(output[3], ''); - t.is(output[4], ' ' + chalk.red('1. Uncaught Exception')); - t.match(output[5], /Error: failure/); - t.match(output[6], /test\/reporters\/mini\.js/); - var next = 6 + output.slice(6).indexOf('') + 1; - t.is(output[next], ' ' + chalk.red(cross + ' A futuristic test runner')); + var output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + chalk.green('1 passed') + time, + ' ' + chalk.red('2 exceptions'), + '', + '', + ' ' + chalk.red('1. Uncaught Exception'), + /Error: failure/, + /test\/reporters\/mini\.js/, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + ' ' + chalk.red(cross + ' A futuristic test runner') + ]); t.end(); }); @@ -264,24 +286,37 @@ test('results with errors', function (t) { var reporter = miniReporter(); reporter.failCount = 1; - var err = new Error('failure'); - err.stack = beautifyStack(err.stack); + var err1 = new Error('failure one'); + err1.stack = beautifyStack(err1.stack); + var err2 = new Error('failure two'); + err2.stack = 'first line is stripped\nstack line with trailing whitespace\t\n'; var runStatus = { errors: [{ - title: 'failed', - error: err + title: 'failed one', + error: err1 + }, { + title: 'failed two', + error: err2 }] }; - var output = reporter.finish(runStatus).split('\n'); - - t.is(output[0], ''); - t.is(output[1], ' ' + chalk.red('1 failed')); - t.is(output[2], ''); - t.is(output[3], ' ' + chalk.red('1. failed')); - t.match(output[4], /failure/); - t.match(output[5], /test\/reporters\/mini\.js/); + var output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + chalk.red('1 failed') + time, + '', + '', + ' ' + chalk.red('1. failed one'), + /failure/, + /test\/reporters\/mini\.js/, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + ' ' + chalk.red('2. failed two') + ].concat( + colors.stack(' failure two\n stack line with trailing whitespace').split('\n') + )); t.end(); }); @@ -295,3 +330,11 @@ test('empty results after reset', function (t) { t.is(output, '\n'); t.end(); }); + +test('full-width line when sectioning', function (t) { + var reporter = miniReporter(); + + var output = reporter.section(); + t.is(output, '\n' + fullWidthLine); + t.end(); +}); diff --git a/test/reporters/verbose.js b/test/reporters/verbose.js index 3e3f8cc2f..d775dcc29 100644 --- a/test/reporters/verbose.js +++ b/test/reporters/verbose.js @@ -2,11 +2,22 @@ var figures = require('figures'); var chalk = require('chalk'); var test = require('tap').test; +var lolex = require('lolex'); +var repeating = require('repeating'); var beautifyStack = require('../../lib/beautify-stack'); +var colors = require('../../lib/colors'); var verboseReporter = require('../../lib/reporters/verbose'); +var compareLineOutput = require('../helper/compare-line-output'); chalk.enabled = true; +// tap doesn't emulate a tty environment and thus process.stdout.columns is +// undefined. Expect an 80 character wide line to be rendered. +var fullWidthLine = chalk.gray.dim(repeating('\u2500', 80)); + +lolex.install(new Date('2014-11-19T00:19:12+0700').getTime(), ['Date']); +var time = ' ' + chalk.grey.dim('[17:19:12]'); + function createReporter() { var reporter = verboseReporter(); return reporter; @@ -195,7 +206,7 @@ test('results with passing tests', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, '' ].join('\n'); @@ -212,7 +223,7 @@ test('results with skipped tests', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, ' ' + chalk.yellow('1 test skipped'), '' ].join('\n'); @@ -230,7 +241,7 @@ test('results with todo tests', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, ' ' + chalk.blue('1 test todo'), '' ].join('\n'); @@ -248,7 +259,7 @@ test('results with passing tests and rejections', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, ' ' + chalk.red('1 unhandled rejection'), '' ].join('\n'); @@ -266,7 +277,7 @@ test('results with passing tests and exceptions', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, ' ' + chalk.red('1 uncaught exception'), '' ].join('\n'); @@ -285,7 +296,7 @@ test('results with passing tests, rejections and exceptions', function (t) { var actualOutput = reporter.finish(runStatus); var expectedOutput = [ '', - ' ' + chalk.green('1 test passed'), + ' ' + chalk.green('1 test passed') + time, ' ' + chalk.red('1 unhandled rejection'), ' ' + chalk.red('1 uncaught exception'), '' @@ -296,23 +307,45 @@ test('results with passing tests, rejections and exceptions', function (t) { }); test('results with errors', function (t) { - var error = new Error('error message'); - error.stack = beautifyStack(error.stack); + var error1 = new Error('error one message'); + error1.stack = beautifyStack(error1.stack); + var error2 = new Error('error two message'); + error2.stack = 'stack line with trailing whitespace\t\n'; var reporter = createReporter(); var runStatus = createTestData(); runStatus.failCount = 1; runStatus.tests = [{ - title: 'fail', - error: error + title: 'fail one', + error: error1 + }, { + title: 'fail two', + error: error2 }]; - var output = reporter.finish(runStatus).split('\n'); + var output = reporter.finish(runStatus); + compareLineOutput(t, output, [ + '', + ' ' + chalk.red('1 test failed') + time, + '', + '', + ' ' + chalk.red('1. fail one'), + /Error: error one message/, + /test\/reporters\/verbose\.js/, + compareLineOutput.SKIP_UNTIL_EMPTY_LINE, + '', + '', + ' ' + chalk.red('2. fail two'), + ' ' + colors.stack('stack line with trailing whitespace') + ]); + t.end(); +}); + +test('full-width line when sectioning', function (t) { + var reporter = createReporter(); - t.is(output[1], ' ' + chalk.red('1 test failed')); - t.is(output[3], ' ' + chalk.red('1. fail')); - t.match(output[4], /Error: error message/); - t.match(output[5], /test\/reporters\/verbose\.js/); + var output = reporter.section(); + t.is(output, fullWidthLine); t.end(); }); diff --git a/test/watcher.js b/test/watcher.js index cbeb4d03e..57f384d1b 100644 --- a/test/watcher.js +++ b/test/watcher.js @@ -58,7 +58,10 @@ group('chokidar is installed', function (beforeEach, test, group) { var debug = sinon.spy(); var logger = { + start: sinon.spy(), finish: sinon.spy(), + section: sinon.spy(), + clear: sinon.stub().returns(true), reset: sinon.spy() }; @@ -99,9 +102,14 @@ group('chokidar is installed', function (beforeEach, test, group) { debug.reset(); + logger.start.reset(); logger.finish.reset(); + logger.section.reset(); logger.reset.reset(); + logger.clear.reset(); + logger.clear.returns(true); + avaFiles.reset(); avaFiles.defaultExcludePatterns.reset(); avaFiles.defaultIncludePatterns.reset(); @@ -209,14 +217,20 @@ group('chokidar is installed', function (beforeEach, test, group) { }); test('starts running the initial tests', function (t) { - t.plan(4); + t.plan(8); var done; + var runStatus = {}; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve(runStatus); + }; })); start(); + t.ok(logger.clear.notCalled); + t.ok(logger.reset.notCalled); + t.ok(logger.start.notCalled); t.ok(api.run.calledOnce); t.same(api.run.firstCall.args, [files, {runOnlyExclusive: false}]); @@ -225,6 +239,7 @@ group('chokidar is installed', function (beforeEach, test, group) { done(); return delay().then(function () { t.ok(logger.finish.calledOnce); + t.is(logger.finish.firstCall.args[0], runStatus); }); }); @@ -249,37 +264,117 @@ group('chokidar is installed', function (beforeEach, test, group) { {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()); + t.plan(12); + + var runStatus = {failCount: 0}; + api.run.returns(Promise.resolve(runStatus)); start(); var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve(runStatus); + }; })); variant.fire(); return debounce().then(function () { - t.ok(logger.reset.calledTwice); + t.ok(logger.clear.calledOnce); + t.ok(logger.reset.calledOnce); + t.ok(logger.start.calledOnce); t.ok(api.run.calledTwice); + // clear is called before reset. + t.ok(logger.clear.firstCall.calledBefore(logger.reset.firstCall)); // reset is called before the second run. - t.ok(logger.reset.secondCall.calledBefore(api.run.secondCall)); + t.ok(logger.reset.firstCall.calledBefore(api.run.secondCall)); + // reset is called before start + t.ok(logger.reset.firstCall.calledBefore(logger.start.firstCall)); // no explicit files are provided. t.same(api.run.secondCall.args, [files, {runOnlyExclusive: false}]); // finish is only called after the run promise fulfils. t.ok(logger.finish.calledOnce); + t.is(logger.finish.firstCall.args[0], runStatus); + + runStatus = {failCount: 0}; done(); return delay(); }).then(function () { t.ok(logger.finish.calledTwice); + t.is(logger.finish.secondCall.args[0], runStatus); }); }); }); + test('does not clear logger if the previous run had failures', function (t) { + t.plan(2); + + api.run.returns(Promise.resolve({failCount: 1})); + start(); + + api.run.returns(Promise.resolve({failCount: 0})); + change(); + return debounce().then(function () { + t.ok(logger.clear.notCalled); + + change(); + return debounce(); + }).then(function () { + t.ok(logger.clear.calledOnce); + }); + }); + + test('sections the logger if it was not cleared', function (t) { + t.plan(5); + + api.run.returns(Promise.resolve({failCount: 1})); + start(); + + api.run.returns(Promise.resolve({failCount: 0})); + change(); + return debounce().then(function () { + t.ok(logger.clear.notCalled); + t.ok(logger.reset.calledTwice); + t.ok(logger.section.calledOnce); + t.ok(logger.reset.firstCall.calledBefore(logger.section.firstCall)); + t.ok(logger.reset.secondCall.calledAfter(logger.section.firstCall)); + }); + }); + + test('sections the logger if it could not be cleared', function (t) { + t.plan(5); + + logger.clear.returns(false); + api.run.returns(Promise.resolve({failCount: 0})); + start(); + + change(); + return debounce().then(function () { + t.ok(logger.clear.calledOnce); + t.ok(logger.reset.calledTwice); + t.ok(logger.section.calledOnce); + t.ok(logger.reset.firstCall.calledBefore(logger.section.firstCall)); + t.ok(logger.reset.secondCall.calledAfter(logger.section.firstCall)); + }); + }); + + test('does not section the logger if it was cleared', function (t) { + t.plan(3); + + api.run.returns(Promise.resolve({failCount: 0})); + start(); + + change(); + return debounce().then(function () { + t.ok(logger.clear.calledOnce); + t.ok(logger.section.notCalled); + t.ok(logger.reset.calledOnce); + }); + }); + test('debounces by 10ms', function (t) { t.plan(1); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); change(); @@ -291,7 +386,7 @@ group('chokidar is installed', function (beforeEach, test, group) { test('debounces again if changes occur in the interval', function (t) { t.plan(2); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); change(); @@ -312,7 +407,9 @@ group('chokidar is installed', function (beforeEach, test, group) { var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); start(); @@ -330,12 +427,14 @@ group('chokidar is installed', function (beforeEach, test, group) { test('only reruns tests once the previous run has finished', function (t) { t.plan(3); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); change(); @@ -361,36 +460,40 @@ group('chokidar is installed', function (beforeEach, test, group) { ].forEach(function (variant) { test('(re)runs a test file when it ' + variant.label, function (t) { t.plan(6); - api.run.returns(Promise.resolve()); + + var runStatus = {}; + api.run.returns(Promise.resolve(runStatus)); start(); var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve(runStatus); + }; })); variant.fire('test.js'); return debounce().then(function () { - t.ok(logger.reset.calledTwice); t.ok(api.run.calledTwice); - // reset is called before the second run. - t.ok(logger.reset.secondCall.calledBefore(api.run.secondCall)); // the test.js file is provided t.same(api.run.secondCall.args, [['test.js'], {runOnlyExclusive: false}]); // finish is only called after the run promise fulfils. t.ok(logger.finish.calledOnce); + t.is(logger.finish.firstCall.args[0], runStatus); + done(); return delay(); }).then(function () { t.ok(logger.finish.calledTwice); + t.is(logger.finish.secondCall.args[0], runStatus); }); }); }); test('(re)runs several test files when they are added or changed', function (t) { t.plan(2); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('test-one.js'); @@ -404,7 +507,7 @@ group('chokidar is installed', function (beforeEach, test, group) { test('reruns initial tests if both source and test files are added or changed', function (t) { t.plan(2); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('test.js'); @@ -417,13 +520,12 @@ group('chokidar is installed', function (beforeEach, test, group) { }); test('does nothing if tests are deleted', function (t) { - t.plan(2); - api.run.returns(Promise.resolve()); + t.plan(1); + api.run.returns(Promise.resolve({})); start(); unlink('test.js'); return debounce().then(function () { - t.ok(logger.reset.calledOnce); t.ok(api.run.calledOnce); }); }); @@ -432,7 +534,7 @@ group('chokidar is installed', function (beforeEach, test, group) { t.plan(2); files = ['foo-{bar,baz}.js']; - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('foo-bar.js'); @@ -449,7 +551,7 @@ group('chokidar is installed', function (beforeEach, test, group) { files = ['foo-{bar,baz}.js']; // TODO(@jamestalmage, @novemberborn): There is no way for users to actually set exclude patterns yet. avaFiles.defaultExcludePatterns.returns(['!*bar*']); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('foo-bar.js'); @@ -466,7 +568,7 @@ group('chokidar is installed', function (beforeEach, test, group) { t.plan(2); files = ['foo.bar']; - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('foo.bar'); @@ -481,7 +583,7 @@ group('chokidar is installed', function (beforeEach, test, group) { t.plan(2); api.files = ['_foo.bar']; - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add('_foo.bar'); @@ -496,7 +598,7 @@ group('chokidar is installed', function (beforeEach, test, group) { t.plan(2); files = ['dir', 'another-dir/*/deeper']; - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add(path.join('dir', 'test.js')); @@ -521,7 +623,7 @@ group('chokidar is installed', function (beforeEach, test, group) { files = ['dir']; // TODO(@jamestalmage, @novemberborn): There is no way for users to actually set exclude patterns yet. avaFiles.defaultExcludePatterns.returns(['!**/exclude/**']); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); add(path.join('dir', 'exclude', 'foo.js')); @@ -535,30 +637,46 @@ group('chokidar is installed', function (beforeEach, test, group) { ["r", "rs"].forEach(function (input) { test('reruns initial tests when "' + input + '" is entered on stdin', function (t) { - t.plan(2); - api.run.returns(Promise.resolve()); + t.plan(4); + api.run.returns(Promise.resolve({})); start().observeStdin(stdin); stdin.write(input + '\n'); return delay().then(function () { t.ok(api.run.calledTwice); + t.same(api.run.secondCall.args, [files, {runOnlyExclusive: false}]); stdin.write('\t' + input + ' \n'); return delay(); }).then(function () { t.ok(api.run.calledThrice); + t.same(api.run.thirdCall.args, [files, {runOnlyExclusive: false}]); + }); + }); + + test('entering "' + input + '" on stdin prevents the logger from being cleared', function (t) { + t.plan(2); + api.run.returns(Promise.resolve({failCount: 0})); + start().observeStdin(stdin); + + stdin.write(input + '\n'); + return delay().then(function () { + t.ok(api.run.calledTwice); + t.ok(logger.clear.notCalled); }); }); test('entering "' + input + '" on stdin cancels any debouncing', function (t) { t.plan(7); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start().observeStdin(stdin); var before = clock.now; var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); add(); @@ -589,7 +707,9 @@ group('chokidar is installed', function (beforeEach, test, group) { var previous = done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); // Finish the previous run. @@ -624,25 +744,23 @@ group('chokidar is installed', function (beforeEach, test, group) { }); test('does nothing if anything other than "rs" is entered on stdin', function (t) { - t.plan(2); - api.run.returns(Promise.resolve()); + t.plan(1); + api.run.returns(Promise.resolve({})); start().observeStdin(stdin); stdin.write('foo\n'); return debounce().then(function () { - t.ok(logger.reset.calledOnce); t.ok(api.run.calledOnce); }); }); test('ignores unexpected events from chokidar', function (t) { - t.plan(2); - api.run.returns(Promise.resolve()); + t.plan(1); + api.run.returns(Promise.resolve({})); start(); emitChokidar('foo'); return debounce().then(function () { - t.ok(logger.reset.calledOnce); t.ok(api.run.calledOnce); }); }); @@ -667,7 +785,7 @@ group('chokidar is installed', function (beforeEach, test, group) { test('subsequent run rejects', function (t) { t.plan(1); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); start(); var expected = new Error(); @@ -702,7 +820,9 @@ group('chokidar is installed', function (beforeEach, test, group) { var seed = function (sources) { var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); var watcher = start(sources); @@ -824,7 +944,7 @@ group('chokidar is installed', function (beforeEach, test, group) { change('index.js'); change(path.join('lib', 'util.js')); - api.run.returns(Promise.resolve()); + api.run.returns(Promise.resolve({})); return debounce(3).then(function () { t.ok(api.run.calledTwice); t.same(api.run.secondCall.args, [[path.join('test', '1.js')], {runOnlyExclusive: false}]); @@ -934,7 +1054,9 @@ group('chokidar is installed', function (beforeEach, test, group) { var seed = function () { var done; api.run.returns(new Promise(function (resolve) { - done = resolve; + done = function () { + resolve({}); + }; })); var watcher = start();