diff --git a/api.js b/api.js index 6cb688777..ea0060ee5 100644 --- a/api.js +++ b/api.js @@ -232,11 +232,14 @@ class Api extends Emittery { filename => { throw new Error(`Cannot apply full precompilation, possible bad usage: ${filename}`); }; - const precompileEnhancementsOnly = compileEnhancements && this.options.extensions.enhancementsOnly.length > 0 ? - babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) : - filename => { - throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`); - }; + let precompileEnhancementsOnly = () => null; + if (compileEnhancements) { + precompileEnhancementsOnly = this.options.extensions.enhancementsOnly.length > 0 ? + babelPipeline.build(projectDir, cacheDir, null, compileEnhancements) : + filename => { + throw new Error(`Cannot apply enhancement-only precompilation, possible bad usage: ${filename}`); + }; + } this._precompiler = { cacheDir, diff --git a/lib/cli.js b/lib/cli.js index 73d4397a4..6df36a098 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -207,7 +207,7 @@ exports.run = () => { // eslint-disable-line complexity reportStream: process.stdout, stdStream: process.stderr }); - } else if (conf.verbose || isCi) { + } else if (conf.verbose || isCi || !process.stdout.isTTY) { reporter = new VerboseReporter({ reportStream: process.stdout, stdStream: process.stderr, diff --git a/package-lock.json b/package-lock.json index f27306ead..a625e1607 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5267,6 +5267,12 @@ } } }, + "make-error": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.4.tgz", + "integrity": "sha512-0Dab5btKVPhibSalc9QGXb559ED7G7iLjFXBaj9Wq8O3vorueR5K5jaE3hkG6ZQINyhA/JgG6Qk4qdFQjsYV6g==", + "dev": true + }, "map-cache": { "version": "0.2.2", "resolved": "https://registry.npmjs.org/map-cache/-/map-cache-0.2.2.tgz", @@ -10269,6 +10275,29 @@ "integrity": "sha1-N21NKdlR1jaKb3oK6FwvTV4GWPM=", "dev": true }, + "ts-node": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/ts-node/-/ts-node-6.1.1.tgz", + "integrity": "sha512-79FnymLGDBd/nXoiak1L6w6fd9Zz9Ge/x8/Aglaeh31KkqRLDzbfT1vBGlO5dqc76WzufTlW4IYl7e01CVUF5A==", + "dev": true, + "requires": { + "arrify": "^1.0.0", + "diff": "^3.1.0", + "make-error": "^1.1.1", + "minimist": "^1.2.0", + "mkdirp": "^0.5.1", + "source-map-support": "^0.5.6", + "yn": "^2.0.0" + }, + "dependencies": { + "minimist": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", + "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "dev": true + } + } + }, "tsame": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/tsame/-/tsame-2.0.0.tgz", @@ -10922,6 +10951,12 @@ "camelcase": "^4.1.0" } }, + "yn": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yn/-/yn-2.0.0.tgz", + "integrity": "sha1-5a2ryKz0CPY4X8dklWhMiOavaJo=", + "dev": true + }, "zen-observable": { "version": "0.8.8", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.8.tgz", diff --git a/package.json b/package.json index 98136dfde..b86663940 100644 --- a/package.json +++ b/package.json @@ -161,6 +161,7 @@ "tap": "^12.0.1", "temp-write": "^3.4.0", "touch": "^3.1.0", + "ts-node": "^6.1.1", "typescript": "^2.8.3", "xo": "^0.21.1", "zen-observable": "^0.8.8" diff --git a/test/cli.js b/test/cli.js deleted file mode 100644 index 41c9509c6..000000000 --- a/test/cli.js +++ /dev/null @@ -1,919 +0,0 @@ -'use strict'; -const fs = require('fs'); -const path = require('path'); -const childProcess = require('child_process'); -const test = require('tap').test; -const getStream = require('get-stream'); -const figures = require('figures'); -const makeDir = require('make-dir'); -const touch = require('touch'); -const uniqueTempDir = require('unique-temp-dir'); -const execa = require('execa'); -const stripAnsi = require('strip-ansi'); -const pkg = require('../package.json'); - -const cliPath = path.join(__dirname, '../cli.js'); - -function execCli(args, opts, cb) { - let dirname; - let env; - - if (typeof opts === 'function') { - cb = opts; - dirname = __dirname; - env = {}; - } else { - dirname = path.join(__dirname, opts.dirname ? opts.dirname : ''); - env = opts.env || {}; - } - - let child; - let stdout; - let stderr; - - const processPromise = new Promise(resolve => { - child = childProcess.spawn(process.execPath, [cliPath].concat(args), { - cwd: dirname, - env: Object.assign({CI: '1'}, env), // Force CI to ensure the correct reporter is selected - // env, - stdio: [null, 'pipe', 'pipe'] - }); - - child.on('close', (code, signal) => { - if (code) { - const err = new Error(`test-worker exited with a non-zero exit code: ${code}`); - err.code = code; - err.signal = signal; - resolve(err); - return; - } - - resolve(code); - }); - - stdout = getStream(child.stdout); - stderr = getStream(child.stderr); - }); - - Promise.all([processPromise, stdout, stderr]).then(args => { - cb.apply(null, args); - }); - - return child; -} - -for (const which of [ - 'bad-key', - 'bad-shortcut', - 'array-test-options', - 'false-test-options', - 'null-test-options', - 'null-extensions', - 'obj-extensions', - 'string-extensions', - 'non-string-value-extensions', - 'empty-string-value-extensions' -]) { - test(`validates babel config: ${which}`, t => { - execCli(['es2015.js'], {dirname: `fixture/invalid-babel-config/${which}`}, (err, stdout, stderr) => { - t.ok(err); - - let expectedOutput = '\n'; - expectedOutput += figures.cross + ' Unexpected Babel configuration for AVA.'; - expectedOutput += ` See https://github.com/avajs/ava/blob/v${pkg.version}/docs/recipes/babel.md for allowed values.`; - expectedOutput += '\n'; - - t.is(stderr, expectedOutput); - t.end(); - }); - }); -} - -test('errors if top-level extensions include "js" without babel=false', t => { - execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/top-level`}, (err, stdout, stderr) => { - t.ok(err); - - let expectedOutput = '\n'; - expectedOutput += figures.cross + ' Cannot specify generic \'js\' extension without disabling AVA\'s Babel usage.'; - expectedOutput += '\n'; - - t.is(stderr, expectedOutput); - t.end(); - }); -}); - -for (const [where, which, msg = '\'js\', \'jsx\''] of [ - ['top-level', 'top-level-duplicates'], - ['babel', 'babel-duplicates'], - ['top-level and babel', 'shared-duplicates', '\'jsx\''] -]) { - test(`errors if ${where} extensions include duplicates`, t => { - execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/${which}`}, (err, stdout, stderr) => { - t.ok(err); - - let expectedOutput = '\n'; - expectedOutput += figures.cross + ` Unexpected duplicate extensions in options: ${msg}.`; - expectedOutput += '\n'; - - t.is(stderr, expectedOutput); - t.end(); - }); - }); -} - -test('formats errors from ava.config.js', t => { - execCli(['es2015.js'], {dirname: 'fixture/load-config/throws'}, (err, stdout, stderr) => { - t.ok(err); - - const lines = stderr.split('\n'); - t.is(lines[0], ''); - t.is(lines[1], figures.cross + ' Error loading ava.config.js'); - t.is(lines[2], ''); - t.match(lines[3], /ava\.config\.js/); - t.match(lines[4], /foo/); - t.end(); - }); -}); - -test('enabling long stack traces will provide detailed debug information', t => { - execCli('fixture/long-stack-trace', (err, stdout, stderr) => { - t.ok(err); - t.match(stderr, /From previous event/); - t.end(); - }); -}); - -test('`AssertionError` should capture infinity stack trace', t => { - execCli('fixture/infinity-stack-trace.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /c \(.+?infinity-stack-trace\.js:6:20\)/); - t.match(stdout, /b \(.+?infinity-stack-trace\.js:7:18\)/); - t.match(stdout, /a \(.+?infinity-stack-trace\.js:8:18\)/); - t.end(); - }); -}); - -test('timeout', t => { - execCli(['fixture/long-running.js', '-T', '1s'], (err, stdout) => { - t.ok(err); - t.match(stdout, /Exited because no new tests completed within the last 1000ms of inactivity/); - t.end(); - }); -}); - -test('include anonymous functions in error reports', t => { - execCli('fixture/error-in-anonymous-function.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /test\/fixture\/error-in-anonymous-function\.js:4:8/); - t.end(); - }); -}); - -test('improper use of t.throws will be reported to the console', t => { - execCli('fixture/improper-t-throws/throws.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws from within a Promise will be reported to the console', t => { - execCli('fixture/improper-t-throws/promise.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws from within a pending promise, even if caught and rethrown immediately, will be reported to the console', t => { - execCli('fixture/improper-t-throws/leaked-from-promise.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws from within an async callback will be reported to the console', t => { - execCli('fixture/improper-t-throws/async-callback.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws, swallowed as an unhandled rejection, will be reported to the console', t => { - execCli('fixture/improper-t-throws/unhandled-rejection.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws, even if caught, will be reported to the console', t => { - execCli('fixture/improper-t-throws/caught.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.notMatch(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws, even if caught and then rethrown immediately, will be reported to the console', t => { - execCli('fixture/improper-t-throws/caught-and-leaked.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws, even if caught and then later rethrown, will be reported to the console', t => { - execCli('fixture/improper-t-throws/caught-and-leaked-slowly.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.match(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('improper use of t.throws, even if caught and then rethrown too slowly, will be reported to the console', t => { - execCli('fixture/improper-t-throws/caught-and-leaked-too-slowly.js', (err, stdout) => { - t.ok(err); - t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); - t.notMatch(stdout, /should be detected/); - t.match(stdout, /Try wrapping the first argument/); - t.end(); - }); -}); - -test('precompiler require hook does not apply to source files', t => { - t.plan(3); - - execCli('fixture/babel-hook.js', (err, stdout) => { - t.ok(err); - t.is(err.code, 1); - t.match(stdout, /Unexpected (token|reserved word)/); - t.end(); - }); -}); - -test('pkg-conf(resolve-dir): works as expected when run from the package.json directory', t => { - execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir'}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /dir-a-base-1/); - t.match(stdout, /dir-a-base-2/); - t.notMatch(stdout, /dir-a-wrapper/); - t.notMatch(stdout, /dir-a-wrapper/); - t.end(); - }); -}); - -test('pkg-conf(resolve-dir): resolves tests from the package.json dir if none are specified on cli', t => { - execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /dir-a-base-1/); - t.match(stdout, /dir-a-base-2/); - t.notMatch(stdout, /dir-a-wrapper/); - t.notMatch(stdout, /dir-a-wrapper/); - t.end(); - }); -}); - -test('pkg-conf(resolve-dir): resolves tests process.cwd() if globs are passed on the command line', t => { - execCli(['--verbose', 'dir-a/*.js'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /dir-a-wrapper-3/); - t.match(stdout, /dir-a-wrapper-4/); - t.notMatch(stdout, /dir-a-base/); - t.notMatch(stdout, /dir-a-base/); - t.end(); - }); -}); - -test('watcher reruns test files when they changed', t => { - let killed = false; - - const child = execCli(['--verbose', '--watch', 'test.js'], {dirname: 'fixture/watcher', env: {CI: ''}}, err => { - t.ok(killed); - t.ifError(err); - t.end(); - }); - - let buffer = ''; - let passedFirst = false; - child.stdout.on('data', str => { - buffer += str; - if (/1 test passed/.test(buffer)) { - if (!passedFirst) { - touch.sync(path.join(__dirname, 'fixture/watcher/test.js')); - buffer = ''; - passedFirst = true; - } else if (!killed) { - child.kill(); - killed = true; - } - } - }); -}); - -test('watcher reruns test files when source dependencies change', t => { - let killed = false; - - const child = execCli(['--verbose', '--watch', 'test-*.js'], {dirname: 'fixture/watcher/with-dependencies', env: {CI: ''}}, err => { - t.ok(killed); - t.ifError(err); - t.end(); - }); - - let buffer = ''; - let passedFirst = false; - child.stdout.on('data', str => { - buffer += str; - if (/2 tests passed/.test(buffer) && !passedFirst) { - touch.sync(path.join(__dirname, 'fixture/watcher/with-dependencies/source.js')); - buffer = ''; - passedFirst = true; - } else if (/1 test passed/.test(buffer) && !killed) { - child.kill(); - killed = true; - } - }); -}); - -test('watcher does not rerun test files when they write snapshot files', t => { - let killed = false; - - const child = execCli(['--verbose', '--watch', '--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots', env: {CI: ''}}, err => { - t.ok(killed); - t.ifError(err); - t.end(); - }); - - let buffer = ''; - let passedFirst = false; - child.stdout.on('data', str => { - buffer += str; - if (/2 tests passed/.test(buffer) && !passedFirst) { - buffer = ''; - passedFirst = true; - setTimeout(() => { - child.kill(); - killed = true; - }, 500); - } else if (passedFirst && !killed) { - t.is(buffer.replace(/\s/g, ''), ''); - } - }); -}); - -test('watcher reruns test files when snapshot dependencies change', t => { - let killed = false; - - const child = execCli(['--verbose', '--watch', '--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots', env: {CI: ''}}, err => { - t.ok(killed); - t.ifError(err); - t.end(); - }); - - let buffer = ''; - let passedFirst = false; - child.stdout.on('data', str => { - buffer += str; - if (/2 tests passed/.test(buffer)) { - buffer = ''; - if (passedFirst) { - child.kill(); - killed = true; - } else { - passedFirst = true; - setTimeout(() => { - touch.sync(path.join(__dirname, 'fixture/snapshots/test.js.snap')); - }, 500); - } - } - }); -}); - -test('`"tap": true` config is ignored when --watch is given', t => { - let killed = false; - - const child = execCli(['--watch', '--verbose', 'test.js'], {dirname: 'fixture/watcher/tap-in-conf', env: {CI: ''}}, () => { - t.ok(killed); - t.end(); - }); - - let combined = ''; - const testOutput = output => { - combined += output; - t.notMatch(combined, /TAP/); - if (/works/.test(combined)) { - child.kill(); - killed = true; - } - }; - child.stdout.on('data', testOutput); - child.stderr.on('data', testOutput); -}); - -['--watch', '-w'].forEach(watchFlag => { - ['--tap', '-t'].forEach(tapFlag => { - test(`bails when ${tapFlag} reporter is used while ${watchFlag} is given`, t => { - execCli([tapFlag, watchFlag, 'test.js'], {dirname: 'fixture/watcher', env: {CI: ''}}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'The TAP reporter is not available when using watch mode.'); - t.end(); - }); - }); - }); -}); - -['--watch', '-w'].forEach(watchFlag => { - test(`bails when CI is used while ${watchFlag} is given`, t => { - execCli([watchFlag, 'test.js'], {dirname: 'fixture/watcher', env: {CI: true}}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'Watch mode is not available in CI, as it prevents AVA from terminating.'); - t.end(); - }); - }); -}); - -['--concurrency', '-c'].forEach(concurrencyFlag => { - test(`bails when ${concurrencyFlag} is provided without value`, t => { - execCli(['test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); - t.end(); - }); - }); -}); - -['--concurrency', '-c'].forEach(concurrencyFlag => { - test(`bails when ${concurrencyFlag} is provided with an input that is a string`, t => { - execCli([`${concurrencyFlag}=foo`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); - t.end(); - }); - }); -}); - -['--concurrency', '-c'].forEach(concurrencyFlag => { - test(`bails when ${concurrencyFlag} is provided with an input that is a float`, t => { - execCli([`${concurrencyFlag}=4.7`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); - t.end(); - }); - }); -}); - -['--concurrency', '-c'].forEach(concurrencyFlag => { - test(`bails when ${concurrencyFlag} is provided with an input that is negative`, t => { - execCli([`${concurrencyFlag}=-1`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { - t.is(err.code, 1); - t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); - t.end(); - }); - }); -}); - -['--concurrency', '-c'].forEach(concurrencyFlag => { - test(`works when ${concurrencyFlag} is provided with a value`, t => { - execCli([`${concurrencyFlag}=1`, 'test.js'], {dirname: 'fixture/concurrency'}, err => { - t.ifError(err); - t.end(); - }); - }); -}); - -test('--match works', t => { - execCli(['-m=foo', '-m=bar', '-m=!baz', '-m=t* a* f*', '-m=!t* a* n* f*', 'fixture/matcher-skip.js'], err => { - t.ifError(err); - t.end(); - }); -}); - -['--tap', '-t'].forEach(tapFlag => { - test(`${tapFlag} should produce TAP output`, t => { - execCli([tapFlag, 'test.js'], {dirname: 'fixture/watcher'}, err => { - t.ok(!err); - t.end(); - }); - }); -}); - -test('handles NODE_PATH', t => { - const nodePaths = `fixture/node-paths/modules${path.delimiter}fixture/node-paths/deep/nested`; - - execCli('fixture/node-paths.js', {env: {NODE_PATH: nodePaths}}, err => { - t.ifError(err); - t.end(); - }); -}); - -test('works when no files are found', t => { - execCli('!*', (err, stdout) => { - t.is(err.code, 1); - t.match(stdout, 'Couldn\'t find any files to test'); - t.end(); - }); -}); - -test('should warn ava is required without the cli', t => { - childProcess.execFile(process.execPath, [path.resolve(__dirname, '../index.js')], error => { - t.ok(error); - t.match(error.message, /Test files must be run with the AVA CLI/); - t.end(); - }); -}); - -test('prefers local version of ava', t => { - execCli('', { - dirname: 'fixture/local-bin', - env: { - DEBUG: 'ava' - } - }, (err, stdout, stderr) => { - t.ifError(err); - t.match(stderr, 'Using local install of AVA'); - t.end(); - }); -}); - -test('use current working directory if `package.json` is not found', () => { - const cwd = uniqueTempDir({create: true}); - const testFilePath = path.join(cwd, 'test.js'); - const cliPath = require.resolve('../cli.js'); - const avaPath = require.resolve('../'); - - fs.writeFileSync(testFilePath, `import test from ${JSON.stringify(avaPath)};\ntest('test', t => { t.pass(); });`); - - return execa(process.execPath, [cliPath], {cwd, env: {CI: '1'}}); -}); - -test('workers ensure test files load the same version of ava', t => { - const target = path.join(__dirname, 'fixture', 'ava-paths', 'target'); - - // Copy the index.js so the testFile imports it. It should then load the correct AVA install. - const targetInstall = path.join(target, 'node_modules/ava'); - makeDir.sync(targetInstall); - fs.writeFileSync( - path.join(targetInstall, 'index.js'), - fs.readFileSync(path.join(__dirname, '../index.js')) - ); - - const testFile = path.join(target, 'test.js'); - execCli([testFile], {dirname: path.join('fixture', 'ava-paths', 'cwd')}, err => { - t.ifError(err); - t.end(); - }); -}); - -test('tests without assertions do not fail if failWithoutAssertions option is set to false', t => { - execCli([], {dirname: 'fixture/pkg-conf/fail-without-assertions'}, err => { - t.ifError(err); - t.end(); - }); -}); - -test('callback tests fail if event loop empties before they\'re ended', t => { - execCli('callback.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { - t.match(stdout, /`t\.end\(\)` was never called/); - t.end(); - }); -}); - -test('observable tests fail if event loop empties before they\'re resolved', t => { - execCli('observable.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { - t.match(stdout, /Observable returned by test never completed/); - t.end(); - }); -}); - -test('promise tests fail if event loop empties before they\'re resolved', t => { - execCli('promise.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { - t.match(stdout, /Promise returned by test never resolved/); - t.end(); - }); -}); - -for (const obj of [ - {type: 'colocated', rel: '', dir: ''}, - {type: '__tests__', rel: '__tests__-dir', dir: '__tests__/__snapshots__'}, - {type: 'test', rel: 'test-dir', dir: 'test/snapshots'}, - {type: 'tests', rel: 'tests-dir', dir: 'tests/snapshots'} -]) { - test(`snapshots work (${obj.type})`, t => { - const snapPath = path.join(__dirname, 'fixture', 'snapshots', obj.rel, obj.dir, 'test.js.snap'); - try { - fs.unlinkSync(snapPath); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - - const dirname = path.join('fixture/snapshots', obj.rel); - // Test should pass, and a snapshot gets written - execCli(['--update-snapshots'], {dirname}, err => { - t.ifError(err); - t.true(fs.existsSync(snapPath)); - - // Test should pass, and the snapshot gets used - execCli([], {dirname}, err => { - t.ifError(err); - t.end(); - }); - }); - }); -} - -test('appends to existing snapshots', t => { - const cliPath = require.resolve('../cli.js'); - const avaPath = require.resolve('../'); - - const cwd = uniqueTempDir({create: true}); - fs.writeFileSync(path.join(cwd, 'package.json'), '{}'); - - const initial = `import test from ${JSON.stringify(avaPath)} -test('one', t => { - t.snapshot({one: true}) -})`; - fs.writeFileSync(path.join(cwd, 'test.js'), initial); - - const run = () => execa(process.execPath, [cliPath, '--verbose', '--no-color'], {cwd, env: {CI: '1'}, reject: false}); - return run().then(result => { - t.match(result.stdout, /1 test passed/); - - fs.writeFileSync(path.join(cwd, 'test.js'), `${initial} -test('two', t => { - t.snapshot({two: true}) -})`); - return run(); - }).then(result => { - t.match(result.stdout, /2 tests passed/); - - fs.writeFileSync(path.join(cwd, 'test.js'), `${initial} -test('two', t => { - t.snapshot({two: false}) -})`); - - return run(); - }).then(result => { - t.match(result.stdout, /1 test failed/); - }); -}); - -test('outdated snapshot version is reported to the console', t => { - const snapPath = path.join(__dirname, 'fixture', 'snapshots', 'test.js.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x00, 0x00])); - - execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /The snapshot file is v0, but only v1 is supported\./); - t.match(stdout, /File path:/); - t.match(stdout, snapPath); - t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to upgrade\./); - t.end(); - }); -}); - -test('newer snapshot version is reported to the console', t => { - const snapPath = path.join(__dirname, 'fixture', 'snapshots', 'test.js.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0xFF, 0xFF])); - - execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /The snapshot file is v65535, but only v1 is supported\./); - t.match(stdout, /File path:/); - t.match(stdout, snapPath); - t.match(stdout, /You should upgrade AVA\./); - t.end(); - }); -}); - -test('snapshot corruption is reported to the console', t => { - const snapPath = path.join(__dirname, 'fixture', 'snapshots', 'test.js.snap'); - fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x01, 0x00])); - - execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /The snapshot file is corrupted\./); - t.match(stdout, /File path:/); - t.match(stdout, snapPath); - t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to recreate it\./); - t.end(); - }); -}); - -test('legacy snapshot files are reported to the console', t => { - const snapPath = path.join(__dirname, 'fixture', 'snapshots', 'test.js.snap'); - fs.writeFileSync(snapPath, Buffer.from('// Jest Snapshot v1, https://goo.gl/fbAQLP\n')); - - execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /The snapshot file was created with AVA 0\.19\. It's not supported by this AVA version\./); - t.match(stdout, /File path:/); - t.match(stdout, snapPath); - t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to upgrade\./); - t.end(); - }); -}); - -test('snapshots infer their location from sourcemaps', t => { - t.plan(8); - const relativeFixtureDir = path.join('fixture/snapshots/test-sourcemaps'); - const snapDirStructure = [ - 'src', - 'src/test/snapshots', - 'src/feature/__tests__/__snapshots__' - ]; - const snapFixtureFilePaths = snapDirStructure - .map(snapRelativeDir => { - const snapPath = path.join(__dirname, relativeFixtureDir, snapRelativeDir); - return [ - path.join(snapPath, 'test.js.md'), - path.join(snapPath, 'test.js.snap') - ]; - }) - .reduce((a, b) => a.concat(b), []); - const removeExistingSnapFixtureFiles = snapPath => { - try { - fs.unlinkSync(snapPath); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - }; - snapFixtureFilePaths.forEach(x => removeExistingSnapFixtureFiles(x)); - const verifySnapFixtureFiles = relFilePath => { - t.true(fs.existsSync(relFilePath)); - }; - execCli([], {dirname: relativeFixtureDir}, (err, stdout) => { - t.ifError(err); - snapFixtureFilePaths.forEach(x => verifySnapFixtureFiles(x)); - t.match(stdout, /6 tests passed/); - t.end(); - }); -}); - -test('snapshots resolved location from "snapshotDir" in AVA config', t => { - t.plan(8); - const relativeFixtureDir = 'fixture/snapshots/test-snapshot-location'; - const snapDir = 'snapshot-fixtures'; - const snapDirStructure = [ - 'src', - 'src/feature', - 'src/feature/nested-feature' - ]; - const snapFixtureFilePaths = snapDirStructure - .map(snapRelativeDir => { - const snapPath = path.join(__dirname, relativeFixtureDir, snapDir, snapRelativeDir); - return [ - path.join(snapPath, 'test.js.md'), - path.join(snapPath, 'test.js.snap') - ]; - }) - .reduce((a, b) => a.concat(b), []); - const removeExistingSnapFixtureFiles = snapPath => { - try { - fs.unlinkSync(snapPath); - } catch (err) { - if (err.code !== 'ENOENT') { - throw err; - } - } - }; - snapFixtureFilePaths.forEach(x => removeExistingSnapFixtureFiles(x)); - const verifySnapFixtureFiles = relFilePath => { - t.true(fs.existsSync(relFilePath)); - }; - execCli([], {dirname: relativeFixtureDir}, (err, stdout) => { - t.ifError(err); - snapFixtureFilePaths.forEach(x => verifySnapFixtureFiles(x)); - t.match(stdout, /6 tests passed/); - t.end(); - }); -}); - -test('--no-color disables formatting colors', t => { - execCli(['--no-color', '--verbose', 'formatting-color.js'], {dirname: 'fixture'}, (err, stdout) => { - t.ok(err); - t.is(stripAnsi(stdout), stdout); - t.end(); - }); -}); - -test('--color enables formatting colors', t => { - execCli(['--color', '--verbose', 'formatting-color.js'], {dirname: 'fixture'}, (err, stdout) => { - t.ok(err); - t.isNot(stripAnsi(stdout), stdout); - t.end(); - }); -}); - -test('sets NODE_ENV to test when it is not set', t => { - execCli([path.join('fixture', 'node-env-test.js')], {env: {}}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /1 test passed/); - t.end(); - }); -}); - -test('doesn\'t set NODE_ENV when it is set', t => { - execCli([path.join('fixture', 'node-env-foo.js')], {env: {NODE_ENV: 'foo'}}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /1 test passed/); - t.end(); - }); -}); - -test('skips test file compilation when babel=false and compileEnhancements=false', t => { - execCli(['import.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /SyntaxError: Unexpected (reserved word|token import|identifier)/); - t.end(); - }); -}); - -test('skips helper file compilation when babel=false and compileEnhancements=false', t => { - execCli(['require-helper.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { - t.ifError(err); - t.match(stdout, /1 test passed/); - t.end(); - }); -}); - -test('no power-assert when babel=false and compileEnhancements=false', t => { - execCli(['no-power-assert.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { - t.ok(err); - t.notMatch(stripAnsi(stdout), /bool\n.*=> false/); - t.end(); - }); -}); - -test('skips stage-4 transform when babel=false and compileEnhancements=true', t => { - execCli(['import.js'], {dirname: 'fixture/just-enhancement-compilation'}, (err, stdout) => { - t.ok(err); - t.match(stdout, /SyntaxError: Unexpected (reserved word|token import|identifier)/); - t.end(); - }); -}); - -test('power-assert when babel=false and compileEnhancements=true', t => { - execCli(['power-assert.js'], {dirname: 'fixture/just-enhancement-compilation'}, (err, stdout) => { - t.ok(err); - t.match(stripAnsi(stdout), /bool\n.*=> false/); - t.end(); - }); -}); - -test('power-assert with custom extension and no regular babel pipeline', t => { - execCli(['.'], {dirname: 'fixture/just-enhancement-compilation/custom-extension'}, (err, stdout) => { - t.ok(err); - t.match(stripAnsi(stdout), /bool\n.*=> false/); - t.end(); - }); -}); - -test('workers load compiled helpers if in the require configuration', t => { - execCli(['test/verify.js'], {dirname: 'fixture/require-compiled-helper'}, err => { - t.ifError(err); - t.end(); - }); -}); - -test('additional arguments are forwarded to the worker', t => { - execCli(['worker-argv.js', '--serial', '--', '--hello', 'world'], {dirname: 'fixture'}, err => { - t.ifError(err); - t.end(); - }); -}); - -test('--reset-cache resets cache', t => { - const cacheDir = path.join(__dirname, 'fixture', 'reset-cache', 'node_modules', '.cache', 'ava'); - execCli([], {dirname: 'fixture/reset-cache'}, err => { - t.ifError(err); - t.true(fs.readdirSync(cacheDir).length > 0); - - execCli(['--reset-cache'], {dirname: 'fixture/reset-cache'}, err => { - t.ifError(err); - t.true(fs.readdirSync(cacheDir).length === 0); - t.end(); - }); - }); -}); diff --git a/test/fixture/ts-node/package.json b/test/fixture/ts-node/package.json new file mode 100644 index 000000000..ae2b2e819 --- /dev/null +++ b/test/fixture/ts-node/package.json @@ -0,0 +1,7 @@ +{ + "ava": { + "compileEnhancements": false, + "extensions": ["ts"], + "require": ["ts-node/register"] + } +} diff --git a/test/fixture/ts-node/test.ts b/test/fixture/ts-node/test.ts new file mode 100644 index 000000000..3d087bb5f --- /dev/null +++ b/test/fixture/ts-node/test.ts @@ -0,0 +1,5 @@ +import test from '../../../'; + +test('pass', t => { + t.pass(); +}); diff --git a/test/helper/cli.js b/test/helper/cli.js new file mode 100644 index 000000000..7a0fad5d8 --- /dev/null +++ b/test/helper/cli.js @@ -0,0 +1,55 @@ +'use strict'; +const path = require('path'); +const childProcess = require('child_process'); +const getStream = require('get-stream'); + +const cliPath = path.join(__dirname, '../../cli.js'); + +function execCli(args, opts, cb) { + let dirname; + let env; + + if (typeof opts === 'function') { + cb = opts; + dirname = path.resolve(__dirname, '..'); + env = {}; + } else { + dirname = path.resolve(__dirname, '..', opts.dirname ? opts.dirname : ''); + env = opts.env || {}; + } + + let child; + let stdout; + let stderr; + + const processPromise = new Promise(resolve => { + child = childProcess.spawn(process.execPath, [cliPath].concat(args), { + cwd: dirname, + env: Object.assign({CI: '1'}, env), // Force CI to ensure the correct reporter is selected + // env, + stdio: [null, 'pipe', 'pipe'] + }); + + child.on('close', (code, signal) => { + if (code) { + const err = new Error(`test-worker exited with a non-zero exit code: ${code}`); + err.code = code; + err.signal = signal; + resolve(err); + return; + } + + resolve(code); + }); + + stdout = getStream(child.stdout); + stderr = getStream(child.stderr); + }); + + Promise.all([processPromise, stdout, stderr]).then(args => { + cb.apply(null, args); + }); + + return child; +} +exports.execCli = execCli; diff --git a/test/integration/assorted.js b/test/integration/assorted.js new file mode 100644 index 000000000..77ff1ca26 --- /dev/null +++ b/test/integration/assorted.js @@ -0,0 +1,156 @@ +'use strict'; +const fs = require('fs'); +const childProcess = require('child_process'); +const path = require('path'); +const makeDir = require('make-dir'); +const stripAnsi = require('strip-ansi'); +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +test('timeout', t => { + execCli(['fixture/long-running.js', '-T', '1s'], (err, stdout) => { + t.ok(err); + t.match(stdout, /Exited because no new tests completed within the last 1000ms of inactivity/); + t.end(); + }); +}); + +test('include anonymous functions in error reports', t => { + execCli('fixture/error-in-anonymous-function.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /test\/fixture\/error-in-anonymous-function\.js:4:8/); + t.end(); + }); +}); + +test('--match works', t => { + execCli(['-m=foo', '-m=bar', '-m=!baz', '-m=t* a* f*', '-m=!t* a* n* f*', 'fixture/matcher-skip.js'], err => { + t.ifError(err); + t.end(); + }); +}); + +['--tap', '-t'].forEach(tapFlag => { + test(`${tapFlag} should produce TAP output`, t => { + execCli([tapFlag, 'test.js'], {dirname: 'fixture/watcher'}, err => { + t.ok(!err); + t.end(); + }); + }); +}); + +test('handles NODE_PATH', t => { + const nodePaths = `fixture/node-paths/modules${path.delimiter}fixture/node-paths/deep/nested`; + + execCli('fixture/node-paths.js', {env: {NODE_PATH: nodePaths}}, err => { + t.ifError(err); + t.end(); + }); +}); + +test('works when no files are found', t => { + execCli('!*', (err, stdout) => { + t.is(err.code, 1); + t.match(stdout, 'Couldn\'t find any files to test'); + t.end(); + }); +}); + +test('should warn ava is required without the cli', t => { + childProcess.execFile(process.execPath, [path.resolve(__dirname, '../../index.js')], error => { + t.ok(error); + t.match(error.message, /Test files must be run with the AVA CLI/); + t.end(); + }); +}); + +test('prefers local version of ava', t => { + execCli('', { + dirname: 'fixture/local-bin', + env: { + DEBUG: 'ava' + } + }, (err, stdout, stderr) => { + t.ifError(err); + t.match(stderr, 'Using local install of AVA'); + t.end(); + }); +}); + +test('workers ensure test files load the same version of ava', t => { + const target = path.join(__dirname, '..', 'fixture', 'ava-paths', 'target'); + + // Copy the index.js so the testFile imports it. It should then load the correct AVA install. + const targetInstall = path.join(target, 'node_modules/ava'); + makeDir.sync(targetInstall); + fs.writeFileSync( + path.join(targetInstall, 'index.js'), + fs.readFileSync(path.join(__dirname, '../../index.js')) + ); + + const testFile = path.join(target, 'test.js'); + execCli([testFile], {dirname: path.join('fixture', 'ava-paths', 'cwd')}, err => { + t.ifError(err); + t.end(); + }); +}); + +test('tests without assertions do not fail if failWithoutAssertions option is set to false', t => { + execCli([], {dirname: 'fixture/pkg-conf/fail-without-assertions'}, err => { + t.ifError(err); + t.end(); + }); +}); + +test('--no-color disables formatting colors', t => { + execCli(['--no-color', '--verbose', 'formatting-color.js'], {dirname: 'fixture'}, (err, stdout) => { + t.ok(err); + t.is(stripAnsi(stdout), stdout); + t.end(); + }); +}); + +test('--color enables formatting colors', t => { + execCli(['--color', '--verbose', 'formatting-color.js'], {dirname: 'fixture'}, (err, stdout) => { + t.ok(err); + t.isNot(stripAnsi(stdout), stdout); + t.end(); + }); +}); + +test('sets NODE_ENV to test when it is not set', t => { + execCli([path.join('fixture', 'node-env-test.js')], {env: {}}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /1 test passed/); + t.end(); + }); +}); + +test('doesn\'t set NODE_ENV when it is set', t => { + execCli([path.join('fixture', 'node-env-foo.js')], {env: {NODE_ENV: 'foo'}}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /1 test passed/); + t.end(); + }); +}); + +test('additional arguments are forwarded to the worker', t => { + execCli(['worker-argv.js', '--serial', '--', '--hello', 'world'], {dirname: 'fixture'}, err => { + t.ifError(err); + t.end(); + }); +}); + +test('--reset-cache resets cache', t => { + const cacheDir = path.join(__dirname, '..', 'fixture', 'reset-cache', 'node_modules', '.cache', 'ava'); + execCli([], {dirname: 'fixture/reset-cache'}, err => { + t.ifError(err); + t.true(fs.readdirSync(cacheDir).length > 0); + + execCli(['--reset-cache'], {dirname: 'fixture/reset-cache'}, err => { + t.ifError(err); + t.true(fs.readdirSync(cacheDir).length === 0); + t.end(); + }); + }); +}); diff --git a/test/integration/babel.js b/test/integration/babel.js new file mode 100644 index 000000000..d7c7819dc --- /dev/null +++ b/test/integration/babel.js @@ -0,0 +1,32 @@ +'use strict'; +const test = require('tap').test; +const figures = require('figures'); +const pkg = require('../../package.json'); +const {execCli} = require('../helper/cli'); + +for (const which of [ + 'bad-key', + 'bad-shortcut', + 'array-test-options', + 'false-test-options', + 'null-test-options', + 'null-extensions', + 'obj-extensions', + 'string-extensions', + 'non-string-value-extensions', + 'empty-string-value-extensions' +]) { + test(`validates babel config: ${which}`, t => { + execCli(['es2015.js'], {dirname: `fixture/invalid-babel-config/${which}`}, (err, stdout, stderr) => { + t.ok(err); + + let expectedOutput = '\n'; + expectedOutput += figures.cross + ' Unexpected Babel configuration for AVA.'; + expectedOutput += ` See https://github.com/avajs/ava/blob/v${pkg.version}/docs/recipes/babel.md for allowed values.`; + expectedOutput += '\n'; + + t.is(stderr, expectedOutput); + t.end(); + }); + }); +} diff --git a/test/integration/compilation.js b/test/integration/compilation.js new file mode 100644 index 000000000..f385bea96 --- /dev/null +++ b/test/integration/compilation.js @@ -0,0 +1,77 @@ +'use strict'; +const stripAnsi = require('strip-ansi'); +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +test('precompiler require hook does not apply to source files', t => { + t.plan(3); + + execCli('fixture/babel-hook.js', (err, stdout) => { + t.ok(err); + t.is(err.code, 1); + t.match(stdout, /Unexpected (token|reserved word)/); + t.end(); + }); +}); + +test('skips test file compilation when babel=false and compileEnhancements=false', t => { + execCli(['import.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /SyntaxError: Unexpected (reserved word|token import|identifier)/); + t.end(); + }); +}); + +test('skips helper file compilation when babel=false and compileEnhancements=false', t => { + execCli(['require-helper.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /1 test passed/); + t.end(); + }); +}); + +test('no power-assert when babel=false and compileEnhancements=false', t => { + execCli(['no-power-assert.js'], {dirname: 'fixture/no-babel-compilation'}, (err, stdout) => { + t.ok(err); + t.notMatch(stripAnsi(stdout), /bool\n.*=> false/); + t.end(); + }); +}); + +test('skips stage-4 transform when babel=false and compileEnhancements=true', t => { + execCli(['import.js'], {dirname: 'fixture/just-enhancement-compilation'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /SyntaxError: Unexpected (reserved word|token import|identifier)/); + t.end(); + }); +}); + +test('power-assert when babel=false and compileEnhancements=true', t => { + execCli(['power-assert.js'], {dirname: 'fixture/just-enhancement-compilation'}, (err, stdout) => { + t.ok(err); + t.match(stripAnsi(stdout), /bool\n.*=> false/); + t.end(); + }); +}); + +test('power-assert with custom extension and no regular babel pipeline', t => { + execCli(['.'], {dirname: 'fixture/just-enhancement-compilation/custom-extension'}, (err, stdout) => { + t.ok(err); + t.match(stripAnsi(stdout), /bool\n.*=> false/); + t.end(); + }); +}); + +test('workers load compiled helpers if in the require configuration', t => { + execCli(['test/verify.js'], {dirname: 'fixture/require-compiled-helper'}, err => { + t.ifError(err); + t.end(); + }); +}); + +test('skips babel compilation for custom extensions, with disabled enhancement compilation', t => { + execCli(['test.ts'], {dirname: 'fixture/ts-node'}, err => { + t.ifError(err); + t.end(); + }); +}); diff --git a/test/integration/concurrency.js b/test/integration/concurrency.js new file mode 100644 index 000000000..6d2980e06 --- /dev/null +++ b/test/integration/concurrency.js @@ -0,0 +1,52 @@ +'use strict'; +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +['--concurrency', '-c'].forEach(concurrencyFlag => { + test(`bails when ${concurrencyFlag} is provided without value`, t => { + execCli(['test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); + t.end(); + }); + }); +}); + +['--concurrency', '-c'].forEach(concurrencyFlag => { + test(`bails when ${concurrencyFlag} is provided with an input that is a string`, t => { + execCli([`${concurrencyFlag}=foo`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); + t.end(); + }); + }); +}); + +['--concurrency', '-c'].forEach(concurrencyFlag => { + test(`bails when ${concurrencyFlag} is provided with an input that is a float`, t => { + execCli([`${concurrencyFlag}=4.7`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); + t.end(); + }); + }); +}); + +['--concurrency', '-c'].forEach(concurrencyFlag => { + test(`bails when ${concurrencyFlag} is provided with an input that is negative`, t => { + execCli([`${concurrencyFlag}=-1`, 'test.js', concurrencyFlag], {dirname: 'fixture/concurrency'}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'The --concurrency or -c flag must be provided with a nonnegative integer.'); + t.end(); + }); + }); +}); + +['--concurrency', '-c'].forEach(concurrencyFlag => { + test(`works when ${concurrencyFlag} is provided with a value`, t => { + execCli([`${concurrencyFlag}=1`, 'test.js'], {dirname: 'fixture/concurrency'}, err => { + t.ifError(err); + t.end(); + }); + }); +}); diff --git a/test/integration/config.js b/test/integration/config.js new file mode 100644 index 000000000..c74dbbb9e --- /dev/null +++ b/test/integration/config.js @@ -0,0 +1,66 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const test = require('tap').test; +const execa = require('execa'); +const figures = require('figures'); +const uniqueTempDir = require('unique-temp-dir'); +const {execCli} = require('../helper/cli'); + +test('formats errors from ava.config.js', t => { + execCli(['es2015.js'], {dirname: 'fixture/load-config/throws'}, (err, stdout, stderr) => { + t.ok(err); + + const lines = stderr.split('\n'); + t.is(lines[0], ''); + t.is(lines[1], figures.cross + ' Error loading ava.config.js'); + t.is(lines[2], ''); + t.match(lines[3], /ava\.config\.js/); + t.match(lines[4], /foo/); + t.end(); + }); +}); + +test('pkg-conf(resolve-dir): works as expected when run from the package.json directory', t => { + execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir'}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /dir-a-base-1/); + t.match(stdout, /dir-a-base-2/); + t.notMatch(stdout, /dir-a-wrapper/); + t.notMatch(stdout, /dir-a-wrapper/); + t.end(); + }); +}); + +test('pkg-conf(resolve-dir): resolves tests from the package.json dir if none are specified on cli', t => { + execCli(['--verbose'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /dir-a-base-1/); + t.match(stdout, /dir-a-base-2/); + t.notMatch(stdout, /dir-a-wrapper/); + t.notMatch(stdout, /dir-a-wrapper/); + t.end(); + }); +}); + +test('pkg-conf(resolve-dir): resolves tests process.cwd() if globs are passed on the command line', t => { + execCli(['--verbose', 'dir-a/*.js'], {dirname: 'fixture/pkg-conf/resolve-dir/dir-a-wrapper'}, (err, stdout) => { + t.ifError(err); + t.match(stdout, /dir-a-wrapper-3/); + t.match(stdout, /dir-a-wrapper-4/); + t.notMatch(stdout, /dir-a-base/); + t.notMatch(stdout, /dir-a-base/); + t.end(); + }); +}); + +test('use current working directory if `package.json` is not found', () => { + const cwd = uniqueTempDir({create: true}); + const testFilePath = path.join(cwd, 'test.js'); + const cliPath = require.resolve('../../cli.js'); + const avaPath = require.resolve('../../'); + + fs.writeFileSync(testFilePath, `import test from ${JSON.stringify(avaPath)};\ntest('test', t => { t.pass(); });`); + + return execa(process.execPath, [cliPath], {cwd, env: {CI: '1'}}); +}); diff --git a/test/integration/extensions.js b/test/integration/extensions.js new file mode 100644 index 000000000..6bc660f79 --- /dev/null +++ b/test/integration/extensions.js @@ -0,0 +1,36 @@ +'use strict'; +const test = require('tap').test; +const figures = require('figures'); +const {execCli} = require('../helper/cli'); + +test('errors if top-level extensions include "js" without babel=false', t => { + execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/top-level`}, (err, stdout, stderr) => { + t.ok(err); + + let expectedOutput = '\n'; + expectedOutput += figures.cross + ' Cannot specify generic \'js\' extension without disabling AVA\'s Babel usage.'; + expectedOutput += '\n'; + + t.is(stderr, expectedOutput); + t.end(); + }); +}); + +for (const [where, which, msg = '\'js\', \'jsx\''] of [ + ['top-level', 'top-level-duplicates'], + ['babel', 'babel-duplicates'], + ['top-level and babel', 'shared-duplicates', '\'jsx\''] +]) { + test(`errors if ${where} extensions include duplicates`, t => { + execCli(['es2015.js'], {dirname: `fixture/invalid-extensions/${which}`}, (err, stdout, stderr) => { + t.ok(err); + + let expectedOutput = '\n'; + expectedOutput += figures.cross + ` Unexpected duplicate extensions in options: ${msg}.`; + expectedOutput += '\n'; + + t.is(stderr, expectedOutput); + t.end(); + }); + }); +} diff --git a/test/integration/snapshots.js b/test/integration/snapshots.js new file mode 100644 index 000000000..b5d101b6d --- /dev/null +++ b/test/integration/snapshots.js @@ -0,0 +1,207 @@ +'use strict'; +const fs = require('fs'); +const path = require('path'); +const execa = require('execa'); +const uniqueTempDir = require('unique-temp-dir'); +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +for (const obj of [ + {type: 'colocated', rel: '', dir: ''}, + {type: '__tests__', rel: '__tests__-dir', dir: '__tests__/__snapshots__'}, + {type: 'test', rel: 'test-dir', dir: 'test/snapshots'}, + {type: 'tests', rel: 'tests-dir', dir: 'tests/snapshots'} +]) { + test(`snapshots work (${obj.type})`, t => { + const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', obj.rel, obj.dir, 'test.js.snap'); + try { + fs.unlinkSync(snapPath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + + const dirname = path.join('fixture/snapshots', obj.rel); + // Test should pass, and a snapshot gets written + execCli(['--update-snapshots'], {dirname}, err => { + t.ifError(err); + t.true(fs.existsSync(snapPath)); + + // Test should pass, and the snapshot gets used + execCli([], {dirname}, err => { + t.ifError(err); + t.end(); + }); + }); + }); +} + +test('appends to existing snapshots', t => { + const cliPath = require.resolve('../../cli.js'); + const avaPath = require.resolve('../../'); + + const cwd = uniqueTempDir({create: true}); + fs.writeFileSync(path.join(cwd, 'package.json'), '{}'); + + const initial = `import test from ${JSON.stringify(avaPath)} +test('one', t => { + t.snapshot({one: true}) +})`; + fs.writeFileSync(path.join(cwd, 'test.js'), initial); + + const run = () => execa(process.execPath, [cliPath, '--verbose', '--no-color'], {cwd, env: {CI: '1'}, reject: false}); + return run().then(result => { + t.match(result.stdout, /1 test passed/); + + fs.writeFileSync(path.join(cwd, 'test.js'), `${initial} +test('two', t => { + t.snapshot({two: true}) +})`); + return run(); + }).then(result => { + t.match(result.stdout, /2 tests passed/); + + fs.writeFileSync(path.join(cwd, 'test.js'), `${initial} +test('two', t => { + t.snapshot({two: false}) +})`); + + return run(); + }).then(result => { + t.match(result.stdout, /1 test failed/); + }); +}); + +test('outdated snapshot version is reported to the console', t => { + const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.js.snap'); + fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x00, 0x00])); + + execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /The snapshot file is v0, but only v1 is supported\./); + t.match(stdout, /File path:/); + t.match(stdout, snapPath); + t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to upgrade\./); + t.end(); + }); +}); + +test('newer snapshot version is reported to the console', t => { + const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.js.snap'); + fs.writeFileSync(snapPath, Buffer.from([0x0A, 0xFF, 0xFF])); + + execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /The snapshot file is v65535, but only v1 is supported\./); + t.match(stdout, /File path:/); + t.match(stdout, snapPath); + t.match(stdout, /You should upgrade AVA\./); + t.end(); + }); +}); + +test('snapshot corruption is reported to the console', t => { + const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.js.snap'); + fs.writeFileSync(snapPath, Buffer.from([0x0A, 0x01, 0x00])); + + execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /The snapshot file is corrupted\./); + t.match(stdout, /File path:/); + t.match(stdout, snapPath); + t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to recreate it\./); + t.end(); + }); +}); + +test('legacy snapshot files are reported to the console', t => { + const snapPath = path.join(__dirname, '..', 'fixture', 'snapshots', 'test.js.snap'); + fs.writeFileSync(snapPath, Buffer.from('// Jest Snapshot v1, https://goo.gl/fbAQLP\n')); + + execCli(['test.js'], {dirname: 'fixture/snapshots'}, (err, stdout) => { + t.ok(err); + t.match(stdout, /The snapshot file was created with AVA 0\.19\. It's not supported by this AVA version\./); + t.match(stdout, /File path:/); + t.match(stdout, snapPath); + t.match(stdout, /Please run AVA again with the .*--update-snapshots.* flag to upgrade\./); + t.end(); + }); +}); + +test('snapshots infer their location from sourcemaps', t => { + t.plan(8); + const relativeFixtureDir = path.join('fixture/snapshots/test-sourcemaps'); + const snapDirStructure = [ + 'src', + 'src/test/snapshots', + 'src/feature/__tests__/__snapshots__' + ]; + const snapFixtureFilePaths = snapDirStructure + .map(snapRelativeDir => { + const snapPath = path.join(__dirname, '..', relativeFixtureDir, snapRelativeDir); + return [ + path.join(snapPath, 'test.js.md'), + path.join(snapPath, 'test.js.snap') + ]; + }) + .reduce((a, b) => a.concat(b), []); + const removeExistingSnapFixtureFiles = snapPath => { + try { + fs.unlinkSync(snapPath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + }; + snapFixtureFilePaths.forEach(x => removeExistingSnapFixtureFiles(x)); + const verifySnapFixtureFiles = relFilePath => { + t.true(fs.existsSync(relFilePath)); + }; + execCli([], {dirname: relativeFixtureDir}, (err, stdout) => { + t.ifError(err); + snapFixtureFilePaths.forEach(x => verifySnapFixtureFiles(x)); + t.match(stdout, /6 tests passed/); + t.end(); + }); +}); + +test('snapshots resolved location from "snapshotDir" in AVA config', t => { + t.plan(8); + const relativeFixtureDir = 'fixture/snapshots/test-snapshot-location'; + const snapDir = 'snapshot-fixtures'; + const snapDirStructure = [ + 'src', + 'src/feature', + 'src/feature/nested-feature' + ]; + const snapFixtureFilePaths = snapDirStructure + .map(snapRelativeDir => { + const snapPath = path.join(__dirname, '..', relativeFixtureDir, snapDir, snapRelativeDir); + return [ + path.join(snapPath, 'test.js.md'), + path.join(snapPath, 'test.js.snap') + ]; + }) + .reduce((a, b) => a.concat(b), []); + const removeExistingSnapFixtureFiles = snapPath => { + try { + fs.unlinkSync(snapPath); + } catch (err) { + if (err.code !== 'ENOENT') { + throw err; + } + } + }; + snapFixtureFilePaths.forEach(x => removeExistingSnapFixtureFiles(x)); + const verifySnapFixtureFiles = relFilePath => { + t.true(fs.existsSync(relFilePath)); + }; + execCli([], {dirname: relativeFixtureDir}, (err, stdout) => { + t.ifError(err); + snapFixtureFilePaths.forEach(x => verifySnapFixtureFiles(x)); + t.match(stdout, /6 tests passed/); + t.end(); + }); +}); diff --git a/test/integration/stack-traces.js b/test/integration/stack-traces.js new file mode 100644 index 000000000..e28d13967 --- /dev/null +++ b/test/integration/stack-traces.js @@ -0,0 +1,21 @@ +'use strict'; +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +test('enabling long stack traces will provide detailed debug information', t => { + execCli('fixture/long-stack-trace', (err, stdout, stderr) => { + t.ok(err); + t.match(stderr, /From previous event/); + t.end(); + }); +}); + +test('`AssertionError` should capture infinity stack trace', t => { + execCli('fixture/infinity-stack-trace.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /c \(.+?infinity-stack-trace\.js:6:20\)/); + t.match(stdout, /b \(.+?infinity-stack-trace\.js:7:18\)/); + t.match(stdout, /a \(.+?infinity-stack-trace\.js:8:18\)/); + t.end(); + }); +}); diff --git a/test/integration/stalled.js b/test/integration/stalled.js new file mode 100644 index 000000000..8b992179f --- /dev/null +++ b/test/integration/stalled.js @@ -0,0 +1,24 @@ +'use strict'; +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +test('callback tests fail if event loop empties before they\'re ended', t => { + execCli('callback.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { + t.match(stdout, /`t\.end\(\)` was never called/); + t.end(); + }); +}); + +test('observable tests fail if event loop empties before they\'re resolved', t => { + execCli('observable.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { + t.match(stdout, /Observable returned by test never completed/); + t.end(); + }); +}); + +test('promise tests fail if event loop empties before they\'re resolved', t => { + execCli('promise.js', {dirname: 'fixture/stalled-tests'}, (_, stdout) => { + t.match(stdout, /Promise returned by test never resolved/); + t.end(); + }); +}); diff --git a/test/integration/t-throws.js b/test/integration/t-throws.js new file mode 100644 index 000000000..7f3ce68da --- /dev/null +++ b/test/integration/t-throws.js @@ -0,0 +1,93 @@ +'use strict'; +const test = require('tap').test; +const {execCli} = require('../helper/cli'); + +test('improper use of t.throws will be reported to the console', t => { + execCli('fixture/improper-t-throws/throws.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws from within a Promise will be reported to the console', t => { + execCli('fixture/improper-t-throws/promise.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws from within a pending promise, even if caught and rethrown immediately, will be reported to the console', t => { + execCli('fixture/improper-t-throws/leaked-from-promise.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws from within an async callback will be reported to the console', t => { + execCli('fixture/improper-t-throws/async-callback.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws, swallowed as an unhandled rejection, will be reported to the console', t => { + execCli('fixture/improper-t-throws/unhandled-rejection.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws, even if caught, will be reported to the console', t => { + execCli('fixture/improper-t-throws/caught.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.notMatch(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws, even if caught and then rethrown immediately, will be reported to the console', t => { + execCli('fixture/improper-t-throws/caught-and-leaked.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws, even if caught and then later rethrown, will be reported to the console', t => { + execCli('fixture/improper-t-throws/caught-and-leaked-slowly.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.match(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); + +test('improper use of t.throws, even if caught and then rethrown too slowly, will be reported to the console', t => { + execCli('fixture/improper-t-throws/caught-and-leaked-too-slowly.js', (err, stdout) => { + t.ok(err); + t.match(stdout, /Improper usage of `t\.throws\(\)` detected/); + t.notMatch(stdout, /should be detected/); + t.match(stdout, /Try wrapping the first argument/); + t.end(); + }); +}); diff --git a/test/integration/watcher.js b/test/integration/watcher.js new file mode 100644 index 000000000..a241a3914 --- /dev/null +++ b/test/integration/watcher.js @@ -0,0 +1,152 @@ +'use strict'; +const path = require('path'); +const test = require('tap').test; +const touch = require('touch'); +const {execCli} = require('../helper/cli'); + +test('watcher reruns test files when they changed', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch', 'test.js'], {dirname: 'fixture/watcher', env: {CI: ''}}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stdout.on('data', str => { + buffer += str; + if (/1 test passed/.test(buffer)) { + if (!passedFirst) { + touch.sync(path.join(__dirname, '../fixture/watcher/test.js')); + buffer = ''; + passedFirst = true; + } else if (!killed) { + child.kill(); + killed = true; + } + } + }); +}); + +test('watcher reruns test files when source dependencies change', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch', 'test-*.js'], {dirname: 'fixture/watcher/with-dependencies', env: {CI: ''}}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stdout.on('data', str => { + buffer += str; + if (/2 tests passed/.test(buffer) && !passedFirst) { + touch.sync(path.join(__dirname, '../fixture/watcher/with-dependencies/source.js')); + buffer = ''; + passedFirst = true; + } else if (/1 test passed/.test(buffer) && !killed) { + child.kill(); + killed = true; + } + }); +}); + +test('watcher does not rerun test files when they write snapshot files', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch', '--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots', env: {CI: ''}}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stdout.on('data', str => { + buffer += str; + if (/2 tests passed/.test(buffer) && !passedFirst) { + buffer = ''; + passedFirst = true; + setTimeout(() => { + child.kill(); + killed = true; + }, 500); + } else if (passedFirst && !killed) { + t.is(buffer.replace(/\s/g, ''), ''); + } + }); +}); + +test('watcher reruns test files when snapshot dependencies change', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch', '--update-snapshots', 'test.js'], {dirname: 'fixture/snapshots', env: {CI: ''}}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stdout.on('data', str => { + buffer += str; + if (/2 tests passed/.test(buffer)) { + buffer = ''; + if (passedFirst) { + child.kill(); + killed = true; + } else { + passedFirst = true; + setTimeout(() => { + touch.sync(path.join(__dirname, '../fixture/snapshots/test.js.snap')); + }, 500); + } + } + }); +}); + +test('`"tap": true` config is ignored when --watch is given', t => { + let killed = false; + + const child = execCli(['--watch', '--verbose', 'test.js'], {dirname: 'fixture/watcher/tap-in-conf', env: {CI: ''}}, () => { + t.ok(killed); + t.end(); + }); + + let combined = ''; + const testOutput = output => { + combined += output; + t.notMatch(combined, /TAP/); + if (/works/.test(combined)) { + child.kill(); + killed = true; + } + }; + child.stdout.on('data', testOutput); + child.stderr.on('data', testOutput); +}); + +['--watch', '-w'].forEach(watchFlag => { + ['--tap', '-t'].forEach(tapFlag => { + test(`bails when ${tapFlag} reporter is used while ${watchFlag} is given`, t => { + execCli([tapFlag, watchFlag, 'test.js'], {dirname: 'fixture/watcher', env: {CI: ''}}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'The TAP reporter is not available when using watch mode.'); + t.end(); + }); + }); + }); +}); + +['--watch', '-w'].forEach(watchFlag => { + test(`bails when CI is used while ${watchFlag} is given`, t => { + execCli([watchFlag, 'test.js'], {dirname: 'fixture/watcher', env: {CI: true}}, (err, stdout, stderr) => { + t.is(err.code, 1); + t.match(stderr, 'Watch mode is not available in CI, as it prevents AVA from terminating.'); + t.end(); + }); + }); +});