├── .gitignore ├── .jezebel ├── .npmignore ├── LICENSE ├── README.rdoc ├── bin └── jezebel ├── lib ├── jezebel.js └── jezebel │ ├── test_selector.js │ ├── utils.js │ └── watcher.js ├── package.json └── spec ├── jezebel_spec.js ├── spec_helper.js ├── test_selector_spec.js └── watcher_spec.js /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.jezebel: -------------------------------------------------------------------------------- 1 | var utils = require('./lib/jezebel/utils'); 2 | var stats; 3 | var context; 4 | 5 | function reloadModule(path) { 6 | delete require.cache[path]; 7 | context.$$ = require(path); 8 | } 9 | 10 | exports.settings = { 11 | onStart: function(replSession) { 12 | context = replSession.context; 13 | stats = { 14 | passes: 0, 15 | fails: 0, 16 | changes: 0 17 | }; 18 | }, 19 | onPass: function() { 20 | stats.passes += 1; 21 | }, 22 | onFail: function() { 23 | stats.fails += 1; 24 | }, 25 | onChange: function(path) { 26 | stats.changes += 1; 27 | // Broken in Node 0.6 :-( 28 | //if (path.match(/.*\.js$/)) { 29 | // // load the most recently changed module into the $$ variable in the REPL 30 | // reloadModule(path); 31 | //} 32 | } 33 | }; 34 | 35 | // Run me from the REPL to see your test stats! 36 | exports.stats = function() { 37 | return stats; 38 | } 39 | 40 | exports.files = utils.projectFiles; 41 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | spec 2 | .jezebel 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2011 by Ben Rady. 2 | 3 | Permission is hereby granted, free of charge, to any person 4 | obtaining a copy of this software and associated documentation 5 | files (the "Software"), to deal in the Software without 6 | restriction, including without limitation the rights to use, 7 | copy, modify, merge, publish, distribute, sublicense, and/or sell 8 | copies of the Software, and to permit persons to whom the 9 | Software is furnished to do so, subject to the following 10 | conditions: 11 | 12 | The above copyright notice and this permission notice shall be 13 | included in all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 16 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 17 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 18 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 19 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 20 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 21 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | -------------------------------------------------------------------------------- /README.rdoc: -------------------------------------------------------------------------------- 1 | You shoudn't use this. This (https://github.com/guard/guard-jasmine-headless-webkit) is a much better alternative. 2 | 3 | = Jezebel - a Node.js REPL and continuous test runner for Jasmine 4 | 5 | If you're writing tests using Node.js and Jasmine, Jezebel will help you get faster feedback. It runs your tests continuously (on each change to your code), and provides a shell (a.k.a REPL -- Read, Evaluate, Print, Loop) that lets you interact with your project and your tests. 6 | 7 | === Installation 8 | 9 | Stable version is available on npm, so you can install by using: 10 | 11 | npm install jezebel --global 12 | 13 | If you want to install the edge development version, simply clone this repo and install from it: 14 | 15 | git clone git://github.com/benrady/jezebel.git 16 | npm install ./jezebel 17 | 18 | === Using Jezebel 19 | 20 | $ jezebel 21 | > ................................................................ 22 | 23 | Completed in 0.037 seconds 24 | 64 examples 25 | 26 | > 27 | 28 | Jezebel runs all your tests when it starts, and every time a .js file in or under the current directory is changed. You can force a test run by calling the runTests() function from the REPL. Other functions will become available as jezebel improves. 29 | 30 | === Customization 31 | 32 | Jezebel can be customized by creating a .jezebel file in the root of your project (where Jezebel is run from). This file is loaded as a module and any functions that it exports will be available in the REPL. By exporting a settings object you can configure the behavior of Jezebel, and attach hooks to events such as onChange, onPass, onFail. See https://github.com/benrady/jezebel/blob/master/.jezebel for an example of this configuration. 33 | 34 | === Jezebel "Plugins" 35 | 36 | Additional modules can easily be added to Jezebel by requiring them in your .jezebel file. The jezebel/utils module is included with Jezebel, but any node.js package included in your load path can be used. You can either use these modules in your config file, or re-export them for use in the REPL. 37 | 38 | == Acknowledgement 39 | 40 | Under the covers, Jezebel uses Jessie (https://github.com/futuresimple/jessie) to run tests. 41 | 42 | === Copyright 43 | 44 | Copyright (c) 2011 Ben Rady. See LICENSE for details. 45 | -------------------------------------------------------------------------------- /bin/jezebel: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env NODE_PATH=./lib:$NODE_PATH node 2 | 3 | var path = require('path'), 4 | fs = require('fs'); 5 | 6 | var jezebel = require(path.join('../lib', 'jezebel')); 7 | jezebel.run(); 8 | -------------------------------------------------------------------------------- /lib/jezebel.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'), 2 | path = require('path'), 3 | watcher = require('./jezebel/watcher'), 4 | repl = require('repl'), 5 | childProcess = require('child_process'), 6 | sys = require('util'); 7 | var rootDir = fs.realpathSync(path.dirname(__filename) + '/..'); 8 | var selector = require('./jezebel/test_selector'); 9 | 10 | var session; 11 | var settings = {}; 12 | 13 | function evalInRepl(statement) { 14 | // FIXME Really would be cool if we could emit an escape or something to clear the current command (if any) 15 | process.stdin.emit('data', new Buffer(statement)); 16 | } 17 | 18 | function returnInRepl(arg) { 19 | session.context._ = arg; 20 | process.nextTick(function() { 21 | evalInRepl('_\n'); 22 | }); 23 | } 24 | 25 | function runHook(name, args) { 26 | func = settings["on" + name]; 27 | if (func) { 28 | return func.apply(session.context, args); 29 | // load the most recently changed module into the $$ variable in the REPL 30 | } 31 | } 32 | 33 | function fileChanged(path) { 34 | var tests = runHook('Change', [path]) || selector.select(path); 35 | if (tests) { 36 | console.log("Running examples in " + tests.join(' ')); 37 | runTests(tests); 38 | } 39 | } 40 | 41 | function runTests(tests) { 42 | tests = tests || ['spec']; 43 | var childPath; 44 | var windowsPath = rootDir + '/node_modules/.bin/jessie.cmd'; 45 | fs.exists(windowsPath, function(pathExists) { 46 | if (pathExists) { 47 | childPath = windowsPath; 48 | } else { 49 | childPath = rootDir + '/node_modules/jessie/bin/jessie'; 50 | } 51 | 52 | var child = childProcess.spawn(childPath, tests); 53 | child.stdout.addListener("data", function (chunk) { chunk && sys.print(chunk) }); 54 | child.stderr.addListener("data", function (chunk) { chunk && sys.debug(chunk) }); 55 | child.on('exit', function(fail) { 56 | if (fail) { 57 | runHook('Fail'); 58 | } else { 59 | runHook('Pass'); 60 | } 61 | returnInRepl(!fail); 62 | }); 63 | }); 64 | } 65 | 66 | function loadConfig(callback) { 67 | var configFile = process.cwd() + '/.jezebel'; 68 | fs.exists(configFile, function(exists) { 69 | if (exists) { 70 | config = require(configFile); 71 | extend(session.context, config); 72 | extend(settings, config.settings); 73 | } 74 | callback(); 75 | }); 76 | } 77 | 78 | function extend(obj1, obj2) { 79 | for(i in obj2) { 80 | obj1[i] = obj2[i]; 81 | } 82 | } 83 | 84 | function startRepl() { 85 | session = repl.start("> "); 86 | session.context.runTests = runTests; 87 | } 88 | 89 | // FIXME Hackity, hack, hack. For some reason, this module is not cached. Spying on a copy won't work. 90 | exports.repl = repl; 91 | 92 | exports.runTests = runTests; 93 | exports.fileChanged = fileChanged; 94 | exports.loadConfig = loadConfig; 95 | exports.settings = settings 96 | 97 | global.returnInRepl = returnInRepl; 98 | 99 | exports.run = function(args, options) { 100 | watcher.watchFiles(process.cwd(), fileChanged); 101 | startRepl(); 102 | loadConfig(function() { 103 | runHook('Start', [session]); 104 | runTests(['spec']); 105 | }); 106 | }; 107 | -------------------------------------------------------------------------------- /lib/jezebel/test_selector.js: -------------------------------------------------------------------------------- 1 | exports.select = function(path) { 2 | if (path.match(/\_spec.(js|coffee)$/)) { 3 | return [path.replace(process.cwd() + '/', '')]; 4 | } 5 | if (path.match(/\.(js|coffee)$/)) { 6 | return ['spec']; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /lib/jezebel/utils.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var fs = require('fs'); 3 | 4 | function isInProject(path) { 5 | return exports.ignoredFiles.indexOf(path.toLowerCase()) === -1; 6 | } 7 | 8 | function isSupportedFile(path) { 9 | var javascriptPattern = /.*\.js$/; 10 | var coffeescriptPattern = /.*\.coffee$/; 11 | return path.match(javascriptPattern) || path.match(coffeescriptPattern); 12 | } 13 | 14 | exports.ignoredFiles = ['.git', 'node_modules']; 15 | exports.isInProject = isInProject; 16 | exports.isSupportedFile = isSupportedFile; 17 | 18 | -------------------------------------------------------------------------------- /lib/jezebel/watcher.js: -------------------------------------------------------------------------------- 1 | // Initially stolen from supervisor.js 2 | // https://github.com/isaacs/node-supervisor 3 | 4 | var fs = require('fs'); 5 | var utils = require('./utils'); 6 | var sys = require('util'); 7 | 8 | function watchDirectory(path, callback, watched) { 9 | fs.readdir(path, function(err, fileNames) { 10 | if(err) { 11 | sys.error('Error reading path: ' + path); 12 | } 13 | else { 14 | fileNames.forEach(function (fileName) { 15 | if (utils.isInProject(fileName)) { 16 | watchFiles(path + '/' + fileName, callback, watched); 17 | } 18 | }); 19 | } 20 | }); 21 | } 22 | 23 | function watchFileOnce(watched, path, callback) { 24 | if(watched.indexOf(path) == -1){ 25 | watched.push(path) 26 | fs.watch(path, {persistent: true, interval: 500}, callback); 27 | } 28 | } 29 | 30 | function watchFiles(path, callback, watched) { 31 | watched = watched || []; 32 | fs.stat(path, function(err, stats){ 33 | if (err) { 34 | sys.error('Error (' + err + ') retrieving stats for file: ' + path); 35 | } 36 | else { 37 | if (stats.isDirectory()) { 38 | watchDirectory(path, callback, watched); 39 | watchFileOnce(watched, path, function() { watchDirectory(path, callback, watched); }); 40 | } 41 | else { 42 | if (utils.isSupportedFile(path)) { 43 | watchFileOnce(watched, path, function(event, filename) { callback(path); }); 44 | } 45 | } 46 | } 47 | }); 48 | } 49 | 50 | exports.watchFiles = watchFiles; 51 | 52 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "author": "Ben Rady (http://benrady.com)", 3 | "name": "jezebel", 4 | "description": "A REPL and continuous test runner for Jasmine tests", 5 | "version": "0.4.0", 6 | "homepage": "http://github.com/benrady/jezebel", 7 | "repository": { 8 | "type": "git", 9 | "url": "git://github.com/benrady/jezebel.git" 10 | }, 11 | "directories": { 12 | "lib": "." 13 | }, 14 | "engines": { 15 | "node": ">=0.6.2" 16 | }, 17 | "dependencies": {"jessie": ">=0.4.1"}, 18 | "bin": {"jezebel": "./bin/jezebel"} 19 | } 20 | -------------------------------------------------------------------------------- /spec/jezebel_spec.js: -------------------------------------------------------------------------------- 1 | describe('jezebel', function() { 2 | var childProcess = require('child_process'), 3 | sys = require('util'); 4 | var watcher, jezebel, repl, session, settings; 5 | 6 | beforeEach(function() { 7 | watcher = require('jezebel/watcher'); 8 | jezebel = require('jezebel'); 9 | session = {context: {}}; 10 | spyOn(jezebel.repl, 'start').andReturn(session); 11 | spyOn(watcher, 'watchFiles'); 12 | spyOn(childProcess, 'spawn'); 13 | spyOn(process.stdin, 'emit'); 14 | spyOn(process, 'nextTick'); 15 | spyOn(require('fs'), 'exists').andCallFake(function(file, callback) { 16 | if(file == process.cwd() + '/.jezebel') 17 | callback(true); 18 | else 19 | callback(false); 20 | }); 21 | require(process.cwd() + '/.jezebel').settings = settings = {}; 22 | }); 23 | 24 | function rootDir() { 25 | var path = require('path') 26 | var fs = require('fs'); 27 | return fs.realpathSync(path.dirname(fs.realpathSync(__filename)) + '/..'); 28 | } 29 | 30 | function expectReplEval(statement) { 31 | expect(process.stdin.emit).toHaveBeenCalled(); 32 | expect(process.stdin.emit.argsForCall[0][0]).toEqual('data'); 33 | expect(process.stdin.emit.argsForCall[0][1].toString()).toEqual(statement); 34 | } 35 | 36 | describe('run', function() { 37 | beforeEach(function() { 38 | childProcess.spawn.andReturn(process); 39 | }); 40 | 41 | it('watches for all the files in the specified directory', function() { 42 | jezebel.run([], {}); 43 | expect(watcher.watchFiles).toHaveBeenCalledWith(process.cwd(), jezebel.fileChanged); 44 | }); 45 | 46 | it('stars the repl', function() { 47 | jezebel.run([], {}); 48 | expect(jezebel.repl.start).toHaveBeenCalledWith('> '); 49 | }); 50 | 51 | it('calls the onStart hook', function() { 52 | settings.onStart = jasmine.createSpy('onStart'); 53 | jezebel.run([], {}); 54 | expect(settings.onStart).toHaveBeenCalled(); 55 | }); 56 | }); 57 | 58 | describe('fileChanged', function() { 59 | beforeEach(function() { 60 | spyOn(console, 'log'); 61 | childProcess.spawn.andReturn(process); 62 | }); 63 | 64 | it('runs tests if the file has changed', function() { 65 | jezebel.fileChanged(__filename, {mtime: new Date(100)}, {mtime: new Date(0)}); 66 | expect(childProcess.spawn).toHaveBeenCalled(); 67 | expect(console.log).toHaveBeenCalledWith('Running examples in spec/jezebel_spec.js'); 68 | }); 69 | 70 | it('does not run the tests if the file has not actually changed', function() { 71 | jezebel.fileChanged("", {mtime: new Date(0)}, {mtime: new Date(0)}); 72 | expect(process.stdin.emit).not.toHaveBeenCalled(); 73 | }); 74 | 75 | it('invokes the onChange hook to determine the tests to run', function() { 76 | jezebel.settings.onChange = function() { 77 | return ['myspecs']; 78 | }; 79 | jezebel.fileChanged(__filename, {mtime: new Date(100)}, {mtime: new Date(0)}); 80 | expect(childProcess.spawn.argsForCall[0][1]).toEqual(['myspecs']); 81 | }); 82 | }); 83 | 84 | describe('runTests', function() { 85 | var child; 86 | 87 | beforeEach(function() { 88 | childProcess.spawn.andReturn(child = { 89 | stdout: jasmine.createSpyObj('stdout', ['addListener']), 90 | stderr: jasmine.createSpyObj('stderr', ['addListener']), 91 | on: jasmine.createSpy('on') 92 | }); 93 | }); 94 | 95 | it('runs all tests in the spec directory by default', function() { 96 | jezebel.runTests(); 97 | expect(childProcess.spawn).toHaveBeenCalledWith(rootDir() + '/node_modules/jessie/bin/jessie', ['spec']); 98 | }); 99 | 100 | it('runs the specified tests', function() { 101 | jezebel.runTests(['spec']); 102 | expect(childProcess.spawn).toHaveBeenCalledWith(rootDir() + '/node_modules/jessie/bin/jessie', ['spec']); 103 | }); 104 | 105 | it('writes stdout to sys.print', function() { 106 | spyOn(sys, 'print'); 107 | jezebel.runTests(['spec']); 108 | child.stdout.addListener.argsForCall[0][1]('hello'); 109 | expect(sys.print).toHaveBeenCalledWith('hello'); 110 | }); 111 | 112 | it('writes stderr to sys.debug', function() { 113 | spyOn(sys, 'debug'); 114 | jezebel.runTests(['spec']); 115 | child.stderr.addListener.argsForCall[0][1]('goodbye'); 116 | expect(sys.debug).toHaveBeenCalledWith('goodbye'); 117 | }); 118 | 119 | it('adds a line feed after running to re-prompt the repl', function() { 120 | jezebel.runTests(['spec']); 121 | child.on.argsForCall[0][1](); 122 | expect(process.nextTick).toHaveBeenCalled(); 123 | }); 124 | }); 125 | 126 | describe('returnInRepl', function() { 127 | it('sets the argument as the return value', function() { 128 | returnInRepl('foo'); 129 | pending(); 130 | expect(session.context._).toEqual('foo'); 131 | }); 132 | 133 | it('evaluates the return value on the next tick', function() { 134 | returnInRepl('foo'); 135 | process.nextTick.argsForCall[0][0](); 136 | expectReplEval("_\n"); 137 | }); 138 | 139 | }); 140 | }); 141 | 142 | -------------------------------------------------------------------------------- /spec/spec_helper.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | 4 | jasmine.Spy.prototype.invokeCallback = function(){ 5 | var argsToInvokeWith = arguments; 6 | this.argsForCall.forEach(function(args){ 7 | args[args.length -1].apply(this, argsToInvokeWith); 8 | }); 9 | this.reset(); 10 | }; 11 | -------------------------------------------------------------------------------- /spec/test_selector_spec.js: -------------------------------------------------------------------------------- 1 | describe('test selector', function() { 2 | var selector; 3 | 4 | beforeEach(function() { 5 | selector = require('jezebel/test_selector'); 6 | }); 7 | 8 | describe('find', function() { 9 | it('runs a single spec if thats all that changed', function() { 10 | expect(selector.select(process.cwd() + '/path/to/some_spec.js')).toEqual(['path/to/some_spec.js']); 11 | }); 12 | 13 | it('runs all specs if any other javascript file has changed', function() { 14 | expect(selector.select(process.cwd() + '/path/to/file.js')).toEqual(['spec']); 15 | }); 16 | 17 | it('runs no specs if an non-javascript file changes', function() { 18 | expect(selector.select(process.cwd() + '/path/to/somefile')).toBeUndefined(); 19 | }); 20 | }); 21 | 22 | describe('find coffeescripts', function() { 23 | it('runs a single spec if thats all that changed', function() { 24 | expect(selector.select(process.cwd() + '/path/to/some_spec.coffee')).toEqual(['path/to/some_spec.coffee']); 25 | }); 26 | 27 | it('runs all specs if any other javascript file has changed', function() { 28 | expect(selector.select(process.cwd() + '/path/to/file.coffee')).toEqual(['spec']); 29 | }); 30 | 31 | it('runs no specs if an non-javascript file changes', function() { 32 | expect(selector.select(process.cwd() + '/path/to/somefile')).toBeUndefined(); 33 | }); 34 | }); 35 | }); 36 | -------------------------------------------------------------------------------- /spec/watcher_spec.js: -------------------------------------------------------------------------------- 1 | describe('watcher', function() { 2 | var watcher, fs, sys; 3 | 4 | beforeEach(function() { 5 | watcher = require('jezebel/watcher'); 6 | fs = require('fs'); 7 | spyOn(fs, 'watch'); 8 | spyOn(fs, 'unwatchFile'); 9 | spyOn(fs, 'stat'); 10 | spyOn(fs, 'readdir'); 11 | sys = require('util'); 12 | spyOn(sys, 'error'); 13 | }); 14 | 15 | describe('watching a single file', function() { 16 | var callback; 17 | beforeEach(function() { 18 | callback = jasmine.createSpy('callback'); 19 | watcher.watchFiles('filename.js', callback); 20 | fs.stat.invokeCallback('', {isDirectory : function(){return false;}}); 21 | }); 22 | 23 | it('provides the correct filename and options to watch', function() { 24 | expect(fs.watch.argsForCall[0][0]).toEqual('filename.js'); 25 | expect(fs.watch.argsForCall[0][1]).toEqual({persistent : true, interval : 500}); 26 | }); 27 | 28 | it('curries filename to the callback of watch', function() { 29 | fs.watch.invokeCallback(); 30 | expect(callback).toHaveBeenCalledWith('filename.js'); 31 | }); 32 | }); 33 | 34 | describe('watching a directory', function() { 35 | beforeEach(function() { 36 | watcher.watchFiles('dirName', function() {}); 37 | fs.stat.invokeCallback('', {isDirectory : function(){return true;}}); 38 | }); 39 | 40 | it('calls watch on dir and all files in the dir', function() { 41 | fs.readdir.invokeCallback('', ['file0.js', 'file1.coffee']); 42 | fs.stat.invokeCallback('', {isDirectory : function(){return false;}}); 43 | 44 | expect(fs.watch.callCount).toEqual(3); 45 | expect(fs.watch.argsForCall[0][0]).toEqual('dirName'); 46 | expect(fs.watch.argsForCall[1][0]).toEqual('dirName/file0.js'); 47 | expect(fs.watch.argsForCall[2][0]).toEqual('dirName/file1.coffee'); 48 | }); 49 | 50 | it('detects new files and watches those', function() { 51 | fs.readdir.reset(); 52 | fs.watch.invokeCallback(''); 53 | 54 | fs.readdir.invokeCallback('', ['file0.js']); 55 | fs.stat.invokeCallback('', {isDirectory : function(){return false;}}); 56 | 57 | expect(fs.watch.callCount).toEqual(1); 58 | expect(fs.watch.argsForCall[0][0]).toEqual('dirName/file0.js'); 59 | }); 60 | }); 61 | 62 | it('ignores non-supported files', function() { 63 | fs.readdir.invokeCallback('', ['file0']); 64 | fs.stat.invokeCallback('', {isDirectory : function(){return false;}}); 65 | 66 | expect(fs.watch.callCount).toEqual(0); 67 | }); 68 | 69 | it('only watches a file once', function() { 70 | watcher.watchFiles('dirName', function() {}); 71 | fs.stat.invokeCallback('', {isDirectory : function(){return true;}}); 72 | var dirChanged = fs.watch.argsForCall[0][2]; 73 | fs.readdir.invokeCallback('', ['file0.js']); 74 | 75 | dirChanged(); 76 | fs.readdir.invokeCallback('', ['file0.js']); 77 | fs.stat.invokeCallback('', {isDirectory : function(){return false;}}); 78 | 79 | expect(fs.watch.callCount).toEqual(2); 80 | expect(fs.watch.argsForCall[0][0]).toEqual('dirName'); 81 | expect(fs.watch.argsForCall[1][0]).toEqual('dirName/file0.js'); 82 | }); 83 | 84 | it('writes errors to sys.error', function() { 85 | watcher.watchFiles('dirName', function() {}); 86 | fs.stat.invokeCallback(true); 87 | expect(sys.error).toHaveBeenCalled(); 88 | }); 89 | }); 90 | --------------------------------------------------------------------------------