├── .eslintrc ├── .gitignore ├── .travis.yml ├── HISTORY.md ├── README.md ├── bin └── tape-watch ├── fixtures └── othertest.js ├── lib ├── mutex.js └── wait.js ├── package.json └── test ├── tape.js └── test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": [ 3 | "standard", 4 | "standard-jsx" 5 | ] 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '4' 4 | cache: 5 | directories: 6 | - node_modules 7 | -------------------------------------------------------------------------------- /HISTORY.md: -------------------------------------------------------------------------------- 1 | ## [v2.3.0] 2 | > Mar 15, 2017 3 | 4 | - [#116] - Add `--clear` option. ([@lsanwick]) 5 | 6 | [v2.3.0]: https://github.com/rstacruz/tape-watch/compare/v2.2.4...v2.3.0 7 | 8 | ## [v2.2.4] 9 | > Nov 15, 2016 10 | 11 | - [#67] - Fix support for absolute paths. ([@tuckerconnelly]) 12 | 13 | [v2.2.4]: https://github.com/rstacruz/tape-watch/compare/v2.2.3...v2.2.4 14 | 15 | ## [v2.2.3] 16 | > Aug 20, 2016 17 | 18 | - Remove stray debugging text. 19 | 20 | [v2.2.3]: https://github.com/rstacruz/tape-watch/compare/v2.2.2...v2.2.3 21 | 22 | ## [v2.2.2] 23 | > Aug 18, 2016 24 | 25 | - [#51] - Fix globs not being re-interpreted on changes. ([#57], [@tuckerconnelly]) 26 | 27 | [v2.2.2]: https://github.com/rstacruz/tape-watch/compare/v2.2.1...v2.2.2 28 | 29 | ## [v2.2.1] 30 | > Aug 18, 2016 31 | 32 | - [#19] - Fix bug where running stops when there's an unhandled exception. 33 | 34 | [v2.2.1]: https://github.com/rstacruz/tape-watch/compare/v2.2.0...v2.2.1 35 | 36 | ## [v2.2.0] 37 | > Aug 18, 2016 38 | 39 | - [#53] - Add support for `-1` / `--once` to only run tests once. 40 | - [#53] - Add support for `-w` / `--watch` to cancel out `--once`; useful for using tape-watch as the `npm test` script. 41 | 42 | [v2.2.0]: https://github.com/rstacruz/tape-watch/compare/v2.1.0...v2.2.0 43 | 44 | ## [v2.1.0] 45 | > May 28, 2016 46 | 47 | - Add glob support (`tape-watch 'test/**/*.js`). 48 | 49 | [v2.1.0]: https://github.com/rstacruz/tape-watch/compare/v2.0.2...v2.1.0 50 | 51 | ## [v2.0.2] 52 | > May 28, 2016 53 | 54 | - Fix bug when tape-watch is invoked with multiple filenames. 55 | 56 | [v2.0.2]: https://github.com/rstacruz/tape-watch/compare/v2.0.0...v2.0.2 57 | 58 | ## [v2.0.0] 59 | > May 27, 2016 60 | 61 | - [#15] - Implement `-r` / `--require` to support preprocessors like Babel. ([#26]) 62 | - __Breaking:__ `--refresh` is now `-R`, from what used to be `-r`. 63 | 64 | [v2.0.0]: https://github.com/rstacruz/tape-watch/compare/v1.3.0...v2.0.0 65 | 66 | ## [v1.3.0] 67 | > Jan 15, 2016 68 | 69 | - Catch uncaught exceptions. 70 | 71 | [v1.3.0]: https://github.com/rstacruz/tape-watch/compare/v1.2.0...v1.3.0 72 | 73 | ## [v1.2.0] 74 | > Jan 11, 2016 75 | 76 | - Add `--refresh` support. 77 | 78 | [v1.2.0]: https://github.com/rstacruz/tape-watch/compare/v1.1.0...v1.2.0 79 | 80 | ## [v1.1.0] 81 | > Jan 11, 2016 82 | 83 | - Fix 'no tests' problem. 84 | 85 | [v1.1.0]: https://github.com/rstacruz/tape-watch/compare/v1.0.0...v1.1.0 86 | 87 | ## [v1.0.0] 88 | > Jan 11, 2016 89 | 90 | - Initial release. 91 | 92 | [v1.0.0]: https://github.com/rstacruz/tape-watch/tree/v1.0.0 93 | [#15]: https://github.com/rstacruz/tape-watch/issues/15 94 | [#26]: https://github.com/rstacruz/tape-watch/issues/26 95 | [#51]: https://github.com/rstacruz/tape-watch/issues/51 96 | [#57]: https://github.com/rstacruz/tape-watch/issues/57 97 | [#19]: https://github.com/rstacruz/tape-watch/issues/19 98 | [#53]: https://github.com/rstacruz/tape-watch/issues/53 99 | [#67]: https://github.com/rstacruz/tape-watch/issues/67 100 | [@tuckerconnelly]: https://github.com/tuckerconnelly 101 | [#116]: https://github.com/rstacruz/tape-watch/issues/116 102 | [@lsanwick]: https://github.com/lsanwick 103 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tape-watch 2 | 3 | > Rerun tape tests when files change 4 | 5 | tape-watch is an auto-test runner for [tape]. It will re-run your tests when any of the files in your project changes. This is better than using [watch][] or [nodemon][]: it will reload your tests in the same Node.js process, saving you precious startup time. 6 | 7 | [![Status](https://travis-ci.org/rstacruz/tape-watch.svg?branch=master)](https://travis-ci.org/rstacruz/tape-watch "See test builds") 8 | 9 | [nodemon]: https://www.npmjs.com/package/nodemon 10 | [watch]: https://www.npmjs.com/package/watch 11 | 12 | ## Usage 13 | 14 | Can happily live in your project (`--save-dev`) or globally (`-g`). I prefer global, since it's compatible with every project with [tape][] in it. 15 | 16 | ``` 17 | npm install -g tape-watch 18 | ``` 19 | 20 | [tape]: https://github.com/substack/tape 21 | 22 | ## Reference 23 | 24 | ``` 25 | Usage: 26 | $ tape-watch [options] -- [options for the test] 27 | 28 | Options: 29 | -p, --pipe PACKAGE pipe to this package 30 | -o, --out CMD output to this file/cmd 31 | -R, --refresh PACKAGE ensure this PACKAGE gets refreshed 32 | -r, --require PACKAGE require a PACKAGE before startup 33 | -1, --once only run once 34 | -w, --watch cancel out --once 35 | -c, --clear clear console between test runs 36 | 37 | Other options: 38 | -h, --help show usage information 39 | -v, --version print version info and exit 40 | ``` 41 | Examples: 42 | 43 | ```sh 44 | tape-watch test/index.js 45 | tape-watch test/index.js -p tap-spec 46 | tape-watch test/index.js -o '| tap-spec --color' 47 | 48 | # ensure require('jquery') and require('react') always gets reevaluated 49 | tape-watch test/index.js -r jquery -r react 50 | 51 | # run all test files in test/ 52 | tape-watch 'test/**/*.js' 53 | ``` 54 | 55 | ## Using with Babel 56 | 57 | Use the `-r` *(--require)* flag with [babel-register](https://www.npmjs.com/package/babel-register). 58 | 59 | ```sh 60 | tape-watch -r babel-register 61 | ``` 62 | 63 | Before you do this, of course, you'll need to install the requisite modules first. 64 | 65 | ```sh 66 | npm install --save-dev babel-register babel-preset-es2015 67 | ``` 68 | 69 | ```js 70 | /* package.json */ 71 | "babel": { 72 | "presets": ["es2015"] 73 | } 74 | ``` 75 | 76 | ## Adding `npm test` 77 | 78 | Add this to your `package.json`: 79 | 80 | ```js 81 | "scripts": { 82 | "test": "tape-watch -1 test/*" 83 | } 84 | ``` 85 | 86 | You can now run tests with `npm test`, or make it auto-run with `npm test -- --watch`. 87 | 88 | ## Also see 89 | 90 | - [tape-growl](https://www.npmjs.com/package/tape-growl) for notifications on macOS 91 | - [tape-plus](https://www.npmjs.com/package/tape-plus) for grouping tape tests 92 | 93 | ## Thanks 94 | 95 | **tape-watch** © 2016+, Rico Sta. Cruz. Released under the [MIT] License.
96 | Authored and maintained by Rico Sta. Cruz with help from contributors ([list][contributors]). 97 | 98 | > [ricostacruz.com](http://ricostacruz.com)  ·  99 | > GitHub [@rstacruz](https://github.com/rstacruz)  ·  100 | > Twitter [@rstacruz](https://twitter.com/rstacruz) 101 | 102 | [MIT]: http://mit-license.org/ 103 | [contributors]: http://github.com/rstacruz/tape-watch/contributors 104 | -------------------------------------------------------------------------------- /bin/tape-watch: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | var chokidar = require('chokidar') 3 | var debounce = require('debounce-collect') 4 | var path = require('path') 5 | var once = require('once') 6 | var debug = require('debug')('tape-watch') 7 | var mutex = require('../lib/mutex') 8 | var wait = require('../lib/wait') 9 | var glob = require('glob') 10 | 11 | var cli = require('meow')([ 12 | 'Usage:', 13 | ' $ tape-watch [options] -- [options for the test]', 14 | '', 15 | 'Options:', 16 | ' -p, --pipe PACKAGE pipe to this package', 17 | ' -o, --out CMD output to this file/cmd', 18 | ' -R, --refresh PACKAGE ensure this PACKAGE gets refreshed', 19 | ' -r, --require PACKAGE require a PACKAGE before startup', 20 | ' -1, --once only run once', 21 | ' -w, --watch cancel out --once', 22 | ' -c, --clear clear console between test runs', 23 | '', 24 | 'Other options:', 25 | ' -h, --help show usage information', 26 | ' -v, --version print version info and exit', 27 | '', 28 | 'For examples and docs, see:', 29 | ' ' + require('../package.json').homepage 30 | ].join('\n'), { 31 | boolean: ['help', 'version', 'once', 'clear'], 32 | string: ['pipe', 'out', 'refresh', 'require'], 33 | '--': true, 34 | alias: { 35 | h: 'help', v: 'version', p: 'pipe', o: 'out', R: 'refresh', 36 | r: 'require', 1: 'once', c: 'clear' 37 | } 38 | }) 39 | 40 | var filenames = cli.input 41 | var cwd = process.cwd() 42 | process.argv = cli.input.slice(1) 43 | 44 | if (filenames.length === 0) { 45 | console.error('Usage: tape-watch ') 46 | console.error('See `tape-watch --help` for more options.') 47 | process.exit(1) 48 | } 49 | 50 | if (cli.flags.require) { 51 | requireMany(cli.flags.require) 52 | } 53 | 54 | var report = debounce(mutex(function (files) { 55 | debug('start', files && files.map(function (file) { return file[0] })) 56 | if (cli.flags.clear) { 57 | // Trigger a console clear command 58 | console.log('\u001b[2J\u001b[0;0H') 59 | } 60 | return invoke() 61 | .then(function (ok) { 62 | debug('done') 63 | }) 64 | .catch(function (err) { 65 | debug('error') 66 | console.error(err.stack || err.message || err) 67 | }) 68 | .then(function () { 69 | // cleanup 70 | }) 71 | .then(wait(250)) 72 | .then(function () { 73 | debug('listening again') 74 | }) 75 | }), 25) 76 | 77 | /* 78 | * invoke the test. installs a tape hook to listen for when tests finish. 79 | */ 80 | 81 | var invoke = function (options) { 82 | return new Promise(function (resolve, reject) { 83 | flush() 84 | var stream = tape().createStream() 85 | 86 | if (cli.flags.pipe) { 87 | stream = stream.pipe(requireHere(cli.flags.pipe)()) 88 | } 89 | 90 | if (cli.flags.out) { 91 | stream = stream.pipe(require('outpipe')(cli.flags.out)) 92 | } 93 | 94 | stream.pipe(process.stdout) 95 | tape().onFinish(once(function () { 96 | debug('tape finished') 97 | if (!options || !options.noClose) { 98 | try { tape().getHarness().close() } catch (e) {} 99 | } 100 | resolve() 101 | })) 102 | 103 | toArray(filenames).map(function (spec) { 104 | const files = glob.sync(spec) 105 | files.forEach(function (fname) { 106 | debug('requiring', fname) 107 | require(path.resolve(cwd, fname)) 108 | }) 109 | }) 110 | }) 111 | } 112 | 113 | /* 114 | * Requires a package relative to the project dir. 115 | */ 116 | 117 | function requireHere (pkg) { 118 | return require(path.join(cwd, 'node_modules', pkg)) 119 | } 120 | 121 | /* 122 | * Returns the `tape` module as the project sees it. 123 | */ 124 | 125 | function tape () { 126 | return requireHere('tape') 127 | } 128 | 129 | /* 130 | * Delete require cache for any app code (non-modules) and tape itself. 131 | * We need to clear out tape so that its state is reset. 132 | */ 133 | 134 | function flush () { 135 | Object.keys(require.cache).forEach(function (fname) { 136 | if (fname.indexOf('node_modules') === -1) { 137 | delete require.cache[fname] 138 | } 139 | 140 | var mods = ['tape'].concat(toArray(cli.flags.refresh)) 141 | mods.forEach(function (mod) { 142 | if (fname.indexOf(path.join(cwd, mod) + path.sep) > -1 || 143 | fname.indexOf(path.join(cwd, 'node_modules', mod) + path.sep) > -1) { 144 | delete require.cache[fname] 145 | } 146 | }) 147 | }) 148 | } 149 | 150 | function toArray (list) { 151 | if (!list) return [] 152 | return Array.isArray(list) ? list : [ list ] 153 | } 154 | 155 | /* 156 | * Requires many modules at once. 157 | * 158 | * requireMany(['babel-register', 'coffee-script/register']) 159 | * requireMany('babel-register') 160 | */ 161 | 162 | function requireMany (modules) { 163 | toArray(modules).forEach(function (mod) { 164 | debug('require', mod) 165 | requireHere(mod) 166 | }) 167 | } 168 | 169 | /* 170 | * Run 171 | */ 172 | 173 | if (cli.flags.once && !cli.flags.watch) { 174 | process.on('uncaughtException', function (err) { 175 | console.error(err.stack || err.message || err) 176 | process.exit(2) 177 | }) 178 | 179 | invoke({ noClose: true }) 180 | .then(function () { 181 | process.exit(0) 182 | }) 183 | .catch(function (err) { 184 | console.error(err.stack || err.message || err) 185 | process.exit(1) 186 | }) 187 | } else { 188 | process.on('uncaughtException', function (err) { 189 | console.error(err.stack || err.message || err) 190 | }) 191 | 192 | var watcher = chokidar.watch('.', { 193 | ignored: /[\/\\]\.|node_modules/, 194 | persistent: true, 195 | ignoreInitial: true 196 | }) 197 | 198 | watcher.on('change', report) 199 | watcher.on('add', report) 200 | watcher.on('unlink', report) 201 | report() 202 | } 203 | -------------------------------------------------------------------------------- /fixtures/othertest.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | 3 | test('dummy test', function (t) { 4 | t.pass('works') 5 | t.end() 6 | }) 7 | -------------------------------------------------------------------------------- /lib/mutex.js: -------------------------------------------------------------------------------- 1 | /* 2 | * returns a mutex-locked version of async function `fn`, which is assumed to 3 | * return a promies. 4 | */ 5 | 6 | module.exports = function mutex (fn) { 7 | var running 8 | return function () { 9 | if (running) return 10 | running = true 11 | process.on('uncaughtException', function () { 12 | running = false 13 | }) 14 | fn.apply(this, arguments) 15 | .then(function (data) { running = false; return data }) 16 | .catch(function (error) { running = false; throw error }) 17 | } 18 | } 19 | 20 | -------------------------------------------------------------------------------- /lib/wait.js: -------------------------------------------------------------------------------- 1 | /* 2 | * waits for `ms` milliseconds and resolves a promise. 3 | */ 4 | 5 | module.exports = function wait (ms) { 6 | return function () { 7 | return new Promise(function (resolve, reject) { 8 | setTimeout(function () { 9 | resolve() 10 | }, ms) 11 | }) 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tape-watch", 3 | "description": "Reruns tape tests when files change", 4 | "version": "2.3.0", 5 | "author": "Rico Sta. Cruz ", 6 | "babel": { 7 | "presets": [ 8 | "es2015" 9 | ] 10 | }, 11 | "bin": { 12 | "tape-watch": "bin/tape-watch" 13 | }, 14 | "bugs": { 15 | "url": "https://github.com/rstacruz/tape-watch/issues" 16 | }, 17 | "dependencies": { 18 | "chokidar": "1.6.0", 19 | "debounce-collect": "1.0.2", 20 | "debug": "2.6.0", 21 | "glob": "7.0.4", 22 | "meow": "3.7.0", 23 | "once": "1.3.3", 24 | "outpipe": "1.1.1", 25 | "simpler-debounce": "1.0.0" 26 | }, 27 | "devDependencies": { 28 | "babel-preset-es2015": "6.9.0", 29 | "babel-register": "6.9.0", 30 | "eslint": "2.11.1", 31 | "eslint-config-standard": "5.3.1", 32 | "eslint-config-standard-jsx": "1.2.0", 33 | "eslint-engine": "0.3.0", 34 | "eslint-plugin-promise": "1.3.1", 35 | "eslint-plugin-react": "5.2.2", 36 | "eslint-plugin-standard": "1.3.2", 37 | "tape": "4.5.1" 38 | }, 39 | "homepage": "https://github.com/rstacruz/tape-watch#readme", 40 | "keywords": [ 41 | "tape", 42 | "test", 43 | "watch" 44 | ], 45 | "license": "MIT", 46 | "main": "index.js", 47 | "repository": { 48 | "type": "git", 49 | "url": "git+https://github.com/rstacruz/tape-watch.git" 50 | }, 51 | "scripts": { 52 | "test": "./bin/tape-watch -r babel-register -1 test/*", 53 | "lint": "eslint-check" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /test/tape.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var spawn = require('child_process').spawn 3 | 4 | test('things', function (t) { 5 | var proc = spawnTest(['fixtures/othertest.js'], function (output) { 6 | t.equal(output.stderr, '', 'no stderr') 7 | t.equal(output.stdout, 8 | 'TAP version 13\n# dummy test\nok 1 works\n\n1..1\n' + 9 | '# tests 1\n# pass 1\n\n# ok\n', 10 | 'prints tap output') 11 | t.equal(output.code, null, 'exited successfully') 12 | t.end() 13 | }) 14 | 15 | setTimeout(function () { 16 | proc.kill('SIGHUP') 17 | }, 1000) 18 | }) 19 | 20 | test('eslint', require('eslint-engine/tape')()) 21 | 22 | function spawnTest (args, fn) { 23 | var proc = spawn('./bin/tape-watch', args) 24 | var output = { stdout: '', stderr: '', code: undefined } 25 | 26 | proc.stdout.on('data', function (data) { 27 | output.stdout += data 28 | }) 29 | 30 | proc.stderr.on('data', function (data) { 31 | output.stderr += data 32 | }) 33 | 34 | proc.on('close', function (code) { 35 | output.code = code 36 | fn(output) 37 | }) 38 | 39 | return proc 40 | } 41 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | var test = require('tape') 2 | var spawn = require('child_process').spawn 3 | 4 | test('things', function (t) { 5 | var proc = spawnTest(['fixtures/othertest.js'], function (output) { 6 | t.equal(output.stderr, '', 'no stderr') 7 | t.equal(output.stdout, 8 | 'TAP version 13\n# dummy test\nok 1 works\n\n1..1\n' + 9 | '# tests 1\n# pass 1\n\n# ok\n', 10 | 'prints tap output') 11 | t.equal(output.code, null, 'exited successfully') 12 | t.end() 13 | }) 14 | 15 | setTimeout(function () { 16 | proc.kill('SIGHUP') 17 | }, 1000) 18 | }) 19 | 20 | test('eslint', require('eslint-engine/tape')()) 21 | 22 | function spawnTest (args, fn) { 23 | var proc = spawn('./bin/tape-watch', args) 24 | var output = { stdout: '', stderr: '', code: undefined } 25 | 26 | proc.stdout.on('data', function (data) { 27 | output.stdout += data 28 | }) 29 | 30 | proc.stderr.on('data', function (data) { 31 | output.stderr += data 32 | }) 33 | 34 | proc.on('close', function (code) { 35 | output.code = code 36 | fn(output) 37 | }) 38 | 39 | return proc 40 | } 41 | --------------------------------------------------------------------------------