├── test ├── __playground │ ├── lib │ │ ├── dir │ │ │ ├── other │ │ │ ├── module.js │ │ │ ├── dir │ │ │ │ ├── module.js │ │ │ │ └── module.2.js │ │ │ └── module.2.js │ │ ├── no-tests.js │ │ ├── module.js │ │ ├── evaluation-error.js │ │ ├── context-error │ │ │ └── module.js │ │ ├── index-test │ │ │ ├── sub │ │ │ │ └── index.js │ │ │ ├── file-name-1.js │ │ │ ├── file-name-2.js │ │ │ ├── file-name-3.js │ │ │ └── index.js │ │ └── test-evaluation-error.js │ ├── test │ │ ├── dir │ │ │ └── dir │ │ │ │ └── dummy │ │ ├── context │ │ │ ├── module.js │ │ │ ├── dir │ │ │ │ └── module.js │ │ │ ├── context │ │ │ │ ├── module.js │ │ │ │ └── __tad.js │ │ │ └── __tad.js │ │ ├── context-error │ │ │ └── __tad.js │ │ ├── test-evaluation-error.js │ │ ├── function-test.js │ │ ├── generics-test │ │ │ ├── __scopes.js │ │ │ └── test.js │ │ └── module.js │ ├── module.js │ └── package.json ├── .lintignore ├── lib │ ├── find-test-path.js │ ├── load.js │ ├── logger.js │ ├── assert.js │ ├── find-context.js │ ├── configure.js │ ├── console.js │ └── run.js └── index.js ├── .gitignore ├── .lint ├── .travis.yml ├── lib ├── utils │ ├── factory.js │ └── index-test.js ├── tad-ignore-mode.js ├── find-test-path.js ├── find-context.js ├── fix-test-utils.js ├── logger.js ├── load.js ├── configure.js ├── run.js ├── console.js └── assert.js ├── LICENSE ├── package.json ├── bin └── tad ├── index.js ├── README.md └── CHANGES /test/__playground/lib/dir/other: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/.lintignore: -------------------------------------------------------------------------------- 1 | /__playground 2 | -------------------------------------------------------------------------------- /test/__playground/test/dir/dir/dummy: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/__playground/test/context/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; -------------------------------------------------------------------------------- /test/__playground/test/context/dir/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /.lintcache 2 | /node_modules 3 | /npm-debug.log 4 | -------------------------------------------------------------------------------- /test/__playground/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; -------------------------------------------------------------------------------- /test/__playground/test/context/context/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | -------------------------------------------------------------------------------- /test/__playground/lib/no-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; -------------------------------------------------------------------------------- /test/__playground/lib/dir/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/dir/dir/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/dir/module.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/evaluation-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | 4 | generate.error(); -------------------------------------------------------------------------------- /test/__playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "dummy", 3 | "private": true 4 | } 5 | -------------------------------------------------------------------------------- /test/__playground/lib/context-error/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/dir/dir/module.2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/index-test/sub/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/test/context-error/__tad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | generate.error(); 4 | -------------------------------------------------------------------------------- /test/__playground/test/context/__tad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.context = { x: {} }; -------------------------------------------------------------------------------- /test/__playground/test/test-evaluation-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | generate.error(); 4 | -------------------------------------------------------------------------------- /test/__playground/lib/index-test/file-name-1.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/index-test/file-name-2.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/index-test/file-name-3.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/lib/test-evaluation-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = {}; 4 | -------------------------------------------------------------------------------- /test/__playground/test/function-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (a, d) { 4 | 5 | }; 6 | -------------------------------------------------------------------------------- /test/__playground/test/context/context/__tad.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.context = function (lpath) { 4 | return lpath; 5 | }; 6 | -------------------------------------------------------------------------------- /test/__playground/test/generics-test/__scopes.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.First = { name: 'First' }; 4 | exports.Second = { name: 'Second' }; 5 | -------------------------------------------------------------------------------- /.lint: -------------------------------------------------------------------------------- 1 | @root 2 | 3 | module 4 | 5 | indent 2 6 | maxlen 100 7 | tabs 8 | 9 | ass 10 | nomen 11 | plusplus 12 | predef+ process, global, __dirname, console 13 | -------------------------------------------------------------------------------- /test/__playground/test/generics-test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.__generic = { 4 | "Sample test": function (t, a) { 5 | a.ok(true, this.name); 6 | } 7 | }; 8 | -------------------------------------------------------------------------------- /test/__playground/test/module.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | exports.pass = function (t, a) { 4 | a.ok(true, "Pass"); 5 | }; 6 | exports.fail = function (t, a) { 7 | a.ok(false, "Fail"); 8 | }; 9 | -------------------------------------------------------------------------------- /test/__playground/lib/index-test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = { 4 | fileName1: require('./file-name-1'), 5 | fileName2: require('./file-name-2'), 6 | fileName3: require('./file-name-3'), 7 | sub: require('./sub') 8 | }; 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | sudo: false # http://docs.travis-ci.com/user/workers/container-based-infrastructure/ 2 | language: node_js 3 | node_js: 4 | - 0.12 5 | - 4 6 | - 5 7 | - 6 8 | 9 | notifications: 10 | email: 11 | - medikoo+tad@medikoo.com 12 | 13 | script: "npm test && npm run lint" 14 | -------------------------------------------------------------------------------- /lib/utils/factory.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isFunction = require('es5-ext/function/is-function') 4 | , oForEach = require('es5-ext/object/for-each'); 5 | 6 | module.exports = function (scopes, tests) { 7 | var r = {}; 8 | oForEach(scopes, function (scope, typeName) { 9 | if (isFunction(tests)) { 10 | r[typeName] = tests.bind(scope); 11 | } else { 12 | oForEach(tests, function (test, testName) { 13 | r[typeName + ": " + testName] = test.bind(scope); 14 | }); 15 | } 16 | }); 17 | 18 | return r; 19 | }; 20 | -------------------------------------------------------------------------------- /lib/tad-ignore-mode.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var findRoot = require('next/module/find-package-root') 4 | , resolve = require('path').resolve 5 | 6 | , isRoot; 7 | 8 | require('fs2/lib/ignore-modes').tad = module.exports = { 9 | filename: '.testignore', 10 | isRoot: isRoot = function (path) { 11 | var promise = findRoot(resolve(path, '_find-that-root_'))(function (root) { 12 | return root === path; 13 | }); 14 | promise.path = path; 15 | return promise; 16 | }, 17 | isRootWatcher: isRoot 18 | }; 19 | isRoot.returnsPromise = true; 20 | -------------------------------------------------------------------------------- /lib/find-test-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path') 4 | , findRoot = require('next/module/find-package-root') 5 | 6 | , resolve = path.resolve, sep = path.sep; 7 | 8 | module.exports = function (tpath) { 9 | return findRoot(tpath)(function (root) { 10 | var e; 11 | tpath = tpath.slice(root.length + 1).split(sep); 12 | if (tpath[0] === 'test') { 13 | e = new Error("Input seems to be a test file"); 14 | e.type = 'testfile'; 15 | throw e; 16 | } 17 | return resolve(root, 'test' + sep + tpath.join(sep)); 18 | }); 19 | }; 20 | -------------------------------------------------------------------------------- /test/lib/find-test-path.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var resolve = require('path').resolve 4 | 5 | , playground = resolve(__dirname, '../__playground'); 6 | 7 | module.exports = { 8 | "In lib": function (t, a, d) { 9 | t(resolve(playground, 'lib/dir/module.js')).then(function (tpath) { 10 | a(tpath, resolve(playground, 'test/lib/dir/module.js')); 11 | }).done(d); 12 | }, 13 | "In main": function (t, a, d) { 14 | t(resolve(playground, 'module.js')).then(function (tpath) { 15 | a(tpath, resolve(playground, 'test/module.js')); 16 | }).done(d); 17 | } 18 | }; 19 | -------------------------------------------------------------------------------- /test/lib/load.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var resolve = require('path').resolve 4 | 5 | , pg = resolve(__dirname, '../__playground'); 6 | 7 | module.exports = function (t, a) { 8 | var o = t(pg + '/lib/evaluation-error.js', pg + '/not/existing/path', global); 9 | a.ok(o.testee instanceof Error, "Evaluation error"); 10 | a(o.test, undefined, "Not found"); 11 | 12 | o = t(resolve(pg, 'lib/index-test/index.js'), 13 | resolve(pg, 'not/existing/path'), global); 14 | a(typeof o.test, 'function', "Automatic index test"); 15 | 16 | o = t(pg + '/lib/module.js', pg + '/test/generics-test/test.js', global); 17 | a(typeof o.test.__generic, 'undefined', "Generics test"); 18 | }; 19 | -------------------------------------------------------------------------------- /lib/find-context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isFunction = require('es5-ext/function/is-function') 4 | , path = require('path') 5 | , createContext = require('vm').createContext 6 | , findRoot = require('next/module/find-package-root') 7 | , requireFirst = require('next/module/require-silent')( 8 | require('next/module/require-first-in-tree') 9 | ); 10 | 11 | module.exports = function (lpath, tpath) { 12 | tpath = path.dirname(tpath); 13 | return findRoot(tpath)(function (root) { 14 | var c = requireFirst('__tad', tpath, root); 15 | if (c) { 16 | if (c instanceof Error) throw c; 17 | if (c.context) { 18 | c = c.context; 19 | if (isFunction(c)) c = c(lpath); 20 | c.console = console; 21 | return createContext(c); 22 | } 23 | } 24 | return global; 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /test/lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function (t, a) { 4 | var ondata = [], ended = false; 5 | t = t(); 6 | t.on('data', function (o) { 7 | ondata.push(o.type, o.data); 8 | }); 9 | t.on('end', function () { 10 | ended = true; 11 | }); 12 | t.pass('foo'); 13 | t.in('ONE'); 14 | t.fail('bar'); 15 | t.out(); 16 | t.error('error'); 17 | 18 | a.deep(t.msg, [], "Msg"); 19 | a(t.length, 3, "Length"); 20 | a.deep([t[0].type, t[0].data, t[0].msg.toString()], 21 | ['pass', 'foo', ''], "#1 pass"); 22 | a.deep([t[1].type, t[1].data, t[1].msg.toString()], 23 | ['fail', 'bar', 'ONE'], "#2 fail"); 24 | a.deep([t[2].type, t[2].data, t[2].msg.toString()], 25 | ['error', 'error', ''], "#3 error"); 26 | a(ended, false, "Not ended"); 27 | t.end(); 28 | a(ended, true, "Ended"); 29 | a.deep(ondata, ['pass', 'foo', 'fail', 'bar', 'error', 'error'], "Log"); 30 | }; 31 | -------------------------------------------------------------------------------- /test/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = __dirname + '/__playground' 4 | , n4 = (process.version.indexOf('v0.4') === 0); 5 | 6 | module.exports = function (t, a, d) { 7 | if (!n4) { 8 | d(); 9 | return; 10 | } 11 | var outorg, errorg; 12 | outorg = process.stdout._writeOut; 13 | errorg = process.stderr._writeOut; 14 | process.stdout._writeOut = function (data) {}; 15 | process.stderr._writeOut = function (data) {}; 16 | 17 | t([ 18 | '/wrong/path', 19 | pg + '/lib/context-error/module.js', 20 | pg + '/lib/evaluation-error.js', 21 | pg + '/lib/test-evaluation-error.js', 22 | pg + '/lib/no-tests.js', 23 | pg + '/lib/module.js' 24 | ])(function () { 25 | process.stdout._writeOut = outorg; 26 | process.stderr._writeOut = errorg; 27 | a.ok(true); 28 | d(); 29 | }, function (err) { 30 | process.stdout._writeOut = outorg; 31 | process.stderr._writeOut = errorg; 32 | d(err); 33 | }).end(); 34 | }; 35 | -------------------------------------------------------------------------------- /test/lib/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var customError = require('es5-ext/error/custom') 4 | , logger = require('../../lib/logger')(); 5 | 6 | module.exports = function (t, a) { 7 | t = t(logger); 8 | t(true, true, 'foo'); 9 | t.ok(false, 'bar'); 10 | t.not(false, true, 'not'); 11 | t.deep([1, 2], [1, 2], 'deep'); 12 | t.notDeep([1, 2], [2, 1], 'not deep'); 13 | t.throws(function () { throw customError('Test', 'TEST'); }, 'TEST', 14 | 'throws'); 15 | 16 | a.deep([logger[0].type, logger[0].data], ['pass', 'foo']); 17 | a.deep([logger[1].type, logger[1].data.message], ['fail', 'bar']); 18 | a.deep([logger[2].type, logger[2].data], ['pass', 'not'], "'not' support"); 19 | a.deep([logger[3].type, logger[3].data], ['pass', 'deep'], "'deep' support"); 20 | a.deep([logger[4].type, logger[4].data], ['pass', 'not deep'], 21 | "'not deep' support"); 22 | a.deep([logger[5].type, logger[5].data], ['pass', 'throws'], 23 | "custom trhows support"); 24 | }; 25 | -------------------------------------------------------------------------------- /test/lib/find-context.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var pg = require('path').resolve(__dirname, '../__playground'); 4 | 5 | module.exports = { 6 | Default: function (t, a, d) { 7 | t('ignore', pg + '/test/module.js')(function (context) { 8 | a(context, global); 9 | }).done(d); 10 | }, 11 | Custom: { 12 | "": function (t, a, d) { 13 | var path = pg + '/test/context/'; 14 | t('ignore', path + 'module.js')(function (context) { 15 | a(context.x, require(path + '__tad').context.x); 16 | }).done(d); 17 | }, 18 | Nested: function (t, a, d) { 19 | var path = pg + '/test/context/context/', o = { x: {} }; 20 | t(o, path + 'module.js')(function (context) { 21 | a(context.x, require(path + '__tad').context(o).x); 22 | }).done(d); 23 | }, 24 | "Nested fallback": function (t, a, d) { 25 | var path = pg + '/test/context/'; 26 | t('ignore', path + '/dir/module.js')(function (context) { 27 | a(context.x, require(path + '__tad').context.x); 28 | }).done(d); 29 | } 30 | } 31 | }; 32 | -------------------------------------------------------------------------------- /lib/fix-test-utils.js: -------------------------------------------------------------------------------- 1 | // Temporary fix for https://github.com/Gozala/test-commonjs/pull/8 2 | 3 | 'use strict'; 4 | 5 | var utils = require('test/utils') 6 | 7 | , instanceOf; 8 | 9 | try { 10 | if (utils.instanceOf) utils.instanceOf(Object.create(null), Date); 11 | } catch (e) { 12 | instanceOf = utils.instanceOf = function (value, Type) { 13 | var constructor, isConstructorNameSame, isConstructorSourceSame 14 | , isInstanceOf = value instanceof Type; 15 | 16 | if (!isInstanceOf && value) { 17 | constructor = value.constructor; 18 | isConstructorNameSame = constructor && (constructor.name === Type.name); 19 | isConstructorSourceSame = String(constructor) === String(Type); 20 | isInstanceOf = (isConstructorNameSame && isConstructorSourceSame) || 21 | instanceOf(Object.getPrototypeOf(value), Type); 22 | } 23 | return isInstanceOf; 24 | }; 25 | 26 | utils.isDate = function (value) { 27 | return utils.isObject(value) && instanceOf(value, Date); 28 | }; 29 | 30 | utils.isRegExp = function (value) { return instanceOf(value, RegExp); }; 31 | } 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (C) 2012-2016 Mariusz Nowak (www.medikoo.com) 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in 13 | all copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 21 | THE SOFTWARE. 22 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var aFrom = require('es5-ext/array/from') 4 | , partial = require('es5-ext/function/#/partial') 5 | , mixin = require('es5-ext/object/mixin') 6 | , ee = require('event-emitter') 7 | 8 | , o; 9 | 10 | o = ee(exports = { 11 | init: function () { 12 | this.msg = []; 13 | this.closure = 0; 14 | this.passed = []; 15 | this.errored = []; 16 | this.failed = []; 17 | this.started = new Date(); 18 | return this; 19 | }, 20 | in: function (msg, closure) { 21 | this.msg.push(msg); 22 | if (closure) ++this.closure; 23 | }, 24 | out: function (closure) { 25 | this.msg.pop(); 26 | if (closure) --this.closure; 27 | }, 28 | log: function (type, data) { 29 | var o = { type: type, time: new Date(), data: data, msg: aFrom(this.msg) }; 30 | this.push(o); 31 | this[type + 'ed'].push(o); 32 | this.emit('data', o); 33 | }, 34 | end: function () { 35 | this.emit('end'); 36 | } 37 | }); 38 | 39 | o.log.partial = partial; 40 | o.error = o.log.partial('error'); 41 | o.pass = o.log.partial('pass'); 42 | o.fail = o.log.partial('fail'); 43 | 44 | module.exports = function () { 45 | return mixin([], o).init(); 46 | }; 47 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tad", 3 | "version": "0.2.7", 4 | "description": "JavaScript test suite", 5 | "author": "Mariusz Nowak (http://www.medikoo.com/)", 6 | "keywords": [ 7 | "test", 8 | "factory", 9 | "unit", 10 | "unittest", 11 | "runner", 12 | "tests", 13 | "tdd", 14 | "testing" 15 | ], 16 | "bin": { 17 | "tad": "./bin/tad" 18 | }, 19 | "repository": { 20 | "type": "git", 21 | "url": "git://github.com/medikoo/tad.git" 22 | }, 23 | "dependencies": { 24 | "cli-color": "~1.1", 25 | "d": "~0.1.1", 26 | "deferred": "~0.7.5", 27 | "duration": "0.2", 28 | "es5-ext": "~0.10.12", 29 | "event-emitter": "~0.3.4", 30 | "fs2": "~0.2.3", 31 | "next": "~0.4.1", 32 | "optimist": "~0.6.1", 33 | "path2": "0.1", 34 | "test": "0.6" 35 | }, 36 | "devDependencies": { 37 | "xlint": "~0.2.2", 38 | "xlint-jslint-medikoo": "~0.1.4" 39 | }, 40 | "scripts": { 41 | "lint": "node node_modules/xlint/bin/xlint --linter=node_modules/xlint-jslint-medikoo/index.js --no-cache --no-stream", 42 | "lint-console": "node node_modules/xlint/bin/xlint --linter=node_modules/xlint-jslint-medikoo/index.js --watch", 43 | "test": "node ./bin/tad" 44 | }, 45 | "license": "MIT" 46 | } 47 | -------------------------------------------------------------------------------- /lib/load.js: -------------------------------------------------------------------------------- 1 | // Imports lib and tests from given paths 2 | 3 | 'use strict'; 4 | 5 | var assign = require('es5-ext/object/assign') 6 | , endsWith = require('es5-ext/string/#/ends-with') 7 | , path = require('path') 8 | , commonPath = require('path2/common') 9 | , requireSilent = require('next/module/require-silent')( 10 | require('next/module/require-in-context') 11 | ) 12 | , requireFirst = require('next/module/require-first-in-tree') 13 | 14 | , dirname = path.dirname, sep = path.sep 15 | , ptrim; 16 | 17 | ptrim = function (path) { 18 | return path.match(/[\u0000-\.0-\[\]-\uffff][\/\\]$/) ? 19 | path.slice(0, -1) : path; 20 | }; 21 | 22 | module.exports = function (testeePath, testPath, context) { 23 | var o, scopes; 24 | o = { 25 | test: requireSilent(testPath, context), 26 | testee: requireSilent(testeePath, context) 27 | }; 28 | 29 | if (!o.test && endsWith.call(testeePath, sep + 'index.js')) { 30 | o.test = require('./utils/index-test')(dirname(testeePath), null, context); 31 | } 32 | 33 | if (o.test && o.test.__generic) { 34 | scopes = requireFirst('__scopes', dirname(testPath), 35 | ptrim(commonPath(testeePath, testPath))); 36 | assign(o.test, require('./utils/factory')(scopes, o.test.__generic)); 37 | delete o.test.__generic; 38 | } 39 | 40 | return o; 41 | }; 42 | -------------------------------------------------------------------------------- /test/lib/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var findTestPath = require('../../lib/find-test-path') 4 | , pg = require('path').resolve(__dirname, '../__playground/lib') + '/'; 5 | 6 | module.exports = function (t, a, d) { 7 | var l, data, paths = [pg + 'module.js', '/wrong/path', pg + 'dir']; 8 | l = t(paths); 9 | data = []; 10 | l('data', function () { 11 | data.push(arguments); 12 | }); 13 | l('end', function () { 14 | d({ 15 | File: function (t, a, d) { 16 | var o = data[0]; 17 | a(o[0], paths[0], "Path"); 18 | a(o[1], paths[0], "File"); 19 | a(o[3], global, "Context"); 20 | findTestPath(o[1])(function (p) { 21 | a(p, o[2], "Test path"); 22 | }).done(d); 23 | }, 24 | "Wrong path": function () { 25 | var o = data[1]; 26 | a(o[0], paths[1], "Path"); 27 | a.ok(o[1] instanceof Error, "Error"); 28 | }, 29 | Directory: function () { 30 | a(data.length, 6, "Files length"); 31 | return { 32 | "File #1": function (t, a, d) { 33 | var o = data[2]; 34 | // console.log(o); 35 | a(o[0], paths[2], "Path"); 36 | a(o[3], global, "Context"); 37 | findTestPath(o[1])(function (p) { 38 | a(p, o[2], "Test path"); 39 | }).done(d); 40 | }, 41 | "File #2": function (t, a, d) { 42 | var o = data[5]; 43 | a(o[0], paths[2], "Path"); 44 | a(o[3], global, "Context"); 45 | findTestPath(o[1])(function (p) { 46 | a(p, o[2], "Test path"); 47 | }).done(d); 48 | } 49 | }; 50 | } 51 | }); 52 | }); 53 | }; 54 | -------------------------------------------------------------------------------- /lib/configure.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var lock = require('es5-ext/function/#/lock') 4 | , partial = require('es5-ext/function/#/partial') 5 | , deferred = require('deferred') 6 | , ee = require('event-emitter') 7 | , fs = require('fs') 8 | , resolve = require('path').resolve 9 | , readdir = require('fs2/readdir') 10 | , findTestPath = require('./find-test-path') 11 | , findContext = require('./find-context') 12 | 13 | , stat = deferred.promisify(fs.stat) 14 | 15 | , configure, readdirOpts; 16 | 17 | require('./tad-ignore-mode'); 18 | 19 | readdirOpts = { depth: Infinity, type: { file: true }, pattern: /\.js$/, 20 | ignoreRules: ['git', 'tad'] }; 21 | configure = ee(); 22 | 23 | module.exports = function (paths) { 24 | var o, emitdata, emitend; 25 | if (typeof paths === 'string') { 26 | paths = arguments; 27 | } 28 | 29 | o = Object.create(configure); 30 | emitdata = o.emit.bind(o, 'data'); 31 | emitend = lock.call(o.emit.bind(o, 'end')); 32 | 33 | deferred.reduce(paths, function (ignore, p) { 34 | var emit; 35 | emit = partial.call(emitdata, p); 36 | return stat(p)(function (stats) { 37 | if (stats.isFile()) return [p]; 38 | if (stats.isDirectory()) { 39 | return readdir(p, readdirOpts).map(function (file) { 40 | return resolve(p, file); 41 | }); 42 | } 43 | throw new Error('Invalid path'); 44 | })(function (paths) { 45 | return deferred.reduce(paths, function (ignore, testee) { 46 | emit = partial.call(emitdata, p, testee); 47 | return findTestPath(testee)(function (test) { 48 | if (!test) return; 49 | return findContext(testee, test)(function (context) { 50 | emit(test, context); 51 | }); 52 | })(null, emit); 53 | }, null); 54 | }, emit); 55 | }, null)(function () { process.nextTick(emitend); }); 56 | 57 | return o.on.bind(o); 58 | }; 59 | -------------------------------------------------------------------------------- /bin/tad: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | 'use strict'; 4 | 5 | Error.stackTraceLimit = Infinity; 6 | 7 | var compact = require('es5-ext/array/#/compact') 8 | , flatten = require('es5-ext/array/#/flatten') 9 | , deferred = require('deferred') 10 | , path = require('path') 11 | , findRoot = require('next/module/find-package-root') 12 | , readdir = require('fs2/readdir') 13 | , stat = require('fs2/stat') 14 | 15 | , argv = require('optimist') 16 | .usage("Usage: $0 [options] [paths]") 17 | .boolean(['a', 'm']) 18 | .describe('a', "Display all tests names, including passed") 19 | .describe('m', "Minimise output, verbose only for fails or errors").argv 20 | 21 | , extname = path.extname, resolve = path.resolve 22 | , suite = require('../'); 23 | 24 | if (!argv._.length) argv._ = ['.']; 25 | 26 | require('../lib/tad-ignore-mode'); 27 | 28 | deferred.map(argv._, function (path) { 29 | if (path !== '.') return path; 30 | path = resolve('.'); 31 | return findRoot(resolve(path, 'x'))(function (root) { 32 | if (root !== path) return path; 33 | return readdir(path, { type: { file: true, directory: true }, 34 | ignoreRules: ['git', 'tad'] }).map(function (name) { 35 | var filename = resolve(path, name); 36 | return stat(resolve(path, name))(function (stats) { 37 | if (stats.isDirectory()) { 38 | if (name === 'node_modules') return null; 39 | if (name === 'bin') return null; 40 | if (name === 'test') return null; 41 | if (name === 'examples') return null; 42 | return filename; 43 | } 44 | if (extname(name) === '.js') return filename; 45 | return null; 46 | }); 47 | }).invoke(compact); 48 | }); 49 | })(function (paths) { 50 | return suite(flatten.call(paths), argv)(function (suite) { 51 | var c = suite.console; 52 | process.on('exit', function () { 53 | if (c.errored) process.exitCode = 2; 54 | else if (c.failed) process.exitCode = 1; 55 | }); 56 | }); 57 | }).done(); 58 | -------------------------------------------------------------------------------- /lib/utils/index-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var curry = require('es5-ext/function/#/curry') 4 | , contains = curry.call(require('es5-ext/array/#/contains')) 5 | , noop = require('es5-ext/function/noop') 6 | , not = require('es5-ext/function/#/not') 7 | , oForEach = require('es5-ext/object/for-each') 8 | , convert = require('es5-ext/string/#/hyphen-to-camel') 9 | , d = require('d') 10 | , a2p = require('deferred').promisify 11 | , isPromise = require('deferred/is-promise') 12 | , reqInContext = require('next/module/require-in-context') 13 | , fs = require('fs') 14 | , normalize = require('path').normalize 15 | 16 | , defineProperty = Object.defineProperty 17 | , isConstant = RegExp.prototype.test.bind(/^[A-Z0-9_]+$/) 18 | , readDir; 19 | 20 | readDir = function (dir) { 21 | var o = {}; 22 | if (isPromise(dir)) { 23 | return dir; 24 | } 25 | dir = normalize(dir); 26 | return a2p(fs.readdir)(dir).map(function (f) { 27 | if (f[0] === '_') return; 28 | if (f[0] === '.') return; 29 | if (f === 'lib') return; 30 | if (f === 'node_modules') return; 31 | if (f === 'test') return; 32 | return a2p(fs.stat)(dir + '/' + f)(function (stats) { 33 | if (stats.isFile()) { 34 | if ((f.slice(-3) !== '.js') || (f === 'index.js')) { 35 | return; 36 | } 37 | f = f.slice(0, -3); 38 | } else if (!stats.isDirectory()) { 39 | return; 40 | } 41 | defineProperty(o, convert.call(f), d('cew', normalize(dir + '/' + f))); 42 | }, noop); 43 | })(o); 44 | }; 45 | 46 | module.exports = function (dir, ignores, context) { 47 | if (context == null) context = global; 48 | return function (t, a, d) { 49 | readDir(dir)(function (o) { 50 | var keys = Object.keys(t), keysLc; 51 | if (ignores) { 52 | keys = keys.filter(not.call(contains), ignores); 53 | } 54 | keysLc = keys.map(function (name) { 55 | if (isConstant(name)) name = name.replace(/_/g, ''); 56 | return name.toLowerCase(); 57 | }); 58 | oForEach(o, function (path, f) { 59 | var i = keysLc.indexOf(f.toLowerCase()); 60 | if (i === -1) { 61 | a.ok(false, f + " - is present ?"); 62 | } else { 63 | a.ok(true, f + " - is present ?"); 64 | } 65 | if (i !== -1) { 66 | a(t[keys[i]], reqInContext(require.resolve(path), context), 67 | f + " - points its module ?"); 68 | keys.splice(i, 1); 69 | keysLc.splice(i, 1); 70 | } 71 | }); 72 | a.ok(keys.length === 0, "[" + keys.toString() + "] - no extras found ?"); 73 | d(); 74 | }).done(); 75 | }; 76 | }; 77 | 78 | module.exports.readDir = readDir; 79 | -------------------------------------------------------------------------------- /test/lib/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var oForEach = require('es5-ext/object/for-each') 4 | , AssertionError = require('test/assert').AssertionError 5 | 6 | , n4 = (process.version.indexOf('v0.4') === 0); 7 | 8 | module.exports = function (t, a) { 9 | if (!n4) { 10 | return; 11 | } 12 | var outorg, errorg, outl = '', errl = '', console, results = {}; 13 | outorg = process.stdout._writeOut; 14 | errorg = process.stderr._writeOut; 15 | process.stdout._writeOut = function (data) { 16 | outl += data; 17 | }; 18 | process.stderr._writeOut = function (data) { 19 | errl += data; 20 | }; 21 | 22 | console = t({}); 23 | 24 | console.pass('foo', 'bar'); 25 | results['Pass content'] = [outl.length > 0]; 26 | results['Pass lines'] = [outl.split('\n').length, 1]; 27 | outl = ''; 28 | 29 | console.pass('foo', 'bar'); 30 | results['Second Pass content'] = [outl.length > 0]; 31 | results['Second Pass lines'] = [outl.split('\n').length, 1]; 32 | outl = ''; 33 | 34 | console.fail('foo', 'bar', new AssertionError({ 35 | message: 'foo', 36 | actual: 'foo', 37 | expected: 'foo', 38 | operator: 'foo' 39 | })); 40 | results['Fail content'] = [outl.length > 0]; 41 | results['Fail lines'] = [outl.split('\n').length, 6]; 42 | outl = ''; 43 | 44 | console.error('foo', 'bar', new Error('foo')); 45 | results['Error content'] = [outl.length > 0]; 46 | results['Error lines'] = [outl.split('\n').length > 4]; 47 | outl = ''; 48 | 49 | console.fail('foo', 'bar', new Error('foo')); 50 | results['Fail error content'] = [outl.length > 0]; 51 | results['Fail error lines'] = [outl.split('\n').length > 4]; 52 | outl = ''; 53 | 54 | console.fail('foo', 'bar', new AssertionError({ 55 | message: 'foo', 56 | operator: 'throws' 57 | })); 58 | results['Fail throws content'] = [outl.length > 0]; 59 | results['Fail throws lines'] = [outl.split('\n').length, 3]; 60 | outl = ''; 61 | 62 | console.end(); 63 | results['Summary content'] = [outl.length > 0]; 64 | results['Summary length'] = [outl.split('\n').length, 4]; 65 | outl = ''; 66 | 67 | results['No errors stdout'] = [errl.length, 0]; 68 | errl = ''; 69 | 70 | console = t({ a: true }); 71 | console.pass('foo', 'bar'); 72 | results['Show all Pass content'] = [outl.length > 0]; 73 | results['Show all Pass lines'] = [outl.split('\n').length, 2]; 74 | outl = ''; 75 | 76 | console.pass('foo', 'bar'); 77 | results['Show all second Pass content'] = [outl.length > 0]; 78 | results['Show all second Pass lines'] = [outl.split('\n').length, 2]; 79 | outl = ''; 80 | 81 | console.end(); 82 | 83 | results['Show all no errors stdout'] = [errl.length, 0]; 84 | errl = ''; 85 | 86 | process.stdout._writeOut = outorg; 87 | process.stderr._writeOut = errorg; 88 | 89 | oForEach(results, function (r, name) { 90 | if (r.length === 1) { 91 | a.ok(r[0], name); 92 | } else { 93 | a(r[0], r[1], name); 94 | } 95 | }); 96 | }; 97 | -------------------------------------------------------------------------------- /lib/run.js: -------------------------------------------------------------------------------- 1 | // Runs tests 2 | 3 | 'use strict'; 4 | 5 | var isError = require('es5-ext/error/is-error') 6 | , isFunction = require('es5-ext/function/is-function') 7 | , noop = require('es5-ext/function/noop') 8 | , toArray = require('es5-ext/object/to-array') 9 | , hforEach = require('es5-ext/object/for-each') 10 | , deferred = require('deferred') 11 | , createLogger = require('./logger') 12 | , createAssert = require('./assert') 13 | 14 | , nextTick = process.nextTick 15 | , pattern = /^\s*function\s*\(\s*([tad])(?:\s*,\s*([tad]))?\s*\)/ 16 | , run; 17 | 18 | run = function run(testee, tests, assert, logger) { 19 | if (isFunction(tests)) { 20 | tests = { "": tests }; 21 | } 22 | 23 | hforEach(tests, function (t) { 24 | var conf, match; 25 | if (isFunction(t)) { 26 | conf = t.conf = { t: true, a: true, d: false }; 27 | if (t.length > 2) { 28 | conf.d = true; 29 | } else if ((match = t.toString().match(pattern))) { 30 | conf.t = conf.a = false; 31 | conf[match[1]] = true; 32 | if (match[2]) { 33 | conf[match[2]] = true; 34 | } 35 | } 36 | } 37 | }); 38 | 39 | return deferred.reduce(toArray(tests), function (ignore, data) { 40 | var o, d, finish, done, name, f; 41 | name = data[0]; 42 | f = data[1]; 43 | d = deferred(); 44 | finish = function () { 45 | logger.out(true); 46 | d.resolve(); 47 | }; 48 | logger.in(name, true); 49 | if (isFunction(f)) { 50 | try { 51 | if (f.conf.d) { 52 | done = function (o) { 53 | if (o) { 54 | if (isError(o)) { 55 | assert.fail(o); 56 | finish(); 57 | } else { 58 | run(testee, o, assert, logger)(finish).done(); 59 | } 60 | } else { 61 | finish(); 62 | } 63 | }; 64 | if (f.conf.t) { 65 | if (f.conf.a) { 66 | f(testee, assert, done); 67 | } else { 68 | f(testee, done); 69 | } 70 | } else if (f.conf.a) { 71 | f(assert, done); 72 | } else { 73 | f(done); 74 | } 75 | } else { 76 | if (f.conf.t) { 77 | o = f(testee, assert); 78 | } else { 79 | o = f(assert); 80 | } 81 | if (o) { 82 | run(testee, o, assert, logger)(finish).done(); 83 | } else { 84 | finish(); 85 | } 86 | } 87 | } catch (e) { 88 | logger.error(e); 89 | finish(); 90 | } 91 | } else if (f == null) { 92 | assert.fail(new Error("No tests found at '" + name + "' property")); 93 | finish(); 94 | } else { 95 | run(testee, f, assert, logger)(finish).done(); 96 | } 97 | return d.promise; 98 | }, null)(noop); 99 | }; 100 | 101 | module.exports = function (testee, test, assert, logger) { 102 | var r; 103 | logger = logger || createLogger(); 104 | assert = assert || createAssert(logger); 105 | nextTick(function () { 106 | r = run(testee, test, assert, logger); 107 | if (logger.end) { 108 | r = r(logger.end.bind(logger)); 109 | } 110 | r.done(); 111 | }); 112 | return logger; 113 | }; 114 | -------------------------------------------------------------------------------- /test/lib/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var identity = require('es5-ext/function/identity') 4 | , createLogger = require('../../lib/logger') 5 | , createAssert = require('../../lib/assert'); 6 | 7 | module.exports = function (t, a, d) { 8 | var inProgress = false 9 | , logger = createLogger() 10 | , assert = createAssert(logger) 11 | , aa = a; 12 | 13 | t(identity, { 14 | Regular: function (x, y) { 15 | var o = {}; 16 | a.ok(!inProgress, "Regular: Progress"); 17 | a(x, identity, "Regular: Testee"); 18 | y(x(o), o, 'foo'); 19 | a.deep([logger[0].type, logger[0].data], ['pass', 'foo'], 20 | "Regular: Logger"); 21 | a.deep(logger.msg, ['Regular'], "Regular: Name"); 22 | }, 23 | Async: function (x, y, z) { 24 | var o = {}; 25 | a.ok(!inProgress, "Async: Progress"); 26 | inProgress = true; 27 | a(x, identity, "Async: Testee"); 28 | y(x(o), o, 'bar'); 29 | a.deep([logger[1].type, logger[1].data], ['pass', 'bar'], 30 | "Async: Logger"); 31 | a.deep(logger.msg, ['Async'], "Async: Name"); 32 | process.nextTick(function () { 33 | inProgress = false; 34 | z(); 35 | }); 36 | }, 37 | "Async nested": function (x, y, z) { 38 | a.ok(!inProgress, "Async nested: Progress"); 39 | inProgress = true; 40 | process.nextTick(function () { 41 | z({ 42 | "inner test": function (a) { 43 | aa.deep(logger.msg, ['Async nested', 'inner test'], 44 | "Async nested: Name"); 45 | aa(a, assert, "Assert by single arg"); 46 | inProgress = false; 47 | } 48 | }); 49 | }); 50 | }, 51 | "Sync nested": function () { 52 | a.ok(!inProgress, "Sync nested: Progress"); 53 | inProgress = true; 54 | a.deep(logger.msg, ['Sync nested'], "Sync nested: Name"); 55 | return { 56 | "inner other": function (t) { 57 | a.deep(logger.msg, ['Sync nested', 'inner other'], 58 | "Sync nested: inner: Name"); 59 | a(t, identity, "Testee by single arg"); 60 | inProgress = false; 61 | } 62 | }; 63 | }, 64 | Nested: { 65 | "in nested": function () { 66 | a.ok(!inProgress, "Nested: Progress"); 67 | inProgress = true; 68 | a.deep(logger.msg, ['Nested', 'in nested'], "Nested: Name"); 69 | inProgress = false; 70 | } 71 | }, 72 | "Check args": function (a, d) { 73 | aa.ok(!inProgress, "Args: Progress"); 74 | inProgress = true; 75 | aa(a, assert, "Assert as first arg when two args"); 76 | d(function (d) { 77 | var e, l = logger.length; 78 | aa.deep(logger.msg, ['Check args', ''], "Tests as function"); 79 | inProgress = false; 80 | d(e = new Error('Foo')); 81 | aa(logger[l].data, e, "Async error"); 82 | }); 83 | } 84 | }, assert, logger); 85 | 86 | logger.on('end', function () { 87 | var l, seen = false; 88 | l = t(identity, function (t, a) { 89 | a.ok(true, "Ok"); 90 | }); 91 | l.on('data', function () { 92 | seen = true; 93 | }); 94 | l.on('end', function () { 95 | a.ok(seen, "Tests are run in nextTick"); 96 | d(); 97 | }); 98 | }); 99 | 100 | }; 101 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var spread = require('es5-ext/function/#/spread') 4 | , deferred = require('deferred') 5 | , path = require('path') 6 | , commonPath = require('path2/common') 7 | , runInContext = require('vm').runInContext 8 | , out = require('./lib/console') 9 | , configure = require('./lib/configure') 10 | , load = require('./lib/load') 11 | , run = require('./lib/run') 12 | 13 | , resolve = path.resolve, map = Array.prototype.map 14 | , suite, isError; 15 | 16 | isError = function (e, context) { 17 | if (e instanceof Error) return true; 18 | if (context !== global) { 19 | return runInContext('(function () { return this instanceof Error; })', 20 | context).call(e); 21 | } 22 | return false; 23 | }; 24 | 25 | suite = { 26 | init: function (paths, options) { 27 | var conf, d, root; 28 | d = deferred(); 29 | paths = map.call(paths, function (path) { return resolve(path); }); 30 | this.resolve = d.resolve; 31 | this.console = out(options); 32 | this.tail = deferred(null); 33 | if (paths.length > 1) { 34 | root = commonPath.apply(null, paths); 35 | this.rindex = root ? (root.length + 1) : 0; 36 | } else if (paths.length) { 37 | this.rindex = paths[0].length + 1; 38 | } 39 | 40 | conf = configure(paths); 41 | conf('data', this.ondata.bind(this)); 42 | conf('end', this.onend.bind(this)); 43 | return d.promise; 44 | }, 45 | ondata: function () { 46 | this.tail = this.tail(spread.call(this.process).bind(this, arguments)); 47 | }, 48 | process: function (p, fpath, tpath, context) { 49 | var pname = p.slice(this.rindex), fname, logger, o, d; 50 | d = deferred(); 51 | this.console.break(); 52 | if (fpath instanceof Error) { 53 | // Wrong path 54 | this.console.error(pname, null, fpath); 55 | return d.resolve(); 56 | } 57 | 58 | fname = fpath.slice(this.rindex); 59 | if (tpath instanceof Error) { 60 | if (tpath.type === 'testfile') { 61 | // Input is a test file, ignore 62 | return d.resolve(); 63 | } 64 | // Could not assume test file path (not within package) 65 | // or there were problems with obtaining context 66 | this.console.error(pname, fname, tpath); 67 | return d.resolve(); 68 | } 69 | 70 | // Configured ok, load files 71 | o = load(fpath, tpath, context); 72 | 73 | // Any files missing, any evaluation errors ? 74 | if (o.testee === undefined) { 75 | // File not accessible 76 | this.console.error(pname, fname, "Couldn't load module '" + fpath + "'"); 77 | return d.resolve(); 78 | } 79 | 80 | if (isError(o.test, context)) { 81 | this.console.error(pname, fname, o.test); 82 | return d.resolve(); 83 | } 84 | if (isError(o.testee, context)) { 85 | this.console.error(pname, fname, o.testee); 86 | return d.resolve(); 87 | } 88 | if (!o.test) { 89 | this.console.error(pname, fname, "Tests could not be loaded, tried '" + 90 | tpath + "'"); 91 | return d.resolve(); 92 | } 93 | 94 | // Loaded ok, run tests 95 | logger = run(o.testee, o.test); 96 | logger.on('data', function (o) { 97 | var name = [fname].concat(o.msg); 98 | if (o.type === 'pass') { 99 | name.push(o.data); 100 | } else if ((o.type === 'fail') && o.data.operator) { 101 | name.push(o.data.message); 102 | } 103 | name = name.filter(Boolean).join(': '); 104 | this.console[o.type](fname, name, o.data); 105 | }.bind(this)); 106 | logger.on('end', function () { 107 | d.resolve(); 108 | }); 109 | 110 | return d.promise; 111 | }, 112 | onend: function () { 113 | this.tail(this.end.bind(this)).done(); 114 | delete this.tail; 115 | }, 116 | end: function () { 117 | this.console.end(); 118 | this.resolve(this); 119 | } 120 | }; 121 | 122 | module.exports = function (paths, options) { 123 | return Object.create(suite).init(paths, options); 124 | }; 125 | -------------------------------------------------------------------------------- /lib/console.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var call = Function.prototype.call 4 | , partial = require('es5-ext/function/#/partial') 5 | , last = require('es5-ext/array/#/last') 6 | , indent = call.bind(partial.call(require('es5-ext/string/#/indent'), 7 | ' ', 17)) 8 | , format = call.bind(partial 9 | .call(require('es5-ext/date/#/format'), '%H:%M:%S.%L ')) 10 | , dpad = partial.call(require('es5-ext/string/#/pad'), ' ', 12) 11 | , duration = require('duration') 12 | , inspect = require('util').inspect 13 | , clc = require('cli-color') 14 | , ctrim = require('cli-color/strip') 15 | , cthrobber = require('cli-color/throbber') 16 | 17 | , write = process.stdout.write.bind(process.stdout) 18 | , lerror = clc.magenta, lfail = clc.red, lpass = clc.green 19 | , lsummary = clc.cyan 20 | , o; 21 | 22 | o = { 23 | init: function (options) { 24 | this.mode = options.a ? 2 : (options.m ? 0 : 1); 25 | this.passed = 0; 26 | this.failed = 0; 27 | this.errored = 0; 28 | this.started = new Date(); 29 | this.writeLog = []; 30 | this.write = function (msg) { 31 | this.writeLog.push(arguments); 32 | return write.apply(this, arguments); 33 | }; 34 | this.progress = cthrobber(write, 200); 35 | return this; 36 | }, 37 | atNewLine: function () { 38 | return !this.writeLog.length || 39 | last.call(ctrim(last.call(this.writeLog)[0])) === '\n'; 40 | }, 41 | break: function () { 42 | this.progress.restart(); 43 | if (!this.atNewLine() && this.mode) { 44 | this.write('\n'); 45 | } 46 | }, 47 | pass: function (path, name) { 48 | ++this.passed; 49 | this.progress.restart(); 50 | if (this.mode === 2) { 51 | this.write(lpass(format(new Date()) + ' ✓ ' + name + '\n')); 52 | } else { 53 | this.write(lpass(this.atNewLine() ? (format(new Date()) + ' ✓ ' + 54 | ((path && this.mode) ? path + ' ' : '') + '.') : '.')); 55 | } 56 | }, 57 | fail: function (path, name, e) { 58 | var s; 59 | ++this.failed; 60 | this.progress.restart(); 61 | if (!this.atNewLine()) { 62 | this.write('\n'); 63 | } 64 | s = ''; 65 | if (e.operator) { 66 | if (e.hasOwnProperty('expected')) { 67 | s += 'Expected: ' + inspect(e.expected, false, 1) + '\n'; 68 | } 69 | if (e.hasOwnProperty('actual')) { 70 | s += 'Actual: ' + inspect(e.actual, false, 1) + '\n'; 71 | } 72 | s += 'Operator: ' + e.operator + '\n'; 73 | } else { 74 | s += (e.stack || e) + '\n'; 75 | } 76 | this.write(lfail(format(new Date()) + ' ✗ ' + name + '\n' + indent(s))); 77 | }, 78 | error: function (path, name, e) { 79 | var s, eStr, index; 80 | ++this.errored; 81 | this.progress.restart(); 82 | if (!this.atNewLine()) { 83 | this.write('\n'); 84 | } 85 | name = name || path; 86 | s = format(new Date()) + ' - '; 87 | eStr = String(e.stack || e); 88 | if (name) { 89 | s += name + '\n' + indent(eStr); 90 | } else { 91 | index = eStr.indexOf('\n') + 1; 92 | s += index ? (eStr.slice(0, index) + indent(eStr.slice(index))) : eStr; 93 | } 94 | this.write(lerror(s + '\n')); 95 | }, 96 | end: function () { 97 | var s, all = this.passed + this.failed + this.errored; 98 | this.progress.stop(); 99 | if (!this.atNewLine()) { 100 | this.write('\n'); 101 | } 102 | this.write('\n'); 103 | if (!all) { 104 | this.write(lsummary('No tests run\n\n')); 105 | } else { 106 | this.write(lsummary(dpad.call(duration(this.started, 107 | new Date()).toString()) + ' ')); 108 | s = []; 109 | s.push(clc.green(this.passed + ' Ok [' + 110 | ((this.passed / all) * 100).toFixed(2) + '%]')); 111 | if (this.failed) { 112 | s.push(clc.red(this.failed + ' Failed')); 113 | } 114 | if (this.errored) { 115 | s.push(clc.magenta(this.errored + ' Errors')); 116 | } 117 | this.write(s.join(' ') + '\n\n'); 118 | } 119 | } 120 | }; 121 | 122 | module.exports = function (options) { 123 | return Object.create(o).init(options || {}); 124 | }; 125 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # TAD - JavaScript test suite 2 | 3 | Goal of this framework is to allow writing tests with minimal hassle. 4 | TAD will locate your test file, and provide tested module for your test functions. 5 | 6 | Example console output: 7 | 8 | 9 | 10 | * [Installation](#installation) 11 | * [Usage](#usage) 12 | * [File managment](#usage-file-management) 13 | * [Test files](#usage-test-files) 14 | * [Test functions](#usage-test-functions) 15 | * [Assertions](#usage-assertions) 16 | * [Running tests](#usage-running-tests) 17 | * [TODO](#todo) 18 | 19 | 20 | 21 | ## Installation 22 | 23 | $ npm install tad 24 | 25 | 26 | 27 | ## Usage 28 | 29 | 30 | 31 | ### File management 32 | 33 | Keep your tests in _test_ folder. For each file in in main folder have corresponding test file in 34 | _test_ folder. 35 | 36 | 37 | 38 | ### Test files 39 | 40 | Tests should be written as set of functions, it can be just one function: 41 | ```js 42 | module.exports = function (t, a, d) { 43 | // tests 44 | }; 45 | ``` 46 | or many thematically grouped functions: 47 | ```js 48 | exports["Test this"] = function (t, a, d) { 49 | // tests 50 | }; 51 | exports["Test that"] = function (t, a, d) { 52 | // tests 53 | }; 54 | ``` 55 | 56 | 57 | ### Test functions 58 | 59 | Arguments passed to test functions are: 60 | 61 | * __t__ - Tested module 62 | * __a__ - Assert object 63 | * __d__ - _Done_ function, it's for tests that need to be run asynchronously. 64 | You may pass additional block of tests to this 65 | function and they'll be run right after. _d_ argument makes no sense for 66 | synchrounous tests, declare such tests without it. 67 | 68 | All arguments are optional, and by the way function is declared suite detect 69 | which arguments should be passed to test function. Examples: 70 | 71 | * Asynchronous test: 72 | ```js 73 | exports["Some tests"] = funtcion (t, a, d) { 74 | // tests 75 | setTimeout(function () { 76 | // tests 77 | d(); 78 | }, 100); 79 | }; 80 | ``` 81 | * Synchronous test: 82 | ```js 83 | exports["Some tests"] = function (t, a) { 84 | // tests 85 | }; 86 | ``` 87 | Tests can be nested, and declared various ways (synchronous/asynchronous) 88 | ```js 89 | module.exports["Test all"] = function (t, a) { 90 | // Preparation code 91 | 92 | // ... tests ... 93 | 94 | return { 95 | "Test this": function () { 96 | // We already have module and assert object 97 | // ... tests ... 98 | }, 99 | "Test that async way": function (d) { 100 | // This one is asynchronous 101 | // ... tests .... 102 | 103 | seTimeout(function () { 104 | // ... tests ... 105 | d({ 106 | "Some extra tests": function () { 107 | // ... tests ... 108 | } 109 | }) 110 | }, 100); 111 | } 112 | }; 113 | }; 114 | ``` 115 | 116 | 117 | ### Assertions 118 | 119 | TAD uses assert object from [UncommonJS tests runner](https://github.com/Gozala/test-commonjs/), 120 | It's API is nearly same as of _assert_ that can be found in Node. Full spec is available at 121 | https://github.com/kriskowal/uncommonjs/blob/master/tests/specification.md . 122 | 123 | TAD adds some extra sugar to UncommonJS Assert object: 124 | 125 | * `a === a.strictEqual`, so you can write your assertions as: 126 | ```js 127 | a(shouldBeTrue, true, "It's true"); 128 | // it has same effect as: 129 | a.strictEqual(shouldBeTrue, true, "It's true"); 130 | ``` 131 | * `a.not` is an alias for `a.notStrictEqual` 132 | * `a.deep` is an alias for `a.deepEqual` 133 | * `a.notDeep` is an alias for `a.notDeepEqual` 134 | * `assert.never` with that you can check function paths that should never be called. 135 | 136 | 137 | 138 | ### Running tests 139 | 140 | Test your file with provided binary: 141 | 142 | $ bin/tad lib/test-file 143 | 144 | or test all files in path: 145 | 146 | $ bin/tad lib 147 | 148 | 149 | 150 | ## TODO 151 | 152 | * Full custom context support 153 | * Code coverage 154 | * TAP support 155 | * jslint, jshint as side validation option 156 | * Port tests to browsers -------------------------------------------------------------------------------- /CHANGES: -------------------------------------------------------------------------------- 1 | v0.2.7 -- 2016.10.19 2 | * Do not crash in case there are no files to test 3 | 4 | v0.2.6 -- 2016.09.01 5 | * Ensure to not test files that are ignored by .gitignore rules 6 | 7 | v0.2.5 -- 2016.08.30 8 | * Fix process exit handling. Process didn't end gracefully when test reported errors. 9 | It may made some following exceptions hidden 10 | 11 | v0.2.4 -- 2015.10.14 12 | * Fix automatic lines resolution (in case no message is provided) 13 | 14 | v0.2.3 -- 2015.06.08 15 | * Ignore by default 'examples' folder 16 | * Update up to changes in cli-color 17 | 18 | v0.2.2 -- 2015.03.14 19 | * Fix index resolution, so it's not affected by Symbol polyfill workaround 20 | 21 | v0.2.1 -- 2015.01.22 22 | * Make assertion messages optional. If message is not provided line and 23 | column number of assertion is provided instead 24 | * Fix issue in assert headings resolution 25 | * Configure lint scripts 26 | * Fix LICENSE spelling 27 | 28 | v0.2.0 -- 2014.04.27 29 | * Move lib/suite.js so it's index.js module 30 | * Remove special handling for `lib` module 31 | * Cleanup organization of modules in lib folder 32 | * Update internals to use latest versions of dependencies 33 | * Remove Makefile (it's environment agnostic project) 34 | 35 | v0.1.21 -- 2014.02.18 36 | * Support NaN comparision in assert.strictEqual 37 | * Support CONSTANT_NAME convention for index validation 38 | 39 | v0.1.20 -- 2013.10.25 40 | * `h1`, `h2`, `h3`, `h4`, `h5`, `h6` methods on assert, which allow inline 41 | customization of message prefixes 42 | 43 | v0.1.19 -- 2013.09.02 44 | * Workaround for [test package issue](https://github.com/Gozala/test-commonjs/pull/8) 45 | of no support for Object.create(null) objects 46 | 47 | v0.1.18 -- 2013.08.28 48 | * Fix optional context handling in indexTest 49 | * Better error reporting in case of not compliant text configurations 50 | 51 | v0.1.17 -- 2013.08.08 52 | * Fix context in smart index tests 53 | * Ignore rules handling (provided via .testignore files) 54 | * Ignore specific (test, node_modules etc.) folders in index resolution 55 | * Fix leading path resolution (minor) 56 | * Internal logic improvements 57 | * Lint cleanup 58 | 59 | v0.1.16 -- 2013.05.15 60 | * Smart resolution of testable modules if TAD run on main package folder 61 | 62 | v0.1.15 -- 2013.03.14 63 | * Fix path resolution (bug exposed with Node v0.10) 64 | 65 | v0.1.14 -- 2013.03.11 66 | * Support error.code in assert.throws 67 | * Add missing licence file 68 | * Fix error stringification for console output 69 | 70 | v0.1.13 -- 2013.01.10 71 | * Ignore test folder if tad run on main package folder 72 | 73 | v0.1.12 -- 2012.10.11 74 | * Support modules that export `null` 75 | 76 | v0.1.11 -- 2012.10.04 77 | * Maintenance: 78 | * Update to latest versions of dependencies 79 | * Convention and lint cleanup 80 | * Print long stack traces on error 81 | * When testing index content do not take into account directories 82 | staring with '_' 83 | 84 | v0.1.10 -- 2012.08.06 85 | * Removed descriptor usage from logger, it caused error on Node v0.6 86 | in v0.2 branch of event-emitter package (V8 bug) 87 | 88 | v0.1.9 -- 2012.06.13 89 | * Depend on v0.5 release of deferred 90 | 91 | v0.1.8 -- 2012.05.28 92 | * Do not allow install on pre v0.6.6 Node.js version 93 | * Configure binary as binary in package.json 94 | 95 | v0.1.7 -- 2012.05.28 96 | * Fix name (from path) resolution 97 | * npm friendly package.json 98 | * Exit process at actual process exit (before we forced exit of process right 99 | after tests were completed, that forced exit not finished background 100 | processes) 101 | * Update es5-ext to latest (v0.8) version 102 | 103 | v0.1.6 -- 2012.03.22 104 | Fixes: 105 | * Correct input paths handling (should be bulletproof on both *nix and windows) 106 | 107 | Improvements: 108 | * JSLint code validation 109 | 110 | v0.1.5 -- 2012.01.22 111 | * Better diff on deep asserts error notifications 112 | * Update dependencies to newest version 113 | * Travis CI configuration 114 | 115 | v0.1.4 -- 2012.01.05 116 | * Index test now accepts ignores list 117 | 118 | v0.1.3 -- 2011.12.22 119 | * Windows support (small fix: use process.cwd() instead of process.env.PWD) 120 | 121 | v0.1.2 -- 2011.12.22 122 | * Proper exit codes 123 | * When checking index consistency ignore filenames prefixed with '_' 124 | 125 | v0.1.1 -- 2011.12.22 126 | * Custom scopes are now searched up tree 127 | 128 | v0.1.0 -- 2011.08.08 129 | * Initial version 130 | -------------------------------------------------------------------------------- /lib/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var isFunction = require('es5-ext/function/is-function') 4 | , assign = require('es5-ext/object/assign') 5 | , eq = require('es5-ext/object/eq') 6 | , map = require('es5-ext/object/map') 7 | , isRegExp = require('es5-ext/reg-exp/is-reg-exp') 8 | , Assert = require('test/assert').Assert 9 | 10 | , never, neverBind, throws, resolveMessage, wrapAssert 11 | , lineRe = /(\d+:\d+)\)?$/ 12 | , isErrorCode = RegExp.prototype.test.bind(/^[A-Z_]+$/); 13 | 14 | require('./fix-test-utils'); 15 | 16 | never = function (message) { 17 | message = resolveMessage(message); 18 | this.fail({ 19 | message: message, 20 | operator: "never" 21 | }); 22 | }; 23 | 24 | neverBind = function (message) { return never.bind(this, message); }; 25 | 26 | resolveMessage = function (message) { 27 | var stack, line, match; 28 | if (message != null) return message; 29 | stack = (new Error()).stack; 30 | if (!stack) return ''; 31 | line = stack.split('\n')[3]; 32 | if (!line) return ''; 33 | match = line.match(lineRe); 34 | if (!match) return ''; 35 | return '@' + match[1]; 36 | }; 37 | 38 | throws = function (block, err, message) { 39 | var threw = false, exception = null, failure; 40 | 41 | // If third argument is not provided and second argument is a string it 42 | // means that optional `Error` argument was not passed, so we shift 43 | // arguments. 44 | if (message === undefined) { 45 | if (!isFunction(err) && !isErrorCode(err)) { 46 | message = err; 47 | err = null; 48 | } 49 | } 50 | 51 | message = resolveMessage(message); 52 | 53 | // Executing given `block`. 54 | try { 55 | block(); 56 | } catch (e) { 57 | threw = true; 58 | exception = e; 59 | } 60 | 61 | // If exception was thrown and `Error` argument was not passed assert is 62 | // passed. 63 | if (threw && ((err == null) || 64 | // If Error is thrown exception 65 | (err === exception) || 66 | // If passed `Error` is RegExp using it's test method to 67 | // assert thrown exception message. 68 | (isRegExp(err) && err.test(exception.message)) || 69 | // If passed `Error` is a constructor function testing if 70 | // thrown exception is an instance of it. 71 | (isFunction(err) && (exception instanceof err)) || 72 | (err === exception.code))) { 73 | this.pass(message); 74 | 75 | // Otherwise we report assertion failure. 76 | } else { 77 | failure = { 78 | message: message, 79 | operator: "throws" 80 | }; 81 | 82 | if (exception) failure.actual = exception; 83 | if (err) failure.expected = err; 84 | 85 | this.fail(failure); 86 | } 87 | }; 88 | 89 | wrapAssert = function (assert, context) { 90 | return function (actual, expected, message) { 91 | return assert.call(context, actual, expected, resolveMessage(message)); 92 | }; 93 | }; 94 | 95 | module.exports = function (logger) { 96 | var assert, getHeading; 97 | 98 | assert = new Assert({ 99 | pass: logger.pass.bind(logger), 100 | fail: logger.fail.bind(logger), 101 | error: logger.error.bind(logger) 102 | }); 103 | 104 | assert = assign(function (actual, expected, message) { 105 | message = resolveMessage(message); 106 | if (eq(actual, expected)) { 107 | this.pass(message); 108 | return; 109 | } 110 | this.fail({ 111 | actual: actual, 112 | expected: expected, 113 | message: message, 114 | operator: "===" 115 | }); 116 | }.bind(assert), map(Assert.prototype, 117 | function (method) { return method.bind(assert); })); 118 | 119 | assert.strictEqual = assert; 120 | assert.not = assert.notStrictEqual = function (actual, expected, message) { 121 | message = resolveMessage(message); 122 | if (!eq(actual, expected)) { 123 | this.pass(message); 124 | return; 125 | } 126 | this.fail({ 127 | actual: actual, 128 | expected: expected, 129 | message: message, 130 | operator: "!==" 131 | }); 132 | }; 133 | assert.deep = assert.deepEqual = wrapAssert(assert.deepEqual, assert); 134 | assert.notDeep = assert.notDeepEqual = wrapAssert(assert.notDeepEqual, assert); 135 | assert.never = never.bind(assert); 136 | assert.never.bind = neverBind.bind(assert); 137 | assert.throws = throws.bind(assert); 138 | 139 | getHeading = function (level) { 140 | return function (msg) { 141 | var index = level - 1 + logger.closure; 142 | if (!logger.msg.hasOwnProperty(index)) logger.msg[index] = undefined; 143 | logger.msg.splice(index, Infinity, msg); 144 | }; 145 | }; 146 | 147 | assert.h1 = getHeading(1); 148 | assert.h2 = getHeading(2); 149 | assert.h3 = getHeading(3); 150 | assert.h4 = getHeading(4); 151 | assert.h5 = getHeading(5); 152 | assert.h6 = getHeading(6); 153 | 154 | return assert; 155 | }; 156 | --------------------------------------------------------------------------------