├── test ├── fixture │ ├── with-dependencies │ │ ├── dep-1.js │ │ ├── dep-2.js │ │ ├── dep-3.custom │ │ ├── require-custom.js │ │ ├── no-tests.js │ │ ├── test.js │ │ ├── test-failure.js │ │ └── test-uncaught-exception.js │ ├── immediate-0-exit.js │ ├── install-global.js │ ├── no-tests.js │ ├── babel-hook-imported.js │ ├── babel-hook.js │ ├── node-paths │ │ ├── modules │ │ │ └── nested │ │ │ │ └── foo.js │ │ └── deep │ │ │ └── nested │ │ │ └── path │ │ │ └── bar.js │ ├── pkg-conf │ │ ├── precedence │ │ │ ├── default-required.js │ │ │ ├── required.js │ │ │ ├── a.js │ │ │ ├── b.js │ │ │ ├── package.json │ │ │ └── c.js │ │ ├── pkg-overrides │ │ │ ├── required.js │ │ │ ├── test.js │ │ │ ├── package.json │ │ │ └── actual.js │ │ └── defaults │ │ │ ├── package.json │ │ │ └── test.js │ ├── babelrc │ │ ├── .babelrc │ │ ├── test.js │ │ └── package.json │ ├── es2015.js │ ├── caching │ │ ├── test.js │ │ └── package.json │ ├── syntax-error.js │ ├── exclusive.js │ ├── skip-only.js │ ├── validate-installed-global.js │ ├── watcher │ │ └── test.js │ ├── ignored-dirs │ │ ├── fixtures │ │ │ └── test.js │ │ └── helpers │ │ │ └── test.js │ ├── subdir │ │ ├── in-a-subdir.js │ │ ├── nested │ │ │ └── nested.js │ │ └── failing-subdir.js │ ├── process-cwd.js │ ├── error-without-message.js │ ├── exclusive-nonexclusive.js │ ├── fake-timers.js │ ├── throw-anonymous-function.js │ ├── hooks-failing.js │ ├── uncaught-exception.js │ ├── circular-reference-on-assertion.js │ ├── hooks-passing.js │ ├── node-paths.js │ ├── generators.js │ ├── one-pass-one-fail.js │ ├── throw-named-function.js │ ├── match-no-match.js │ ├── fail-fast.js │ ├── loud-rejection.js │ ├── power-assert.js │ ├── empty.js │ ├── test-count.js │ ├── matcher-skip.js │ ├── observable.js │ ├── async-await.js │ ├── serial.js │ ├── destructuring-public-api.js │ ├── source-with-source-map-pragma.js │ ├── source-with-source-map-pragma.map │ ├── source-map-file.js │ ├── source-map-inline.js │ ├── source-map-initial.js.map │ ├── source-map-initial.js │ ├── long-running.js │ ├── _generate_source-map-initial.js │ └── babel-plugin-test-doubler.js ├── ava-error.js ├── visual │ ├── console-log.js │ ├── lorem-ipsum.js │ ├── stdout-write.js │ ├── print-lorem-ipsum.js │ ├── text-ends-at-terminal-width.js │ ├── lorem-ipsum.txt │ └── run-visual-tests.js ├── fork.js ├── logger.js ├── caching-precompiler.js ├── observable.js ├── reporters │ ├── tap.js │ └── mini.js ├── cli.js ├── assert.js ├── hooks.js ├── test-collection.js └── promise.js ├── .gitattributes ├── .gitignore ├── media ├── logo.ai ├── logo.png ├── header.png ├── header.psd ├── tap-output.png ├── stack-traces.png ├── pronunciation.m4a └── logo.svg ├── screenshot.png ├── screenshot-mini-reporter.gif ├── bench ├── concurrent │ ├── sync.js │ ├── async-immediate.js │ ├── async-timeout.js │ └── alternating-sync-async.js ├── serial │ ├── sync.js │ ├── async-timeout.js │ ├── async-immediate.js │ └── alternating-sync-async.js ├── other │ └── failures.js ├── compare.js └── run.js ├── .iron-node.js ├── .mention-bot ├── lib ├── colors.js ├── send.js ├── globals.js ├── ava-error.js ├── hook.js ├── beautify-stack.js ├── enhance-assert.js ├── logger.js ├── concurrent.js ├── sequence.js ├── reporters │ ├── tap.js │ ├── verbose.js │ └── mini.js ├── runner.js ├── assert.js ├── caching-precompiler.js ├── fork.js ├── test-collection.js └── test-worker.js ├── .editorconfig ├── .travis.yml ├── .github ├── pull_request_template.md └── issue_template.md ├── appveyor.yml ├── license ├── docs └── recipes │ ├── typescript.md │ ├── endpoint-testing.md │ ├── browser-testing.md │ ├── watch-mode.md │ ├── when-to-use-plan.md │ └── code-coverage.md ├── index.js ├── code-of-conduct.md ├── profile.js ├── contributing.md ├── maintaining.md ├── cli.js ├── package.json └── index.d.ts /test/fixture/with-dependencies/dep-1.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/dep-2.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/dep-3.custom: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/fixture/immediate-0-exit.js: -------------------------------------------------------------------------------- 1 | process.exit(0); 2 | -------------------------------------------------------------------------------- /test/fixture/install-global.js: -------------------------------------------------------------------------------- 1 | global.foo = 'bar'; 2 | -------------------------------------------------------------------------------- /test/fixture/no-tests.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.ai binary 3 | *.psd binary 4 | -------------------------------------------------------------------------------- /test/fixture/babel-hook-imported.js: -------------------------------------------------------------------------------- 1 | module.export = async () => {}; 2 | -------------------------------------------------------------------------------- /test/fixture/babel-hook.js: -------------------------------------------------------------------------------- 1 | import x from './babel-hook-imported'; 2 | -------------------------------------------------------------------------------- /test/fixture/node-paths/modules/nested/foo.js: -------------------------------------------------------------------------------- 1 | module.exports = 'bar'; 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .nyc_output 3 | coverage 4 | bench/.results 5 | -------------------------------------------------------------------------------- /test/fixture/node-paths/deep/nested/path/bar.js: -------------------------------------------------------------------------------- 1 | module.exports = 'baz'; 2 | -------------------------------------------------------------------------------- /media/logo.ai: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/logo.ai -------------------------------------------------------------------------------- /media/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/logo.png -------------------------------------------------------------------------------- /screenshot.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/screenshot.png -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/default-required.js: -------------------------------------------------------------------------------- 1 | module.exports = "bar"; 2 | -------------------------------------------------------------------------------- /media/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/header.png -------------------------------------------------------------------------------- /media/header.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/header.psd -------------------------------------------------------------------------------- /media/tap-output.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/tap-output.png -------------------------------------------------------------------------------- /test/fixture/babelrc/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "plugins": ["this-plugin-does-not-exist"] 3 | } 4 | -------------------------------------------------------------------------------- /test/fixture/babelrc/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../' 2 | 3 | test(t => t.pass()); 4 | -------------------------------------------------------------------------------- /media/stack-traces.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/stack-traces.png -------------------------------------------------------------------------------- /test/fixture/es2015.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test(t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/pkg-overrides/required.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'foo'; 4 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/required.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = 'foo'; 4 | -------------------------------------------------------------------------------- /media/pronunciation.m4a: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/media/pronunciation.m4a -------------------------------------------------------------------------------- /test/fixture/caching/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../' 2 | 3 | test(t => t.true(2 + 2 === 4)); 4 | -------------------------------------------------------------------------------- /test/fixture/syntax-error.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | test.(t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/babelrc/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/caching/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/exclusive.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.only(t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/skip-only.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.skip(t => { 4 | t.fail(); 5 | }); 6 | -------------------------------------------------------------------------------- /screenshot-mini-reporter.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/no-problemo/ava/master/screenshot-mini-reporter.gif -------------------------------------------------------------------------------- /test/fixture/with-dependencies/require-custom.js: -------------------------------------------------------------------------------- 1 | require.extensions['.custom'] = require.extensions['.js'] 2 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/defaults/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1" 4 | } 5 | -------------------------------------------------------------------------------- /test/fixture/validate-installed-global.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test(t => t.is(global.foo, 'bar')); 4 | -------------------------------------------------------------------------------- /test/fixture/watcher/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | test('works', t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/no-tests.js: -------------------------------------------------------------------------------- 1 | import './dep-1'; 2 | import './dep-2'; 3 | import './dep-3.custom'; 4 | -------------------------------------------------------------------------------- /test/fixture/ignored-dirs/fixtures/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | test(t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/ignored-dirs/helpers/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | test(t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/subdir/in-a-subdir.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | test('subdir', t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/process-cwd.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test(t => { 4 | t.is(process.cwd(), __dirname); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/subdir/nested/nested.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | test('subdir', t => { 4 | t.pass(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/subdir/failing-subdir.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | test('subdir fail', t => { 4 | t.fail(); 5 | }); 6 | -------------------------------------------------------------------------------- /bench/concurrent/sync.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | test('test' + i, () => {}); 5 | } 6 | -------------------------------------------------------------------------------- /bench/serial/sync.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | test.serial('test' + i, () => {}); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/error-without-message.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('throw an error without a message', () => { 4 | throw new Error(); 5 | }); 6 | -------------------------------------------------------------------------------- /test/fixture/exclusive-nonexclusive.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.only(t => { 4 | t.pass(); 5 | }); 6 | 7 | test(t => { 8 | t.fail(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/fake-timers.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import sinon from 'sinon'; 3 | 4 | test(t => { 5 | sinon.useFakeTimers(Date.now() + 10000); 6 | t.pass(); 7 | }); 8 | -------------------------------------------------------------------------------- /bench/concurrent/async-immediate.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | test('test' + i, () => new Promise(resolve => setImmediate(resolve))); 5 | } 6 | -------------------------------------------------------------------------------- /bench/concurrent/async-timeout.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | test('test' + i, () => new Promise(resolve => setTimeout(resolve, 0))); 5 | } 6 | -------------------------------------------------------------------------------- /bench/other/failures.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 1000; i++) { 4 | test.serial('test' + i, t => { 5 | t.is(Math.random(), Math.random()); 6 | }); 7 | } 8 | -------------------------------------------------------------------------------- /bench/serial/async-timeout.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 3000; i++) { 4 | test.serial('test' + i, () => new Promise(resolve => setTimeout(resolve, 0))); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/throw-anonymous-function.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('throw an uncaught exception', () => { 4 | setImmediate(() => { 5 | throw () => {}; 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /bench/serial/async-immediate.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | test.serial('test' + i, () => new Promise(resolve => setImmediate(resolve))); 5 | } 6 | -------------------------------------------------------------------------------- /test/fixture/hooks-failing.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.beforeEach(fail); 4 | test(pass); 5 | 6 | function pass() {} 7 | 8 | function fail(t) { 9 | t.fail(); 10 | } 11 | -------------------------------------------------------------------------------- /test/fixture/uncaught-exception.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('throw an uncaught exception', () => { 4 | setImmediate(() => { 5 | throw new Error(`Can't catch me!`); 6 | }); 7 | }); 8 | -------------------------------------------------------------------------------- /.iron-node.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | app: { 3 | openDevToolsDetached: true, 4 | hideMainWindow: true 5 | }, 6 | workSpaceDirectory: function () { 7 | return __dirname; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /.mention-bot: -------------------------------------------------------------------------------- 1 | { 2 | "userBlacklist": [ 3 | "sindresorhus", 4 | "kevva", 5 | "vdemedes", 6 | "jamestalmage", 7 | "novemberborn" 8 | ], 9 | "fileBlacklist": [ 10 | "*.md" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/a.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | // this should never be loaded - package.json overrides files to call `actual.js` 4 | 5 | test(t => { 6 | t.fail(); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/b.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | // this should never be loaded - package.json overrides files to call `actual.js` 4 | 5 | test(t => { 6 | t.fail(); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixture/circular-reference-on-assertion.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test(t => { 4 | const circular = ['a', 'b']; 5 | circular.push(circular); 6 | t.same([circular, 'c'], [circular, 'd']); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixture/hooks-passing.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.before(pass); 4 | test.beforeEach(pass); 5 | test.after(pass); 6 | test.afterEach(pass); 7 | test(pass); 8 | 9 | function pass() {} 10 | -------------------------------------------------------------------------------- /test/fixture/node-paths.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import foo from 'nested/foo'; 3 | import bar from 'path/bar'; 4 | 5 | test('relative require', t => { 6 | t.is(foo, 'bar'); 7 | t.is(bar, 'baz'); 8 | }); 9 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/pkg-overrides/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | 3 | // this should never be loaded - package.json overrides files to call `actual.js` 4 | 5 | test(t => { 6 | t.fail(); 7 | }); 8 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | import './dep-1'; 4 | import './dep-2'; 5 | import './dep-3.custom'; 6 | 7 | test('hey ho', t => { 8 | t.pass(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/generators.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('generator function', function * (t) { 4 | t.plan(1); 5 | 6 | const value = yield Promise.resolve(1); 7 | 8 | t.is(value, 1); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/one-pass-one-fail.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('this is a passing test', t => { 4 | t.ok(true); 5 | }); 6 | 7 | test('this is a failing test', t => { 8 | t.ok(false); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/throw-named-function.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | function fooFn() {} 4 | 5 | test('throw an uncaught exception', () => { 6 | setImmediate(() => { 7 | throw fooFn; 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/test-failure.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | import './dep-1'; 4 | import './dep-2'; 5 | import './dep-3.custom'; 6 | 7 | test('hey ho', t => { 8 | t.fail(); 9 | }); 10 | -------------------------------------------------------------------------------- /test/fixture/match-no-match.js: -------------------------------------------------------------------------------- 1 | import test from 'ava'; 2 | 3 | test('foo', t => { 4 | t.pass(); 5 | }); 6 | 7 | test.only('bar', t => { 8 | t.pass(); 9 | }); 10 | 11 | test.only(t => { 12 | t.pass(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/fixture/fail-fast.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('first pass', t => { 4 | t.pass(); 5 | }); 6 | 7 | test('second fail', t => { 8 | t.fail(); 9 | }); 10 | 11 | test('third pass', t => { 12 | t.pass(); 13 | }); 14 | -------------------------------------------------------------------------------- /test/fixture/loud-rejection.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.cb('creates an unhandled rejection', t => { 4 | Promise.reject(new Error(`You can't handle this!`)); 5 | 6 | setTimeout(() => { 7 | t.end(); 8 | }); 9 | }); 10 | -------------------------------------------------------------------------------- /lib/colors.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('chalk'); 3 | 4 | module.exports = { 5 | error: chalk.red, 6 | skip: chalk.yellow, 7 | todo: chalk.blue, 8 | pass: chalk.green, 9 | duration: chalk.gray.dim, 10 | stack: chalk.red 11 | }; 12 | -------------------------------------------------------------------------------- /test/fixture/power-assert.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.serial(t => { 4 | const a = 'foo'; 5 | 6 | t.ok(a === 'bar'); 7 | }); 8 | 9 | test.serial(t => { 10 | const a = 'bar'; 11 | 12 | t.ok(a === 'foo', 'with message'); 13 | }); 14 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = tab 5 | end_of_line = lf 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true 9 | 10 | [{package.json,*.yml}] 11 | indent_style = space 12 | indent_size = 2 13 | -------------------------------------------------------------------------------- /test/fixture/empty.js: -------------------------------------------------------------------------------- 1 | /* 2 | __ 3 | ____ _____ _______/ |_ ___.__. 4 | _/ __ \ / \\____ \ __< | | 5 | \ ___/| Y Y \ |_> > | \___ | 6 | \___ >__|_| / __/|__| / ____| 7 | \/ \/|__| \/ 8 | */ 9 | -------------------------------------------------------------------------------- /bench/concurrent/alternating-sync-async.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | if (i % 2) { 5 | test('test' + i, () => new Promise(resolve => setImmediate(resolve))); 6 | } else { 7 | test('test' + i, () => {}); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /test/fixture/test-count.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('passCount', t => { 4 | t.pass(); 5 | }); 6 | 7 | test('failCount', t => { 8 | t.fail(); 9 | }); 10 | 11 | test.skip('skipCount', t => { 12 | t.pass(); 13 | }); 14 | 15 | test.todo('todoCount'); 16 | -------------------------------------------------------------------------------- /bench/serial/alternating-sync-async.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | for (var i = 0; i < 10000; i++) { 4 | if (i % 2) { 5 | test.serial('test' + i, () => new Promise(resolve => setImmediate(resolve))); 6 | } else { 7 | test.serial('test' + i, () => {}); 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - '5' 4 | - '4' 5 | - '0.12' 6 | - '0.10' 7 | before_install: 8 | - 'npm install -g npm@latest' 9 | after_success: 10 | - '[ -z "$COVERALLS_REPO_TOKEN" ] && tap --coverage-report=text-lcov | ./node_modules/.bin/coveralls' 11 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/defaults/test.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../' 2 | 3 | const opts = JSON.parse(process.argv[2]); 4 | 5 | test(t => { 6 | t.is(opts.failFast, false); 7 | t.is(opts.serial, false); 8 | t.is(opts.cacheEnabled, true); 9 | t.same(opts.require, []); 10 | }); 11 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/pkg-overrides/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "ava": { 5 | "files": "actual.js", 6 | "serial": true, 7 | "failFast": true, 8 | "cache": false, 9 | "require": "./required.js" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /test/fixture/with-dependencies/test-uncaught-exception.js: -------------------------------------------------------------------------------- 1 | import test from '../../../'; 2 | 3 | import './dep-1'; 4 | import './dep-2'; 5 | import './dep-3.custom'; 6 | 7 | test('hey ho', t => { 8 | t.pass(); 9 | }); 10 | 11 | setImmediate(() => { 12 | throw new Error('oops'); 13 | }); 14 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "application-name", 3 | "version": "0.0.1", 4 | "ava": { 5 | "files": ["a.js", "b.js"], 6 | "serial": true, 7 | "failFast": true, 8 | "cache": false, 9 | "match": ["*"], 10 | "require": "./default-required.js" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /lib/send.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // utility to send messages to processes 4 | module.exports = function (ps, name, data) { 5 | if (typeof ps === 'string') { 6 | data = name || {}; 7 | name = ps; 8 | ps = process; 9 | } 10 | 11 | ps.send({ 12 | name: 'ava-' + name, 13 | data: data, 14 | ava: true 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /test/fixture/matcher-skip.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('foo', t => { 4 | t.pass(); 5 | }); 6 | 7 | test('bar', t => { 8 | t.pass(); 9 | }); 10 | 11 | test('baz', t => { 12 | t.fail(); 13 | }); 14 | 15 | test('tests are fun', t => { 16 | t.pass(); 17 | }); 18 | 19 | test('tests are not fun', t => { 20 | t.fail(); 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixture/observable.js: -------------------------------------------------------------------------------- 1 | var Promise = require('bluebird'); 2 | 3 | // global Promise for zen-observable 4 | if (!global.Promise) { 5 | Object.defineProperty(global, 'Promise', { 6 | value: Promise, 7 | configurable: true, 8 | enumerable: false, 9 | writable: true 10 | }); 11 | } 12 | 13 | module.exports = require('zen-observable'); 14 | -------------------------------------------------------------------------------- /lib/globals.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // Global objects / functions to be bound before requiring test file, so tests do not interfere. 4 | 5 | var x = module.exports; 6 | 7 | x.now = Date.now; 8 | 9 | x.setTimeout = setTimeout; 10 | 11 | x.clearTimeout = clearTimeout; 12 | 13 | x.setImmediate = require('set-immediate-shim'); 14 | 15 | x.options = {}; 16 | -------------------------------------------------------------------------------- /test/fixture/async-await.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test('async function', async function (t) { 4 | t.plan(1); 5 | 6 | const value = await Promise.resolve(1); 7 | 8 | t.is(value, 1); 9 | }); 10 | 11 | test('arrow async function', async t => { 12 | t.plan(1); 13 | 14 | const value = await Promise.resolve(1); 15 | 16 | t.is(value, 1); 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/pkg-overrides/actual.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | import path from 'path'; 3 | 4 | const opts = JSON.parse(process.argv[2]); 5 | 6 | test(t => { 7 | t.is(opts.failFast, true); 8 | t.is(opts.serial, true); 9 | t.is(opts.cacheEnabled, false); 10 | t.same(opts.require, [ 11 | path.join(__dirname, "required.js") 12 | ]); 13 | }); 14 | -------------------------------------------------------------------------------- /test/ava-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | var AvaError = require('../lib/ava-error'); 4 | 5 | test('must be called with new', function (t) { 6 | t.throws(function () { 7 | var avaError = AvaError; 8 | avaError('test message'); 9 | }, {message: 'Class constructor AvaError cannot be invoked without \'new\''}); 10 | t.end(); 11 | }); 12 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | Read the [contributing guidelines](../contributing.md). We are excited about pull requests, but please try to limit the scope, provide a general description of the changes, and remember, it's up to you to convince us to land it. If this fixes an open issue, link to it in the following way: `Fixes #321`. New features and bug fixes should come with tests. 2 | -------------------------------------------------------------------------------- /lib/ava-error.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function AvaError(message) { 4 | if (!(this instanceof AvaError)) { 5 | throw new TypeError('Class constructor AvaError cannot be invoked without \'new\''); 6 | } 7 | 8 | this.message = message; 9 | this.name = 'AvaError'; 10 | } 11 | 12 | AvaError.prototype = Object.create(Error.prototype); 13 | 14 | module.exports = AvaError; 15 | -------------------------------------------------------------------------------- /test/visual/console-log.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import delay from 'delay'; 3 | 4 | test('testName1', async t => { 5 | console.log('foo'); 6 | await delay(2000); 7 | console.log('baz'); 8 | t.pass(); 9 | }); 10 | 11 | test('testName2', async t => { 12 | await delay(1000); 13 | console.log('bar'); 14 | await delay(2000); 15 | console.log('quz'); 16 | t.pass(); 17 | }); 18 | -------------------------------------------------------------------------------- /test/fixture/serial.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | var tests = []; 4 | 5 | test.cb('first', t => { 6 | setTimeout(() => { 7 | tests.push('first'); 8 | t.end(); 9 | }, 300); 10 | }); 11 | 12 | test.cb('second', t => { 13 | setTimeout(() => { 14 | tests.push('second'); 15 | t.end(); 16 | }, 100); 17 | }); 18 | 19 | test(t => { 20 | t.same(tests, ['first', 'second']); 21 | }); 22 | -------------------------------------------------------------------------------- /test/fixture/pkg-conf/precedence/c.js: -------------------------------------------------------------------------------- 1 | import test from '../../../../'; 2 | import path from 'path'; 3 | 4 | const opts = JSON.parse(process.argv[2]); 5 | 6 | test('foo', t => { 7 | t.is(opts.failFast, false); 8 | t.is(opts.serial, false); 9 | t.is(opts.cacheEnabled, true); 10 | t.same(opts.match, ['foo*']); 11 | t.same(opts.require, [ 12 | path.join(__dirname, "required.js") 13 | ]); 14 | }); 15 | -------------------------------------------------------------------------------- /test/fixture/destructuring-public-api.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | 3 | test.beforeEach(t => t.context = 'bar'); 4 | 5 | test.cb('callback mode', ({context: foo, ... t}) => { 6 | t.is(foo, 'bar'); 7 | t.end(); 8 | }); 9 | 10 | test.cb('callback mode #2', ({context: foo, end, ... t}) => { 11 | t.is(foo, 'bar'); 12 | end(); 13 | }); 14 | 15 | test('sync', ({context: foo, ... t}) => { 16 | t.is(foo, 'bar'); 17 | }); 18 | -------------------------------------------------------------------------------- /test/visual/lorem-ipsum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('../../'); 3 | var delay = require('delay'); 4 | require('./print-lorem-ipsum'); 5 | 6 | async function testFn(t) { 7 | await delay(40); 8 | t.pass(); 9 | } 10 | 11 | async function failFn(t) { 12 | await delay(40); 13 | t.fail(); 14 | } 15 | 16 | for (var i = 0; i < 400; i++) { 17 | test.serial('test number ' + i, i === 125 ? failFn : testFn); 18 | } 19 | -------------------------------------------------------------------------------- /test/fixture/source-with-source-map-pragma.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | module.exports = foo; 4 | 5 | function foo() { 6 | bar(); 7 | } 8 | 9 | function bar() { 10 | throw new Error("Can't catch me!"); 11 | } 12 | 13 | //# sourceMappingURL=./source-with-source-map-pragma.map 14 | 15 | /* original source: 16 | module.exports = foo 17 | 18 | function foo() { 19 | bar() 20 | } 21 | 22 | function bar() { 23 | throw new Error(`Can't catch me!`) 24 | } 25 | */ 26 | -------------------------------------------------------------------------------- /test/fixture/source-with-source-map-pragma.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["test/fixture/source-with-source-map-pragma.js"],"names":[],"mappings":";;AAAA,MAAM,CAAC,OAAO,GAAG,GAAG,CAAA;;AAEpB,SAAS,GAAG,GAAG;AACd,IAAG,EAAE,CAAA;CACL;;AAED,SAAS,GAAG,GAAG;AACd,OAAM,IAAI,KAAK,mBAAmB,CAAA;CAClC","file":"test/fixture/source-with-source-map-pragma.js","sourcesContent":["module.exports = foo\n\nfunction foo() {\n\tbar()\n}\n\nfunction bar() {\n\tthrow new Error(`Can't catch me!`)\n}\n"]} 2 | -------------------------------------------------------------------------------- /test/fixture/source-map-file.js: -------------------------------------------------------------------------------- 1 | const fixture = require('source-map-fixtures').mapFile('throws').require(); 2 | const test = require('../../'); 3 | 4 | // The uncaught exception is passed to the corresponding cli test. The line 5 | // numbers from the 'throws' fixture (which uses a map file), as well as the 6 | // line of the fixture.run() call, should match the source lines. 7 | test('throw an uncaught exception', () => { 8 | setImmediate(run); 9 | }); 10 | 11 | const run = () => fixture.run(); 12 | -------------------------------------------------------------------------------- /test/fixture/source-map-inline.js: -------------------------------------------------------------------------------- 1 | const fixture = require('source-map-fixtures').inline('throws').require(); 2 | const test = require('../../'); 3 | 4 | // The uncaught exception is passed to the corresponding cli test. The line 5 | // numbers from the 'throws' fixture (using an inline source map), as well as 6 | // the line of the fixture.run() call, should match the source lines. 7 | test('throw an uncaught exception', () => { 8 | setImmediate(run); 9 | }); 10 | 11 | const run = () => fixture.run(); 12 | -------------------------------------------------------------------------------- /test/visual/stdout-write.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import delay from 'delay'; 3 | 4 | test('testName1', async t => { 5 | process.stdout.write('foo '); 6 | await delay(2000); 7 | process.stdout.write('baz '); 8 | t.pass(); 9 | }); 10 | 11 | test('testName2', async t => { 12 | await delay(1000); 13 | process.stdout.write('bar '); 14 | await delay(2000); 15 | process.stdout.write('quz '); 16 | await delay(1000); 17 | t.pass(); 18 | }); 19 | 20 | test('testName3', async t => { 21 | await delay(5000); 22 | t.pass(); 23 | }); 24 | -------------------------------------------------------------------------------- /test/fixture/source-map-initial.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["source-map-initial-input.js"],"names":[],"mappings":";;;;iCAAwB,qBAAqB;;gBAC5B,QAAQ;;;;AACzB,IAAM,OAAO,GAAG,gCAAQ,QAAQ,CAAC,CAAC,OAAO,EAAE,CAAA;AAC3C,mBAAK,6BAA6B,EAAE,UAAA,CAAC,EAAI;AACvC,cAAY,CAAC,GAAG,CAAC,CAAA;CAClB,CAAC,CAAA;AACF,IAAM,GAAG,GAAG,SAAN,GAAG;SAAS,OAAO,CAAC,GAAG,EAAE;CAAA,CAAA","file":"source-map-initial-input.js","sourcesContent":["import { mapFile } from 'source-map-fixtures'\nimport test from '../../'\nconst fixture = mapFile('throws').require()\ntest('throw an uncaught exception', t => {\n setImmediate(run)\n})\nconst run = () => fixture.run()"]} -------------------------------------------------------------------------------- /lib/hook.js: -------------------------------------------------------------------------------- 1 | var Test = require('./test'); 2 | 3 | module.exports = Hook; 4 | 5 | function Hook(title, fn) { 6 | if (!(this instanceof Hook)) { 7 | throw new TypeError('Class constructor Hook cannot be invoked without \'new\''); 8 | } 9 | 10 | if (typeof title === 'function') { 11 | fn = title; 12 | title = null; 13 | } 14 | 15 | this.title = title; 16 | this.fn = fn; 17 | } 18 | 19 | Hook.prototype.test = function (testTitle) { 20 | var title = this.title || (this.metadata.type + ' for "' + testTitle + '"'); 21 | var test = new Test(title, this.fn); 22 | test.metadata = this.metadata; 23 | return test; 24 | }; 25 | -------------------------------------------------------------------------------- /appveyor.yml: -------------------------------------------------------------------------------- 1 | environment: 2 | matrix: 3 | - nodejs_version: '5' 4 | - nodejs_version: '4' 5 | - nodejs_version: '0.12' 6 | install: 7 | - ps: Install-Product node $env:nodejs_version 8 | - set CI=true 9 | - set AVA_APPVEYOR=true 10 | - npm install -g npm@latest || (timeout 30 && npm install -g npm@latest) 11 | - set PATH=%APPDATA%\npm;%PATH% 12 | - npm install || (timeout 30 && npm install) 13 | matrix: 14 | fast_finish: true 15 | build: off 16 | version: '{build}' 17 | shallow_clone: true 18 | clone_depth: 1 19 | test_script: 20 | - node --version 21 | - npm --version 22 | - npm run test-win || (timeout 30 && npm run test-win) 23 | -------------------------------------------------------------------------------- /test/fixture/source-map-initial.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { 'default': obj }; } 4 | 5 | var _sourceMapFixtures = require('source-map-fixtures'); 6 | 7 | var _ = require('../../'); 8 | 9 | var _2 = _interopRequireDefault(_); 10 | 11 | var fixture = (0, _sourceMapFixtures.mapFile)('throws').require(); 12 | (0, _2['default'])('throw an uncaught exception', function (t) { 13 | setImmediate(run); 14 | }); 15 | var run = function run() { 16 | return fixture.run(); 17 | }; 18 | //# sourceMappingURL=./source-map-initial.js.map 19 | // Generated using node test/fixtures/_generate-source-map-initial.js 20 | -------------------------------------------------------------------------------- /test/fixture/long-running.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import signalExit from 'signal-exit'; 3 | 4 | test.cb('long running', t => { 5 | t.plan(1); 6 | 7 | signalExit(() => { 8 | // simulate an exit hook that lasts a short while 9 | const start = Date.now(); 10 | 11 | while (Date.now() - start < 2000) { 12 | // synchronously wait for 2 seconds 13 | } 14 | 15 | process.send({ 16 | name: 'cleanup-completed', 17 | data: {completed: true}, 18 | ava: true 19 | }); 20 | }, {alwaysLast: true}); 21 | 22 | setTimeout(() => { 23 | t.ok(true); 24 | t.end(); 25 | }); 26 | 27 | setTimeout(() => { 28 | // this would keep the process running for a long time 29 | console.log('I\'m gonna live forever!!'); 30 | }, 15000); 31 | }); 32 | -------------------------------------------------------------------------------- /.github/issue_template.md: -------------------------------------------------------------------------------- 1 | ## Prerequisites 2 | 3 | - [ ] Read the [contributing guidelines](../contributing.md). 4 | - [ ] This is not a support question. (Those are better asked in our [chat](https://gitter.im/sindresorhus/ava)). 5 | - [ ] I've checked that the issue doesn't already exist. 6 | - [ ] Can reproduce with latest AVA if an issue. Ensure `ava --version` matches ![](https://img.shields.io/npm/v/ava.svg) 7 | 8 | *Remove the above section when done.* 9 | 10 | 11 | ## Description 12 | 13 | [Description of the issue. Include why it should be added if it's a feature request.] 14 | 15 | 16 | ## Environment 17 | 18 | [Include the Node.js version and operating system. Run the following to get it quickly:] 19 | 20 | ``` 21 | node -e "var os=require('os');console.log('Node.js ' + process.version + '\n' + os.platform() + ' ' + os.release())" 22 | ``` 23 | -------------------------------------------------------------------------------- /test/visual/print-lorem-ipsum.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var text = fs.readFileSync(path.join(__dirname, 'lorem-ipsum.txt'), 'utf8'); 5 | 6 | var lines = text.split(/\r?\n/g).map(function (line) { 7 | return line.split(' '); 8 | }); 9 | 10 | setTimeout(function () { 11 | var lineNum = 0; 12 | var wordNum = 0; 13 | 14 | var interval = setInterval(function () { 15 | if (lineNum >= lines.length) { 16 | clearInterval(interval); 17 | return; 18 | } 19 | var line = lines[lineNum]; 20 | if (wordNum >= line.length) { 21 | process.stdout.write('\n'); 22 | lineNum++; 23 | wordNum = 0; 24 | return; 25 | } 26 | var word = line[wordNum]; 27 | wordNum++; 28 | if (wordNum < line.length) { 29 | word += ' '; 30 | } 31 | process.stdout.write(word); 32 | }, 50); 33 | }, 200); 34 | -------------------------------------------------------------------------------- /lib/beautify-stack.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var StackUtils = require('stack-utils'); 3 | var debug = require('debug')('ava'); 4 | 5 | function indent(str) { 6 | return ' ' + str; 7 | } 8 | 9 | // ignore unimportant stack trace lines 10 | var ignoreStackLines = []; 11 | 12 | var avaInternals = /\/ava\/(?:lib\/)?[\w-]+\.js:\d+:\d+\)?$/; 13 | var avaDependencies = /\/node_modules\/(?:bluebird|empower-core|(?:ava\/node_modules\/)?(?:babel-runtime|core-js))\//; 14 | 15 | if (!debug.enabled) { 16 | ignoreStackLines = StackUtils.nodeInternals(); 17 | ignoreStackLines.push(avaInternals); 18 | ignoreStackLines.push(avaDependencies); 19 | } 20 | 21 | var stackUtils = new StackUtils({internals: ignoreStackLines}); 22 | 23 | module.exports = function (stack) { 24 | if (!stack) { 25 | return ''; 26 | } 27 | 28 | var title = stack.split('\n')[0]; 29 | var lines = stackUtils 30 | .clean(stack) 31 | .split('\n') 32 | .map(indent) 33 | .join('\n'); 34 | 35 | return title + '\n' + lines; 36 | }; 37 | -------------------------------------------------------------------------------- /license: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) Sindre Sorhus (sindresorhus.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 | -------------------------------------------------------------------------------- /test/fixture/_generate_source-map-initial.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var babel = require('babel-core'); 4 | var fs = require('fs'); 5 | var path = require('path'); 6 | 7 | var transformed = babel.transform([ 8 | "import { mapFile } from 'source-map-fixtures'", 9 | "import test from '../../'", 10 | "const fixture = mapFile('throws').require()", 11 | // The uncaught exception is passed to the corresponding cli test. The line 12 | // numbers from the 'throws' fixture (which uses a map file), as well as the 13 | // line of the fixture.run() call, should match the source lines from this 14 | // string. 15 | "test('throw an uncaught exception', t => {", 16 | " setImmediate(run)", 17 | "})", 18 | "const run = () => fixture.run()" 19 | ].join('\n'), { 20 | filename: 'source-map-initial-input.js', 21 | sourceMaps: true 22 | }); 23 | 24 | fs.writeFileSync( 25 | path.join(__dirname, 'source-map-initial.js'), 26 | transformed.code + '\n//# sourceMappingURL=./source-map-initial.js.map\n// Generated using node test/fixtures/_generate-source-map-initial.js\n'); 27 | fs.writeFileSync( 28 | path.join(__dirname, 'source-map-initial.js.map'), 29 | JSON.stringify(transformed.map)); 30 | console.log('Generated source-map-initial.js'); 31 | -------------------------------------------------------------------------------- /test/visual/text-ends-at-terminal-width.js: -------------------------------------------------------------------------------- 1 | import test from '../../'; 2 | import delay from 'delay'; 3 | 4 | function writeFullWidth(even, adjust) { 5 | return async function (t) { 6 | await delay(200); 7 | var len = Math[even ? 'floor' : 'ceil']((process.stdout.columns + adjust) / 2); 8 | for (var i = 0; i < len; i++) { 9 | process.stdout.write(String(i % 10)); 10 | await delay(1); 11 | } 12 | await delay(200); 13 | t.pass(); 14 | }; 15 | } 16 | 17 | // line 1 (exactly full width) 18 | test.serial(writeFullWidth(true, 0)); 19 | test.serial(writeFullWidth(false, 0)); 20 | 21 | // line 2 (one extra char on line 3) 22 | test.serial(writeFullWidth(true, 1)); 23 | test.serial(writeFullWidth(false, 1)); 24 | 25 | // line 3 (ends one char short of complete width) 26 | test.serial(writeFullWidth(true, -2)); 27 | test.serial(writeFullWidth(false, -2)); 28 | 29 | // line 4 (completes line 3 and ends the next line exactly complete width. 30 | test.serial(writeFullWidth(true, 1)); 31 | test.serial(writeFullWidth(false, 1)); 32 | 33 | // line 5 (exact complete width) 34 | test.serial(writeFullWidth(true, 0)); 35 | test.serial(writeFullWidth(false, 0)); 36 | 37 | // line 6 (exact complete width) 38 | test.serial(writeFullWidth(true, 0)); 39 | test.serial(writeFullWidth(false, 0)); 40 | -------------------------------------------------------------------------------- /test/fixture/babel-plugin-test-doubler.js: -------------------------------------------------------------------------------- 1 | /* 2 | A Babel plugin that causes each AVA test to be duplicated with a new title. 3 | 4 | test('foo', t => {}); 5 | 6 | becomes 7 | 8 | test('foo', t => {}); 9 | test('repeated test: foo', t => {}); 10 | 11 | This is used by some integration tests to validate correct handling of Babel config options. 12 | */ 13 | 14 | function plugin(babel) { 15 | var t = babel.types; 16 | var anonCount = 1; 17 | 18 | return { 19 | visitor: { 20 | CallExpression: function (path) { 21 | var node = path.node; 22 | var callee = node.callee; 23 | var args = node.arguments; 24 | if (callee.type === 'Identifier' && callee.name === 'test') { 25 | if (args.length === 1) { 26 | args = [t.StringLiteral('repeated test: anonymous' + anonCount++), args[0]]; 27 | } else if (args.length === 2 && args[0].type === 'StringLiteral') { 28 | if (/^repeated test/.test(args[0].value)) { 29 | return; 30 | } 31 | args = args.slice(); 32 | args[0] = t.StringLiteral('repeated test: ' + args[0].value); 33 | } else { 34 | throw new Error('the plugin does not know how to handle this call to test'); 35 | } 36 | path.insertAfter(t.CallExpression( 37 | t.Identifier('test'), 38 | args 39 | )); 40 | } 41 | } 42 | } 43 | }; 44 | } 45 | 46 | module.exports = plugin; 47 | -------------------------------------------------------------------------------- /docs/recipes/typescript.md: -------------------------------------------------------------------------------- 1 | # TypeScript 2 | 3 | Translations: [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/typescript.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/typescript.md) 4 | 5 | AVA comes bundled with a TypeScript definition file. This allows developers to leverage TypeScript for writing tests. 6 | 7 | ## Setup 8 | 9 | First install the TypeScript compiler [tsc](https://github.com/Microsoft/TypeScript). 10 | 11 | ``` 12 | $ npm install --save-dev tsc 13 | ``` 14 | 15 | Create a [`tsconfig.json`](https://github.com/Microsoft/TypeScript/wiki/tsconfig.json) file. This file specifies the compiler options required to compile the project or the test file. 16 | 17 | ```json 18 | { 19 | "compilerOptions": { 20 | "module": "commonjs", 21 | "target": "es2015" 22 | } 23 | } 24 | ``` 25 | 26 | Add a `test` script in the `package.json` file. It will compile the project first and then run AVA. 27 | 28 | ```json 29 | { 30 | "scripts": { 31 | "test": "tsc && ava" 32 | } 33 | } 34 | ``` 35 | 36 | 37 | ## Add tests 38 | 39 | Create a `test.ts` file. 40 | 41 | ```ts 42 | import test from 'ava'; 43 | 44 | async function fn() { 45 | return Promise.resolve('foo'); 46 | } 47 | 48 | test(async (t) => { 49 | t.is(await fn(), 'foo'); 50 | }); 51 | ``` 52 | 53 | 54 | ## Execute the tests 55 | 56 | ``` 57 | $ npm test 58 | ``` 59 | -------------------------------------------------------------------------------- /test/visual/lorem-ipsum.txt: -------------------------------------------------------------------------------- 1 | Four score and seven years ago our fathers brought forth on this continent, a new nation, conceived in Liberty, and dedicated to the proposition that all men are created equal. 2 | 3 | Now we are engaged in a great civil war, testing whether that nation, or any nation so conceived and dedicated, can long endure. We are met on a great battle-field of that war. We have come to dedicate a portion of that field, as a final resting place for those who here gave their lives that that nation might live. It is altogether fitting and proper that we should do this. 4 | 5 | But, in a larger sense, we can not dedicate -- we can not consecrate -- we can not hallow -- this ground. The brave men, living and dead, who struggled here, have consecrated it, far above our poor power to add or detract. The world will little note, nor long remember what we say here, but it can never forget what they did here. It is for us the living, rather, to be dedicated here to the unfinished work which they who fought here have thus far so nobly advanced. It is rather for us to be here dedicated to the great task remaining before us -- that from these honored dead we take increased devotion to that cause for which they gave the last full measure of devotion -- that we here highly resolve that these dead shall not have died in vain -- that this nation, under God, shall have a new birth of freedom -- and that government of the people, by the people, for the people, shall not perish from the earth. 6 | -------------------------------------------------------------------------------- /lib/enhance-assert.js: -------------------------------------------------------------------------------- 1 | module.exports = enhanceAssert; 2 | module.exports.formatter = formatter; 3 | 4 | module.exports.PATTERNS = [ 5 | 't.ok(value, [message])', 6 | 't.notOk(value, [message])', 7 | 't.true(value, [message])', 8 | 't.false(value, [message])', 9 | 't.is(value, expected, [message])', 10 | 't.not(value, expected, [message])', 11 | 't.same(value, expected, [message])', 12 | 't.notSame(value, expected, [message])', 13 | 't.regex(contents, regex, [message])' 14 | ]; 15 | 16 | module.exports.NON_ENHANCED_PATTERNS = [ 17 | 't.pass([message])', 18 | 't.fail([message])', 19 | 't.throws(fn, [message])', 20 | 't.notThrows(fn, [message])', 21 | 't.ifError(error, [message])' 22 | ]; 23 | 24 | function enhanceAssert(opts) { 25 | var empower = require('empower-core'); 26 | var enhanced = empower( 27 | opts.assert, 28 | { 29 | destructive: false, 30 | onError: opts.onError, 31 | onSuccess: opts.onSuccess, 32 | patterns: module.exports.PATTERNS, 33 | wrapOnlyPatterns: module.exports.NON_ENHANCED_PATTERNS, 34 | bindReceiver: false 35 | } 36 | ); 37 | 38 | enhanced.AssertionError = opts.assert.AssertionError; 39 | 40 | return enhanced; 41 | } 42 | 43 | function formatter() { 44 | var powerAssertFormatter = require('power-assert-formatter'); 45 | var powerAssertRenderers = require('power-assert-renderers'); 46 | 47 | return powerAssertFormatter({ 48 | renderers: [ 49 | powerAssertRenderers.AssertionRenderer, 50 | powerAssertRenderers.SuccinctRenderer 51 | ] 52 | }); 53 | } 54 | -------------------------------------------------------------------------------- /docs/recipes/endpoint-testing.md: -------------------------------------------------------------------------------- 1 | # Endpoint testing 2 | 3 | Translations: [Español](https://github.com/sindresorhus/ava-docs/blob/master/es_ES/docs/recipes/endpoint-testing.md), [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/endpoint-testing.md), [日本語](https://github.com/sindresorhus/ava-docs/blob/master/ja_JP/docs/recipes/endpoint-testing.md), [Português](https://github.com/sindresorhus/ava-docs/blob/master/pt_BR/docs/recipes/endpoint-testing.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/endpoint-testing.md) 4 | 5 | AVA doesn't have a builtin method for testing endpoints, but you can use any assertion library with it. Let's use [`supertest-as-promised`](https://github.com/WhoopInc/supertest-as-promised). 6 | 7 | Since tests run concurrently, it's best to create a fresh server instance for each test, because if we referenced the same instance, it could be mutated between tests. This can be accomplished with a `test.beforeEach` and `t.context`, or with simply a factory function: 8 | 9 | ```js 10 | function makeApp() { 11 | const app = express(); 12 | app.post('/signup', signupHandler); 13 | return app; 14 | } 15 | ``` 16 | 17 | Next, just inject your server instance into supertest. The only gotcha is to use a promise or async/await syntax instead of the supertest `end` method: 18 | 19 | ```js 20 | test('signup:Success', async t => { 21 | t.plan(2); 22 | 23 | const res = await request(makeApp()) 24 | .post('/signup') 25 | .send({email: 'ava@rocks.com', password: '123123'}); 26 | 27 | t.is(res.status, 200); 28 | t.is(res.body.email, 'ava@rocks.com'); 29 | }); 30 | ``` 31 | -------------------------------------------------------------------------------- /lib/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | function Logger(reporter) { 4 | if (!(this instanceof Logger)) { 5 | throw new TypeError('Class constructor Logger cannot be invoked without \'new\''); 6 | } 7 | 8 | Object.keys(Logger.prototype).forEach(function (key) { 9 | this[key] = this[key].bind(this); 10 | }, this); 11 | 12 | this.reporter = reporter; 13 | } 14 | 15 | module.exports = Logger; 16 | 17 | Logger.prototype.start = function () { 18 | if (!this.reporter.start) { 19 | return; 20 | } 21 | 22 | this.write(this.reporter.start()); 23 | }; 24 | 25 | Logger.prototype.reset = function () { 26 | if (!this.reporter.reset) { 27 | return; 28 | } 29 | 30 | this.write(this.reporter.reset()); 31 | }; 32 | 33 | Logger.prototype.test = function (test) { 34 | this.write(this.reporter.test(test)); 35 | }; 36 | 37 | Logger.prototype.unhandledError = function (err) { 38 | if (!this.reporter.unhandledError) { 39 | return; 40 | } 41 | 42 | this.write(this.reporter.unhandledError(err)); 43 | }; 44 | 45 | Logger.prototype.finish = function () { 46 | if (!this.reporter.finish) { 47 | return; 48 | } 49 | 50 | this.write(this.reporter.finish()); 51 | }; 52 | 53 | Logger.prototype.write = function (str) { 54 | if (typeof str === 'undefined') { 55 | return; 56 | } 57 | 58 | this.reporter.write(str); 59 | }; 60 | 61 | Logger.prototype.stdout = function (data) { 62 | if (!this.reporter.stdout) { 63 | return; 64 | } 65 | 66 | this.reporter.stdout(data); 67 | }; 68 | 69 | Logger.prototype.stderr = function (data) { 70 | if (!this.reporter.stderr) { 71 | return; 72 | } 73 | 74 | this.reporter.stderr(data); 75 | }; 76 | 77 | Logger.prototype.exit = function (code) { 78 | // TODO: figure out why this needs to be here to 79 | // correctly flush the output when multiple test files 80 | process.stdout.write(''); 81 | process.stderr.write(''); 82 | 83 | // timeout required to correctly flush IO on Node.js 0.10 on Windows 84 | setTimeout(function () { 85 | process.exit(code); 86 | }, process.env.AVA_APPVEYOR ? 500 : 0); 87 | }; 88 | -------------------------------------------------------------------------------- /docs/recipes/browser-testing.md: -------------------------------------------------------------------------------- 1 | # Setting up AVA for browser testing 2 | 3 | Translations: [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/browser-testing.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/browser-testing.md) 4 | 5 | AVA does not support running tests in browsers [yet](https://github.com/sindresorhus/ava/issues/24). Some libraries require browser specific globals (`window`, `document`, `navigator`, etc). 6 | An example of this is React, at least if you want to use ReactDOM.render and simulate events with ReactTestUtils. 7 | 8 | This recipe works for any library that needs a mocked browser environment. 9 | 10 | ## Install jsdom 11 | 12 | Install [jsdom](https://github.com/tmpvar/jsdom). 13 | 14 | > A JavaScript implementation of the WHATWG DOM and HTML standards, for use with node.js 15 | 16 | ``` 17 | $ npm install --save-dev jsdom 18 | ``` 19 | 20 | ## Setup jsdom 21 | 22 | Create a helper file and place it in the `test/helpers` folder. This ensures AVA does not treat it as a test. 23 | 24 | `test/helpers/setup-browser-env.js`: 25 | 26 | ```js 27 | global.document = require('jsdom').jsdom(''); 28 | global.window = document.defaultView; 29 | global.navigator = window.navigator; 30 | ``` 31 | 32 | ## Configure tests to use jsdom 33 | 34 | Configure AVA to `require` the helper before every test file. 35 | 36 | `package.json`: 37 | 38 | ```json 39 | { 40 | "ava": { 41 | "require": [ 42 | "./test/helpers/setup-browser-env.js" 43 | ] 44 | } 45 | } 46 | ``` 47 | 48 | ## Enjoy! 49 | 50 | Write your tests and enjoy a mocked window object. 51 | 52 | `test/my.react.test.js`: 53 | 54 | ```js 55 | import test from 'ava'; 56 | import React from 'react'; 57 | import {render} from 'react-dom'; 58 | import {Simulate} from 'react-addons-test-utils'; 59 | import sinon from 'sinon'; 60 | import CustomInput from './components/custom-input.jsx'; 61 | 62 | test('Input calls onBlur', t => { 63 | const onUserBlur = sinon.spy(); 64 | const input = render( 65 | React.createElement(CustomInput, {onUserBlur), 66 | div 67 | ) 68 | 69 | Simulate.blur(input); 70 | 71 | t.true(onUserBlur.calledOnce); 72 | }); 73 | ``` 74 | -------------------------------------------------------------------------------- /lib/concurrent.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Promise = require('bluebird'); 3 | var isPromise = require('is-promise'); 4 | var AvaError = require('./ava-error'); 5 | 6 | function noop() {} 7 | 8 | module.exports = Concurrent; 9 | 10 | function Concurrent(tests, bail) { 11 | if (!(this instanceof Concurrent)) { 12 | throw new TypeError('Class constructor Concurrent cannot be invoked without \'new\''); 13 | } 14 | 15 | if (!Array.isArray(tests)) { 16 | throw new TypeError('Expected an array of tests'); 17 | } 18 | 19 | this.results = []; 20 | this.passed = true; 21 | this.reason = null; 22 | this.tests = tests; 23 | this.bail = bail || false; 24 | 25 | Object.keys(Concurrent.prototype).forEach(function (key) { 26 | this[key] = this[key].bind(this); 27 | }, this); 28 | } 29 | 30 | Concurrent.prototype.run = function () { 31 | var results; 32 | 33 | try { 34 | results = this.tests.map(this._runTest); 35 | } catch (err) { 36 | if (err instanceof AvaError) { 37 | return this._results(); 38 | } 39 | 40 | throw err; 41 | } 42 | 43 | var isAsync = results.some(isPromise); 44 | 45 | if (isAsync) { 46 | return Promise.all(results) 47 | .catch(AvaError, noop) 48 | .then(this._results); 49 | } 50 | 51 | return this._results(); 52 | }; 53 | 54 | Concurrent.prototype._runTest = function (test, index) { 55 | var result = test.run(); 56 | 57 | if (isPromise(result)) { 58 | var self = this; 59 | 60 | return result.then(function (result) { 61 | return self._addResult(result, index); 62 | }); 63 | } 64 | 65 | return this._addResult(result, index); 66 | }; 67 | 68 | Concurrent.prototype._addResult = function (result, index) { 69 | // always save result when not in bail mode or all previous tests pass 70 | if ((this.bail && this.passed) || !this.bail) { 71 | this.results[index] = result; 72 | } 73 | 74 | if (result.passed === false) { 75 | this.passed = false; 76 | 77 | // only set reason once 78 | if (!this.reason) { 79 | this.reason = result.reason; 80 | } 81 | 82 | if (this.bail) { 83 | throw new AvaError('Error in Concurrent while in bail mode'); 84 | } 85 | } 86 | 87 | return result; 88 | }; 89 | 90 | Concurrent.prototype._results = function () { 91 | return { 92 | passed: this.passed, 93 | reason: this.reason, 94 | result: this.results 95 | }; 96 | }; 97 | -------------------------------------------------------------------------------- /test/fork.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var test = require('tap').test; 4 | var _fork = require('../lib/fork.js'); 5 | var CachingPrecompiler = require('../lib/caching-precompiler'); 6 | var cacheDir = path.join(__dirname, '../node_modules/.cache/ava'); 7 | var precompiler = new CachingPrecompiler(cacheDir); 8 | 9 | function fork(testPath) { 10 | return _fork(testPath, { 11 | cacheDir: cacheDir, 12 | precompiled: precompiler.generateHashForFile(testPath) 13 | }); 14 | } 15 | 16 | function fixture(name) { 17 | return path.join(__dirname, 'fixture', name); 18 | } 19 | 20 | test('emits test event', function (t) { 21 | t.plan(1); 22 | 23 | fork(fixture('generators.js')) 24 | .run({}) 25 | .on('test', function (tt) { 26 | t.is(tt.title, 'generator function'); 27 | t.end(); 28 | }); 29 | }); 30 | 31 | test('resolves promise with tests info', function (t) { 32 | t.plan(3); 33 | 34 | var file = fixture('generators.js'); 35 | 36 | fork(file) 37 | .run({}) 38 | .then(function (info) { 39 | t.is(info.stats.passCount, 1); 40 | t.is(info.tests.length, 1); 41 | t.is(info.file, path.relative('.', file)); 42 | t.end(); 43 | }); 44 | }); 45 | 46 | test('exit after tests are finished', function (t) { 47 | t.plan(2); 48 | 49 | var start = Date.now(); 50 | var cleanupCompleted = false; 51 | 52 | fork(fixture('long-running.js')) 53 | .run({}) 54 | .on('exit', function () { 55 | t.true(Date.now() - start < 10000, 'test waited for a pending setTimeout'); 56 | t.true(cleanupCompleted, 'cleanup did not complete'); 57 | }) 58 | .on('cleanup-completed', function (event) { 59 | cleanupCompleted = event.completed; 60 | }); 61 | }); 62 | 63 | test('fake timers do not break duration', function (t) { 64 | fork(fixture('fake-timers.js')) 65 | .run({}) 66 | .then(function (info) { 67 | var duration = info.tests[0].duration; 68 | t.true(duration < 1000, duration + ' < 1000'); 69 | t.is(info.stats.failCount, 0); 70 | t.is(info.stats.passCount, 1); 71 | t.end(); 72 | }); 73 | }); 74 | 75 | /* 76 | test('destructuring of `t` is allowed', function (t) { 77 | fork(fixture('destructuring-public-api.js')) 78 | .run({}) 79 | .then(function (info) { 80 | t.is(info.stats.failCount, 0); 81 | t.is(info.stats.passCount, 3); 82 | t.end(); 83 | }); 84 | }); 85 | */ 86 | 87 | test('babelrc is ignored', function (t) { 88 | fork(fixture('babelrc/test.js')) 89 | .run({}) 90 | .then(function (info) { 91 | t.is(info.stats.passCount, 1); 92 | t.end(); 93 | }); 94 | }); 95 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var chalk = require('chalk'); 4 | var serializeError = require('serialize-error'); 5 | var beautifyStack = require('./lib/beautify-stack'); 6 | var globals = require('./lib/globals'); 7 | var Runner = require('./lib/runner'); 8 | var send = require('./lib/send'); 9 | 10 | // note that test files have require('ava') 11 | require('./lib/test-worker').avaRequired = true; 12 | 13 | var opts = globals.options; 14 | var runner = new Runner({ 15 | serial: opts.serial, 16 | bail: opts.failFast, 17 | match: opts.match 18 | }); 19 | 20 | // check if the test is being run without AVA cli 21 | var isForked = typeof process.send === 'function'; 22 | 23 | if (!isForked) { 24 | var fp = path.relative('.', process.argv[1]); 25 | 26 | console.log(); 27 | console.error('Test files must be run with the AVA CLI:\n\n ' + chalk.grey.dim('$') + ' ' + chalk.cyan('ava ' + fp) + '\n'); 28 | 29 | process.exit(1); 30 | } 31 | 32 | // if fail-fast is enabled, use this variable to detect 33 | // that no more tests should be logged 34 | var isFailed = false; 35 | 36 | Error.stackTraceLimit = Infinity; 37 | 38 | function test(props) { 39 | if (isFailed) { 40 | return; 41 | } 42 | 43 | var hasError = typeof props.error !== 'undefined'; 44 | 45 | // don't display anything if it's a passed hook 46 | if (!hasError && props.type !== 'test') { 47 | return; 48 | } 49 | 50 | if (hasError) { 51 | props.error = serializeError(props.error); 52 | if (props.error.stack) { 53 | props.error.stack = beautifyStack(props.error.stack); 54 | } 55 | } else { 56 | props.error = null; 57 | } 58 | 59 | send('test', props); 60 | 61 | if (hasError && opts.failFast) { 62 | isFailed = true; 63 | exit(); 64 | } 65 | } 66 | 67 | function exit() { 68 | send('results', { 69 | stats: runner.stats 70 | }); 71 | } 72 | 73 | globals.setImmediate(function () { 74 | var hasExclusive = runner.tests.hasExclusive; 75 | var numberOfTests = runner.tests.tests.concurrent.length + runner.tests.tests.serial.length; 76 | 77 | if (numberOfTests === 0) { 78 | send('no-tests', {avaRequired: true}); 79 | return; 80 | } 81 | 82 | send('stats', { 83 | testCount: numberOfTests, 84 | hasExclusive: hasExclusive 85 | }); 86 | 87 | runner.on('test', test); 88 | 89 | process.on('ava-run', function (options) { 90 | runner.run(options).then(exit); 91 | }); 92 | }); 93 | 94 | module.exports = runner.test; 95 | // TypeScript imports the `default` property for 96 | // an ES2015 default import (`import test from 'ava'`) 97 | // See: https://github.com/Microsoft/TypeScript/issues/2242#issuecomment-83694181 98 | module.exports.default = runner.test; 99 | -------------------------------------------------------------------------------- /lib/sequence.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var isPromise = require('is-promise'); 3 | var AvaError = require('./ava-error'); 4 | 5 | function noop() {} 6 | 7 | module.exports = Sequence; 8 | 9 | function Sequence(tests, bail) { 10 | if (!(this instanceof Sequence)) { 11 | throw new TypeError('Class constructor Sequence cannot be invoked without \'new\''); 12 | } 13 | 14 | if (!tests) { 15 | throw new Error('Sequence items can\'t be undefined'); 16 | } 17 | 18 | this.results = []; 19 | this.passed = true; 20 | this.reason = null; 21 | this.tests = tests; 22 | this.bail = bail || false; 23 | 24 | // TODO(vdemedes): separate into a utility (it's being used in serveral places) 25 | Object.keys(Sequence.prototype).forEach(function (key) { 26 | this[key] = this[key].bind(this); 27 | }, this); 28 | } 29 | 30 | Sequence.prototype.run = function () { 31 | var length = this.tests.length; 32 | 33 | for (var i = 0; i < length; i++) { 34 | // if last item failed and we should bail, return results and stop 35 | if (this.bail && !this.passed) { 36 | return this._results(); 37 | } 38 | 39 | var result = this.tests[i].run(); 40 | 41 | // if a Promise returned, we don't need to check for Promises after this test 42 | // so we can just use Promise.each() on the rest of the tests 43 | if (isPromise(result)) { 44 | return result 45 | .then(this._addResult) 46 | .return(this.tests.slice(i + 1)) 47 | .each(this._runTest) 48 | .catch(AvaError, noop) 49 | .then(this._results); 50 | } 51 | 52 | try { 53 | this._addResult(result); 54 | } catch (err) { 55 | // in bail mode, don't execute the next tests 56 | if (err instanceof AvaError) { 57 | return this._results(); 58 | } 59 | 60 | throw err; 61 | } 62 | } 63 | 64 | return this._results(); 65 | }; 66 | 67 | Sequence.prototype._runTest = function (test) { 68 | var result = test.run(); 69 | 70 | if (isPromise(result)) { 71 | return result 72 | .then(this._addResult); 73 | } 74 | 75 | return this._addResult(result); 76 | }; 77 | 78 | Sequence.prototype._addResult = function (result) { 79 | this.results.push(result); 80 | 81 | if (result.passed === false) { 82 | this.passed = false; 83 | 84 | // only set reason once 85 | if (!this.reason) { 86 | this.reason = result.reason; 87 | } 88 | 89 | if (this.bail) { 90 | throw new AvaError('Error in Sequence while in bail mode'); 91 | } 92 | } 93 | 94 | return result; 95 | }; 96 | 97 | Sequence.prototype._results = function () { 98 | return { 99 | passed: this.passed, 100 | reason: this.reason, 101 | result: this.results 102 | }; 103 | }; 104 | -------------------------------------------------------------------------------- /lib/reporters/tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var format = require('util').format; 3 | 4 | // Parses stack trace and extracts original function name, file name and line. 5 | function getSourceFromStack(stack, index) { 6 | return stack 7 | .split('\n') 8 | .slice(index, index + 1) 9 | .join('') 10 | .replace(/^\s+ /, ''); 11 | } 12 | 13 | function TapReporter() { 14 | if (!(this instanceof TapReporter)) { 15 | return new TapReporter(); 16 | } 17 | 18 | this.i = 0; 19 | } 20 | 21 | module.exports = TapReporter; 22 | 23 | TapReporter.prototype.start = function () { 24 | return 'TAP version 13'; 25 | }; 26 | 27 | TapReporter.prototype.test = function (test) { 28 | var output; 29 | 30 | var directive = ''; 31 | var passed = test.todo ? 'not ok' : 'ok'; 32 | 33 | if (test.todo) { 34 | directive = '# TODO'; 35 | } else if (test.skip) { 36 | directive = '# SKIP'; 37 | } 38 | 39 | if (test.error) { 40 | output = [ 41 | '# ' + test.title, 42 | format('not ok %d - %s', ++this.i, test.error.message), 43 | ' ---', 44 | ' operator: ' + test.error.operator, 45 | ' expected: ' + test.error.expected, 46 | ' actual: ' + test.error.actual, 47 | ' at: ' + getSourceFromStack(test.error.stack, 1), 48 | ' ...' 49 | ]; 50 | } else { 51 | output = [ 52 | '# ' + test.title, 53 | format('%s %d - %s %s', passed, ++this.i, test.title, directive).trim() 54 | ]; 55 | } 56 | 57 | return output.join('\n'); 58 | }; 59 | 60 | TapReporter.prototype.unhandledError = function (err) { 61 | var output = [ 62 | '# ' + err.message, 63 | format('not ok %d - %s', ++this.i, err.message) 64 | ]; 65 | // AvaErrors don't have stack traces. 66 | if (err.type !== 'exception' || err.name !== 'AvaError') { 67 | output.push( 68 | ' ---', 69 | ' name: ' + err.name, 70 | ' at: ' + getSourceFromStack(err.stack, 1), 71 | ' ...' 72 | ); 73 | } 74 | 75 | return output.join('\n'); 76 | }; 77 | 78 | TapReporter.prototype.finish = function () { 79 | var output = [ 80 | '', 81 | '1..' + (this.api.passCount + this.api.failCount + this.api.skipCount), 82 | '# tests ' + (this.api.passCount + this.api.failCount + this.api.skipCount), 83 | '# pass ' + this.api.passCount 84 | ]; 85 | 86 | if (this.api.skipCount > 0) { 87 | output.push('# skip ' + this.api.skipCount); 88 | } 89 | 90 | output.push('# fail ' + (this.api.failCount + this.api.rejectionCount + this.api.exceptionCount), ''); 91 | 92 | return output.join('\n'); 93 | }; 94 | 95 | TapReporter.prototype.write = function (str) { 96 | console.log(str); 97 | }; 98 | 99 | TapReporter.prototype.stdout = TapReporter.prototype.stderr = function (data) { 100 | process.stderr.write(data); 101 | }; 102 | -------------------------------------------------------------------------------- /bench/compare.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var path = require('path'); 4 | var fs = require('fs'); 5 | var Table = require('cli-table2'); 6 | var chalk = require('chalk'); 7 | 8 | var files = fs.readdirSync(path.join(__dirname, '.results')) 9 | .map(function (file) { 10 | var result = JSON.parse(fs.readFileSync(path.join(__dirname, '.results', file), 'utf8')); 11 | result['.file'] = path.basename(file, '.json'); 12 | return result; 13 | }) 14 | // find the most recent benchmark runs 15 | .sort(function (fileA, fileB) { 16 | return fileB['.time'] - fileA['.time']; 17 | }); 18 | 19 | // Only the 3 most recent runs 20 | files = files.slice(0, 3); 21 | 22 | function prepStats(times) { 23 | times = times 24 | .map(function (time) { 25 | return time.time; 26 | }) 27 | .sort(function (timeA, timeB) { 28 | return timeA - timeB; 29 | }); 30 | 31 | // remove fastest and slowest 32 | times = times.slice(1, times.length - 1); 33 | 34 | var sum = times.reduce(function (a, b) { 35 | return a + b; 36 | }, 0); 37 | 38 | return { 39 | mean: Math.round((sum / times.length) * 1000) / 1000, 40 | median: times[Math.floor(times.length / 2)], 41 | min: times[0], 42 | max: times[times.length - 1] 43 | }; 44 | } 45 | 46 | var results = {}; 47 | var fileNames = files.map(function (file) { 48 | return file['.file']; 49 | }); 50 | var stats = ['mean', 'median', 'min', 'max']; 51 | 52 | files.forEach(function (file) { 53 | Object.keys(file) 54 | .filter(function (key) { 55 | return !/^\./.test(key); 56 | }) 57 | .forEach(function (key) { 58 | results[key] = results[key] || {}; 59 | results[key][file['.file']] = prepStats(file[key]); 60 | }); 61 | }); 62 | 63 | var table = new Table(); 64 | table.push( 65 | [''].concat(stats.map(function (stat) { 66 | return { 67 | content: stat, 68 | colSpan: fileNames.length, 69 | hAlign: 'center' 70 | }; 71 | })), 72 | stats.reduce(function (arr) { 73 | return arr.concat(fileNames); 74 | }, ['args']) 75 | ); 76 | 77 | Object.keys(results) 78 | .forEach(function (key) { 79 | table.push(stats.reduce(function (arr, stat) { 80 | var min = Infinity; 81 | var max = -Infinity; 82 | 83 | var statGroup = fileNames.map(function (fileName) { 84 | var result = results[key][fileName]; 85 | result = result && result[stat]; 86 | if (result) { 87 | min = Math.min(min, result); 88 | max = Math.max(max, result); 89 | return result; 90 | } 91 | return ''; 92 | }); 93 | return arr.concat(statGroup.map(function (stat) { 94 | if (stat === min) { 95 | return chalk.green(stat); 96 | } 97 | if (stat === max) { 98 | return chalk.red(stat); 99 | } 100 | return stat; 101 | })); 102 | }, [key])); 103 | }); 104 | 105 | console.log(table.toString()); 106 | -------------------------------------------------------------------------------- /code-of-conduct.md: -------------------------------------------------------------------------------- 1 | # Contributor Code of Conduct 2 | 3 | Translations: [Español](https://github.com/sindresorhus/ava-docs/blob/master/es_ES/code-of-conduct.md), [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/code-of-conduct.md), [日本語](https://github.com/sindresorhus/ava-docs/blob/master/ja_JP/code-of-conduct.md), [Português](https://github.com/sindresorhus/ava-docs/blob/master/pt_BR/code-of-conduct.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/code-of-conduct.md) 4 | 5 | As contributors and maintainers of this project, and in the interest of 6 | fostering an open and welcoming community, we pledge to respect all people who 7 | contribute through reporting issues, posting feature requests, updating 8 | documentation, submitting pull requests or patches, and other activities. 9 | 10 | We are committed to making participation in this project a harassment-free 11 | experience for everyone, regardless of level of experience, gender, gender 12 | identity and expression, sexual orientation, disability, personal appearance, 13 | body size, race, ethnicity, age, religion, or nationality. 14 | 15 | Examples of unacceptable behavior by participants include: 16 | 17 | * The use of sexualized language or imagery 18 | * Personal attacks 19 | * Trolling or insulting/derogatory comments 20 | * Public or private harassment 21 | * Publishing other's private information, such as physical or electronic 22 | addresses, without explicit permission 23 | * Other unethical or unprofessional conduct 24 | 25 | Project maintainers have the right and responsibility to remove, edit, or 26 | reject comments, commits, code, wiki edits, issues, and other contributions 27 | that are not aligned to this Code of Conduct, or to ban temporarily or 28 | permanently any contributor for other behaviors that they deem inappropriate, 29 | threatening, offensive, or harmful. 30 | 31 | By adopting this Code of Conduct, project maintainers commit themselves to 32 | fairly and consistently applying these principles to every aspect of managing 33 | this project. Project maintainers who do not follow or enforce the Code of 34 | Conduct may be permanently removed from the project team. 35 | 36 | This Code of Conduct applies both within project spaces and in public spaces 37 | when an individual is representing the project or its community. 38 | 39 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 40 | reported by contacting a project maintainer at sindresorhus@gmail.com. All 41 | complaints will be reviewed and investigated and will result in a response that 42 | is deemed necessary and appropriate to the circumstances. Maintainers are 43 | obligated to maintain confidentiality with regard to the reporter of an 44 | incident. 45 | 46 | 47 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 48 | version 1.3.0, available at 49 | [http://contributor-covenant.org/version/1/3/0/][version] 50 | 51 | [homepage]: http://contributor-covenant.org 52 | [version]: http://contributor-covenant.org/version/1/3/0/ 53 | -------------------------------------------------------------------------------- /profile.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | // iron-node does not work with forked processes 4 | // This cli command will run a single file in the current process. 5 | // Intended to be used with iron-node for profiling purposes. 6 | 7 | var path = require('path'); 8 | var EventEmitter = require('events').EventEmitter; 9 | var meow = require('meow'); 10 | var Promise = require('bluebird'); 11 | var pkgConf = require('pkg-conf'); 12 | var arrify = require('arrify'); 13 | var findCacheDir = require('find-cache-dir'); 14 | var uniqueTempDir = require('unique-temp-dir'); 15 | var CachingPrecompiler = require('./lib/caching-precompiler'); 16 | var globals = require('./lib/globals'); 17 | 18 | // Chrome gets upset when the `this` value is non-null for these functions. 19 | globals.setTimeout = setTimeout.bind(null); 20 | globals.clearTimeout = clearTimeout.bind(null); 21 | 22 | Promise.longStackTraces(); 23 | var conf = pkgConf.sync('ava', { 24 | defaults: { 25 | babel: 'default' 26 | } 27 | }); 28 | 29 | // Define a minimal set of options from the main CLI. 30 | var cli = meow([ 31 | 'Usage', 32 | ' $ iron-node node_modules/ava/profile.js ', 33 | '', 34 | 'Options', 35 | ' --fail-fast Stop after first test failure', 36 | ' --serial, -s Run tests serially', 37 | ' --require, -r Module to preload (Can be repeated)', 38 | '' 39 | ], { 40 | string: [ 41 | '_', 42 | 'require' 43 | ], 44 | boolean: [ 45 | 'fail-fast', 46 | 'verbose', 47 | 'serial', 48 | 'tap' 49 | ], 50 | default: conf, 51 | alias: { 52 | r: 'require', 53 | s: 'serial' 54 | } 55 | }); 56 | 57 | if (cli.input.length !== 1) { 58 | throw new Error('Specify a test file'); 59 | } 60 | 61 | var file = path.resolve(cli.input[0]); 62 | var cacheDir = findCacheDir({name: 'ava', files: [file]}) || uniqueTempDir(); 63 | var opts = { 64 | file: file, 65 | failFast: cli.flags.failFast, 66 | serial: cli.flags.serial, 67 | require: arrify(cli.flags.require), 68 | tty: false, 69 | cacheDir: cacheDir, 70 | precompiled: new CachingPrecompiler(cacheDir, conf.babel).generateHashForFile(file) 71 | }; 72 | 73 | var events = new EventEmitter(); 74 | 75 | // Mock the behavior of a parent process. 76 | process.send = function (data) { 77 | if (data && data.ava) { 78 | var name = data.name.replace(/^ava-/, ''); 79 | 80 | if (events.listenerCount(name)) { 81 | events.emit(name, data.data); 82 | } else { 83 | console.log('UNHANDLED AVA EVENT:', name, data.data); 84 | } 85 | 86 | return; 87 | } 88 | 89 | console.log('NON AVA EVENT:', data); 90 | }; 91 | 92 | events.on('test', function (data) { 93 | console.log('TEST:', data.title, data.error); 94 | }); 95 | 96 | events.on('results', function (data) { 97 | console.profileEnd(); 98 | console.log('RESULTS:', data.stats); 99 | }); 100 | 101 | events.on('stats', function () { 102 | setImmediate(function () { 103 | process.emit('ava-run'); 104 | }); 105 | }); 106 | 107 | // test-worker will read process.argv[2] for options 108 | process.argv[2] = JSON.stringify(opts); 109 | process.argv.length = 3; 110 | 111 | console.profile('AVA test-worker process'); 112 | 113 | setImmediate(function () { 114 | require('./lib/test-worker'); 115 | }); 116 | -------------------------------------------------------------------------------- /test/visual/run-visual-tests.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | require('loud-rejection'); 4 | var path = require('path'); 5 | var childProcess = require('child_process'); 6 | var chalk = require('chalk'); 7 | var arrify = require('arrify'); 8 | var Promise = require('bluebird'); 9 | var pify = require('pify'); 10 | var inquirer = pify(require('inquirer'), Promise); 11 | var cwd = path.resolve(__dirname, '../../'); 12 | 13 | function fixture(fixtureName) { 14 | if (!path.extname(fixtureName)) { 15 | fixtureName += '.js'; 16 | } 17 | return path.join(__dirname, fixtureName); 18 | } 19 | 20 | function exec(args) { 21 | childProcess.spawnSync(process.execPath, ['./cli.js'].concat(arrify(args)), { 22 | cwd: cwd, 23 | stdio: 'inherit' 24 | }); 25 | } 26 | 27 | function run(name, args, message, question) { 28 | return new Promise(function (resolve, reject) { 29 | console.log(chalk.cyan('**BEGIN ' + name + '**')); 30 | exec(args); 31 | console.log(chalk.cyan('**END ' + name + '**\n')); 32 | console.log(arrify(message).join('\n') + '\n'); 33 | inquirer.prompt( 34 | [{ 35 | type: 'confirm', 36 | name: 'confirmed', 37 | message: question || 'Does it appear correctly', 38 | default: false 39 | }], 40 | function (data) { 41 | if (!data.confirmed) { 42 | reject(new Error(arrify(args).join(' ') + ' failed')); 43 | } 44 | resolve(); 45 | } 46 | ); 47 | }); 48 | } 49 | 50 | // thunked version of run for promise handlers 51 | function thenRun() { 52 | var args = Array.prototype.slice.call(arguments); 53 | return function () { 54 | return run.apply(null, args); 55 | }; 56 | } 57 | 58 | run( 59 | 'console.log() should not mess up mini reporter', 60 | fixture('console-log'), 61 | [ 62 | 'The output should have four logged lines in the following order (no empty lines in between): ', 63 | '', 64 | ' foo', 65 | ' bar', 66 | ' baz', 67 | ' quz', 68 | '', 69 | 'The mini reporter output (2 passes) should only appear at the end.' 70 | ]) 71 | 72 | .then(thenRun( 73 | 'stdout.write() should not mess up the mini reporter', 74 | fixture('stdout-write'), 75 | [ 76 | 'The output should have a single logged line: ', 77 | '', 78 | ' foo bar baz quz', 79 | '', 80 | 'The mini reporter output (3 passed) should only appear at the end' 81 | ] 82 | )) 83 | 84 | .then(thenRun( 85 | 'stdout.write() of lines that are exactly the same width as the terminal', 86 | fixture('text-ends-at-terminal-width'), 87 | [ 88 | 'The fixture runs twelve tests, each logging about half a line of text.', 89 | 'The end result should be six lines of numbers, with no empty lines in between.', 90 | 'Each line should fill the terminal completely left to right.', 91 | '', 92 | 'The mini reporter output (12 passed) should appear at the end.' 93 | ] 94 | )) 95 | 96 | .then(thenRun( 97 | 'complex output', 98 | fixture('lorem-ipsum'), 99 | [ 100 | 'You should see the entire contents of the Gettysburg address.', 101 | 'Three paragraphs with a blank line in between each.', 102 | 'There should be no other blank lines within the speech text.', 103 | 'The test counter should display "399 passed 1 failed" at the bottom.' 104 | ] 105 | )) 106 | ; 107 | -------------------------------------------------------------------------------- /bench/run.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var childProcess = require('child_process'); 4 | var path = require('path'); 5 | var fs = require('fs'); 6 | var arrify = require('arrify'); 7 | var Promise = require('bluebird'); 8 | var mkdirp = require('mkdirp'); 9 | var branch = require('git-branch').sync(path.join(__dirname, '..')); 10 | var cliPath = require.resolve('../cli'); 11 | 12 | function runTests(_args) { 13 | return new Promise(function (resolve) { 14 | var args = [cliPath] 15 | .concat(arrify(_args)); 16 | var start = Date.now(); 17 | childProcess.execFile(process.execPath, args, { 18 | cwd: __dirname, 19 | maxBuffer: 100000 * 200 20 | }, function (err, stdout, stderr) { 21 | var end = Date.now(); 22 | resolve({ 23 | args: arrify(_args), 24 | time: end - start, 25 | err: err, 26 | stdout: stdout, 27 | stderr: stderr 28 | }); 29 | }); 30 | }); 31 | } 32 | 33 | var list; 34 | 35 | if (process.argv.length === 2) { 36 | list = [ 37 | {args: 'other/failures.js', shouldFail: true}, 38 | 'serial/alternating-sync-async.js', 39 | 'serial/async-immediate.js', 40 | 'serial/async-timeout.js', 41 | 'serial/sync.js', 42 | 'concurrent/alternating-sync-async.js', 43 | 'concurrent/async-immediate.js', 44 | 'concurrent/async-timeout.js', 45 | 'concurrent/sync.js', 46 | ['concurrent/*.js', 'serial/*.js'] 47 | ].map(function (definition) { 48 | if (Array.isArray(definition) || typeof definition === 'string') { 49 | definition = { 50 | shouldFail: false, 51 | args: definition 52 | }; 53 | } 54 | return definition; 55 | }); 56 | } else { 57 | list = []; 58 | var currentArgs = []; 59 | var shouldFail = false; 60 | process.argv.slice(2).forEach(function (arg) { 61 | if (arg === '--') { 62 | list.push({ 63 | args: currentArgs, 64 | shouldFail: shouldFail 65 | }); 66 | currentArgs = []; 67 | shouldFail = false; 68 | return; 69 | } 70 | if (arg === '--should-fail') { 71 | shouldFail = true; 72 | return; 73 | } 74 | currentArgs.push(arg); 75 | }); 76 | if (currentArgs.length) { 77 | list.push({ 78 | args: currentArgs, 79 | shouldFail: shouldFail 80 | }); 81 | } 82 | } 83 | 84 | list.forEach(function (definition) { 85 | definition.args = ['--verbose'].concat(definition.args); 86 | }); 87 | 88 | var combined = []; 89 | for (var i = 0; i < 11; i++) { 90 | combined = combined.concat(list); 91 | } 92 | 93 | var results = {}; 94 | 95 | Promise.each(combined, function (definition) { 96 | var args = definition.args; 97 | return runTests(args).then(function (result) { 98 | var key = result.args.join(' '); 99 | var passedOrFaild = result.err ? 'failed' : 'passed'; 100 | var seconds = result.time / 1000; 101 | console.log('%s %s in %d seconds', key, passedOrFaild, seconds); 102 | if (result.err && !definition.shouldFail) { 103 | console.log(result.stdout); 104 | console.log(result.stderr); 105 | throw (result.err); 106 | } 107 | results[key] = results[key] || []; 108 | results[key].push({passed: !results.err, shouldFail: definition.shouldFail, time: seconds}); 109 | }); 110 | }).then(function () { 111 | mkdirp.sync(path.join(__dirname, '.results')); 112 | results['.time'] = Date.now(); 113 | fs.writeFileSync( 114 | path.join(__dirname, '.results', branch + '.json'), 115 | JSON.stringify(results, null, 4) 116 | ); 117 | }); 118 | -------------------------------------------------------------------------------- /media/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /lib/runner.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var Promise = require('bluebird'); 5 | var optionChain = require('option-chain'); 6 | var matcher = require('matcher'); 7 | var TestCollection = require('./test-collection'); 8 | 9 | function noop() {} 10 | 11 | var chainableMethods = { 12 | spread: true, 13 | defaults: { 14 | type: 'test', 15 | serial: false, 16 | exclusive: false, 17 | skipped: false, 18 | todo: false, 19 | callback: false 20 | }, 21 | chainableMethods: { 22 | test: {}, 23 | serial: {serial: true}, 24 | before: {type: 'before'}, 25 | after: {type: 'after'}, 26 | skip: {skipped: true}, 27 | todo: {todo: true}, 28 | only: {exclusive: true}, 29 | beforeEach: {type: 'beforeEach'}, 30 | afterEach: {type: 'afterEach'}, 31 | cb: {callback: true} 32 | } 33 | }; 34 | 35 | function Runner(options) { 36 | if (!(this instanceof Runner)) { 37 | throw new TypeError('Class constructor Runner cannot be invoked without \'new\''); 38 | } 39 | options = options || {}; 40 | 41 | EventEmitter.call(this); 42 | 43 | this.tests = new TestCollection(); 44 | this._bail = options.bail; 45 | this._serial = options.serial; 46 | this._match = options.match || []; 47 | 48 | this._addTestResult = this._addTestResult.bind(this); 49 | } 50 | 51 | util.inherits(Runner, EventEmitter); 52 | module.exports = Runner; 53 | 54 | optionChain(chainableMethods, function (opts, title, fn) { 55 | if (typeof title === 'function') { 56 | fn = title; 57 | title = null; 58 | } 59 | 60 | if (opts.todo) { 61 | fn = noop; 62 | 63 | if (typeof title !== 'string') { 64 | throw new TypeError('`todo` tests require a title'); 65 | } 66 | } else if (typeof fn !== 'function') { 67 | throw new TypeError('Expected a function. Use `test.todo()` for tests without a function.'); 68 | } 69 | 70 | if (this._serial) { 71 | opts.serial = true; 72 | } 73 | 74 | if (opts.type === 'test' && this._match.length > 0) { 75 | opts.exclusive = title !== null && matcher([title], this._match).length === 1; 76 | } 77 | 78 | this.tests.add({ 79 | metadata: opts, 80 | fn: fn, 81 | title: title 82 | }); 83 | }, Runner.prototype); 84 | 85 | Runner.prototype._addTestResult = function (result) { 86 | var test = result.result; 87 | 88 | if (test.metadata.type === 'test') { 89 | this.stats.testCount++; 90 | 91 | if (test.metadata.todo) { 92 | this.stats.todoCount++; 93 | } else if (test.metadata.skipped) { 94 | this.stats.skipCount++; 95 | } 96 | } 97 | 98 | if (!result.passed) { 99 | this.stats.failCount++; 100 | } 101 | 102 | var props = { 103 | duration: test.duration, 104 | title: test.title, 105 | error: result.reason, 106 | type: test.metadata.type, 107 | skip: test.metadata.skipped, 108 | todo: test.metadata.todo 109 | }; 110 | 111 | this.emit('test', props); 112 | }; 113 | 114 | Runner.prototype.run = function (options) { 115 | var stats = this.stats = { 116 | failCount: 0, 117 | passCount: 0, 118 | skipCount: 0, 119 | todoCount: 0, 120 | testCount: 0 121 | }; 122 | 123 | if (options.runOnlyExclusive && !this.tests.hasExclusive) { 124 | return Promise.resolve(); 125 | } 126 | 127 | this.tests.on('test', this._addTestResult); 128 | 129 | return Promise.resolve(this.tests.build(this._bail).run()).then(function () { 130 | stats.passCount = stats.testCount - stats.failCount - stats.skipCount - stats.todoCount; 131 | }); 132 | }; 133 | -------------------------------------------------------------------------------- /contributing.md: -------------------------------------------------------------------------------- 1 | # Contributing to AVA 2 | 3 | ✨ Thanks for contributing to AVA! ✨ 4 | 5 | Please note that this project is released with a [Contributor Code of Conduct](code-of-conduct.md). By participating in this project you agree to abide by its terms. 6 | 7 | Translations: [Español](https://github.com/sindresorhus/ava-docs/blob/master/es_ES/contributing.md), [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/contributing.md), [日本語](https://github.com/sindresorhus/ava-docs/blob/master/ja_JP/contributing.md), [Português](https://github.com/sindresorhus/ava-docs/blob/master/pt_BR/contributing.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/contributing.md) 8 | 9 | ## How can I contribute? 10 | 11 | ### Improve documentation 12 | 13 | As a user of AVA you're the perfect candidate to help us improve our documentation. Typo corrections, error fixes, better explanations, more examples, etc. Open issues for things that could be improved. [Help translate our docs.](https://github.com/sindresorhus/ava-docs) Anything. Even improvements to this document. 14 | 15 | ### Improve issues 16 | 17 | Some issues are created with missing information, not reproducible, or plain invalid. Help make them easier to resolve. Handling issues takes a lot of time that we could rather spend on fixing bugs and adding features. 18 | 19 | ### Give feedback on issues 20 | 21 | We're always looking for more opinions on discussions in the issue tracker. It's a good opportunity to influence the future direction of AVA. 22 | 23 | ### Hang out in our chat 24 | 25 | We have a [chat](https://gitter.im/sindresorhus/ava). Jump in there and lurk, talk to us, and help others. 26 | 27 | ### Submitting an issue 28 | 29 | - The issue tracker is for issues. Use our [chat](https://gitter.im/sindresorhus/ava) or [Stack Overflow](https://stackoverflow.com/questions/tagged/ava) for support. 30 | - Search the issue tracker before opening an issue. 31 | - Ensure you're using the latest version of AVA. 32 | - Use a clear and descriptive title. 33 | - Include as much information as possible: Steps to reproduce the issue, error message, Node.js version, operating system, etc. 34 | - The more time you put into an issue, the more we will. 35 | - [The best issue report is a failing test proving it.](https://twitter.com/sindresorhus/status/579306280495357953) 36 | 37 | ### Submitting a pull request 38 | 39 | - Non-trivial changes are often best discussed in an issue first, to prevent you from doing unnecessary work. 40 | - For ambitious tasks, you should try to get your work in front of the community for feedback as soon as possible. Open a pull request as soon as you have done the minimum needed to demonstrate your idea. At this early stage, don't worry about making things perfect, or 100% complete. Add a [WIP] prefix to the title, and describe what you still need to do. This lets reviewers know not to nit-pick small details or point out improvements you already know you need to make. 41 | - New features should be accompanied with tests and documentation. 42 | - Don't include unrelated changes. 43 | - Lint and test before submitting the pull request by running `$ npm test`. 44 | - Make the pull request from a [topic branch](https://github.com/dchelimsky/rspec/wiki/Topic-Branches), not master. 45 | - Use a clear and descriptive title for the pull request and commits. 46 | - Write a convincing description of why we should land your pull request. It's your job to convince us. Answer "why" it's needed and provide use-cases. 47 | - You might be asked to do changes to your pull request. There's never a need to open another pull request. [Just update the existing one.](https://github.com/RichardLitt/docs/blob/master/amending-a-commit-guide.md) 48 | -------------------------------------------------------------------------------- /lib/reporters/verbose.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var prettyMs = require('pretty-ms'); 3 | var figures = require('figures'); 4 | var colors = require('../colors'); 5 | var plur = require('plur'); 6 | 7 | Object.keys(colors).forEach(function (key) { 8 | colors[key].enabled = true; 9 | }); 10 | 11 | function VerboseReporter() { 12 | if (!(this instanceof VerboseReporter)) { 13 | return new VerboseReporter(); 14 | } 15 | } 16 | 17 | module.exports = VerboseReporter; 18 | 19 | VerboseReporter.prototype.start = function () { 20 | return ''; 21 | }; 22 | 23 | VerboseReporter.prototype.test = function (test) { 24 | if (test.error) { 25 | return ' ' + colors.error(figures.cross) + ' ' + test.title + ' ' + colors.error(test.error.message); 26 | } 27 | 28 | if (test.todo) { 29 | return ' ' + colors.todo('- ' + test.title); 30 | } else if (test.skip) { 31 | return ' ' + colors.skip('- ' + test.title); 32 | } 33 | 34 | if (this.api.fileCount === 1 && this.api.testCount === 1 && test.title === '[anonymous]') { 35 | return undefined; 36 | } 37 | 38 | // display duration only over a threshold 39 | var threshold = 100; 40 | var duration = test.duration > threshold ? colors.duration(' (' + prettyMs(test.duration) + ')') : ''; 41 | 42 | return ' ' + colors.pass(figures.tick) + ' ' + test.title + duration; 43 | }; 44 | 45 | VerboseReporter.prototype.unhandledError = function (err) { 46 | if (err.type === 'exception' && err.name === 'AvaError') { 47 | return colors.error(' ' + figures.cross + ' ' + err.message); 48 | } 49 | 50 | var types = { 51 | rejection: 'Unhandled Rejection', 52 | exception: 'Uncaught Exception' 53 | }; 54 | 55 | var output = colors.error(types[err.type] + ':', err.file) + '\n'; 56 | 57 | if (err.stack) { 58 | output += ' ' + colors.stack(err.stack) + '\n'; 59 | } else { 60 | output += ' ' + colors.stack(JSON.stringify(err)) + '\n'; 61 | } 62 | 63 | output += '\n'; 64 | 65 | return output; 66 | }; 67 | 68 | VerboseReporter.prototype.finish = function () { 69 | var output = '\n'; 70 | 71 | if (this.api.failCount > 0) { 72 | output += ' ' + colors.error(this.api.failCount, plur('test', this.api.failCount), 'failed') + '\n'; 73 | } else { 74 | output += ' ' + colors.pass(this.api.passCount, plur('test', this.api.passCount), 'passed') + '\n'; 75 | } 76 | 77 | if (this.api.skipCount > 0) { 78 | output += ' ' + colors.skip(this.api.skipCount, plur('test', this.api.skipCount), 'skipped') + '\n'; 79 | } 80 | 81 | if (this.api.todoCount > 0) { 82 | output += ' ' + colors.todo(this.api.todoCount, plur('test', this.api.todoCount), 'todo') + '\n'; 83 | } 84 | 85 | if (this.api.rejectionCount > 0) { 86 | output += ' ' + colors.error(this.api.rejectionCount, 'unhandled', plur('rejection', this.api.rejectionCount)) + '\n'; 87 | } 88 | 89 | if (this.api.exceptionCount > 0) { 90 | output += ' ' + colors.error(this.api.exceptionCount, 'uncaught', plur('exception', this.api.exceptionCount)) + '\n'; 91 | } 92 | 93 | if (this.api.failCount > 0) { 94 | output += '\n'; 95 | 96 | var i = 0; 97 | 98 | this.api.tests.forEach(function (test) { 99 | if (!(test.error && test.error.message)) { 100 | return; 101 | } 102 | 103 | i++; 104 | 105 | output += ' ' + colors.error(i + '.', test.title) + '\n'; 106 | output += ' ' + colors.stack(test.error.stack) + '\n'; 107 | }); 108 | } 109 | 110 | return output; 111 | }; 112 | 113 | VerboseReporter.prototype.write = function (str) { 114 | console.error(str); 115 | }; 116 | 117 | VerboseReporter.prototype.stdout = VerboseReporter.prototype.stderr = function (data) { 118 | process.stderr.write(data); 119 | }; 120 | -------------------------------------------------------------------------------- /test/logger.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | var Logger = require('../lib/logger'); 4 | var tap = require('../lib/reporters/tap'); 5 | 6 | test('must be called with new', function (t) { 7 | t.throws(function () { 8 | var logger = Logger; 9 | logger(); 10 | }, {message: 'Class constructor Logger cannot be invoked without \'new\''}); 11 | t.end(); 12 | }); 13 | 14 | test('only call start if supported by reporter', function (t) { 15 | var tapReporter = tap(); 16 | var logger = new Logger(tapReporter); 17 | tapReporter.start = undefined; 18 | logger.start(); 19 | t.end(); 20 | }); 21 | 22 | test('only write if start is supported by reporter', function (t) { 23 | var tapReporter = tap(); 24 | var logger = new Logger(tapReporter); 25 | tapReporter.start = undefined; 26 | logger.write = t.fail; 27 | logger.start(); 28 | t.end(); 29 | }); 30 | 31 | test('only call reset if supported by reporter', function (t) { 32 | var tapReporter = tap(); 33 | var logger = new Logger(tapReporter); 34 | tapReporter.reset = undefined; 35 | logger.reset(); 36 | t.end(); 37 | }); 38 | 39 | test('only write if reset is supported by reporter', function (t) { 40 | var tapReporter = tap(); 41 | var logger = new Logger(tapReporter); 42 | tapReporter.reset = undefined; 43 | logger.write = t.fail; 44 | logger.reset(); 45 | t.end(); 46 | }); 47 | 48 | test('writes the reporter reset result', function (t) { 49 | var tapReporter = tap(); 50 | var logger = new Logger(tapReporter); 51 | tapReporter.reset = function () { 52 | return 'test reset'; 53 | }; 54 | logger.write = function (str) { 55 | t.equal(str, 'test reset'); 56 | t.end(); 57 | }; 58 | logger.reset(); 59 | }); 60 | 61 | test('only call unhandledError if supported by reporter', function (t) { 62 | var tapReporter = tap(); 63 | var logger = new Logger(tapReporter); 64 | tapReporter.unhandledError = undefined; 65 | logger.unhandledError(); 66 | t.end(); 67 | }); 68 | 69 | test('only write if unhandledError is supported by reporter', function (t) { 70 | var tapReporter = tap(); 71 | var logger = new Logger(tapReporter); 72 | tapReporter.unhandledError = undefined; 73 | logger.write = t.fail; 74 | logger.unhandledError(); 75 | t.end(); 76 | }); 77 | 78 | test('only call finish if supported by reporter', function (t) { 79 | var tapReporter = tap(); 80 | var logger = new Logger(tapReporter); 81 | tapReporter.finish = undefined; 82 | logger.finish(); 83 | t.end(); 84 | }); 85 | 86 | test('only write if finish is supported by reporter', function (t) { 87 | var tapReporter = tap(); 88 | var logger = new Logger(tapReporter); 89 | tapReporter.finish = undefined; 90 | logger.write = t.fail; 91 | logger.finish(); 92 | t.end(); 93 | }); 94 | 95 | test('only call write if supported by reporter', function (t) { 96 | var tapReporter = tap(); 97 | var logger = new Logger(tapReporter); 98 | tapReporter.write = undefined; 99 | logger.write(); 100 | t.end(); 101 | }); 102 | 103 | test('only call stdout if supported by reporter', function (t) { 104 | var tapReporter = tap(); 105 | var logger = new Logger(tapReporter); 106 | tapReporter.stdout = undefined; 107 | logger.stdout(); 108 | t.end(); 109 | }); 110 | 111 | test('don\'t alter data when calling stdout', function (t) { 112 | var tapReporter = tap(); 113 | var logger = new Logger(tapReporter); 114 | tapReporter.stdout = function (data) { 115 | t.equal(data, 'test data'); 116 | t.end(); 117 | }; 118 | logger.stdout('test data'); 119 | }); 120 | 121 | test('only call stderr if supported by reporter', function (t) { 122 | var tapReporter = tap(); 123 | var logger = new Logger(tapReporter); 124 | tapReporter.stderr = undefined; 125 | logger.stderr(); 126 | t.end(); 127 | }); 128 | -------------------------------------------------------------------------------- /lib/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var assert = require('core-assert'); 3 | var deepEqual = require('only-shallow'); 4 | var observableToPromise = require('observable-to-promise'); 5 | var isObservable = require('is-observable'); 6 | var isPromise = require('is-promise'); 7 | var util = require('util'); 8 | var x = module.exports; 9 | 10 | Object.defineProperty(x, 'AssertionError', {value: assert.AssertionError}); 11 | 12 | function noop() {} 13 | 14 | function create(val, expected, operator, msg, fn) { 15 | return { 16 | actual: val, 17 | expected: expected, 18 | message: msg, 19 | operator: operator, 20 | stackStartFunction: fn 21 | }; 22 | } 23 | 24 | function test(ok, opts) { 25 | if (!ok) { 26 | throw new assert.AssertionError(opts); 27 | } 28 | } 29 | 30 | x.pass = function (msg) { 31 | test(true, create(true, true, 'pass', msg, x.pass)); 32 | }; 33 | 34 | x.fail = function (msg) { 35 | msg = msg || 'Test failed via t.fail()'; 36 | test(false, create(false, false, 'fail', msg, x.fail)); 37 | }; 38 | 39 | x.ok = function (val, msg) { 40 | test(val, create(val, true, '==', msg, x.ok)); 41 | }; 42 | 43 | x.notOk = function (val, msg) { 44 | test(!val, create(val, false, '==', msg, x.notOk)); 45 | }; 46 | 47 | x.true = function (val, msg) { 48 | test(val === true, create(val, true, '===', msg, x.true)); 49 | }; 50 | 51 | x.false = function (val, msg) { 52 | test(val === false, create(val, false, '===', msg, x.false)); 53 | }; 54 | 55 | x.is = function (val, expected, msg) { 56 | test(val === expected, create(val, expected, '===', msg, x.is)); 57 | }; 58 | 59 | x.not = function (val, expected, msg) { 60 | test(val !== expected, create(val, expected, '!==', msg, x.not)); 61 | }; 62 | 63 | x.same = function (val, expected, msg) { 64 | test(deepEqual(val, expected), create(val, expected, '===', msg, x.same)); 65 | }; 66 | 67 | x.notSame = function (val, expected, msg) { 68 | test(!deepEqual(val, expected), create(val, expected, '!==', msg, x.notSame)); 69 | }; 70 | 71 | x.throws = function (fn, err, msg) { 72 | if (isObservable(fn)) { 73 | fn = observableToPromise(fn); 74 | } 75 | 76 | if (isPromise(fn)) { 77 | return fn 78 | .then(function () { 79 | x.throws(noop, err, msg); 80 | }, function (fnErr) { 81 | return x.throws(function () { 82 | throw fnErr; 83 | }, err, msg); 84 | }); 85 | } 86 | 87 | try { 88 | if (typeof err === 'string') { 89 | var errMsg = err; 90 | err = function (err) { 91 | return err.message === errMsg; 92 | }; 93 | } 94 | 95 | var result; 96 | 97 | assert.throws(function () { 98 | try { 99 | fn(); 100 | } catch (error) { 101 | result = error; 102 | throw error; 103 | } 104 | }, err, msg); 105 | 106 | return result; 107 | } catch (err) { 108 | test(false, create(err.actual, err.expected, err.operator, err.message, x.throws)); 109 | } 110 | }; 111 | 112 | x.notThrows = function (fn, msg) { 113 | if (isObservable(fn)) { 114 | fn = observableToPromise(fn); 115 | } 116 | 117 | if (isPromise(fn)) { 118 | return fn 119 | .catch(function (err) { 120 | x.notThrows(function () { 121 | throw err; 122 | }, msg); 123 | }); 124 | } 125 | 126 | try { 127 | assert.doesNotThrow(fn, msg); 128 | } catch (err) { 129 | test(false, create(err.actual, err.expected, err.operator, err.message, x.notThrows)); 130 | } 131 | }; 132 | 133 | x.doesNotThrow = util.deprecate(x.notThrows, 't.doesNotThrow is renamed to t.notThrows. The old name still works, but will be removed in AVA 1.0.0. Update your references.'); 134 | 135 | x.regex = function (contents, regex, msg) { 136 | test(regex.test(contents), create(regex, contents, '===', msg, x.regex)); 137 | }; 138 | 139 | x.ifError = x.error = function (err, msg) { 140 | test(!err, create(err, 'Error', '!==', msg, x.ifError)); 141 | }; 142 | -------------------------------------------------------------------------------- /lib/caching-precompiler.js: -------------------------------------------------------------------------------- 1 | var fs = require('fs'); 2 | var path = require('path'); 3 | var cachingTransform = require('caching-transform'); 4 | var md5Hex = require('md5-hex'); 5 | var stripBom = require('strip-bom'); 6 | var objectAssign = require('object-assign'); 7 | 8 | module.exports = CachingPrecompiler; 9 | 10 | function CachingPrecompiler(cacheDir, babelConfig) { 11 | if (!(this instanceof CachingPrecompiler)) { 12 | throw new TypeError('Class constructor CachingPrecompiler cannot be invoked without \'new\''); 13 | } 14 | 15 | this.cacheDir = cacheDir; 16 | this.filenameToHash = {}; 17 | this.transform = this._createTransform(babelConfig); 18 | } 19 | 20 | CachingPrecompiler.prototype._factory = function (babelConfig, cacheDir) { 21 | // This factory method is only called once per process, and only as needed, to defer loading expensive dependencies. 22 | var babel = require('babel-core'); 23 | var convertSourceMap = require('convert-source-map'); 24 | var presetStage2 = require('babel-preset-stage-2'); 25 | var presetES2015 = require('babel-preset-es2015'); 26 | var transformRuntime = require('babel-plugin-transform-runtime'); 27 | 28 | var powerAssert = this._createEspowerPlugin(babel); 29 | 30 | function buildOptions(filename, code) { 31 | // Extract existing source maps from the code. 32 | var sourceMap = convertSourceMap.fromSource(code) || convertSourceMap.fromMapFileSource(code, path.dirname(filename)); 33 | 34 | var options = {babelrc: false}; 35 | 36 | if (!babelConfig || babelConfig === 'default') { 37 | objectAssign(options, {presets: [presetStage2, presetES2015]}); 38 | } else if (babelConfig === 'inherit') { 39 | objectAssign(options, {babelrc: true}); 40 | } else { 41 | objectAssign(options, babelConfig); 42 | } 43 | 44 | objectAssign(options, { 45 | inputSourceMap: sourceMap && sourceMap.toObject(), 46 | filename: filename, 47 | sourceMaps: true, 48 | ast: false 49 | }); 50 | 51 | options.plugins = options.plugins || []; 52 | options.plugins.push(powerAssert, transformRuntime); 53 | 54 | return options; 55 | } 56 | 57 | return function (code, filename, hash) { 58 | code = code.toString(); 59 | var options = buildOptions(filename, code); 60 | var result = babel.transform(code, options); 61 | var mapFile = path.join(cacheDir, hash + '.map'); 62 | fs.writeFileSync(mapFile, JSON.stringify(result.map)); 63 | return result.code; 64 | }; 65 | }; 66 | 67 | CachingPrecompiler.prototype._createEspowerPlugin = function (babel) { 68 | var createEspowerPlugin = require('babel-plugin-espower/create'); 69 | var enhanceAssert = require('./enhance-assert'); 70 | 71 | // initialize power-assert 72 | return createEspowerPlugin(babel, { 73 | patterns: enhanceAssert.PATTERNS 74 | }); 75 | }; 76 | 77 | CachingPrecompiler.prototype._createTransform = function (babelConfig) { 78 | return cachingTransform({ 79 | factory: this._factory.bind(this, babelConfig), 80 | cacheDir: this.cacheDir, 81 | salt: new Buffer(JSON.stringify({ 82 | 'babel-plugin-espower': require('babel-plugin-espower/package.json').version, 83 | 'ava': require('../package.json').version, 84 | 'babel-core': require('babel-core/package.json').version, 85 | 'babelConfig': babelConfig 86 | })), 87 | ext: '.js', 88 | hash: this._hash.bind(this) 89 | }); 90 | }; 91 | 92 | CachingPrecompiler.prototype._hash = function (code, filename, salt) { 93 | var hash = md5Hex([code, filename, salt]); 94 | this.filenameToHash[filename] = hash; 95 | return hash; 96 | }; 97 | 98 | CachingPrecompiler.prototype.precompileFile = function (filename) { 99 | if (!this.filenameToHash[filename]) { 100 | this.transform(stripBom(fs.readFileSync(filename)), filename); 101 | } 102 | 103 | return this.filenameToHash[filename]; 104 | }; 105 | 106 | CachingPrecompiler.prototype.generateHashForFile = function (filename) { 107 | var hash = {}; 108 | hash[filename] = this.precompileFile(filename); 109 | return hash; 110 | }; 111 | -------------------------------------------------------------------------------- /docs/recipes/watch-mode.md: -------------------------------------------------------------------------------- 1 | # Watch mode 2 | 3 | Translations: [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/watch-mode.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/watch-mode.md) 4 | 5 | AVA comes with an intelligent watch mode. It watches for files to change and runs just those tests that are affected. 6 | 7 | ## Running tests with watch mode enabled 8 | 9 | You can enable watch mode using the `--watch` or `-w` flags. If you have installed AVA globally: 10 | 11 | ```console 12 | $ ava --watch 13 | ``` 14 | 15 | If you've configured it in your `package.json` like this: 16 | 17 | ```json 18 | { 19 | "scripts": { 20 | "test": "ava" 21 | } 22 | } 23 | ``` 24 | 25 | You can run: 26 | 27 | ```console 28 | $ npm test -- --watch 29 | ``` 30 | 31 | You could also set up a special script: 32 | 33 | ```json 34 | { 35 | "scripts": { 36 | "test": "ava", 37 | "test:watch": "ava --watch" 38 | } 39 | } 40 | ``` 41 | 42 | And then use: 43 | 44 | ```console 45 | $ npm run test:watch 46 | ``` 47 | 48 | ## Requirements 49 | 50 | AVA uses [`chokidar`] as the file watcher. It's configured as an optional dependency since `chokidar` sometimes can't be installed. Watch mode is not available if `chokidar` fails to install, instead you'll see a message like: 51 | 52 | > The optional dependency chokidar failed to install and is required for --watch. Chokidar is likely not supported on your platform. 53 | 54 | Please refer to the [`chokidar` documentation][`chokidar`] for how to resolve this problem. 55 | 56 | ## Source files and test files 57 | 58 | In AVA there's a distinction between *source files* and *test files*. As you can imagine the *test files* contain your tests. *Source files* are all other files that are needed for the tests to run, be it your source code or test fixtures. 59 | 60 | By default AVA watches for changes to the test files, `package.json`, and any other `.js` files. It'll ignore files in [certain directories](https://github.com/novemberborn/ignore-by-default/blob/master/index.js) as provided by the [`ignore-by-default`] package. 61 | 62 | You can configure patterns for the source files using the [`--source` CLI flag] or in the `ava` section of your `package.json` file. Note that if you specify a negative pattern the directories from [`ignore-by-default`] will no longer be ignored, so you may want to repeat these in your config. 63 | 64 | If your tests write to disk they may trigger the watcher to rerun your tests. If this occurs you will need to use the `--source` flag. 65 | 66 | ## Dependency tracking 67 | 68 | AVA tracks which source files your test files depend on. If you change such a dependency only the test file that depends on it will be rerun. AVA will rerun all tests if it cannot determine which test file depends on the changed source file. 69 | 70 | Dependency tracking works for required modules. Custom extensions and transpilers are supported, provided you loaded them using the [`--require` CLI flag] and not from inside your test file. Files accessed using the `fs` module are not tracked. 71 | 72 | ## Manually rerunning all tests 73 | 74 | You can quickly rerun all tests by typing r on the console, followed by Enter. 75 | 76 | ## Debugging 77 | 78 | Sometimes watch mode does something surprising like rerunning all tests when you thought only a single test would be run. To see its reasoning you can enable a debug mode: 79 | 80 | ```console 81 | $ DEBUG=ava:watcher npm test -- --watch 82 | ``` 83 | 84 | On Windows use: 85 | 86 | ```console 87 | $ set DEBUG=ava:watcher 88 | $ npm test -- --watch 89 | ``` 90 | 91 | ## Help us make watch mode better 92 | 93 | Watch mode is relatively new and there might be some rough edges. Please [report](https://github.com/sindresorhus/ava/issues) any issues you encounter. Thanks! 94 | 95 | [`chokidar`]: https://github.com/paulmillr/chokidar 96 | [`ignore-by-default`]: https://github.com/novemberborn/ignore-by-default 97 | [`--require` CLI flag]: https://github.com/sindresorhus/ava#cli 98 | [`--source` CLI flag]: https://github.com/sindresorhus/ava#cli 99 | -------------------------------------------------------------------------------- /lib/fork.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var childProcess = require('child_process'); 3 | var path = require('path'); 4 | var objectAssign = require('object-assign'); 5 | var Promise = require('bluebird'); 6 | var debug = require('debug')('ava'); 7 | var AvaError = require('./ava-error'); 8 | var doSend = require('./send'); 9 | 10 | var env = process.env; 11 | 12 | // ensure NODE_PATH paths are absolute 13 | if (env.NODE_PATH) { 14 | env = objectAssign({}, env); 15 | 16 | env.NODE_PATH = env.NODE_PATH 17 | .split(path.delimiter) 18 | .map(function (x) { 19 | return path.resolve(x); 20 | }) 21 | .join(path.delimiter); 22 | } 23 | 24 | module.exports = function (file, opts) { 25 | opts = objectAssign({ 26 | file: file, 27 | tty: process.stdout.isTTY ? { 28 | columns: process.stdout.columns, 29 | rows: process.stdout.rows 30 | } : false 31 | }, opts); 32 | 33 | var ps = childProcess.fork(path.join(__dirname, 'test-worker.js'), [JSON.stringify(opts)], { 34 | cwd: path.dirname(file), 35 | silent: true, 36 | env: env 37 | }); 38 | 39 | var relFile = path.relative('.', file); 40 | 41 | var exiting = false; 42 | var send = function (ps, name, data) { 43 | if (!exiting) { 44 | // This seems to trigger a Node bug which kills the AVA master process, at 45 | // least while running AVA's tests. See 46 | // for more details. 47 | doSend(ps, name, data); 48 | } 49 | }; 50 | 51 | var promise = new Promise(function (resolve, reject) { 52 | ps.on('error', reject); 53 | 54 | // emit `test` and `stats` events 55 | ps.on('message', function (event) { 56 | if (!event.ava) { 57 | return; 58 | } 59 | 60 | event.name = event.name.replace(/^ava\-/, ''); 61 | event.data.file = relFile; 62 | 63 | debug('ipc %s:\n%o', event.name, event.data); 64 | 65 | ps.emit(event.name, event.data); 66 | }); 67 | 68 | var testResults = []; 69 | var results; 70 | 71 | ps.on('test', function (props) { 72 | testResults.push(props); 73 | }); 74 | 75 | ps.on('results', function (data) { 76 | results = data; 77 | data.tests = testResults; 78 | send(ps, 'teardown'); 79 | }); 80 | 81 | ps.on('exit', function (code) { 82 | if (code > 0 && code !== 143) { 83 | return reject(new AvaError(relFile + ' exited with a non-zero exit code: ' + code)); 84 | } 85 | 86 | if (results) { 87 | resolve(results); 88 | } else { 89 | reject(new AvaError('Test results were not received from ' + relFile)); 90 | } 91 | }); 92 | 93 | ps.on('no-tests', function (data) { 94 | send(ps, 'teardown'); 95 | 96 | var message = 'No tests found in ' + relFile; 97 | 98 | if (!data.avaRequired) { 99 | message += ', make sure to import "ava" at the top of your test file'; 100 | } 101 | 102 | reject(new AvaError(message)); 103 | }); 104 | }); 105 | 106 | // teardown finished, now exit 107 | ps.on('teardown', function () { 108 | send(ps, 'exit'); 109 | exiting = true; 110 | }); 111 | 112 | // uncaught exception in fork, need to exit 113 | ps.on('uncaughtException', function () { 114 | send(ps, 'teardown'); 115 | }); 116 | 117 | ps.stdout.on('data', function (data) { 118 | if (!opts.silent) { 119 | ps.emit('stdout', data); 120 | } 121 | }); 122 | 123 | ps.stderr.on('data', function (data) { 124 | if (!opts.silent) { 125 | ps.emit('stderr', data); 126 | } 127 | }); 128 | 129 | promise.on = function () { 130 | ps.on.apply(ps, arguments); 131 | 132 | return promise; 133 | }; 134 | 135 | promise.send = function (name, data) { 136 | send(ps, name, data); 137 | 138 | return promise; 139 | }; 140 | 141 | // send 'run' event only when fork is listening for it 142 | var isReady = false; 143 | 144 | ps.on('stats', function () { 145 | isReady = true; 146 | }); 147 | 148 | promise.run = function (options) { 149 | if (isReady) { 150 | send(ps, 'run', options); 151 | return promise; 152 | } 153 | 154 | ps.on('stats', function () { 155 | send(ps, 'run', options); 156 | }); 157 | 158 | return promise; 159 | }; 160 | 161 | return promise; 162 | }; 163 | -------------------------------------------------------------------------------- /test/caching-precompiler.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var fs = require('fs'); 3 | var path = require('path'); 4 | var test = require('tap').test; 5 | var uniqueTempDir = require('unique-temp-dir'); 6 | var sinon = require('sinon'); 7 | var babel = require('babel-core'); 8 | var transformRuntime = require('babel-plugin-transform-runtime'); 9 | 10 | var CachingPrecompiler = require('../lib/caching-precompiler'); 11 | 12 | function fixture(name) { 13 | return path.join(__dirname, 'fixture', name); 14 | } 15 | 16 | function endsWithJs(filename) { 17 | return /\.js$/.test(filename); 18 | } 19 | 20 | function endsWithMap(filename) { 21 | return /\.js$/.test(filename); 22 | } 23 | 24 | sinon.spy(babel, 'transform'); 25 | 26 | test('creation with new', function (t) { 27 | var tempDir = uniqueTempDir(); 28 | var precompiler = new CachingPrecompiler(tempDir, null); 29 | t.is(precompiler.cacheDir, tempDir); 30 | t.end(); 31 | }); 32 | 33 | test('must be called with new', function (t) { 34 | t.throws(function () { 35 | var cachingPrecompiler = CachingPrecompiler; 36 | cachingPrecompiler(uniqueTempDir(), null); 37 | }, {message: 'Class constructor CachingPrecompiler cannot be invoked without \'new\''}); 38 | t.end(); 39 | }); 40 | 41 | test('adds files and source maps to the cache directory as needed', function (t) { 42 | var tempDir = uniqueTempDir(); 43 | var precompiler = new CachingPrecompiler(tempDir, null); 44 | 45 | t.false(fs.existsSync(tempDir), 'cache directory is not created before it is needed'); 46 | 47 | precompiler.precompileFile(fixture('es2015.js')); 48 | t.true(fs.existsSync(tempDir), 'cache directory is lazily created'); 49 | 50 | var files = fs.readdirSync(tempDir); 51 | t.is(files.length, 2); 52 | t.is(files.filter(endsWithJs).length, 1, 'one .js file is saved to the cache'); 53 | t.is(files.filter(endsWithMap).length, 1, 'one .map file is saved to the cache'); 54 | t.end(); 55 | }); 56 | 57 | test('uses default babel options when babelConfig === "default"', function (t) { 58 | var tempDir = uniqueTempDir(); 59 | var precompiler = new CachingPrecompiler(tempDir, 'default'); 60 | babel.transform.reset(); 61 | 62 | precompiler.precompileFile(fixture('es2015.js')); 63 | 64 | t.true(babel.transform.calledOnce); 65 | var options = babel.transform.firstCall.args[1]; 66 | 67 | t.true('filename' in options); 68 | t.true(options.sourceMaps); 69 | t.false(options.ast); 70 | t.true('inputSourceMap' in options); 71 | t.false(options.babelrc); 72 | t.true(Array.isArray(options.presets)); 73 | t.true(Array.isArray(options.plugins)); 74 | t.end(); 75 | }); 76 | 77 | test('allows babel config from package.json/babel when babelConfig === "inherit"', function (t) { 78 | var tempDir = uniqueTempDir(); 79 | var precompiler = new CachingPrecompiler(tempDir, 'inherit'); 80 | babel.transform.reset(); 81 | 82 | precompiler.precompileFile(fixture('es2015.js')); 83 | 84 | t.true(babel.transform.calledOnce); 85 | var options = babel.transform.firstCall.args[1]; 86 | 87 | t.true('filename' in options); 88 | t.true(options.sourceMaps); 89 | t.false(options.ast); 90 | t.true('inputSourceMap' in options); 91 | t.true(options.babelrc); 92 | t.end(); 93 | }); 94 | 95 | test('uses babelConfig for babel options when babelConfig is an object', function (t) { 96 | var tempDir = uniqueTempDir(); 97 | var customPlugin = sinon.stub().returns({visitor: {}}); 98 | var powerAssert = sinon.stub().returns({visitor: {}}); 99 | var precompiler = new CachingPrecompiler(tempDir, { 100 | presets: ['stage-2', 'es2015'], 101 | plugins: [customPlugin] 102 | }); 103 | sinon.stub(precompiler, '_createEspowerPlugin').returns(powerAssert); 104 | babel.transform.reset(); 105 | 106 | precompiler.precompileFile(fixture('es2015.js')); 107 | 108 | t.true(babel.transform.calledOnce); 109 | var options = babel.transform.firstCall.args[1]; 110 | 111 | t.true('filename' in options); 112 | t.true(options.sourceMaps); 113 | t.false(options.ast); 114 | t.true('inputSourceMap' in options); 115 | t.false(options.babelrc); 116 | t.same(options.presets, ['stage-2', 'es2015']); 117 | t.same(options.plugins, [customPlugin, powerAssert, transformRuntime]); 118 | t.end(); 119 | }); 120 | -------------------------------------------------------------------------------- /test/observable.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | var Test = require('../lib/test'); 4 | var Observable = require('./fixture/observable'); 5 | 6 | function ava(fn) { 7 | var a = new Test(fn); 8 | a.metadata = {callback: false}; 9 | return a; 10 | } 11 | 12 | ava.cb = function (fn) { 13 | var a = new Test(fn); 14 | a.metadata = {callback: true}; 15 | return a; 16 | }; 17 | 18 | test('returning an observable from a legacy async fn is an error', function (t) { 19 | ava.cb(function (a) { 20 | a.plan(2); 21 | 22 | var observable = Observable.of(); 23 | 24 | setTimeout(function () { 25 | a.pass(); 26 | a.pass(); 27 | a.end(); 28 | }, 200); 29 | 30 | return observable; 31 | }).run().then(function (result) { 32 | t.is(result.passed, false); 33 | t.match(result.reason.message, /Do not return observables/); 34 | t.end(); 35 | }); 36 | }); 37 | 38 | test('handle throws with thrown observable', function (t) { 39 | ava(function (a) { 40 | a.plan(1); 41 | 42 | var observable = new Observable(function (observer) { 43 | observer.error(new Error()); 44 | }); 45 | 46 | return a.throws(observable); 47 | }).run().then(function (result) { 48 | t.is(result.passed, true); 49 | t.is(result.result.assertCount, 1); 50 | t.end(); 51 | }); 52 | }); 53 | 54 | test('handle throws with long running thrown observable', function (t) { 55 | ava(function (a) { 56 | a.plan(1); 57 | 58 | var observable = new Observable(function (observer) { 59 | setTimeout(function () { 60 | observer.error(new Error('abc')); 61 | }, 2000); 62 | }); 63 | 64 | return a.throws(observable, /abc/); 65 | }).run().then(function (result) { 66 | t.is(result.passed, true); 67 | t.is(result.result.assertCount, 1); 68 | t.end(); 69 | }); 70 | }); 71 | 72 | test('handle throws with completed observable', function (t) { 73 | ava(function (a) { 74 | a.plan(1); 75 | 76 | var observable = Observable.of(); 77 | return a.throws(observable); 78 | }).run().then(function (result) { 79 | t.is(result.passed, false); 80 | t.is(result.reason.name, 'AssertionError'); 81 | t.end(); 82 | }); 83 | }); 84 | 85 | test('handle throws with regex', function (t) { 86 | ava(function (a) { 87 | a.plan(1); 88 | 89 | var observable = new Observable(function (observer) { 90 | observer.error(new Error('abc')); 91 | }); 92 | 93 | return a.throws(observable, /abc/); 94 | }).run().then(function (result) { 95 | t.is(result.passed, true); 96 | t.is(result.result.assertCount, 1); 97 | t.end(); 98 | }); 99 | }); 100 | 101 | test('handle throws with string', function (t) { 102 | ava(function (a) { 103 | a.plan(1); 104 | 105 | var observable = new Observable(function (observer) { 106 | observer.error(new Error('abc')); 107 | }); 108 | 109 | return a.throws(observable, 'abc'); 110 | }).run().then(function (result) { 111 | t.is(result.passed, true); 112 | t.is(result.result.assertCount, 1); 113 | t.end(); 114 | }); 115 | }); 116 | 117 | test('handle throws with false-positive observable', function (t) { 118 | ava(function (a) { 119 | a.plan(1); 120 | 121 | var observable = new Observable(function (observer) { 122 | observer.next(new Error()); 123 | observer.complete(); 124 | }); 125 | 126 | return a.throws(observable); 127 | }).run().then(function (result) { 128 | t.is(result.passed, false); 129 | t.is(result.reason.name, 'AssertionError'); 130 | t.end(); 131 | }); 132 | }); 133 | 134 | test('handle notThrows with completed observable', function (t) { 135 | ava(function (a) { 136 | a.plan(1); 137 | 138 | var observable = Observable.of(); 139 | return a.notThrows(observable); 140 | }).run().then(function (result) { 141 | t.is(result.passed, true); 142 | t.is(result.result.assertCount, 1); 143 | t.end(); 144 | }); 145 | }); 146 | 147 | test('handle notThrows with thrown observable', function (t) { 148 | ava(function (a) { 149 | a.plan(1); 150 | 151 | var observable = new Observable(function (observer) { 152 | observer.error(new Error()); 153 | }); 154 | 155 | return a.notThrows(observable); 156 | }).run().then(function (result) { 157 | t.is(result.passed, false); 158 | t.is(result.reason.name, 'AssertionError'); 159 | t.end(); 160 | }); 161 | }); 162 | -------------------------------------------------------------------------------- /maintaining.md: -------------------------------------------------------------------------------- 1 | # Maintaining [![Dependency Status](https://david-dm.org/sindresorhus/ava.svg)](https://david-dm.org/sindresorhus/ava) [![devDependency Status](https://david-dm.org/sindresorhus/ava/dev-status.svg)](https://david-dm.org/sindresorhus/ava#info=devDependencies) 2 | 3 | 4 | ## Conduct 5 | 6 | **Be kind to everyone.** 7 | Read and adhere to the [Code of Conduct](code-of-conduct.md). 8 | 9 | 10 | ## Testing 11 | 12 | - `npm test`: Lint the code and run the entire test suite with coverage. 13 | - `npm run test-win`: Run the tests on Windows. 14 | - `npm run coverage`: Generate a coverage report for the last test run (opens a browser window). 15 | - `tap test/fork.js --bail`: Run a specific test file and bail on the first failure (useful when hunting bugs). 16 | 17 | 18 | ## Release process 19 | 20 | - Bump dependencies. 21 | - Ensure [Travis CI](https://travis-ci.org/sindresorhus/ava) and [AppVeyor](https://ci.appveyor.com/project/sindresorhus/ava/branch/master) are green. 22 | - Publish a new version using [`np`](https://github.com/sindresorhus/np) with a version number according to [semver](http://semver.org). 23 | - Write a [release note](https://github.com/sindresorhus/ava/releases/new) following the style of previous release notes. 24 | 25 | 26 | ## Pull requests 27 | 28 | - New features should come with tests and documentation. 29 | - Ensure the [contributing guidelines](contributing.md) are followed. 30 | - At least one team member must `LGTM` a pull request before it's merged. 31 | - Squash commits when merging. *[Example](https://github.com/sindresorhus/ava/commit/0675d3444da6958b54c7e5eada91034e516bc97c)* 32 | 33 | 34 | ## Profiling 35 | 36 | You should first install [`iron-node`](https://github.com/s-a/iron-node) and / or [`devtool`](https://github.com/Jam3/devtool) globally: 37 | 38 | ``` 39 | $ npm install --global iron-node devtool 40 | ``` 41 | 42 | In the root of a project using AVA, run: 43 | 44 | ``` 45 | $ iron-node node_modules/ava/profile.js 46 | ``` 47 | 48 | Or: 49 | 50 | ``` 51 | $ devtool node_modules/ava/profile.js 52 | ``` 53 | 54 | Once the Dev Tools window has loaded, activate Memory or CPU profiling, and then hit Cmd R to rerun the tests. 55 | 56 | As soon as the tests finish, stop the recording and inspect the profiler results. The flamegraph can be displayed by choosing `Chart` from the drop down on the `Profiles` tab (other views include `Tree (top down)` and `Heavy (bottom up)`). 57 | 58 | You may also want to check out the Settings page in Dev Tools and enable one or more options in the Profiling section. 59 | 60 | ##### Helpful resources 61 | 62 | - [An introduction to Node.js debugging with `devtool`](http://mattdesl.svbtle.com/debugging-nodejs-in-chrome-devtools). 63 | - [A video introduction to Chrome DevTools CPU and Memory profiling](https://www.youtube.com/watch?v=KKwmdTByxLk). 64 | 65 | 66 | ## Benchmarking 67 | 68 | First collect benchmark data for a branch/commit: 69 | 70 | ``` 71 | $ node bench/run 72 | ``` 73 | 74 | Once you have collected data from two/three branches/commits: 75 | 76 | ``` 77 | $ node bench/compare 78 | ``` 79 | 80 | *You could for example gather benchmark data from the working tree and the last commit.* 81 | 82 | ![](https://cloud.githubusercontent.com/assets/4082216/12700805/bf18f730-c7bf-11e5-8a4f-fec0993c053f.png) 83 | 84 | You can now launch a subset of the suite: 85 | 86 | ``` 87 | $ node bench/run.js concurrent/sync.js serial/sync.js -- concurrent/sync.js -- serial/sync.js 88 | ``` 89 | 90 | Note the `--` separator. The above would be the same as benchmarking all three of the following commands. 91 | 92 | ``` 93 | $ ava concurrent/sync.js serial/sync.js 94 | $ ava concurrent/sync.js 95 | $ ava serial/sync.js 96 | ``` 97 | 98 | Also if you are benchmarking a suite that should fail, you must add the `--should-fail` flag in that group: 99 | 100 | ``` 101 | $ node bench/run.js concurrent/sync.js -- --should-fail other/failures.js 102 | ``` 103 | 104 | The above benchmarks two commands, but expects the second one to fail. 105 | 106 | 107 | ## Onboarding new core members 108 | 109 | - Add the user to the `readme.md` and `package.json`. 110 | - Add the user as a collaborator to all AVA related repos and npm packages. 111 | - Share the Twitter account login info and that he user is free to tweet/retweet relevant stuff. 112 | -------------------------------------------------------------------------------- /test/reporters/tap.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | var tapReporter = require('../../lib/reporters/tap'); 4 | 5 | test('start', function (t) { 6 | var reporter = tapReporter(); 7 | 8 | t.is(reporter.start(), 'TAP version 13'); 9 | t.end(); 10 | }); 11 | 12 | test('passing test', function (t) { 13 | var reporter = tapReporter(); 14 | 15 | var actualOutput = reporter.test({ 16 | title: 'passing' 17 | }); 18 | 19 | var expectedOutput = [ 20 | '# passing', 21 | 'ok 1 - passing' 22 | ].join('\n'); 23 | 24 | t.is(actualOutput, expectedOutput); 25 | t.end(); 26 | }); 27 | 28 | test('failing test', function (t) { 29 | var reporter = tapReporter(); 30 | 31 | var actualOutput = reporter.test({ 32 | title: 'failing', 33 | error: { 34 | message: 'false == true', 35 | operator: '==', 36 | expected: true, 37 | actual: false, 38 | stack: ['', 'Test.fn (test.js:1:2)'].join('\n') 39 | } 40 | }); 41 | 42 | var expectedOutput = [ 43 | '# failing', 44 | 'not ok 1 - false == true', 45 | ' ---', 46 | ' operator: ==', 47 | ' expected: true', 48 | ' actual: false', 49 | ' at: Test.fn (test.js:1:2)', 50 | ' ...' 51 | ].join('\n'); 52 | 53 | t.is(actualOutput, expectedOutput); 54 | t.end(); 55 | }); 56 | 57 | test('unhandled error', function (t) { 58 | var reporter = tapReporter(); 59 | 60 | var actualOutput = reporter.unhandledError({ 61 | message: 'unhandled', 62 | name: 'TypeError', 63 | stack: ['', 'Test.fn (test.js:1:2)'].join('\n') 64 | }); 65 | 66 | var expectedOutput = [ 67 | '# unhandled', 68 | 'not ok 1 - unhandled', 69 | ' ---', 70 | ' name: TypeError', 71 | ' at: Test.fn (test.js:1:2)', 72 | ' ...' 73 | ].join('\n'); 74 | 75 | t.is(actualOutput, expectedOutput); 76 | t.end(); 77 | }); 78 | 79 | test('ava error', function (t) { 80 | var reporter = tapReporter(); 81 | 82 | var actualOutput = reporter.unhandledError({ 83 | type: 'exception', 84 | name: 'AvaError', 85 | message: 'A futuristic test runner' 86 | }); 87 | 88 | var expectedOutput = [ 89 | '# A futuristic test runner', 90 | 'not ok 1 - A futuristic test runner' 91 | ].join('\n'); 92 | 93 | t.is(actualOutput, expectedOutput); 94 | t.end(); 95 | }); 96 | 97 | test('results', function (t) { 98 | var reporter = tapReporter(); 99 | var api = { 100 | passCount: 1, 101 | failCount: 2, 102 | skipCount: 1, 103 | rejectionCount: 3, 104 | exceptionCount: 4 105 | }; 106 | 107 | reporter.api = api; 108 | 109 | var actualOutput = reporter.finish(); 110 | var expectedOutput = [ 111 | '', 112 | '1..' + (api.passCount + api.failCount + api.skipCount), 113 | '# tests ' + (api.passCount + api.failCount + api.skipCount), 114 | '# pass ' + api.passCount, 115 | '# skip ' + api.skipCount, 116 | '# fail ' + (api.failCount + api.rejectionCount + api.exceptionCount), 117 | '' 118 | ].join('\n'); 119 | 120 | t.is(actualOutput, expectedOutput); 121 | t.end(); 122 | }); 123 | 124 | test('results does not show skipped tests if there are none', function (t) { 125 | var reporter = tapReporter(); 126 | var api = { 127 | passCount: 1, 128 | failCount: 2, 129 | skipCount: 0, 130 | rejectionCount: 3, 131 | exceptionCount: 4 132 | }; 133 | 134 | reporter.api = api; 135 | 136 | var actualOutput = reporter.finish(); 137 | var expectedOutput = [ 138 | '', 139 | '1..' + (api.passCount + api.failCount), 140 | '# tests ' + (api.passCount + api.failCount), 141 | '# pass ' + api.passCount, 142 | '# fail ' + (api.failCount + api.rejectionCount + api.exceptionCount), 143 | '' 144 | ].join('\n'); 145 | 146 | t.is(actualOutput, expectedOutput); 147 | t.end(); 148 | }); 149 | 150 | test('todo test', function (t) { 151 | var reporter = tapReporter(); 152 | 153 | var actualOutput = reporter.test({ 154 | title: 'should think about doing this', 155 | passed: false, 156 | skip: true, 157 | todo: true 158 | }); 159 | 160 | var expectedOutput = [ 161 | '# should think about doing this', 162 | 'not ok 1 - should think about doing this # TODO' 163 | ].join('\n'); 164 | 165 | t.is(actualOutput, expectedOutput); 166 | t.end(); 167 | }); 168 | 169 | test('skip test', function (t) { 170 | var reporter = tapReporter(); 171 | 172 | var actualOutput = reporter.test({ 173 | title: 'skipped', 174 | passed: true, 175 | skip: true 176 | }); 177 | 178 | var expectedOutput = [ 179 | '# skipped', 180 | 'ok 1 - skipped # SKIP' 181 | ].join('\n'); 182 | 183 | t.is(actualOutput, expectedOutput); 184 | t.end(); 185 | }); 186 | -------------------------------------------------------------------------------- /cli.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 'use strict'; 3 | 4 | var debug = require('debug')('ava'); 5 | 6 | // Prefer the local installation of AVA. 7 | var resolveCwd = require('resolve-cwd'); 8 | var localCLI = resolveCwd('ava/cli'); 9 | 10 | if (localCLI && localCLI !== __filename) { 11 | debug('Using local install of AVA'); 12 | require(localCLI); 13 | return; 14 | } 15 | 16 | if (debug.enabled) { 17 | require('time-require'); 18 | } 19 | 20 | var updateNotifier = require('update-notifier'); 21 | var figures = require('figures'); 22 | var arrify = require('arrify'); 23 | var meow = require('meow'); 24 | var Promise = require('bluebird'); 25 | var pkgConf = require('pkg-conf'); 26 | var isCi = require('is-ci'); 27 | var colors = require('./lib/colors'); 28 | var verboseReporter = require('./lib/reporters/verbose'); 29 | var miniReporter = require('./lib/reporters/mini'); 30 | var tapReporter = require('./lib/reporters/tap'); 31 | var Logger = require('./lib/logger'); 32 | var Watcher = require('./lib/watcher'); 33 | var Api = require('./api'); 34 | 35 | // Bluebird specific 36 | Promise.longStackTraces(); 37 | 38 | var conf = pkgConf.sync('ava', { 39 | defaults: { 40 | babel: 'default' 41 | } 42 | }); 43 | 44 | var cli = meow([ 45 | 'Usage', 46 | ' ava [ ...]', 47 | '', 48 | 'Options', 49 | ' --init Add AVA to your project', 50 | ' --fail-fast Stop after first test failure', 51 | ' --serial, -s Run tests serially', 52 | ' --require, -r Module to preload (Can be repeated)', 53 | ' --tap, -t Generate TAP output', 54 | ' --verbose, -v Enable verbose output', 55 | ' --no-cache Disable the transpiler cache', 56 | ' --match, -m Only run tests with matching title (Can be repeated)', 57 | ' --watch, -w Re-run tests when tests and source files change', 58 | ' --source, -S Pattern to match source files so tests can be re-run (Can be repeated)', 59 | '', 60 | 'Examples', 61 | ' ava', 62 | ' ava test.js test2.js', 63 | ' ava test-*.js', 64 | ' ava test', 65 | ' ava --init', 66 | ' ava --init foo.js', 67 | '', 68 | 'Default patterns when no arguments:', 69 | 'test.js test-*.js test/**/*.js' 70 | ], { 71 | string: [ 72 | '_', 73 | 'require', 74 | 'source', 75 | 'match' 76 | ], 77 | boolean: [ 78 | 'fail-fast', 79 | 'verbose', 80 | 'serial', 81 | 'tap', 82 | 'watch' 83 | ], 84 | default: conf, 85 | alias: { 86 | t: 'tap', 87 | v: 'verbose', 88 | r: 'require', 89 | s: 'serial', 90 | m: 'match', 91 | w: 'watch', 92 | S: 'source' 93 | } 94 | }); 95 | 96 | updateNotifier({pkg: cli.pkg}).notify(); 97 | 98 | if (cli.flags.init) { 99 | require('ava-init')(); 100 | return; 101 | } 102 | 103 | var api = new Api({ 104 | failFast: cli.flags.failFast, 105 | serial: cli.flags.serial, 106 | require: arrify(cli.flags.require), 107 | cacheEnabled: cli.flags.cache !== false, 108 | explicitTitles: cli.flags.watch, 109 | match: arrify(cli.flags.match), 110 | babelConfig: conf.babel 111 | }); 112 | 113 | var reporter; 114 | 115 | if (cli.flags.tap) { 116 | reporter = tapReporter(); 117 | } else if (cli.flags.verbose || isCi) { 118 | reporter = verboseReporter(); 119 | } else { 120 | reporter = miniReporter(); 121 | } 122 | 123 | reporter.api = api; 124 | var logger = new Logger(reporter); 125 | 126 | logger.start(); 127 | 128 | api.on('test', logger.test); 129 | api.on('error', logger.unhandledError); 130 | 131 | api.on('stdout', logger.stdout); 132 | api.on('stderr', logger.stderr); 133 | 134 | var files = cli.input.length ? cli.input : arrify(conf.files); 135 | if (files.length === 0) { 136 | files = [ 137 | 'test.js', 138 | 'test-*.js', 139 | 'test' 140 | ]; 141 | } 142 | 143 | if (cli.flags.watch) { 144 | try { 145 | var watcher = new Watcher(logger, api, files, arrify(cli.flags.source)); 146 | watcher.observeStdin(process.stdin); 147 | } catch (err) { 148 | if (err.name === 'AvaError') { 149 | // An AvaError may be thrown if chokidar is not installed. Log it nicely. 150 | console.log(' ' + colors.error(figures.cross) + ' ' + err.message); 151 | logger.exit(1); 152 | } else { 153 | // Rethrow so it becomes an uncaught exception. 154 | throw err; 155 | } 156 | } 157 | } else { 158 | api.run(files) 159 | .then(function () { 160 | logger.finish(); 161 | logger.exit(api.failCount > 0 || api.rejectionCount > 0 || api.exceptionCount > 0 ? 1 : 0); 162 | }) 163 | .catch(function (err) { 164 | // Don't swallow exceptions. Note that any expected error should already 165 | // have been logged. 166 | setImmediate(function () { 167 | throw err; 168 | }); 169 | }); 170 | } 171 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ava", 3 | "version": "0.13.0", 4 | "description": "Futuristic test runner 🚀", 5 | "license": "MIT", 6 | "repository": "sindresorhus/ava", 7 | "homepage": "https://ava.li", 8 | "author": { 9 | "name": "Sindre Sorhus", 10 | "email": "sindresorhus@gmail.com", 11 | "url": "sindresorhus.com" 12 | }, 13 | "maintainers": [ 14 | { 15 | "name": "Vadim Demedes", 16 | "email": "vdemedes@gmail.com", 17 | "url": "github.com/vdemedes" 18 | }, 19 | { 20 | "name": "James Talmage", 21 | "email": "james@talmage.io", 22 | "url": "github.com/jamestalmage" 23 | }, 24 | { 25 | "name": "Mark Wubben", 26 | "email": "mark@novemberborn.net", 27 | "url": "novemberborn.net" 28 | } 29 | ], 30 | "bin": "cli.js", 31 | "engines": { 32 | "node": ">=0.10.0" 33 | }, 34 | "scripts": { 35 | "test": "xo && nyc --cache --reporter=lcov --reporter=text tap --no-cov --timeout=150 test/*.js test/reporters/*.js", 36 | "test-win": "tap --no-cov --reporter=classic --timeout=150 test/*.js test/reporters/*.js", 37 | "visual": "node test/visual/run-visual-tests.js" 38 | }, 39 | "files": [ 40 | "lib", 41 | "*.js", 42 | "index.d.ts" 43 | ], 44 | "keywords": [ 45 | "test", 46 | "runner", 47 | "ava", 48 | "concurrent", 49 | "parallel", 50 | "fast", 51 | "tape", 52 | "tap", 53 | "mocha", 54 | "qunit", 55 | "jasmine", 56 | "tdd", 57 | "cli-app", 58 | "cli", 59 | "assert", 60 | "assertion", 61 | "futuristic", 62 | "promise", 63 | "promises", 64 | "async", 65 | "function", 66 | "await", 67 | "generator", 68 | "generators", 69 | "yield", 70 | "observable", 71 | "observables", 72 | "tap" 73 | ], 74 | "dependencies": { 75 | "arr-diff": "^2.0.0", 76 | "arr-flatten": "^1.0.1", 77 | "array-union": "^1.0.1", 78 | "array-uniq": "^1.0.2", 79 | "arrify": "^1.0.0", 80 | "ava-init": "^0.1.0", 81 | "babel-core": "^6.3.21", 82 | "babel-plugin-espower": "^2.1.0", 83 | "babel-plugin-transform-runtime": "^6.3.13", 84 | "babel-preset-es2015": "^6.3.13", 85 | "babel-preset-stage-2": "^6.3.13", 86 | "babel-runtime": "^6.3.19", 87 | "bluebird": "^3.0.0", 88 | "caching-transform": "^1.0.0", 89 | "chalk": "^1.0.0", 90 | "cli-cursor": "^1.0.2", 91 | "cli-spinners": "^0.1.2", 92 | "cli-truncate": "^0.2.0", 93 | "co-with-promise": "^4.6.0", 94 | "common-path-prefix": "^1.0.0", 95 | "convert-source-map": "^1.1.2", 96 | "core-assert": "^0.1.0", 97 | "debug": "^2.2.0", 98 | "empower-core": "^0.5.0", 99 | "figures": "^1.4.0", 100 | "find-cache-dir": "^0.1.1", 101 | "fn-name": "^2.0.0", 102 | "globby": "^4.0.0", 103 | "ignore-by-default": "^1.0.0", 104 | "is-ci": "^1.0.7", 105 | "is-generator-fn": "^1.0.0", 106 | "is-observable": "^0.1.0", 107 | "is-promise": "^2.1.0", 108 | "last-line-stream": "^1.0.0", 109 | "loud-rejection": "^1.2.0", 110 | "matcher": "^0.1.1", 111 | "max-timeout": "^1.0.0", 112 | "md5-hex": "^1.2.0", 113 | "meow": "^3.7.0", 114 | "multimatch": "^2.1.0", 115 | "object-assign": "^4.0.1", 116 | "observable-to-promise": "^0.3.0", 117 | "only-shallow": "^1.2.0", 118 | "option-chain": "^0.1.0", 119 | "pkg-conf": "^1.0.1", 120 | "plur": "^2.0.0", 121 | "power-assert-formatter": "^1.3.0", 122 | "power-assert-renderers": "^0.1.0", 123 | "pretty-ms": "^2.0.0", 124 | "require-precompiled": "^0.1.0", 125 | "resolve-cwd": "^1.0.0", 126 | "serialize-error": "^1.1.0", 127 | "set-immediate-shim": "^1.0.1", 128 | "slash": "^1.0.0", 129 | "source-map-support": "^0.4.0", 130 | "stack-utils": "^0.4.0", 131 | "strip-bom": "^2.0.0", 132 | "time-require": "^0.1.2", 133 | "unique-temp-dir": "^1.0.0", 134 | "update-notifier": "^0.6.0" 135 | }, 136 | "devDependencies": { 137 | "cli-table2": "^0.2.0", 138 | "coveralls": "^2.11.4", 139 | "delay": "^1.3.0", 140 | "get-stream": "^2.0.0", 141 | "git-branch": "^0.3.0", 142 | "inquirer": "^0.12.0", 143 | "lolex": "^1.4.0", 144 | "mkdirp": "^0.5.1", 145 | "nyc": "^6.0.0", 146 | "pify": "^2.3.0", 147 | "proxyquire": "^1.7.4", 148 | "rimraf": "^2.5.0", 149 | "signal-exit": "^2.1.2", 150 | "sinon": "^1.17.2", 151 | "source-map-fixtures": "^2.1.0", 152 | "tap": "^5.4.2", 153 | "touch": "^1.0.0", 154 | "xo": "*", 155 | "zen-observable": "^0.2.1" 156 | }, 157 | "optionalDependencies": { 158 | "chokidar": "^1.4.2" 159 | }, 160 | "xo": { 161 | "ignore": [ 162 | "bench/*/*.js" 163 | ] 164 | } 165 | } 166 | -------------------------------------------------------------------------------- /lib/test-collection.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var EventEmitter = require('events').EventEmitter; 3 | var util = require('util'); 4 | var fnName = require('fn-name'); 5 | var Concurrent = require('./concurrent'); 6 | var Sequence = require('./sequence'); 7 | var Test = require('./test'); 8 | 9 | module.exports = TestCollection; 10 | 11 | function TestCollection() { 12 | if (!(this instanceof TestCollection)) { 13 | throw new TypeError('Class constructor TestCollection cannot be invoked without \'new\''); 14 | } 15 | 16 | EventEmitter.call(this); 17 | 18 | this.hasExclusive = false; 19 | this.tests = { 20 | concurrent: [], 21 | serial: [] 22 | }; 23 | 24 | this.hooks = { 25 | before: [], 26 | beforeEach: [], 27 | after: [], 28 | afterEach: [] 29 | }; 30 | 31 | this._emitTestResult = this._emitTestResult.bind(this); 32 | } 33 | 34 | util.inherits(TestCollection, EventEmitter); 35 | 36 | TestCollection.prototype.add = function (test) { 37 | var metadata = test.metadata; 38 | var type = metadata.type; 39 | 40 | if (!type) { 41 | throw new Error('Test type must be specified'); 42 | } 43 | 44 | if (!test.title && test.fn) { 45 | test.title = fnName(test.fn); 46 | } 47 | 48 | // workaround for Babel giving anonymous functions a name 49 | if (test.title === 'callee$0$0') { 50 | test.title = null; 51 | } 52 | 53 | if (!test.title) { 54 | if (type === 'test') { 55 | test.title = '[anonymous]'; 56 | } else { 57 | test.title = type; 58 | } 59 | } 60 | 61 | // add a hook 62 | if (type !== 'test') { 63 | if (metadata.exclusive) { 64 | throw new Error('"only" cannot be used with a ' + type + ' test'); 65 | } 66 | 67 | this.hooks[type].push(test); 68 | return; 69 | } 70 | 71 | // add .only() tests if .only() was used previously 72 | if (this.hasExclusive && !metadata.exclusive) { 73 | return; 74 | } 75 | 76 | if (metadata.exclusive && !this.hasExclusive) { 77 | this.tests.concurrent = []; 78 | this.tests.serial = []; 79 | this.hasExclusive = true; 80 | } 81 | 82 | if (metadata.serial) { 83 | this.tests.serial.push(test); 84 | } else { 85 | this.tests.concurrent.push(test); 86 | } 87 | }; 88 | 89 | TestCollection.prototype._skippedTest = function (test) { 90 | var self = this; 91 | 92 | return { 93 | run: function () { 94 | var result = { 95 | passed: true, 96 | result: test 97 | }; 98 | 99 | self._emitTestResult(result); 100 | 101 | return result; 102 | } 103 | }; 104 | }; 105 | 106 | TestCollection.prototype._emitTestResult = function (test) { 107 | this.emit('test', test); 108 | }; 109 | 110 | TestCollection.prototype._buildHooks = function (hooks, testTitle, context) { 111 | return hooks.map(function (hook) { 112 | var test = this._buildHook(hook, testTitle, context); 113 | 114 | if (hook.metadata.skipped || hook.metadata.todo) { 115 | return this._skippedTest(test); 116 | } 117 | 118 | return test; 119 | }, this); 120 | }; 121 | 122 | TestCollection.prototype._buildHook = function (hook, testTitle, context) { 123 | var title = hook.title; 124 | 125 | if (testTitle) { 126 | title += ' for ' + testTitle; 127 | } 128 | 129 | if (!context) { 130 | context = null; 131 | } 132 | 133 | var test = new Test(title, hook.fn, context, this._emitTestResult); 134 | test.metadata = hook.metadata; 135 | 136 | return test; 137 | }; 138 | 139 | TestCollection.prototype._buildTest = function (test, context) { 140 | if (!context) { 141 | context = null; 142 | } 143 | 144 | var metadata = test.metadata; 145 | 146 | test = new Test(test.title, test.fn, context, this._emitTestResult); 147 | test.metadata = metadata; 148 | 149 | return test; 150 | }; 151 | 152 | TestCollection.prototype._buildTestWithHooks = function (test) { 153 | if (test.metadata.skipped) { 154 | return [this._skippedTest(this._buildTest(test))]; 155 | } 156 | 157 | var context = {context: {}}; 158 | 159 | var beforeHooks = this._buildHooks(this.hooks.beforeEach, test.title, context); 160 | var afterHooks = this._buildHooks(this.hooks.afterEach, test.title, context); 161 | 162 | return [].concat(beforeHooks, this._buildTest(test, context), afterHooks); 163 | }; 164 | 165 | TestCollection.prototype._buildTests = function (tests) { 166 | return tests.map(function (test) { 167 | return new Sequence(this._buildTestWithHooks(test), true); 168 | }, this); 169 | }; 170 | 171 | TestCollection.prototype.build = function (bail) { 172 | var beforeHooks = new Sequence(this._buildHooks(this.hooks.before)); 173 | var afterHooks = new Sequence(this._buildHooks(this.hooks.after)); 174 | 175 | var serialTests = new Sequence(this._buildTests(this.tests.serial), bail); 176 | var concurrentTests = new Concurrent(this._buildTests(this.tests.concurrent), bail); 177 | var allTests = new Sequence([serialTests, concurrentTests]); 178 | 179 | return new Sequence([beforeHooks, allTests, afterHooks], true); 180 | }; 181 | -------------------------------------------------------------------------------- /lib/test-worker.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var opts = JSON.parse(process.argv[2]); 3 | var testPath = opts.file; 4 | 5 | // Fake TTY support 6 | if (opts.tty) { 7 | process.stdout.isTTY = true; 8 | process.stdout.columns = opts.tty.columns || 80; 9 | process.stdout.rows = opts.tty.rows; 10 | 11 | var tty = require('tty'); 12 | var isatty = tty.isatty; 13 | 14 | tty.isatty = function (fd) { 15 | if (fd === 1 || fd === process.stdout) { 16 | return true; 17 | } 18 | 19 | return isatty(fd); 20 | }; 21 | } 22 | 23 | var path = require('path'); 24 | var fs = require('fs'); 25 | var debug = require('debug')('ava'); 26 | var sourceMapSupport = require('source-map-support'); 27 | 28 | if (debug.enabled) { 29 | // Forward the `time-require` `--sorted` flag. 30 | // Intended for internal optimization tests only. 31 | if (opts._sorted) { 32 | process.argv.push('--sorted'); 33 | } 34 | 35 | require('time-require'); 36 | } 37 | 38 | // bind globals first before anything has a chance to interfere 39 | var globals = require('./globals'); 40 | globals.options = opts; 41 | var Promise = require('bluebird'); 42 | 43 | // Bluebird specific 44 | Promise.longStackTraces(); 45 | 46 | (opts.require || []).forEach(require); 47 | 48 | var sourceMapCache = Object.create(null); 49 | 50 | sourceMapSupport.install({ 51 | handleUncaughtExceptions: false, 52 | retrieveSourceMap: function (source) { 53 | if (sourceMapCache[source]) { 54 | return { 55 | url: source, 56 | map: fs.readFileSync(sourceMapCache[source], 'utf8') 57 | }; 58 | } 59 | } 60 | }); 61 | 62 | var loudRejection = require('loud-rejection/api')(process); 63 | var serializeError = require('serialize-error'); 64 | var beautifyStack = require('./beautify-stack'); 65 | var send = require('./send'); 66 | var installPrecompiler = require('require-precompiled'); 67 | var cacheDir = opts.cacheDir; 68 | 69 | // check if test files required ava and show error, when they didn't 70 | exports.avaRequired = false; 71 | 72 | installPrecompiler(function (filename) { 73 | var precompiled = opts.precompiled[filename]; 74 | 75 | if (precompiled) { 76 | sourceMapCache[filename] = path.join(cacheDir, precompiled + '.map'); 77 | return fs.readFileSync(path.join(cacheDir, precompiled + '.js'), 'utf8'); 78 | } 79 | 80 | return null; 81 | }); 82 | 83 | // Modules need to be able to find `babel-runtime`, which is nested in our node_modules w/ npm@2 84 | var nodeModulesDir = path.join(__dirname, '../node_modules'); 85 | var oldNodeModulesPaths = module.constructor._nodeModulePaths; 86 | module.constructor._nodeModulePaths = function () { 87 | var ret = oldNodeModulesPaths.apply(this, arguments); 88 | ret.push(nodeModulesDir); 89 | return ret; 90 | }; 91 | 92 | var dependencies = []; 93 | Object.keys(require.extensions).forEach(function (ext) { 94 | var wrappedHandler = require.extensions[ext]; 95 | require.extensions[ext] = function (module, filename) { 96 | if (filename !== testPath) { 97 | dependencies.push(filename); 98 | } 99 | wrappedHandler(module, filename); 100 | }; 101 | }); 102 | 103 | require(testPath); 104 | 105 | process.on('uncaughtException', function (exception) { 106 | var serialized = serializeError(exception); 107 | if (serialized.stack) { 108 | serialized.stack = beautifyStack(serialized.stack); 109 | } 110 | send('uncaughtException', {exception: serialized}); 111 | }); 112 | 113 | // if ava was not required, show an error 114 | if (!exports.avaRequired) { 115 | send('no-tests', {avaRequired: false}); 116 | } 117 | 118 | // parse and re-emit ava messages 119 | process.on('message', function (message) { 120 | if (!message.ava) { 121 | return; 122 | } 123 | 124 | process.emit(message.name, message.data); 125 | }); 126 | 127 | process.on('ava-exit', function () { 128 | // use a little delay when running on AppVeyor (because it's shit) 129 | var delay = process.env.AVA_APPVEYOR ? 100 : 0; 130 | 131 | globals.setTimeout(function () { 132 | process.exit(0); 133 | }, delay); 134 | }); 135 | 136 | var tearingDown = false; 137 | process.on('ava-teardown', function () { 138 | // ava-teardown can be sent more than once. 139 | if (tearingDown) { 140 | return; 141 | } 142 | tearingDown = true; 143 | 144 | var rejections = loudRejection.currentlyUnhandled(); 145 | 146 | if (rejections.length === 0) { 147 | exit(); 148 | return; 149 | } 150 | 151 | rejections = rejections.map(function (rejection) { 152 | var error = serializeError(rejection.reason); 153 | if (error.stack) { 154 | error.stack = beautifyStack(error.stack); 155 | } 156 | return error; 157 | }); 158 | 159 | send('unhandledRejections', {rejections: rejections}); 160 | globals.setTimeout(exit, 100); 161 | }); 162 | 163 | function exit() { 164 | // Include dependencies in the final teardown message. This ensures the full 165 | // set of dependencies is included no matter how the process exits, unless 166 | // it flat out crashes. 167 | send('teardown', {dependencies: dependencies}); 168 | } 169 | -------------------------------------------------------------------------------- /docs/recipes/when-to-use-plan.md: -------------------------------------------------------------------------------- 1 | # When to use `t.plan()` 2 | 3 | Translations: [Español](https://github.com/sindresorhus/ava-docs/blob/master/es_ES/docs/recipes/when-to-use-plan.md), [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/when-to-use-plan.md), [日本語](https://github.com/sindresorhus/ava-docs/blob/master/ja_JP/docs/recipes/when-to-use-plan.md), [Português](https://github.com/sindresorhus/ava-docs/blob/master/pt_BR/docs/recipes/when-to-use-plan.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/when-to-use-plan.md) 4 | 5 | One major difference between AVA and [`tap`](https://github.com/tapjs/node-tap)/[`tape`](https://github.com/substack/tape) is the behavior of `t.plan()`. In AVA, `t.plan()` is only used to assert that the expected number of assertions are called; it does not auto-end the test. 6 | 7 | ## Poor uses of `t.plan()` 8 | 9 | Many users transitioning from `tap`/`tape` are accustomed to using `t.plan()` prolifically in every test. However, in AVA, we don't consider that to be a "best practice". Instead, we believe `t.plan()` should only be used in situations where it provides some value. 10 | 11 | ### Sync tests with no branching 12 | 13 | `t.plan()` is unnecessary in most sync tests. 14 | 15 | ```js 16 | test(t => { 17 | // BAD: there is no branching here - t.plan() is pointless 18 | t.plan(2); 19 | 20 | t.is(1 + 1, 2); 21 | t.is(2 + 2, 4); 22 | }); 23 | ``` 24 | 25 | `t.plan()` does not provide any value here, and creates an extra chore if you ever decide to add or remove assertions. 26 | 27 | ### Promises that are expected to resolve 28 | 29 | ```js 30 | test(t => { 31 | t.plan(1); 32 | 33 | return somePromise().then(result => { 34 | t.is(result, 'foo'); 35 | }); 36 | }); 37 | ``` 38 | 39 | At a glance, this tests appears to make good use of `t.plan()` since an async promise handler is involved. However there are several problems with the test: 40 | 41 | 1. `t.plan()` is presumably used here to protect against the possibility that `somePromise()` might be rejected; But returning a rejected promise would fail the test anyways. 42 | 43 | 2. It would be better to take advantage of `async`/`await`: 44 | 45 | ```js 46 | test(async t => { 47 | t.is(await somePromise(), 'foo'); 48 | }); 49 | ``` 50 | 51 | ## Good uses of `t.plan()` 52 | 53 | `t.plan()` has many acceptable uses. 54 | 55 | ### Promises with a `.catch()` block 56 | 57 | ```js 58 | test(t => { 59 | t.plan(2); 60 | 61 | return shouldRejectWithFoo().catch(reason => { 62 | t.is(reason.message, 'Hello') // Prefer t.throws() if all you care about is the message 63 | t.is(reason.foo, 'bar'); 64 | }); 65 | }); 66 | ``` 67 | 68 | Here, `t.plan()` is used to ensure the code inside the `catch` block happens. In most cases, you should prefer the `t.throws()` assertion, but this is an acceptable use since `t.throws()` only allows you to assert against the error's `message` property. 69 | 70 | ### Ensuring a catch statement happens 71 | 72 | ```js 73 | test(t => { 74 | t.plan(2); 75 | 76 | try { 77 | shouldThrow(); 78 | } catch (err) { 79 | t.is(err.message, 'Hello') // Prefer t.throws() if all you care about is the message 80 | t.is(err.foo, 'bar'); 81 | } 82 | }); 83 | ``` 84 | 85 | As stated in the `try`/`catch` example above, using the `t.throws()` assertion is usually a better choice, but it only lets you assert against the error's `message` property. 86 | 87 | ### Ensuring multiple callbacks are actually called 88 | 89 | ```js 90 | test.cb(t => { 91 | t.plan(2); 92 | 93 | const callbackA = () => { 94 | t.pass(); 95 | t.end(); 96 | }; 97 | 98 | const callbackB = () => t.pass(); 99 | 100 | bThenA(callbackA, callbackB); 101 | }); 102 | ``` 103 | 104 | The above ensures `callbackB` is called first (and only once), followed by `callbackA`. Any other combination would not satisfy the plan. 105 | 106 | ### Tests with branching statements 107 | 108 | In most cases, it's a bad idea to use any complex branching inside your tests. A notable exception is for tests that are auto-generated (perhaps from a JSON document). Below `t.plan()` is used to ensure the correctness of the JSON input: 109 | 110 | ```js 111 | const testData = require('./fixtures/test-definitions.json'); 112 | 113 | testData.forEach(testDefinition => { 114 | test(t => { 115 | const result = functionUnderTest(testDefinition.input); 116 | 117 | // testDefinition should have an expectation for `foo` or `bar` but not both 118 | t.plan(1); 119 | 120 | if (testDefinition.foo) { 121 | t.is(result.foo, testDefinition.foo); 122 | } 123 | 124 | if (testDefinition.bar) { 125 | t.is(result.bar, testDefinition.foo); 126 | } 127 | }); 128 | }); 129 | ``` 130 | 131 | ## Conclusion 132 | 133 | `t.plan()` has plenty of valid uses, but it should not be used indiscriminately. A good rule of thumb is to use it any time your *test* does not have straightforward, easily reasoned about, code flow. Tests with assertions inside callbacks, `if`/`then` statements, `for`/`while` loops, and (in some cases) `try`/`catch` blocks, are all good candidates for `t.plan()`. 134 | -------------------------------------------------------------------------------- /test/cli.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var childProcess = require('child_process'); 4 | var test = require('tap').test; 5 | global.Promise = require('bluebird'); 6 | var getStream = require('get-stream'); 7 | var arrify = require('arrify'); 8 | var touch = require('touch'); 9 | var cliPath = path.join(__dirname, '../cli.js'); 10 | 11 | function execCli(args, opts, cb) { 12 | var dirname; 13 | var env; 14 | 15 | if (typeof opts === 'function') { 16 | cb = opts; 17 | dirname = __dirname; 18 | env = {}; 19 | } else { 20 | dirname = path.join(__dirname, opts.dirname ? opts.dirname : ''); 21 | env = opts.env || {}; 22 | } 23 | 24 | if (process.env.AVA_APPVEYOR) { 25 | env.AVA_APPVEYOR = 1; 26 | } 27 | 28 | var child; 29 | var stdout; 30 | var stderr; 31 | 32 | var processPromise = new Promise(function (resolve) { 33 | child = childProcess.spawn(process.execPath, [path.relative(dirname, cliPath)].concat(arrify(args)), { 34 | cwd: dirname, 35 | env: env, 36 | stdio: [null, 'pipe', 'pipe'] 37 | }); 38 | 39 | child.on('close', function (code, signal) { 40 | if (code) { 41 | var err = new Error('test-worker exited with a non-zero exit code: ' + code); 42 | err.code = code; 43 | err.signal = signal; 44 | resolve(err); 45 | return; 46 | } 47 | resolve(code); 48 | }); 49 | 50 | stdout = getStream(child.stdout); 51 | stderr = getStream(child.stderr); 52 | }); 53 | 54 | Promise.all([processPromise, stdout, stderr]).then(function (args) { 55 | cb.apply(null, args); 56 | }); 57 | 58 | return child; 59 | } 60 | 61 | test('throwing a named function will report the to the console', function (t) { 62 | execCli('fixture/throw-named-function.js', function (err, stdout, stderr) { 63 | t.ok(err); 64 | t.match(stderr, /\[Function: fooFn]/); 65 | // TODO(jamestalmage) 66 | // t.ok(/1 uncaught exception[^s]/.test(stdout)); 67 | t.end(); 68 | }); 69 | }); 70 | 71 | test('babel require hook only applies to the test file', function (t) { 72 | t.plan(3); 73 | 74 | execCli('fixture/babel-hook.js', function (err, stdout, stderr) { 75 | t.ok(err); 76 | t.is(err.code, 1); 77 | t.match(stderr, /Unexpected token/); 78 | t.end(); 79 | }); 80 | }); 81 | 82 | test('throwing a anonymous function will report the function to the console', function (t) { 83 | execCli('fixture/throw-anonymous-function.js', function (err, stdout, stderr) { 84 | t.ok(err); 85 | t.match(stderr, /\[Function: anonymous]/); 86 | // TODO(jamestalmage) 87 | // t.ok(/1 uncaught exception[^s]/.test(stdout)); 88 | t.end(); 89 | }); 90 | }); 91 | 92 | test('log failed tests', function (t) { 93 | execCli('fixture/one-pass-one-fail.js', function (err, stdout, stderr) { 94 | t.ok(err); 95 | t.match(stderr, /false == true/); 96 | t.end(); 97 | }); 98 | }); 99 | 100 | test('pkg-conf: defaults', function (t) { 101 | execCli([], {dirname: 'fixture/pkg-conf/defaults'}, function (err) { 102 | t.ifError(err); 103 | t.end(); 104 | }); 105 | }); 106 | 107 | test('pkg-conf: pkg-overrides', function (t) { 108 | execCli([], {dirname: 'fixture/pkg-conf/pkg-overrides'}, function (err) { 109 | t.ifError(err); 110 | t.end(); 111 | }); 112 | }); 113 | 114 | test('pkg-conf: cli takes precedence', function (t) { 115 | execCli(['--match=foo*', '--no-serial', '--cache', '--no-fail-fast', '--require=./required.js', 'c.js'], {dirname: 'fixture/pkg-conf/precedence'}, function (err) { 116 | t.ifError(err); 117 | t.end(); 118 | }); 119 | }); 120 | 121 | test('watcher works', function (t) { 122 | var killed = false; 123 | 124 | var hasChokidar = false; 125 | try { 126 | require('chokidar'); 127 | hasChokidar = true; 128 | } catch (err) {} 129 | 130 | var child = execCli(['--verbose', '--watch', 'test.js'], {dirname: 'fixture/watcher'}, function (err, stdout) { 131 | if (err && err.code === 1 && !hasChokidar) { 132 | t.comment('chokidar dependency is missing, cannot test watcher'); 133 | t.match(stdout, 'The optional dependency chokidar failed to install and is required for --watch. Chokidar is likely not supported on your platform.'); 134 | t.end(); 135 | } else { 136 | t.ok(killed); 137 | t.ifError(err); 138 | t.end(); 139 | } 140 | }); 141 | 142 | var buffer = ''; 143 | var passedFirst = false; 144 | child.stderr.on('data', function (str) { 145 | buffer += str; 146 | if (/1 test passed/.test(buffer)) { 147 | if (!passedFirst) { 148 | touch.sync(path.join(__dirname, 'fixture/watcher/test.js')); 149 | buffer = ''; 150 | passedFirst = true; 151 | } else if (!killed) { 152 | child.kill(); 153 | killed = true; 154 | } 155 | } 156 | }); 157 | }); 158 | 159 | test('--match works', function (t) { 160 | execCli(['-m=foo', '-m=bar', '-m=!baz', '-m=t* a* f*', '-m=!t* a* n* f*', 'fixture/matcher-skip.js'], function (err) { 161 | t.ifError(err); 162 | t.end(); 163 | }); 164 | }); 165 | 166 | test('handles NODE_PATH', function (t) { 167 | var nodePaths = 'fixture/node-paths/modules' + path.delimiter + 'fixture/node-paths/deep/nested'; 168 | 169 | execCli('fixture/node-paths.js', {env: {NODE_PATH: nodePaths}}, function (err) { 170 | t.ifError(err); 171 | t.end(); 172 | }); 173 | }); 174 | -------------------------------------------------------------------------------- /index.d.ts: -------------------------------------------------------------------------------- 1 | export interface Observable { 2 | subscribe(observer: (value: {}) => void): void; 3 | } 4 | 5 | export type Test = (t: TestContext) => Promise | Iterator | Observable | void; 6 | export type ContextualTest = (t: ContextualTestContext) => Promise | Iterator | Observable | void; 7 | export type SerialTest = (t: TestContext) => void; 8 | export type ContextualSerialTest = (t: ContextualTestContext) => void; 9 | export type CallbackTest = (t: CallbackTestContext) => void; 10 | export type ContextualCallbackTest = (t: ContextualCallbackTestContext) => void; 11 | 12 | export interface Runner { 13 | (name: string, run: Test): void; 14 | (run: Test): void; 15 | skip: Runner; 16 | cb: CallbackRunner; 17 | } 18 | export interface ContextualRunner { 19 | (name: string, run: ContextualTest): void; 20 | (run: ContextualTest): void; 21 | skip: ContextualRunner; 22 | cb: ContextualCallbackRunner; 23 | } 24 | export interface SerialRunner { 25 | (name: string, run: SerialTest): void; 26 | (run: SerialTest): void; 27 | skip: SerialRunner; 28 | } 29 | export interface ContextualSerialRunner { 30 | (name: string, run: ContextualSerialTest): void; 31 | (run: ContextualSerialTest): void; 32 | skip: ContextualSerialRunner; 33 | } 34 | export interface CallbackRunner { 35 | (name: string, run: CallbackTest): void; 36 | (run: CallbackTest): void; 37 | skip: CallbackRunner; 38 | } 39 | export interface ContextualCallbackRunner { 40 | (name: string, run: ContextualCallbackTest): void; 41 | (run: ContextualCallbackTest): void; 42 | skip: ContextualCallbackRunner; 43 | } 44 | 45 | export function test(name: string, run: ContextualTest): void; 46 | export function test(run: ContextualTest): void; 47 | export namespace test { 48 | export const before: Runner; 49 | export const after: Runner; 50 | export const beforeEach: ContextualRunner; 51 | export const afterEach: ContextualRunner; 52 | 53 | export const skip: typeof test; 54 | export const only: typeof test; 55 | 56 | export function serial(name: string, run: ContextualSerialTest): void; 57 | export function serial(run: ContextualSerialTest): void; 58 | export function cb(name: string, run: ContextualCallbackTest): void; 59 | export function cb(run: ContextualCallbackTest): void; 60 | } 61 | export namespace test.serial { 62 | export const before: SerialRunner; 63 | export const after: SerialRunner; 64 | export const beforeEach: ContextualSerialRunner; 65 | export const afterEach: ContextualSerialRunner; 66 | 67 | export const skip: typeof test.serial; 68 | export const only: typeof test.serial; 69 | } 70 | export namespace test.cb { 71 | export const before: CallbackRunner; 72 | export const after: CallbackRunner; 73 | export const beforeEach: ContextualCallbackRunner; 74 | export const afterEach: ContextualCallbackRunner; 75 | 76 | export const skip: typeof test.cb; 77 | export const only: typeof test.cb; 78 | } 79 | export default test; 80 | 81 | export type ErrorValidator 82 | = (new (...args: any[]) => any) 83 | | RegExp 84 | | string 85 | | ((error: any) => boolean); 86 | 87 | export interface AssertContext { 88 | /** 89 | * Passing assertion. 90 | */ 91 | pass(message?: string): void; 92 | /** 93 | * Failing assertion. 94 | */ 95 | fail(message?: string): void; 96 | /** 97 | * Assert that value is truthy. 98 | */ 99 | ok(value: any, message?: string): void; 100 | /** 101 | * Assert that value is falsy. 102 | */ 103 | notOk(value: any, message?: string): void; 104 | /** 105 | * Assert that value is true. 106 | */ 107 | true(value: boolean, message?: string): void; 108 | /** 109 | * Assert that value is false. 110 | */ 111 | false(value: boolean, message?: string): void; 112 | /** 113 | * Assert that value is equal to expected. 114 | */ 115 | is(value: U, expected: U, message?: string): void; 116 | /** 117 | * Assert that value is not equal to expected. 118 | */ 119 | not(value: U, expected: U, message?: string): void; 120 | /** 121 | * Assert that value is deep equal to expected. 122 | */ 123 | same(value: U, expected: U, message?: string): void; 124 | /** 125 | * Assert that value is not deep equal to expected. 126 | */ 127 | notSame(value: U, expected: U, message?: string): void; 128 | /** 129 | * Assert that function throws an error or promise rejects. 130 | * @param error Can be a constructor, regex, error message or validation function. 131 | */ 132 | throws(value: Promise<{}>, error?: ErrorValidator, message?: string): Promise; 133 | throws(value: () => void, error?: ErrorValidator, message?: string): any; 134 | /** 135 | * Assert that function doesn't throw an error or promise resolves. 136 | */ 137 | notThrows(value: Promise, message?: string): Promise; 138 | notThrows(value: () => void, message?: string): void; 139 | /** 140 | * Assert that contents matches regex. 141 | */ 142 | regex(contents: string, regex: RegExp, message?: string): void; 143 | /** 144 | * Assert that error is falsy. 145 | */ 146 | ifError(error: any, message?: string): void; 147 | } 148 | export interface TestContext extends AssertContext { 149 | /** 150 | * Plan how many assertion there are in the test. 151 | * The test will fail if the actual assertion count doesn't match planned assertions. 152 | */ 153 | plan(count: number): void; 154 | 155 | skip: AssertContext; 156 | } 157 | export interface CallbackTestContext extends TestContext { 158 | /** 159 | * End the test. 160 | */ 161 | end(): void; 162 | } 163 | export interface ContextualTestContext extends TestContext { 164 | context: any; 165 | } 166 | export interface ContextualCallbackTestContext extends CallbackTestContext { 167 | context: any; 168 | } 169 | -------------------------------------------------------------------------------- /docs/recipes/code-coverage.md: -------------------------------------------------------------------------------- 1 | # Code coverage 2 | 3 | Translations: [Español](https://github.com/sindresorhus/ava-docs/blob/master/es_ES/docs/recipes/code-coverage.md), [Français](https://github.com/sindresorhus/ava-docs/blob/master/fr_FR/docs/recipes/code-coverage.md), [日本語](https://github.com/sindresorhus/ava-docs/blob/master/ja_JP/docs/recipes/code-coverage.md), [Português](https://github.com/sindresorhus/ava-docs/blob/master/pt_BR/docs/recipes/code-coverage.md), [Русский](https://github.com/sindresorhus/ava-docs/blob/master/ru_RU/docs/recipes/code-coverage.md) 4 | 5 | As AVA [spawns the test files][process-isolation], you can't use [`istanbul`] for code coverage; instead, you can achieve this with [`nyc`] which is basically [`istanbul`] with sub-process support. 6 | 7 | ## Setup 8 | 9 | First install NYC: 10 | 11 | ``` 12 | $ npm install nyc --save-dev 13 | ``` 14 | 15 | Then add both the `.nyc_output` and `coverage` directories to your `.gitignore` file. 16 | 17 | `.gitignore`: 18 | 19 | ``` 20 | node_modules 21 | coverage 22 | .nyc_output 23 | ``` 24 | 25 | ## ES5 coverage 26 | 27 | Using NYC to provide coverage for production code written in ES5 is simple. Just prepend your test script with `nyc`: 28 | 29 | ```json 30 | { 31 | "scripts": { 32 | "test": "nyc ava" 33 | } 34 | } 35 | ``` 36 | 37 | That's it! 38 | 39 | If you want to create HTML coverage reports, or upload coverage data to Coveralls, you should skip down to those sections below. 40 | 41 | ## ES2015 coverage 42 | 43 | Using Babel to transpile your production code is a bit more involved. Here we've broken it down into multiple steps. 44 | 45 | ### Configure Babel 46 | 47 | First, we need a Babel configuration. The following is just an example. You will need to modify it to fit your needs. 48 | 49 | `package.json`: 50 | ```json 51 | { 52 | "babel": { 53 | "presets": ["es2015"], 54 | "plugins": ["transform-runtime"], 55 | "ignore": "test.js", 56 | "env": { 57 | "development": { 58 | "sourceMaps": "inline" 59 | } 60 | } 61 | } 62 | } 63 | ``` 64 | 65 | There are two important things to note from the example above. 66 | 67 | 1. We ignore test files because AVA already handles transpiling tests for you. 68 | 69 | 2. We specify `inline` source maps for development. This is important for properly generating coverage. Using the `env` section of the Babel configuration allows us to disable source maps for production builds. 70 | 71 | 72 | ### Create a build script 73 | 74 | Since it is unlikely you want `inline` source maps in your production code. You should specify an alternate environment variable in your build scripts: 75 | 76 | `package.json` 77 | 78 | ```json 79 | { 80 | "scripts": { 81 | "build": "BABEL_ENV=production babel --out-dir=dist index.js" 82 | } 83 | } 84 | ``` 85 | 86 | > WARNING: `BABEL_ENV=production` does not work on Windows, you must use the `set` keyword (`set BABEL_ENV=production`). For cross platform builds, check out [`cross-env`]. 87 | 88 | Note that the build script really has very little to do with AVA, and is just a demonstration of how to use Babel's `env` configuration to manipulate your config so it's compatible with AVA. 89 | 90 | ### Use the Babel require hook 91 | 92 | To use the Babel require hook, add `babel-core/register` to the `require` section of you AVA config in `package.json`. 93 | 94 | ```json 95 | { 96 | "ava": { 97 | "require": ["babel-core/register"] 98 | } 99 | } 100 | ``` 101 | 102 | *Note*: You can also set the require hook from the command line: `ava --require=babel-core/register`. However, configuring it in `package.json` saves you from repeatedly typing that flag. 103 | 104 | ### Putting it all together 105 | 106 | Combining the above steps, your complete `package.json` should look something like this: 107 | 108 | ```json 109 | { 110 | "scripts": { 111 | "test": "nyc ava", 112 | "build": "BABEL_ENV=production babel --out-dir=dist index.js" 113 | }, 114 | "babel": { 115 | "presets": ["es2015"], 116 | "plugins": ["transform-runtime"], 117 | "ignore": "test.js", 118 | "env": { 119 | "development": { 120 | "sourceMaps": "inline" 121 | } 122 | } 123 | }, 124 | "ava": { 125 | "require": ["babel-core/register"] 126 | } 127 | } 128 | ``` 129 | 130 | 131 | ## HTML reports 132 | 133 | NYC creates a `json` coverage file for each forked process in the `.nyc_ouput` directory. 134 | 135 | To combine those into a human readable HTML report, do the following: 136 | 137 | ``` 138 | $ ./node_modules/.bin/nyc report --reporter=html 139 | ``` 140 | 141 | Or, use an npm script to save on typing: 142 | 143 | ```json 144 | { 145 | "scripts": { 146 | "report": "nyc report --reporter=html" 147 | } 148 | } 149 | ``` 150 | 151 | This will output a HTML file to the `coverage` directory. 152 | 153 | 154 | ## Hosted coverage 155 | 156 | ### Travis CI & Coveralls 157 | 158 | First, you must login to [coveralls.io] and activate your repository. 159 | 160 | Once that is done, add [`coveralls`] as a development dependency: 161 | 162 | ``` 163 | $ npm install coveralls --save-dev 164 | ``` 165 | 166 | Then add the following to your `.travis.yml`: 167 | 168 | ```yaml 169 | after_success: 170 | - './node_modules/.bin/nyc report --reporter=text-lcov | ./node_modules/.bin/coveralls' 171 | ``` 172 | 173 | Your coverage report will then appear on coveralls shortly after Travis completes. 174 | 175 | [`babel`]: https://github.com/babel/babel 176 | [coveralls.io]: https://coveralls.io 177 | [`coveralls`]: https://github.com/nickmerwin/node-coveralls 178 | [`cross-env`]: https://github.com/kentcdodds/cross-env 179 | [process-isolation]: https://github.com/sindresorhus/ava#process-isolation 180 | [`istanbul`]: https://github.com/gotwarlost/istanbul 181 | [`nyc`]: https://github.com/bcoe/nyc 182 | -------------------------------------------------------------------------------- /test/assert.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var test = require('tap').test; 3 | var Promise = require('bluebird'); 4 | var assert = require('../lib/assert'); 5 | 6 | test('.pass()', function (t) { 7 | t.doesNotThrow(function () { 8 | assert.pass(); 9 | }); 10 | 11 | t.end(); 12 | }); 13 | 14 | test('.fail()', function (t) { 15 | t.throws(function () { 16 | assert.fail(); 17 | }); 18 | 19 | t.end(); 20 | }); 21 | 22 | test('.ok()', function (t) { 23 | t.throws(function () { 24 | assert.ok(0); 25 | assert.ok(false); 26 | }); 27 | 28 | t.doesNotThrow(function () { 29 | assert.ok(1); 30 | assert.ok(true); 31 | }); 32 | 33 | t.end(); 34 | }); 35 | 36 | test('.notOk()', function (t) { 37 | t.throws(function () { 38 | assert.notOk(1); 39 | assert.notOk(true); 40 | }); 41 | 42 | t.doesNotThrow(function () { 43 | assert.notOk(0); 44 | assert.notOk(false); 45 | }); 46 | 47 | t.end(); 48 | }); 49 | 50 | test('.true()', function (t) { 51 | t.throws(function () { 52 | assert.true(1); 53 | }); 54 | 55 | t.throws(function () { 56 | assert.true(0); 57 | }); 58 | 59 | t.throws(function () { 60 | assert.true(false); 61 | }); 62 | 63 | t.throws(function () { 64 | assert.true('foo'); 65 | }); 66 | 67 | t.doesNotThrow(function () { 68 | assert.true(true); 69 | }); 70 | 71 | t.end(); 72 | }); 73 | 74 | test('.false()', function (t) { 75 | t.throws(function () { 76 | assert.false(0); 77 | }); 78 | 79 | t.throws(function () { 80 | assert.false(1); 81 | }); 82 | 83 | t.throws(function () { 84 | assert.false(true); 85 | }); 86 | 87 | t.throws(function () { 88 | assert.false('foo'); 89 | }); 90 | 91 | t.doesNotThrow(function () { 92 | assert.false(false); 93 | }); 94 | 95 | t.end(); 96 | }); 97 | 98 | test('.is()', function (t) { 99 | t.doesNotThrow(function () { 100 | assert.is('foo', 'foo'); 101 | }); 102 | 103 | t.throws(function () { 104 | assert.is('foo', 'bar'); 105 | }); 106 | 107 | t.end(); 108 | }); 109 | 110 | test('.not()', function (t) { 111 | t.doesNotThrow(function () { 112 | assert.not('foo', 'bar'); 113 | }); 114 | 115 | t.throws(function () { 116 | assert.not('foo', 'foo'); 117 | }); 118 | 119 | t.end(); 120 | }); 121 | 122 | test('.same()', function (t) { 123 | t.doesNotThrow(function () { 124 | assert.same({a: 'a'}, {a: 'a'}); 125 | }); 126 | 127 | t.doesNotThrow(function () { 128 | assert.same(['a', 'b'], ['a', 'b']); 129 | }); 130 | 131 | t.throws(function () { 132 | assert.same({a: 'a'}, {a: 'b'}); 133 | }); 134 | 135 | t.throws(function () { 136 | assert.same(['a', 'b'], ['a', 'a']); 137 | }); 138 | 139 | t.throws(function () { 140 | assert.same([['a', 'b'], 'c'], [['a', 'b'], 'd']); 141 | }, / 'c' ].*? 'd' ]/); 142 | 143 | t.throws(function () { 144 | var circular = ['a', 'b']; 145 | circular.push(circular); 146 | assert.same([circular, 'c'], [circular, 'd']); 147 | }, / 'c' ].*? 'd' ]/); 148 | 149 | t.end(); 150 | }); 151 | 152 | test('.notSame()', function (t) { 153 | t.doesNotThrow(function () { 154 | assert.notSame({a: 'a'}, {a: 'b'}); 155 | }); 156 | 157 | t.doesNotThrow(function () { 158 | assert.notSame(['a', 'b'], ['c', 'd']); 159 | }); 160 | 161 | t.throws(function () { 162 | assert.notSame({a: 'a'}, {a: 'a'}); 163 | }); 164 | 165 | t.throws(function () { 166 | assert.notSame(['a', 'b'], ['a', 'b']); 167 | }); 168 | 169 | t.end(); 170 | }); 171 | 172 | test('.throws()', function (t) { 173 | t.throws(function () { 174 | assert.throws(function () {}); 175 | }); 176 | 177 | t.doesNotThrow(function () { 178 | assert.throws(function () { 179 | throw new Error('foo'); 180 | }); 181 | }); 182 | 183 | t.end(); 184 | }); 185 | 186 | test('.throws() returns the thrown error', function (t) { 187 | var expected = new Error(); 188 | var actual = assert.throws(function () { 189 | throw expected; 190 | }); 191 | 192 | t.is(actual, expected); 193 | 194 | t.end(); 195 | }); 196 | 197 | test('.throws() returns the rejection reason of promise', function (t) { 198 | var expected = new Error(); 199 | 200 | assert.throws(Promise.reject(expected)).then(function (actual) { 201 | t.is(actual, expected); 202 | t.end(); 203 | }); 204 | }); 205 | 206 | test('.notThrows()', function (t) { 207 | t.doesNotThrow(function () { 208 | assert.notThrows(function () {}); 209 | }); 210 | 211 | t.throws(function () { 212 | assert.notThrows(function () { 213 | throw new Error('foo'); 214 | }); 215 | }); 216 | 217 | t.end(); 218 | }); 219 | 220 | test('.doesNotThrow() alias for .notThrows()', function (t) { 221 | process.noDeprecation = true; 222 | 223 | t.doesNotThrow(function () { 224 | assert.doesNotThrow(function () {}); 225 | }); 226 | 227 | t.throws(function () { 228 | assert.doesNotThrow(function () { 229 | throw new Error('foo'); 230 | }); 231 | }); 232 | 233 | process.noDeprecation = false; 234 | 235 | t.end(); 236 | }); 237 | 238 | test('.regex()', function (t) { 239 | t.doesNotThrow(function () { 240 | assert.regex('abc', /^abc$/); 241 | }); 242 | 243 | t.throws(function () { 244 | assert.regex('foo', /^abc$/); 245 | }); 246 | 247 | t.end(); 248 | }); 249 | 250 | test('.ifError()', function (t) { 251 | t.throws(function () { 252 | assert.ifError(new Error()); 253 | }); 254 | 255 | t.doesNotThrow(function () { 256 | assert.ifError(null); 257 | }); 258 | 259 | t.end(); 260 | }); 261 | 262 | test('.same() should not mask RangeError from underlying assert', function (t) { 263 | var Circular = function () { 264 | this.test = this; 265 | }; 266 | 267 | var a = new Circular(); 268 | var b = new Circular(); 269 | 270 | t.throws(function () { 271 | assert.notSame(a, b); 272 | }); 273 | 274 | t.doesNotThrow(function () { 275 | assert.same(a, b); 276 | }); 277 | 278 | t.end(); 279 | }); 280 | -------------------------------------------------------------------------------- /test/hooks.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var path = require('path'); 3 | var test = require('tap').test; 4 | var Runner = require('../lib/runner'); 5 | var _fork = require('../lib/fork.js'); 6 | var CachingPrecompiler = require('../lib/caching-precompiler'); 7 | var cacheDir = path.join(__dirname, '../node_modules/.cache/ava'); 8 | var precompiler = new CachingPrecompiler(cacheDir); 9 | 10 | function fork(testPath) { 11 | return _fork(testPath, { 12 | cacheDir: cacheDir, 13 | precompiled: precompiler.generateHashForFile(testPath) 14 | }); 15 | } 16 | 17 | test('before', function (t) { 18 | t.plan(1); 19 | 20 | var runner = new Runner(); 21 | var arr = []; 22 | 23 | runner.before(function () { 24 | arr.push('a'); 25 | }); 26 | 27 | runner.test(function () { 28 | arr.push('b'); 29 | }); 30 | 31 | runner.run({}).then(function () { 32 | t.same(arr, ['a', 'b']); 33 | }); 34 | }); 35 | 36 | test('after', function (t) { 37 | t.plan(3); 38 | 39 | var runner = new Runner(); 40 | var arr = []; 41 | 42 | runner.after(function () { 43 | arr.push('b'); 44 | }); 45 | 46 | runner.test(function () { 47 | arr.push('a'); 48 | }); 49 | 50 | runner.run({}).then(function () { 51 | t.is(runner.stats.passCount, 1); 52 | t.is(runner.stats.failCount, 0); 53 | t.same(arr, ['a', 'b']); 54 | t.end(); 55 | }); 56 | }); 57 | 58 | test('stop if before hooks failed', function (t) { 59 | t.plan(1); 60 | 61 | var runner = new Runner(); 62 | var arr = []; 63 | 64 | runner.before(function () { 65 | arr.push('a'); 66 | }); 67 | 68 | runner.before(function () { 69 | throw new Error('something went wrong'); 70 | }); 71 | 72 | runner.test(function (a) { 73 | arr.push('b'); 74 | a.end(); 75 | }); 76 | 77 | runner.run({}).then(function () { 78 | t.same(arr, ['a']); 79 | t.end(); 80 | }); 81 | }); 82 | 83 | test('before each with concurrent tests', function (t) { 84 | t.plan(1); 85 | 86 | var runner = new Runner(); 87 | var arr = [[], []]; 88 | var i = 0; 89 | var k = 0; 90 | 91 | runner.beforeEach(function () { 92 | arr[i++].push('a'); 93 | }); 94 | 95 | runner.beforeEach(function () { 96 | arr[k++].push('b'); 97 | }); 98 | 99 | runner.test(function () { 100 | arr[0].push('c'); 101 | }); 102 | 103 | runner.test(function () { 104 | arr[1].push('d'); 105 | }); 106 | 107 | runner.run({}).then(function () { 108 | t.same(arr, [['a', 'b', 'c'], ['a', 'b', 'd']]); 109 | t.end(); 110 | }); 111 | }); 112 | 113 | test('before each with serial tests', function (t) { 114 | t.plan(1); 115 | 116 | var runner = new Runner(); 117 | var arr = []; 118 | 119 | runner.beforeEach(function () { 120 | arr.push('a'); 121 | }); 122 | 123 | runner.beforeEach(function () { 124 | arr.push('b'); 125 | }); 126 | 127 | runner.serial(function () { 128 | arr.push('c'); 129 | }); 130 | 131 | runner.serial(function () { 132 | arr.push('d'); 133 | }); 134 | 135 | runner.run({}).then(function () { 136 | t.same(arr, ['a', 'b', 'c', 'a', 'b', 'd']); 137 | t.end(); 138 | }); 139 | }); 140 | 141 | test('fail if beforeEach hook fails', function (t) { 142 | t.plan(2); 143 | 144 | var runner = new Runner(); 145 | var arr = []; 146 | 147 | runner.beforeEach(function (a) { 148 | arr.push('a'); 149 | a.fail(); 150 | }); 151 | 152 | runner.test(function (a) { 153 | arr.push('b'); 154 | a.pass(); 155 | }); 156 | 157 | runner.run({}).then(function () { 158 | t.is(runner.stats.failCount, 1); 159 | t.same(arr, ['a']); 160 | t.end(); 161 | }); 162 | }); 163 | 164 | test('after each with concurrent tests', function (t) { 165 | t.plan(1); 166 | 167 | var runner = new Runner(); 168 | var arr = [[], []]; 169 | var i = 0; 170 | var k = 0; 171 | 172 | runner.afterEach(function () { 173 | arr[i++].push('a'); 174 | }); 175 | 176 | runner.afterEach(function () { 177 | arr[k++].push('b'); 178 | }); 179 | 180 | runner.test(function () { 181 | arr[0].push('c'); 182 | }); 183 | 184 | runner.test(function () { 185 | arr[1].push('d'); 186 | }); 187 | 188 | runner.run({}).then(function () { 189 | t.same(arr, [['c', 'a', 'b'], ['d', 'a', 'b']]); 190 | t.end(); 191 | }); 192 | }); 193 | 194 | test('after each with serial tests', function (t) { 195 | t.plan(1); 196 | 197 | var runner = new Runner(); 198 | var arr = []; 199 | 200 | runner.afterEach(function () { 201 | arr.push('a'); 202 | }); 203 | 204 | runner.afterEach(function () { 205 | arr.push('b'); 206 | }); 207 | 208 | runner.serial(function () { 209 | arr.push('c'); 210 | }); 211 | 212 | runner.serial(function () { 213 | arr.push('d'); 214 | }); 215 | 216 | runner.run({}).then(function () { 217 | t.same(arr, ['c', 'a', 'b', 'd', 'a', 'b']); 218 | t.end(); 219 | }); 220 | }); 221 | 222 | test('ensure hooks run only around tests', function (t) { 223 | t.plan(1); 224 | 225 | var runner = new Runner(); 226 | var arr = []; 227 | 228 | runner.beforeEach(function () { 229 | arr.push('beforeEach'); 230 | }); 231 | 232 | runner.before(function () { 233 | arr.push('before'); 234 | }); 235 | 236 | runner.afterEach(function () { 237 | arr.push('afterEach'); 238 | }); 239 | 240 | runner.after(function () { 241 | arr.push('after'); 242 | }); 243 | 244 | runner.test(function () { 245 | arr.push('test'); 246 | }); 247 | 248 | runner.run({}).then(function () { 249 | t.same(arr, ['before', 'beforeEach', 'test', 'afterEach', 'after']); 250 | t.end(); 251 | }); 252 | }); 253 | 254 | test('shared context', function (t) { 255 | t.plan(1); 256 | 257 | var runner = new Runner(); 258 | 259 | runner.before(function (a) { 260 | a.is(a.context, null); 261 | }); 262 | 263 | runner.after(function (a) { 264 | a.is(a.context, null); 265 | }); 266 | 267 | runner.beforeEach(function (a) { 268 | a.context.arr = ['a']; 269 | }); 270 | 271 | runner.test(function (a) { 272 | a.context.arr.push('b'); 273 | a.same(a.context.arr, ['a', 'b']); 274 | }); 275 | 276 | runner.afterEach(function (a) { 277 | a.context.arr.push('c'); 278 | a.same(a.context.arr, ['a', 'b', 'c']); 279 | }); 280 | 281 | runner.run({}).then(function () { 282 | t.is(runner.stats.failCount, 0); 283 | t.end(); 284 | }); 285 | }); 286 | 287 | test('shared context of any type', function (t) { 288 | t.plan(1); 289 | 290 | var runner = new Runner(); 291 | 292 | runner.beforeEach(function (a) { 293 | a.context = 'foo'; 294 | }); 295 | 296 | runner.test(function (a) { 297 | a.is(a.context, 'foo'); 298 | }); 299 | 300 | runner.run({}).then(function () { 301 | t.is(runner.stats.failCount, 0); 302 | t.end(); 303 | }); 304 | }); 305 | 306 | test('don\'t display hook title if it did not fail', function (t) { 307 | t.plan(2); 308 | 309 | fork(path.join(__dirname, 'fixture', 'hooks-passing.js')) 310 | .run({}) 311 | .on('test', function (test) { 312 | t.same(test.error, null); 313 | t.is(test.title, 'pass'); 314 | }) 315 | .then(function () { 316 | t.end(); 317 | }); 318 | }); 319 | 320 | test('display hook title if it failed', function (t) { 321 | t.plan(2); 322 | 323 | fork(path.join(__dirname, 'fixture', 'hooks-failing.js')) 324 | .run({}) 325 | .on('test', function (test) { 326 | t.is(test.error.name, 'AssertionError'); 327 | t.is(test.title, 'fail for pass'); 328 | }) 329 | .then(function () { 330 | t.end(); 331 | }); 332 | }); 333 | -------------------------------------------------------------------------------- /test/reporters/mini.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var chalk = require('chalk'); 3 | var test = require('tap').test; 4 | var AvaError = require('../../lib/ava-error'); 5 | var _miniReporter = require('../../lib/reporters/mini'); 6 | var beautifyStack = require('../../lib/beautify-stack'); 7 | 8 | chalk.enabled = true; 9 | 10 | var graySpinner = chalk.gray.dim('⠋'); 11 | 12 | // Needed because tap doesn't emulate a tty environment and thus this is 13 | // undefined, making `cli-truncate` append '...' to test titles 14 | process.stdout.columns = 5000; 15 | 16 | function miniReporter() { 17 | var reporter = _miniReporter(); 18 | reporter.start = function () { 19 | return ''; 20 | }; 21 | return reporter; 22 | } 23 | 24 | process.stderr.setMaxListeners(50); 25 | 26 | test('start', function (t) { 27 | var reporter = _miniReporter(); 28 | 29 | t.is(reporter.start(), ' \n ' + graySpinner + ' '); 30 | reporter.clearInterval(); 31 | t.end(); 32 | }); 33 | 34 | test('passing test', function (t) { 35 | var reporter = miniReporter(); 36 | 37 | var actualOutput = reporter.test({ 38 | title: 'passed' 39 | }); 40 | 41 | var expectedOutput = [ 42 | ' ', 43 | ' ' + graySpinner + ' passed', 44 | '', 45 | ' ' + chalk.green('1 passed') 46 | ].join('\n'); 47 | 48 | t.is(actualOutput, expectedOutput); 49 | t.end(); 50 | }); 51 | 52 | test('failing test', function (t) { 53 | var reporter = miniReporter(); 54 | 55 | var actualOutput = reporter.test({ 56 | title: 'failed', 57 | error: { 58 | message: 'assertion failed' 59 | } 60 | }); 61 | 62 | var expectedOutput = [ 63 | ' ', 64 | ' ' + graySpinner + ' ' + chalk.red('failed'), 65 | '', 66 | ' ' + chalk.red('1 failed') 67 | ].join('\n'); 68 | 69 | t.is(actualOutput, expectedOutput); 70 | t.end(); 71 | }); 72 | 73 | test('passing test after failing', function (t) { 74 | var reporter = miniReporter(); 75 | 76 | reporter.test({ 77 | title: 'failed', 78 | error: { 79 | message: 'assertion failed' 80 | } 81 | }); 82 | 83 | var actualOutput = reporter.test({title: 'passed'}); 84 | 85 | var expectedOutput = [ 86 | ' ', 87 | ' ' + graySpinner + ' passed', 88 | '', 89 | ' ' + chalk.green('1 passed'), 90 | ' ' + chalk.red('1 failed') 91 | ].join('\n'); 92 | 93 | t.is(actualOutput, expectedOutput); 94 | t.end(); 95 | }); 96 | 97 | test('failing test after passing', function (t) { 98 | var reporter = miniReporter(); 99 | 100 | reporter.test({title: 'passed'}); 101 | 102 | var actualOutput = reporter.test({ 103 | title: 'failed', 104 | error: { 105 | message: 'assertion failed' 106 | } 107 | }); 108 | 109 | var expectedOutput = [ 110 | ' ', 111 | ' ' + graySpinner + ' ' + chalk.red('failed'), 112 | '', 113 | ' ' + chalk.green('1 passed'), 114 | ' ' + chalk.red('1 failed') 115 | ].join('\n'); 116 | 117 | t.is(actualOutput, expectedOutput); 118 | t.end(); 119 | }); 120 | 121 | test('skipped test', function (t) { 122 | var reporter = miniReporter(); 123 | 124 | var output = reporter.test({ 125 | title: 'skipped', 126 | skip: true 127 | }); 128 | 129 | t.false(output); 130 | t.end(); 131 | }); 132 | 133 | test('todo test', function (t) { 134 | var reporter = miniReporter(); 135 | 136 | var output = reporter.test({ 137 | title: 'todo', 138 | skip: true, 139 | todo: true 140 | }); 141 | 142 | t.false(output); 143 | t.end(); 144 | }); 145 | 146 | test('results with passing tests', function (t) { 147 | var reporter = miniReporter(); 148 | reporter.passCount = 1; 149 | reporter.failCount = 0; 150 | 151 | var actualOutput = reporter.finish(); 152 | var expectedOutput = [ 153 | '\n ' + chalk.green('1 passed'), 154 | '' 155 | ].join('\n'); 156 | 157 | t.is(actualOutput, expectedOutput); 158 | t.end(); 159 | }); 160 | 161 | test('results with skipped tests', function (t) { 162 | var reporter = miniReporter(); 163 | reporter.passCount = 0; 164 | reporter.skipCount = 1; 165 | reporter.failCount = 0; 166 | 167 | var actualOutput = reporter.finish(); 168 | var expectedOutput = [ 169 | '\n ' + chalk.yellow('1 skipped'), 170 | '' 171 | ].join('\n'); 172 | 173 | t.is(actualOutput, expectedOutput); 174 | t.end(); 175 | }); 176 | 177 | test('results with todo tests', function (t) { 178 | var reporter = miniReporter(); 179 | reporter.passCount = 0; 180 | reporter.todoCount = 1; 181 | reporter.failCount = 0; 182 | 183 | var actualOutput = reporter.finish(); 184 | var expectedOutput = [ 185 | '\n ' + chalk.blue('1 todo'), 186 | '' 187 | ].join('\n'); 188 | 189 | t.is(actualOutput, expectedOutput); 190 | t.end(); 191 | }); 192 | 193 | test('results with passing skipped tests', function (t) { 194 | var reporter = miniReporter(); 195 | reporter.passCount = 1; 196 | reporter.skipCount = 1; 197 | 198 | var output = reporter.finish().split('\n'); 199 | 200 | t.is(output[0], ''); 201 | t.is(output[1], ' ' + chalk.green('1 passed')); 202 | t.is(output[2], ' ' + chalk.yellow('1 skipped')); 203 | t.is(output[3], ''); 204 | t.end(); 205 | }); 206 | 207 | test('results with passing tests and rejections', function (t) { 208 | var reporter = miniReporter(); 209 | reporter.passCount = 1; 210 | reporter.rejectionCount = 1; 211 | 212 | var err = new Error('failure'); 213 | err.type = 'rejection'; 214 | err.stack = beautifyStack(err.stack); 215 | 216 | reporter.api = { 217 | errors: [err] 218 | }; 219 | 220 | var output = reporter.finish().split('\n'); 221 | 222 | t.is(output[0], ''); 223 | t.is(output[1], ' ' + chalk.green('1 passed')); 224 | t.is(output[2], ' ' + chalk.red('1 rejection')); 225 | t.is(output[3], ''); 226 | t.is(output[4], ' ' + chalk.red('1. Unhandled Rejection')); 227 | t.match(output[5], /Error: failure/); 228 | t.match(output[6], /test\/reporters\/mini\.js/); 229 | t.end(); 230 | }); 231 | 232 | test('results with passing tests and exceptions', function (t) { 233 | var reporter = miniReporter(); 234 | reporter.passCount = 1; 235 | reporter.exceptionCount = 2; 236 | 237 | var err = new Error('failure'); 238 | err.type = 'exception'; 239 | err.stack = beautifyStack(err.stack); 240 | 241 | var avaErr = new AvaError('A futuristic test runner'); 242 | avaErr.type = 'exception'; 243 | 244 | reporter.api = { 245 | errors: [err, avaErr] 246 | }; 247 | 248 | var output = reporter.finish().split('\n'); 249 | 250 | t.is(output[0], ''); 251 | t.is(output[1], ' ' + chalk.green('1 passed')); 252 | t.is(output[2], ' ' + chalk.red('2 exceptions')); 253 | t.is(output[3], ''); 254 | t.is(output[4], ' ' + chalk.red('1. Uncaught Exception')); 255 | t.match(output[5], /Error: failure/); 256 | t.match(output[6], /test\/reporters\/mini\.js/); 257 | var next = 6 + output.slice(6).indexOf('') + 1; 258 | t.is(output[next], ' ' + chalk.red('2. A futuristic test runner')); 259 | t.end(); 260 | }); 261 | 262 | test('results with errors', function (t) { 263 | var reporter = miniReporter(); 264 | reporter.failCount = 1; 265 | 266 | var err = new Error('failure'); 267 | err.stack = beautifyStack(err.stack); 268 | 269 | reporter.api = { 270 | errors: [{ 271 | title: 'failed', 272 | error: err 273 | }] 274 | }; 275 | 276 | var output = reporter.finish().split('\n'); 277 | 278 | t.is(output[0], ''); 279 | t.is(output[1], ' ' + chalk.red('1 failed')); 280 | t.is(output[2], ''); 281 | t.is(output[3], ' ' + chalk.red('1. failed')); 282 | t.match(output[4], /failure/); 283 | t.match(output[5], /test\/reporters\/mini\.js/); 284 | t.end(); 285 | }); 286 | 287 | test('empty results after reset', function (t) { 288 | var reporter = miniReporter(); 289 | 290 | reporter.failCount = 1; 291 | reporter.reset(); 292 | 293 | var output = reporter.finish(); 294 | t.is(output, '\n\n'); 295 | t.end(); 296 | }); 297 | -------------------------------------------------------------------------------- /test/test-collection.js: -------------------------------------------------------------------------------- 1 | var test = require('tap').test; 2 | var TestCollection = require('../lib/test-collection'); 3 | var objectAssign = require('object-assign'); 4 | 5 | function defaults() { 6 | return { 7 | type: 'test', 8 | serial: false, 9 | exclusive: false, 10 | skipped: false, 11 | callback: false 12 | }; 13 | } 14 | 15 | function metadata(opts) { 16 | return objectAssign(defaults(), opts); 17 | } 18 | 19 | function mockTest(opts, title) { 20 | return { 21 | title: title, 22 | metadata: metadata(opts) 23 | }; 24 | } 25 | 26 | function titles(tests) { 27 | if (!tests) { 28 | tests = []; 29 | } 30 | 31 | return tests.map(function (test) { 32 | return test.title; 33 | }); 34 | } 35 | 36 | function removeEmptyProps(obj) { 37 | if (Array.isArray(obj) && obj.length === 0) { 38 | return null; 39 | } 40 | 41 | if (obj.constructor !== Object) { 42 | return obj; 43 | } 44 | 45 | var cleanObj = null; 46 | 47 | Object.keys(obj).forEach(function (key) { 48 | var value = removeEmptyProps(obj[key]); 49 | 50 | if (value) { 51 | if (!cleanObj) { 52 | cleanObj = {}; 53 | } 54 | 55 | cleanObj[key] = value; 56 | } 57 | }); 58 | 59 | return cleanObj; 60 | } 61 | 62 | function serialize(collection) { 63 | var serialized = { 64 | tests: { 65 | concurrent: titles(collection.tests.concurrent), 66 | serial: titles(collection.tests.serial) 67 | }, 68 | hooks: { 69 | before: titles(collection.hooks.before), 70 | beforeEach: titles(collection.hooks.beforeEach), 71 | after: titles(collection.hooks.after), 72 | afterEach: titles(collection.hooks.afterEach) 73 | } 74 | }; 75 | 76 | return removeEmptyProps(serialized); 77 | } 78 | 79 | test('must be called with new', function (t) { 80 | var testCollection = TestCollection; 81 | t.throws(function () { 82 | testCollection(); 83 | }, {message: 'Class constructor TestCollection cannot be invoked without \'new\''}); 84 | t.end(); 85 | }); 86 | 87 | test('throws if no type is supplied', function (t) { 88 | var collection = new TestCollection(); 89 | t.throws(function () { 90 | collection.add({title: 'someTitle', metadata: {}}); 91 | }, {message: 'Test type must be specified'}); 92 | t.end(); 93 | }); 94 | 95 | test('throws if you try to set a hook as exclusive', function (t) { 96 | var collection = new TestCollection(); 97 | t.throws(function () { 98 | collection.add(mockTest({type: 'beforeEach', exclusive: true})); 99 | }, {message: '"only" cannot be used with a beforeEach test'}); 100 | t.end(); 101 | }); 102 | 103 | test('hasExclusive is set when an exclusive test is added', function (t) { 104 | var collection = new TestCollection(); 105 | t.false(collection.hasExclusive); 106 | collection.add(mockTest({exclusive: true}, 'foo')); 107 | t.true(collection.hasExclusive); 108 | t.end(); 109 | }); 110 | 111 | test('adding a concurrent test', function (t) { 112 | var collection = new TestCollection(); 113 | collection.add(mockTest({}, 'foo')); 114 | t.same(serialize(collection), { 115 | tests: { 116 | concurrent: ['foo'] 117 | } 118 | }); 119 | t.end(); 120 | }); 121 | 122 | test('adding a serial test', function (t) { 123 | var collection = new TestCollection(); 124 | collection.add(mockTest({serial: true}, 'bar')); 125 | t.same(serialize(collection), { 126 | tests: { 127 | serial: ['bar'] 128 | } 129 | }); 130 | t.end(); 131 | }); 132 | 133 | test('adding a before test', function (t) { 134 | var collection = new TestCollection(); 135 | collection.add(mockTest({type: 'before'}, 'baz')); 136 | t.same(serialize(collection), { 137 | hooks: { 138 | before: ['baz'] 139 | } 140 | }); 141 | t.end(); 142 | }); 143 | 144 | test('adding a beforeEach test', function (t) { 145 | var collection = new TestCollection(); 146 | collection.add(mockTest({type: 'beforeEach'}, 'foo')); 147 | t.same(serialize(collection), { 148 | hooks: { 149 | beforeEach: ['foo'] 150 | } 151 | }); 152 | t.end(); 153 | }); 154 | 155 | test('adding a after test', function (t) { 156 | var collection = new TestCollection(); 157 | collection.add(mockTest({type: 'after'}, 'bar')); 158 | t.same(serialize(collection), { 159 | hooks: { 160 | after: ['bar'] 161 | } 162 | }); 163 | t.end(); 164 | }); 165 | 166 | test('adding a afterEach test', function (t) { 167 | var collection = new TestCollection(); 168 | collection.add(mockTest({type: 'afterEach'}, 'baz')); 169 | t.same(serialize(collection), { 170 | hooks: { 171 | afterEach: ['baz'] 172 | } 173 | }); 174 | t.end(); 175 | }); 176 | 177 | test('adding a bunch of different types', function (t) { 178 | var collection = new TestCollection(); 179 | collection.add(mockTest({}, 'a')); 180 | collection.add(mockTest({}, 'b')); 181 | collection.add(mockTest({serial: true}, 'c')); 182 | collection.add(mockTest({serial: true}, 'd')); 183 | collection.add(mockTest({type: 'before'}, 'e')); 184 | t.same(serialize(collection), { 185 | tests: { 186 | concurrent: ['a', 'b'], 187 | serial: ['c', 'd'] 188 | }, 189 | hooks: { 190 | before: ['e'] 191 | } 192 | }); 193 | t.end(); 194 | }); 195 | 196 | test('foo', function (t) { 197 | var collection = new TestCollection(); 198 | 199 | var log = []; 200 | 201 | function logger(a) { 202 | log.push(a.title); 203 | } 204 | 205 | function add(title, opts) { 206 | collection.add({ 207 | title: title, 208 | metadata: metadata(opts), 209 | fn: logger 210 | }); 211 | } 212 | 213 | add('after1', {type: 'after'}); 214 | add('beforeEach1', {type: 'beforeEach'}); 215 | add('before1', {type: 'before'}); 216 | add('beforeEach2', {type: 'beforeEach'}); 217 | add('afterEach1', {type: 'afterEach'}); 218 | add('test1', {}); 219 | add('afterEach2', {type: 'afterEach'}); 220 | add('test2', {}); 221 | add('after2', {type: 'after'}); 222 | add('before2', {type: 'before'}); 223 | 224 | var result = collection.build().run(); 225 | 226 | t.is(result.passed, true); 227 | 228 | t.same(log, [ 229 | 'before1', 230 | 'before2', 231 | 'beforeEach1 for test1', 232 | 'beforeEach2 for test1', 233 | 'test1', 234 | 'afterEach1 for test1', 235 | 'afterEach2 for test1', 236 | 'beforeEach1 for test2', 237 | 'beforeEach2 for test2', 238 | 'test2', 239 | 'afterEach1 for test2', 240 | 'afterEach2 for test2', 241 | 'after1', 242 | 'after2' 243 | ]); 244 | 245 | t.end(); 246 | }); 247 | 248 | test('foo', function (t) { 249 | var collection = new TestCollection(); 250 | 251 | var log = []; 252 | 253 | function logger(result) { 254 | t.is(result.passed, true); 255 | log.push(result.result.title); 256 | } 257 | 258 | function noop() {} 259 | 260 | function add(title, opts) { 261 | collection.add({ 262 | title: title, 263 | metadata: metadata(opts), 264 | fn: noop 265 | }); 266 | } 267 | 268 | add('after1', {type: 'after'}); 269 | add('beforeEach1', {type: 'beforeEach'}); 270 | add('before1', {type: 'before'}); 271 | add('beforeEach2', {type: 'beforeEach'}); 272 | add('afterEach1', {type: 'afterEach'}); 273 | add('test1', {}); 274 | add('afterEach2', {type: 'afterEach'}); 275 | add('test2', {}); 276 | add('after2', {type: 'after'}); 277 | add('before2', {type: 'before'}); 278 | 279 | collection.on('test', logger); 280 | 281 | var result = collection.build().run(); 282 | 283 | t.is(result.passed, true); 284 | 285 | t.same(log, [ 286 | 'before1', 287 | 'before2', 288 | 'beforeEach1 for test1', 289 | 'beforeEach2 for test1', 290 | 'test1', 291 | 'afterEach1 for test1', 292 | 'afterEach2 for test1', 293 | 'beforeEach1 for test2', 294 | 'beforeEach2 for test2', 295 | 'test2', 296 | 'afterEach1 for test2', 297 | 'afterEach2 for test2', 298 | 'after1', 299 | 'after2' 300 | ]); 301 | 302 | t.end(); 303 | }); 304 | -------------------------------------------------------------------------------- /test/promise.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var Promise = require('bluebird'); 3 | var test = require('tap').test; 4 | var Test = require('../lib/test'); 5 | 6 | function ava(fn) { 7 | var a = new Test(fn); 8 | a.metadata = {callback: false}; 9 | return a; 10 | } 11 | 12 | ava.cb = function (fn) { 13 | var a = new Test(fn); 14 | a.metadata = {callback: true}; 15 | return a; 16 | }; 17 | 18 | function pass() { 19 | return new Promise(function (resolve) { 20 | setImmediate(resolve); 21 | }); 22 | } 23 | 24 | function fail() { 25 | return new Promise(function (resolve, reject) { 26 | setImmediate(function () { 27 | reject(new Error('unicorn')); 28 | }); 29 | }); 30 | } 31 | 32 | test('returning a promise from a legacy async fn is an error', function (t) { 33 | ava.cb(function (a) { 34 | a.plan(1); 35 | 36 | return Promise.resolve(true).then(function () { 37 | a.pass(); 38 | a.end(); 39 | }); 40 | }).run().then(function (result) { 41 | t.is(result.passed, false); 42 | t.match(result.reason.message, /Do not return promises/); 43 | t.end(); 44 | }); 45 | }); 46 | 47 | test('assertion plan is tested after returned promise resolves', function (t) { 48 | var start = Date.now(); 49 | ava(function (a) { 50 | a.plan(2); 51 | 52 | var defer = Promise.defer(); 53 | 54 | setTimeout(function () { 55 | defer.resolve(); 56 | }, 500); 57 | 58 | a.pass(); 59 | a.pass(); 60 | 61 | return defer.promise; 62 | }).run().then(function (result) { 63 | t.is(result.passed, true); 64 | t.is(result.result.planCount, 2); 65 | t.is(result.result.assertCount, 2); 66 | t.true(Date.now() - start >= 500); 67 | t.end(); 68 | }); 69 | }); 70 | 71 | test('missing assertion will fail the test', function (t) { 72 | ava(function (a) { 73 | a.plan(2); 74 | 75 | var defer = Promise.defer(); 76 | 77 | setTimeout(function () { 78 | a.pass(); 79 | defer.resolve(); 80 | }, 200); 81 | 82 | return defer.promise; 83 | }).run().then(function (result) { 84 | t.is(result.passed, false); 85 | t.is(result.reason.expected, 2); 86 | t.is(result.reason.actual, 1); 87 | t.end(); 88 | }); 89 | }); 90 | 91 | test('extra assertion will fail the test', function (t) { 92 | ava(function (a) { 93 | a.plan(2); 94 | 95 | var defer = Promise.defer(); 96 | 97 | setTimeout(function () { 98 | a.pass(); 99 | a.pass(); 100 | }, 200); 101 | 102 | setTimeout(function () { 103 | a.pass(); 104 | defer.resolve(); 105 | }, 500); 106 | 107 | return defer.promise; 108 | }).run().then(function (result) { 109 | t.is(result.passed, false); 110 | t.is(result.reason.expected, 2); 111 | t.is(result.reason.actual, 3); 112 | t.end(); 113 | }); 114 | }); 115 | 116 | test('handle throws with rejected promise', function (t) { 117 | ava(function (a) { 118 | a.plan(1); 119 | 120 | var promise = Promise.reject(new Error()); 121 | return a.throws(promise); 122 | }).run().then(function (result) { 123 | t.is(result.passed, true); 124 | t.is(result.result.assertCount, 1); 125 | t.end(); 126 | }); 127 | }); 128 | 129 | // TODO(team): This is a very slow test, and I can't figure out why we need it - James 130 | test('handle throws with long running rejected promise', function (t) { 131 | ava(function (a) { 132 | a.plan(1); 133 | 134 | var promise = new Promise(function (resolve, reject) { 135 | setTimeout(function () { 136 | reject(new Error('abc')); 137 | }, 2000); 138 | }); 139 | 140 | return a.throws(promise, /abc/); 141 | }).run().then(function (result) { 142 | t.is(result.passed, true); 143 | t.is(result.result.assertCount, 1); 144 | t.end(); 145 | }); 146 | }); 147 | 148 | test('handle throws with resolved promise', function (t) { 149 | ava(function (a) { 150 | a.plan(1); 151 | 152 | var promise = Promise.resolve(); 153 | return a.throws(promise); 154 | }).run().then(function (result) { 155 | t.is(result.passed, false); 156 | t.is(result.reason.name, 'AssertionError'); 157 | t.end(); 158 | }); 159 | }); 160 | 161 | test('handle throws with regex', function (t) { 162 | ava(function (a) { 163 | a.plan(1); 164 | 165 | var promise = Promise.reject(new Error('abc')); 166 | return a.throws(promise, /abc/); 167 | }).run().then(function (result) { 168 | t.is(result.passed, true); 169 | t.is(result.result.assertCount, 1); 170 | t.end(); 171 | }); 172 | }); 173 | 174 | test('throws with regex will fail if error message does not match', function (t) { 175 | ava(function (a) { 176 | a.plan(1); 177 | 178 | var promise = Promise.reject(new Error('abc')); 179 | return a.throws(promise, /def/); 180 | }).run().then(function (result) { 181 | t.is(result.passed, false); 182 | t.is(result.reason.name, 'AssertionError'); 183 | t.end(); 184 | }); 185 | }); 186 | 187 | test('handle throws with string', function (t) { 188 | ava(function (a) { 189 | a.plan(1); 190 | 191 | var promise = Promise.reject(new Error('abc')); 192 | return a.throws(promise, 'abc'); 193 | }).run().then(function (result) { 194 | t.is(result.passed, true); 195 | t.is(result.result.assertCount, 1); 196 | t.end(); 197 | }); 198 | }); 199 | 200 | test('throws with string argument will reject if message does not match', function (t) { 201 | ava(function (a) { 202 | a.plan(1); 203 | 204 | var promise = Promise.reject(new Error('abc')); 205 | return a.throws(promise, 'def'); 206 | }).run().then(function (result) { 207 | t.is(result.passed, false); 208 | t.is(result.reason.name, 'AssertionError'); 209 | t.end(); 210 | }); 211 | }); 212 | 213 | test('does not handle throws with string reject', function (t) { 214 | ava(function (a) { 215 | a.plan(1); 216 | 217 | var promise = Promise.reject('abc'); 218 | return a.throws(promise, 'abc'); 219 | }).run().then(function (result) { 220 | t.is(result.passed, false); 221 | t.is(result.reason.name, 'AssertionError'); 222 | t.end(); 223 | }); 224 | }); 225 | 226 | test('handle throws with false-positive promise', function (t) { 227 | ava(function (a) { 228 | a.plan(1); 229 | 230 | var promise = Promise.resolve(new Error()); 231 | return a.throws(promise); 232 | }).run().then(function (result) { 233 | t.is(result.passed, false); 234 | t.is(result.reason.name, 'AssertionError'); 235 | t.end(); 236 | }); 237 | }); 238 | 239 | test('handle notThrows with resolved promise', function (t) { 240 | ava(function (a) { 241 | a.plan(1); 242 | 243 | var promise = Promise.resolve(); 244 | return a.notThrows(promise); 245 | }).run().then(function (result) { 246 | t.is(result.passed, true); 247 | t.is(result.result.assertCount, 1); 248 | t.end(); 249 | }); 250 | }); 251 | 252 | test('handle notThrows with rejected promise', function (t) { 253 | ava(function (a) { 254 | a.plan(1); 255 | 256 | var promise = Promise.reject(new Error()); 257 | return a.notThrows(promise); 258 | }).run().then(function (result) { 259 | t.is(result.passed, false); 260 | t.is(result.reason.name, 'AssertionError'); 261 | t.end(); 262 | }); 263 | }); 264 | 265 | test('assert pass', function (t) { 266 | ava(function (a) { 267 | return pass().then(function () { 268 | a.pass(); 269 | }); 270 | }).run().then(function (result) { 271 | t.is(result.passed, true); 272 | t.is(result.result.assertCount, 1); 273 | t.end(); 274 | }); 275 | }); 276 | 277 | test('assert fail', function (t) { 278 | ava(function (a) { 279 | return pass().then(function () { 280 | a.fail(); 281 | }); 282 | }).run().then(function (result) { 283 | t.is(result.passed, false); 284 | t.is(result.reason.name, 'AssertionError'); 285 | t.end(); 286 | }); 287 | }); 288 | 289 | test('reject', function (t) { 290 | ava(function (a) { 291 | return fail().then(function () { 292 | a.pass(); 293 | }); 294 | }).run().then(function (result) { 295 | t.is(result.passed, false); 296 | t.is(result.reason.name, 'Error'); 297 | t.is(result.reason.message, 'unicorn'); 298 | t.end(); 299 | }); 300 | }); 301 | 302 | test('reject with non-Error', function (t) { 303 | ava(function () { 304 | return Promise.reject('failure'); 305 | }).run().then(function (result) { 306 | t.is(result.passed, false); 307 | t.is(result.reason.name, 'AssertionError'); 308 | t.is(result.reason.message, 'Promise rejected with: \'failure\''); 309 | t.end(); 310 | }); 311 | }); 312 | -------------------------------------------------------------------------------- /lib/reporters/mini.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | var cliCursor = require('cli-cursor'); 3 | var lastLineTracker = require('last-line-stream/tracker'); 4 | var StringDecoder = require('string_decoder').StringDecoder; 5 | var plur = require('plur'); 6 | var spinners = require('cli-spinners'); 7 | var chalk = require('chalk'); 8 | var colors = require('../colors'); 9 | var cliTruncate = require('cli-truncate'); 10 | 11 | chalk.enabled = true; 12 | Object.keys(colors).forEach(function (key) { 13 | colors[key].enabled = true; 14 | }); 15 | 16 | function MiniReporter() { 17 | if (!(this instanceof MiniReporter)) { 18 | return new MiniReporter(); 19 | } 20 | 21 | var spinnerDef = spinners.dots; 22 | this.spinnerFrames = spinnerDef.frames.map(function (c) { 23 | return chalk.gray.dim(c); 24 | }); 25 | this.spinnerInterval = spinnerDef.interval; 26 | 27 | this.reset(); 28 | this.stream = process.stderr; 29 | this.stringDecoder = new StringDecoder(); 30 | } 31 | 32 | module.exports = MiniReporter; 33 | 34 | MiniReporter.prototype.start = function () { 35 | var self = this; 36 | this.interval = setInterval(function () { 37 | self.spinnerIndex = (self.spinnerIndex + 1) % self.spinnerFrames.length; 38 | self.write(self.prefix()); 39 | }, this.spinnerInterval); 40 | return this.prefix(''); 41 | }; 42 | 43 | MiniReporter.prototype.reset = function () { 44 | this.clearInterval(); 45 | this.passCount = 0; 46 | this.failCount = 0; 47 | this.skipCount = 0; 48 | this.todoCount = 0; 49 | this.rejectionCount = 0; 50 | this.exceptionCount = 0; 51 | this.currentStatus = ''; 52 | this.currentTest = ''; 53 | this.statusLineCount = 0; 54 | this.spinnerIndex = 0; 55 | this.lastLineTracker = lastLineTracker(); 56 | }; 57 | 58 | MiniReporter.prototype.spinnerChar = function () { 59 | return this.spinnerFrames[this.spinnerIndex]; 60 | }; 61 | 62 | MiniReporter.prototype.clearInterval = function () { 63 | clearInterval(this.interval); 64 | this.interval = null; 65 | }; 66 | 67 | MiniReporter.prototype.test = function (test) { 68 | if (test.todo) { 69 | this.todoCount++; 70 | } else if (test.skip) { 71 | this.skipCount++; 72 | } else if (test.error) { 73 | this.failCount++; 74 | } else { 75 | this.passCount++; 76 | } 77 | 78 | if (test.todo || test.skip) { 79 | return; 80 | } 81 | 82 | return this.prefix(this._test(test)); 83 | }; 84 | 85 | MiniReporter.prototype.prefix = function (str) { 86 | str = str || this.currentTest; 87 | this.currentTest = str; 88 | // The space before the newline is required for proper formatting. (Not sure why). 89 | return ' \n ' + this.spinnerChar() + ' ' + str; 90 | }; 91 | 92 | MiniReporter.prototype._test = function (test) { 93 | var SPINNER_WIDTH = 3; 94 | var PADDING = 1; 95 | var title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING); 96 | 97 | if (test.error) { 98 | title = colors.error(test.title); 99 | } 100 | 101 | return title + '\n' + this.reportCounts(); 102 | }; 103 | 104 | MiniReporter.prototype.unhandledError = function (err) { 105 | if (err.type === 'exception') { 106 | this.exceptionCount++; 107 | } else { 108 | this.rejectionCount++; 109 | } 110 | }; 111 | 112 | MiniReporter.prototype.reportCounts = function () { 113 | var status = ''; 114 | 115 | if (this.passCount > 0) { 116 | status += '\n ' + colors.pass(this.passCount, 'passed'); 117 | } 118 | 119 | if (this.failCount > 0) { 120 | status += '\n ' + colors.error(this.failCount, 'failed'); 121 | } 122 | 123 | if (this.skipCount > 0) { 124 | status += '\n ' + colors.skip(this.skipCount, 'skipped'); 125 | } 126 | 127 | if (this.todoCount > 0) { 128 | status += '\n ' + colors.todo(this.todoCount, 'todo'); 129 | } 130 | 131 | return status.length ? status : '\n'; 132 | }; 133 | 134 | MiniReporter.prototype.finish = function () { 135 | this.clearInterval(); 136 | var status = this.reportCounts(); 137 | 138 | if (this.rejectionCount > 0) { 139 | status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount)); 140 | } 141 | 142 | if (this.exceptionCount > 0) { 143 | status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount)); 144 | } 145 | 146 | var i = 0; 147 | 148 | if (this.failCount > 0) { 149 | this.api.errors.forEach(function (test) { 150 | if (!test.error || !test.error.message) { 151 | return; 152 | } 153 | 154 | i++; 155 | 156 | var title = test.error ? test.title : 'Unhandled Error'; 157 | var description; 158 | 159 | if (test.error) { 160 | description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack); 161 | } else { 162 | description = JSON.stringify(test); 163 | } 164 | 165 | status += '\n\n ' + colors.error(i + '.', title) + '\n'; 166 | status += colors.stack(description); 167 | }); 168 | } 169 | 170 | if (this.rejectionCount > 0 || this.exceptionCount > 0) { 171 | this.api.errors.forEach(function (err) { 172 | if (err.title) { 173 | return; 174 | } 175 | 176 | i++; 177 | 178 | if (err.type === 'exception' && err.name === 'AvaError') { 179 | status += '\n\n ' + colors.error(i + '. ' + err.message) + '\n'; 180 | } else { 181 | var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception'; 182 | var description = err.stack ? err.stack : JSON.stringify(err); 183 | 184 | status += '\n\n ' + colors.error(i + '.', title) + '\n'; 185 | status += ' ' + colors.stack(description); 186 | } 187 | }); 188 | } 189 | 190 | if (this.failCount === 0 && this.rejectionCount === 0 && this.exceptionCount === 0) { 191 | status += '\n'; 192 | } 193 | 194 | return status; 195 | }; 196 | 197 | MiniReporter.prototype.write = function (str) { 198 | cliCursor.hide(); 199 | this.currentStatus = str + '\n'; 200 | this._update(); 201 | this.statusLineCount = this.currentStatus.split('\n').length; 202 | }; 203 | 204 | MiniReporter.prototype.stdout = MiniReporter.prototype.stderr = function (data) { 205 | this._update(data); 206 | }; 207 | 208 | MiniReporter.prototype._update = function (data) { 209 | var str = ''; 210 | var ct = this.statusLineCount; 211 | var columns = process.stdout.columns; 212 | var lastLine = this.lastLineTracker.lastLine(); 213 | 214 | // Terminals automatically wrap text. We only need the last log line as seen on the screen. 215 | lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns)); 216 | 217 | // Don't delete the last log line if it's completely empty. 218 | if (lastLine.length) { 219 | ct++; 220 | } 221 | 222 | // Erase the existing status message, plus the last log line. 223 | str += eraseLines(ct); 224 | 225 | // Rewrite the last log line. 226 | str += lastLine; 227 | 228 | if (str.length) { 229 | this.stream.write(str); 230 | } 231 | 232 | if (data) { 233 | // send new log data to the terminal, and update the last line status. 234 | this.lastLineTracker.update(this.stringDecoder.write(data)); 235 | this.stream.write(data); 236 | } 237 | 238 | var currentStatus = this.currentStatus; 239 | 240 | if (currentStatus.length) { 241 | lastLine = this.lastLineTracker.lastLine(); 242 | // We need a newline at the end of the last log line, before the status message. 243 | // However, if the last log line is the exact width of the terminal a newline is implied, 244 | // and adding a second will cause problems. 245 | if (lastLine.length % columns) { 246 | currentStatus = '\n' + currentStatus; 247 | } 248 | // rewrite the status message. 249 | this.stream.write(currentStatus); 250 | } 251 | }; 252 | 253 | // TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution. 254 | var CSI = '\u001b['; 255 | var ERASE_LINE = CSI + '2K'; 256 | var CURSOR_TO_COLUMN_0 = CSI + '0G'; 257 | var CURSOR_UP = CSI + '1A'; 258 | 259 | // Returns a string that will erase `count` lines from the end of the terminal. 260 | function eraseLines(count) { 261 | var clear = ''; 262 | 263 | for (var i = 0; i < count; i++) { 264 | clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : ''); 265 | } 266 | 267 | if (count) { 268 | clear += CURSOR_TO_COLUMN_0; 269 | } 270 | 271 | return clear; 272 | } 273 | 274 | function stripFirstLine(message) { 275 | return message.replace(/^[^\n]*\n/, ''); 276 | } 277 | --------------------------------------------------------------------------------