diff --git a/docs/06-configuration.md b/docs/06-configuration.md index 7724271b3..f99bfbf07 100644 --- a/docs/06-configuration.md +++ b/docs/06-configuration.md @@ -4,7 +4,7 @@ Translations: [Français](https://github.com/avajs/ava-docs/blob/master/fr_FR/do All of the [CLI options](./05-command-line.md) can be configured in the `ava` section of either your `package.json` file, or an `ava.config.js` file. This allows you to modify the default behavior of the `ava` command, so you don't have to repeatedly type the same options on the command prompt. -To ignore a file or directory, prefix the pattern with an `!` (exclamation mark). +To ignore files, prefix the pattern with an `!` (exclamation mark). **`package.json`:** @@ -12,13 +12,15 @@ To ignore a file or directory, prefix the pattern with an `!` (exclamation mark) { "ava": { "files": [ - "my-test-directory/**/*.js", - "!my-test-directory/exclude-this-directory", - "!**/exclude-this-file.js" + "test/**/*", + "!test/exclude-files-in-this-directory", + "!**/exclude-files-with-this-name.*" + ], + "helpers": [ + "**/helpers/**/*" ], "sources": [ - "**/*.{js,jsx}", - "!dist" + "src/**/*" ], "match": [ "*oo", @@ -35,7 +37,7 @@ To ignore a file or directory, prefix the pattern with an `!` (exclamation mark) "@babel/register" ], "babel": { - "extensions": ["jsx"], + "extensions": ["js", "jsx"], "testOptions": { "babelrc": false } @@ -48,8 +50,9 @@ Arguments passed to the CLI will always take precedence over the CLI options con ## Options -- `files`: glob patterns that select which files AVA will run tests from. Files with an underscore prefix are ignored. By default only selects files with `js` extensions, even if the glob pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions -- `sources`: files that, when changed, cause tests to be re-run in watch mode. See the [watch mode recipe for details](https://github.com/avajs/ava/blob/master/docs/recipes/watch-mode.md#source-files-and-test-files) +- `files`: an array of glob patterns to select test files. Files with an underscore prefix are ignored. By default only selects files with `js` extensions, even if the pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions +- `helpers`: an array of glob patterns to select helper files. Files matched here are never considered as tests. By default only selects files with `js` extensions, even if the pattern matches other files. Specify `extensions` and `babel.extensions` to allow other file extensions +- `sources`: an array of glob patterns to match files that, when changed, cause tests to be re-run (when in watch mode). See the [watch mode recipe for details](https://github.com/avajs/ava/blob/master/docs/recipes/watch-mode.md#source-files-and-test-files) - `match`: not typically useful in the `package.json` configuration, but equivalent to [specifying `--match` on the CLI](./05-command-line.md#running-tests-with-matching-titles) - `cache`: cache compiled test and helper files under `node_modules/.cache/ava`. If `false`, files are cached in a temporary directory instead - `failFast`: stop running further tests once a test fails diff --git a/lib/api.js b/lib/api.js index 446eb77ae..952fc0bc8 100644 --- a/lib/api.js +++ b/lib/api.js @@ -110,12 +110,17 @@ class Api extends Emittery { const precompiler = await this._setupPrecompiler(); let helpers = []; if (files.length === 0 || precompiler.enabled) { - const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs}); + let found; + if (precompiler.enabled) { + found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs}); + helpers = found.helpers; + } else { + found = await globs.findTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs}); + } + if (files.length === 0) { ({tests: files} = found); } - - ({helpers} = found); } if (this.options.parallelRuns) { diff --git a/lib/cli.js b/lib/cli.js index a58b5718d..00c6278b3 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -180,7 +180,7 @@ exports.run = async () => { // eslint-disable-line complexity let globs; try { - globs = normalizeGlobs(conf.files, conf.sources, extensions.all); + globs = normalizeGlobs(conf.files, conf.helpers, conf.sources, extensions.all); } catch (error) { exit(error.message); } diff --git a/lib/globs.js b/lib/globs.js index 9cae32270..a2e0b5562 100644 --- a/lib/globs.js +++ b/lib/globs.js @@ -28,11 +28,15 @@ const normalizePatterns = patterns => { }); }; -function normalizeGlobs(testPatterns, sourcePatterns, extensions) { +function normalizeGlobs(testPatterns, helperPatterns, sourcePatterns, extensions) { if (typeof testPatterns !== 'undefined' && (!Array.isArray(testPatterns) || testPatterns.length === 0)) { throw new Error('The \'files\' configuration must be an array containing glob patterns.'); } + if (typeof helperPatterns !== 'undefined' && (!Array.isArray(helperPatterns) || helperPatterns.length === 0)) { + throw new Error('The \'helpers\' configuration must be an array containing glob patterns.'); + } + if (sourcePatterns && (!Array.isArray(sourcePatterns) || sourcePatterns.length === 0)) { throw new Error('The \'sources\' configuration must be an array containing glob patterns.'); } @@ -58,6 +62,12 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) { testPatterns = defaultTestPatterns; } + if (helperPatterns) { + helperPatterns = normalizePatterns(helperPatterns); + } else { + helperPatterns = []; + } + const defaultSourcePatterns = [ '**/*.snap', 'ava.config.js', @@ -75,11 +85,13 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) { sourcePatterns = defaultSourcePatterns; } - return {extensions, testPatterns, sourcePatterns}; + return {extensions, testPatterns, helperPatterns, sourcePatterns}; } exports.normalizeGlobs = normalizeGlobs; +const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1)); + const findFiles = async (cwd, patterns) => { const files = await globby(patterns, { absolute: true, @@ -108,26 +120,60 @@ const findFiles = async (cwd, patterns) => { return files; }; -async function findHelpersAndTests({cwd, extensions, testPatterns}) { - const helpers = []; +async function findHelpersAndTests({cwd, extensions, testPatterns, helperPatterns}) { + // Search for tests concurrently with finding helpers. + const findingTests = findFiles(cwd, testPatterns); + + const uniqueHelpers = new Set(); + if (helperPatterns.length > 0) { + for (const file of await findFiles(cwd, helperPatterns)) { + if (!hasExtension(extensions, file)) { + continue; + } + + uniqueHelpers.add(file); + } + } + const tests = []; - for (const file of await findFiles(cwd, testPatterns)) { - if (!extensions.includes(path.extname(file).slice(1))) { + for (const file of await findingTests) { + if (!hasExtension(extensions, file)) { continue; } if (path.basename(file).startsWith('_')) { - helpers.push(file); - } else { + uniqueHelpers.add(file); + } else if (!uniqueHelpers.has(file)) { // Helpers cannot be tests. tests.push(file); } } - return {helpers, tests}; + return {helpers: [...uniqueHelpers], tests}; } exports.findHelpersAndTests = findHelpersAndTests; +async function findTests({cwd, extensions, testPatterns, helperPatterns}) { + const rejectHelpers = helperPatterns.length > 0; + + const tests = []; + for (const file of await findFiles(cwd, testPatterns)) { + if (!hasExtension(extensions, file) || path.basename(file).startsWith('_')) { + continue; + } + + if (rejectHelpers && matches(normalizeFileForMatching(cwd, file), helperPatterns)) { + continue; + } + + tests.push(file); + } + + return {tests}; +} + +exports.findTests = findTests; + function getChokidarPatterns({sourcePatterns, testPatterns}) { const paths = []; const ignored = defaultIgnorePatterns.map(pattern => `${pattern}/**/*`); @@ -175,14 +221,57 @@ const matches = (file, patterns) => { return micromatch.some(file, patterns, {ignore}); }; -function isSource(file, {testPatterns, sourcePatterns}) { - return !isTest(file, {testPatterns}) && matches(file, sourcePatterns); -} +const NOT_IGNORED = ['**/*']; + +const normalizeFileForMatching = (cwd, file) => { + if (process.platform === 'win32') { + cwd = slash(cwd); + file = slash(file); + } + + if (!cwd) { // TODO: Ensure tests provide an actual value. + return file; + } + + // TODO: If `file` is outside `cwd` we can't normalize it. Need to figure + // out if that's a real-world scenario, but we may have to ensure the file + // isn't even selected. + if (!file.startsWith(cwd)) { + return file; + } -exports.isSource = isSource; + // Assume `cwd` does *not* end in a slash. + return file.slice(cwd.length + 1); +}; + +function classify(file, {cwd, extensions, helperPatterns, testPatterns, sourcePatterns}) { + let isHelper = false; + let isTest = false; + let isSource = false; + + file = normalizeFileForMatching(cwd, file); + + if (hasExtension(extensions, file)) { + if (path.basename(file).startsWith('_')) { + isHelper = matches(file, NOT_IGNORED); + } else { + isHelper = helperPatterns.length > 0 && matches(file, helperPatterns); + + if (!isHelper) { + isTest = testPatterns.length > 0 && matches(file, testPatterns); + + if (!isTest) { + // Note: Don't check sourcePatterns.length since we still need to + // check the file against the default ignore patterns. + isSource = matches(file, sourcePatterns); + } + } + } + } else { + isSource = matches(file, sourcePatterns); + } -function isTest(file, {testPatterns}) { - return !path.basename(file).startsWith('_') && matches(file, testPatterns); + return {isHelper, isTest, isSource}; } -exports.isTest = isTest; +exports.classify = classify; diff --git a/lib/watcher.js b/lib/watcher.js index 4cf881c95..6ef70cf31 100644 --- a/lib/watcher.js +++ b/lib/watcher.js @@ -68,13 +68,13 @@ class Debouncer { } class TestDependency { - constructor(file, sources) { + constructor(file, dependencies) { this.file = file; - this.sources = sources; + this.dependencies = dependencies; } - contains(source) { - return this.sources.includes(source); + contains(dependency) { + return this.dependencies.includes(dependency); } } @@ -177,14 +177,17 @@ class Watcher { return; } - const sourceDeps = evt.dependencies.map(x => relative(x)).filter(filePath => globs.isSource(filePath, this.globs)); - this.updateTestDependencies(evt.testFile, sourceDeps); + const dependencies = evt.dependencies.map(x => relative(x)).filter(filePath => { + const {isHelper, isSource} = globs.classify(filePath, this.globs); + return isHelper || isSource; + }); + this.updateTestDependencies(evt.testFile, dependencies); }); }); } - updateTestDependencies(file, sources) { - if (sources.length === 0) { + updateTestDependencies(file, dependencies) { + if (dependencies.length === 0) { this.testDependencies = this.testDependencies.filter(dep => dep.file !== file); return; } @@ -194,13 +197,13 @@ class Watcher { return false; } - dep.sources = sources; + dep.dependencies = dependencies; return true; }); if (!isUpdate) { - this.testDependencies.push(new TestDependency(file, sources)); + this.testDependencies.push(new TestDependency(file, dependencies)); } } @@ -360,8 +363,19 @@ class Watcher { return true; }); - const dirtyTests = dirtyPaths.filter(filePath => globs.isTest(filePath, this.globs)); - const dirtySources = diff(dirtyPaths, dirtyTests); + const dirtyHelpersAndSources = []; + const dirtyTests = []; + for (const filePath of dirtyPaths) { + const {isHelper, isSource, isTest} = globs.classify(filePath, this.globs); + if (isHelper || isSource) { + dirtyHelpersAndSources.push(filePath); + } + + if (isTest) { + dirtyTests.push(filePath); + } + } + const addedOrChangedTests = dirtyTests.filter(path => dirtyStates[path] !== 'unlink'); const unlinkedTests = diff(dirtyTests, addedOrChangedTests); @@ -372,14 +386,14 @@ class Watcher { return; } - if (dirtySources.length === 0) { + if (dirtyHelpersAndSources.length === 0) { // Run any new or changed tests this.run(addedOrChangedTests); return; } // Try to find tests that depend on the changed source files - const testsBySource = dirtySources.map(path => { + const testsByHelpersOrSource = dirtyHelpersAndSources.map(path => { return this.testDependencies.filter(dep => dep.contains(path)).map(dep => { debug('%s is a dependency of %s', path, dep.file); return dep.file; @@ -388,15 +402,15 @@ class Watcher { // Rerun all tests if source files were changed that could not be traced to // specific tests - if (testsBySource.length !== dirtySources.length) { - debug('Sources remain that cannot be traced to specific tests: %O', dirtySources); + if (testsByHelpersOrSource.length !== dirtyHelpersAndSources.length) { + debug('Helpers & sources remain that cannot be traced to specific tests: %O', dirtyHelpersAndSources); debug('Rerunning all tests'); this.run(); return; } // Run all affected tests - this.run(union(addedOrChangedTests, uniq(flatten(testsBySource)))); + this.run(union(addedOrChangedTests, uniq(flatten(testsByHelpersOrSource)))); } } diff --git a/test/api.js b/test/api.js index d1c7023f0..b26984fce 100644 --- a/test/api.js +++ b/test/api.js @@ -32,7 +32,7 @@ function apiCreator(options = {}) { options.babelConfig = babelPipeline.validate(options.babelConfig); options.concurrency = 2; options.extensions = options.extensions || {all: ['js'], enhancementsOnly: [], full: ['js']}; - options.globs = normalizeGlobs(options.files, options.sources, options.extensions.all); + options.globs = normalizeGlobs(options.files, options.helpers, options.sources, options.extensions.all); options.projectDir = options.projectDir || ROOT_DIR; options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir; const instance = new Api(options); @@ -579,16 +579,15 @@ test('test file in node_modules is ignored', t => { }); }); -// TODO: Re-enable to test helpers patterns. -// test('test file in helpers is ignored', t => { -// t.plan(1); -// -// const api = apiCreator(); -// return api.run([path.join(__dirname, 'fixture/ignored-dirs/helpers/test.js')]) -// .then(runStatus => { -// t.is(runStatus.stats.declaredTests, 0); -// }); -// }); +test('test file in helpers is ignored', t => { + t.plan(1); + + const api = apiCreator({helpers: ['**/helpers/*'], projectDir: path.join(__dirname, 'fixture/ignored-dirs')}); + return api.run() + .then(runStatus => { + t.is(runStatus.stats.declaredTests, 1); + }); +}); test('Node.js-style --require CLI argument', t => { const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.js')).replace(/\\/g, '/'); diff --git a/test/fixture/globs/default-patterns/sub/directory/__tests__/helpers/foo.ts b/test/fixture/globs/default-patterns/sub/directory/__tests__/helpers/foo.ts new file mode 100644 index 000000000..d15abba59 --- /dev/null +++ b/test/fixture/globs/default-patterns/sub/directory/__tests__/helpers/foo.ts @@ -0,0 +1 @@ +// Empty diff --git a/test/fixture/globs/default-patterns/test-foo.ts b/test/fixture/globs/default-patterns/test-foo.ts new file mode 100644 index 000000000..d15abba59 --- /dev/null +++ b/test/fixture/globs/default-patterns/test-foo.ts @@ -0,0 +1 @@ +// Empty diff --git a/test/fixture/ignored-dirs/fixtures/test.js b/test/fixture/ignored-dirs/fixtures/test.js deleted file mode 100644 index 0aaeea531..000000000 --- a/test/fixture/ignored-dirs/fixtures/test.js +++ /dev/null @@ -1,5 +0,0 @@ -import test from '../../../..'; - -test('test', t => { - t.pass(); -}); diff --git a/test/fixture/ignored-dirs/test.js b/test/fixture/ignored-dirs/test.js new file mode 100644 index 000000000..9d6912855 --- /dev/null +++ b/test/fixture/ignored-dirs/test.js @@ -0,0 +1,3 @@ +import test from '../../..'; + +test('pass', t => t.pass()); diff --git a/test/fixture/invalid-globs/helpers/package.json b/test/fixture/invalid-globs/helpers/package.json new file mode 100644 index 000000000..be1370ca5 --- /dev/null +++ b/test/fixture/invalid-globs/helpers/package.json @@ -0,0 +1,5 @@ +{ + "ava": { + "helpers": [] + } +} diff --git a/test/fixture/watcher/ignored-files/ignore.js b/test/fixture/watcher/ignored-files/ignore.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixture/watcher/ignored-files/ignored.js b/test/fixture/watcher/ignored-files/ignored.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixture/watcher/ignored-files/package.json b/test/fixture/watcher/ignored-files/package.json new file mode 100644 index 000000000..7edecc313 --- /dev/null +++ b/test/fixture/watcher/ignored-files/package.json @@ -0,0 +1,6 @@ +{ + "ava": { + "files": ["test.js", "!ignore.js"], + "sources": ["source.js"] + } +} diff --git a/test/fixture/watcher/ignored-files/source.js b/test/fixture/watcher/ignored-files/source.js new file mode 100644 index 000000000..e69de29bb diff --git a/test/fixture/watcher/ignored-files/test.js b/test/fixture/watcher/ignored-files/test.js new file mode 100644 index 000000000..04b164450 --- /dev/null +++ b/test/fixture/watcher/ignored-files/test.js @@ -0,0 +1,3 @@ +import test from '../../../..'; + +test('pass', t => t.pass()); diff --git a/test/globs.js b/test/globs.js index 7bf4569e5..f6cd74189 100644 --- a/test/globs.js +++ b/test/globs.js @@ -17,24 +17,28 @@ function fixture(...args) { } test('ignores relativeness in patterns', t => { - const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, ['js']); + const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, undefined, ['js']); t.deepEqual(testPatterns, ['foo.js', '!bar']); t.end(); }); test('isTest', t => { - const options = globs.normalizeGlobs( - ['**/foo*.js', '**/foo*/**/*.js', '!**/fixtures', '!**/helpers'], - undefined, - ['js'] - ); + const options = { + ...globs.normalizeGlobs( + ['**/foo*.js', '**/foo*/**/*.js', '!**/fixtures', '!**/helpers'], + undefined, + undefined, + ['js'] + ), + cwd: fixture() + }; function isTest(file) { - t.true(globs.isTest(fixture(file), options), `${file} should be a test`); + t.true(globs.classify(fixture(file), options).isTest, `${file} should be a test`); } function notTest(file) { - t.false(globs.isTest(fixture(file), options), `${file} should not be a test`); + t.false(globs.classify(fixture(file), options).isTest, `${file} should not be a test`); } isTest('foo-bar.js'); @@ -54,15 +58,54 @@ test('isTest', t => { t.end(); }); +test('isTest (pattern starts with directory)', t => { + const options = { + ...globs.normalizeGlobs( + ['bar/**/*'], + undefined, + undefined, + ['js'] + ), + cwd: fixture() + }; + + function isTest(file) { + t.true(globs.classify(fixture(file), options).isTest, `${file} should be a test`); + } + + function notTest(file) { + t.false(globs.classify(fixture(file), options).isTest, `${file} should not be a test`); + } + + notTest('foo-bar.js'); + notTest('foo.js'); + notTest('foo/blah.js'); + isTest('bar/foo.js'); + isTest('bar/foo-bar/baz/buz.js'); + isTest('bar/baz/buz.js'); + notTest('bar.js'); + isTest('bar/bar.js'); + notTest('bar/_foo-bar.js'); + notTest('foo/_foo-bar.js'); + notTest('foo-bar.txt'); + notTest('node_modules/foo.js'); + notTest('fixtures/foo.js'); + notTest('helpers/foo.js'); + t.end(); +}); + test('isSource with defaults', t => { - const options = globs.normalizeGlobs(undefined, undefined, ['js']); + const options = { + ...globs.normalizeGlobs(undefined, undefined, undefined, ['js']), + cwd: fixture() + }; function isSource(file) { - t.true(globs.isSource(file, options), `${file} should be a source`); + t.true(globs.classify(fixture(file), options).isSource, `${file} should be a source`); } function notSource(file) { - t.false(globs.isSource(file, options), `${file} should not be a source`); + t.false(globs.classify(fixture(file), options).isSource, `${file} should not be a source`); } isSource('foo-bar.js'); @@ -70,8 +113,8 @@ test('isSource with defaults', t => { isSource('foo/blah.js'); isSource('bar/foo.js'); - isSource('_foo-bar.js'); - isSource('foo/_foo-bar.js'); + notSource('_foo-bar.js'); + notSource('foo/_foo-bar.js'); isSource('fixtures/foo.js'); isSource('helpers/foo.js'); @@ -90,15 +133,129 @@ test('isSource with defaults', t => { }); test('isSource with negation negation patterns', t => { - const options = globs.normalizeGlobs( - ['**/foo*'], - ['!**/bar*'], - ['js'] - ); - - t.false(globs.isSource('node_modules/foo/foo.js', options)); - t.false(globs.isSource('bar.js', options)); - t.false(globs.isSource('foo/bar.js', options)); + const options = { + ...globs.normalizeGlobs( + ['**/foo*'], + undefined, + ['!**/bar*'], + ['js'] + ), + cwd: fixture() + }; + + t.false(globs.classify(fixture('node_modules/foo/foo.js'), options).isSource); + t.false(globs.classify(fixture('bar.js'), options).isSource); + t.false(globs.classify(fixture('foo/bar.js'), options).isSource); + t.end(); +}); + +test('isSource (pattern starts with directory)', t => { + const options = { + ...globs.normalizeGlobs( + ['**/foo*'], + undefined, + ['foo/**/*'], + ['js'] + ), + cwd: fixture() + }; + + t.false(globs.classify(fixture('node_modules/foo/foo.js'), options).isSource); + t.false(globs.classify(fixture('bar.js'), options).isSource); + t.true(globs.classify(fixture('foo/bar.js'), options).isSource); + t.end(); +}); + +test('isHelper (prefixed only)', t => { + const options = { + ...globs.normalizeGlobs(undefined, undefined, undefined, ['js']), + cwd: fixture() + }; + + function isHelper(file) { + t.true(globs.classify(fixture(file), options).isHelper, `${file} should be a helper`); + } + + function notHelper(file) { + t.false(globs.classify(fixture(file), options).isHelper, `${file} should not be a helper`); + } + + notHelper('foo.js'); + notHelper('bar/foo.js'); + + isHelper('_foo.js'); + isHelper('foo/_foo.js'); + notHelper('fixtures/foo.js'); + notHelper('helpers/foo.js'); + isHelper('helpers/_foo.js'); + + notHelper('snapshots/foo.js.snap'); + + notHelper('foo.json'); + notHelper('foo.coffee'); + notHelper('node_modules/_foo.js'); + t.end(); +}); + +test('isHelper (with patterns)', t => { + const options = { + ...globs.normalizeGlobs(undefined, ['**/f*.*'], undefined, ['js']), + cwd: fixture() + }; + + function isHelper(file) { + t.true(globs.classify(fixture(file), options).isHelper, `${file} should be a helper`); + } + + function notHelper(file) { + t.false(globs.classify(fixture(file), options).isHelper, `${file} should not be a helper`); + } + + isHelper('foo.js'); + notHelper('foo/bar.js'); + isHelper('bar/foo.js'); + + isHelper('_foo.js'); + isHelper('foo/_foo.js'); + isHelper('fixtures/foo.js'); + isHelper('helpers/foo.js'); + + notHelper('snapshots/foo.js.snap'); + + notHelper('foo.json'); + notHelper('foo.coffee'); + notHelper('node_modules/foo.js'); + t.end(); +}); + +test('isHelper (pattern stars with directory)', t => { + const options = { + ...globs.normalizeGlobs(undefined, ['foo/**/*'], undefined, ['js']), + cwd: fixture() + }; + + function isHelper(file) { + t.true(globs.classify(fixture(file), options).isHelper, `${file} should be a helper`); + } + + function notHelper(file) { + t.false(globs.classify(fixture(file), options).isHelper, `${file} should not be a helper`); + } + + notHelper('foo.js'); + isHelper('foo/bar.js'); + notHelper('bar/foo.js'); + + isHelper('_foo.js'); + isHelper('foo/_foo.js'); + notHelper('fixtures/foo.js'); + notHelper('helpers/foo.js'); + + notHelper('snapshots/foo.js.snap'); + + notHelper('foo.json'); + notHelper('foo.coffee'); + notHelper('node_modules/foo.js'); t.end(); }); @@ -118,7 +275,7 @@ test('findHelpersAndTests finds tests (just .js)', async t => { const {tests: actual} = await globs.findHelpersAndTests({ cwd: fixtureDir, - ...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, ['js']) + ...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, undefined, ['js']) }); actual.sort(); t.deepEqual(actual, expected); @@ -136,7 +293,7 @@ test('findHelpersAndTests finds tests (.js, .jsx)', async t => { const {tests: actual} = await globs.findHelpersAndTests({ cwd: fixtureDir, - ...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, ['js', 'jsx']) + ...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, undefined, ['js', 'jsx']) }); actual.sort(); t.deepEqual(actual, expected); @@ -146,17 +303,16 @@ test('findHelpersAndTests finds helpers (just .js)', async t => { const fixtureDir = fixture('default-patterns'); process.chdir(fixtureDir); - // TODO: Support pattern to match helpers directories. const expected = [ - // 'sub/directory/__tests__/helpers/foo.js', + 'sub/directory/__tests__/helpers/foo.js', 'sub/directory/__tests__/_foo.js', - // 'test/helpers/test.js', + 'test/helpers/test.js', 'test/_foo-help.js' ].sort().map(file => path.join(fixtureDir, file)); const {helpers: actual} = await globs.findHelpersAndTests({ cwd: fixtureDir, - ...globs.normalizeGlobs(undefined, undefined, ['js']) + ...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js']) }); actual.sort(); t.deepEqual(actual, expected); @@ -166,16 +322,73 @@ test('findHelpersAndTests finds helpers (.js and .jsx)', async t => { const fixtureDir = fixture('custom-extension'); process.chdir(fixtureDir); - // TODO: Support pattern to match helpers directories. const expected = [ - 'test/sub/_helper.jsx' - // 'test/helpers/a.jsx', - // 'test/helpers/b.js' + 'test/sub/_helper.jsx', + 'test/helpers/a.jsx', + 'test/helpers/b.js' ].sort().map(file => path.join(fixtureDir, file)); const {helpers: actual} = await globs.findHelpersAndTests({ cwd: fixtureDir, - ...globs.normalizeGlobs(undefined, undefined, ['js', 'jsx']) + ...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js', 'jsx']) + }); + actual.sort(); + t.deepEqual(actual, expected); +}); + +test('findTests finds tests (just .js)', async t => { + const fixtureDir = fixture('default-patterns'); + process.chdir(fixtureDir); + + const expected = [ + 'sub/directory/__tests__/foo.js', + 'sub/directory/bar.spec.js', + 'sub/directory/bar.test.js', + 'test-foo.js', + 'test.js', + 'test/baz.js', + 'test/deep/deep.js' + ].map(file => path.join(fixtureDir, file)).sort(); + + const {tests: actual} = await globs.findTests({ + cwd: fixtureDir, + ...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, undefined, ['js']) + }); + actual.sort(); + t.deepEqual(actual, expected); +}); + +test('findTests finds tests (.js, .jsx)', async t => { + const fixtureDir = fixture('custom-extension'); + process.chdir(fixtureDir); + + const expected = [ + 'test/do-not-compile.js', + 'test/foo.jsx', + 'test/sub/bar.jsx' + ].sort().map(file => path.join(fixtureDir, file)); + + const {tests: actual} = await globs.findTests({ + cwd: fixtureDir, + ...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, undefined, ['js', 'jsx']) + }); + actual.sort(); + t.deepEqual(actual, expected); +}); + +test('findTests excludes helpers', async t => { + const fixtureDir = fixture('custom-extension'); + process.chdir(fixtureDir); + + const expected = [ + 'test/do-not-compile.js', + 'test/foo.jsx', + 'test/sub/bar.jsx' + ].sort().map(file => path.join(fixtureDir, file)); + + const {tests: actual} = await globs.findTests({ + cwd: fixtureDir, + ...globs.normalizeGlobs(['!**/fixtures/*'], ['test/helpers/**/*'], undefined, ['js', 'jsx']) }); actual.sort(); t.deepEqual(actual, expected); diff --git a/test/helper/report.js b/test/helper/report.js index 792f13b1a..cebc2da5f 100644 --- a/test/helper/report.js +++ b/test/helper/report.js @@ -94,7 +94,7 @@ const run = (type, reporter, match = []) => { pattern = '*.ts'; } - options.globs = normalizeGlobs(undefined, undefined, options.extensions.all); + options.globs = normalizeGlobs(undefined, undefined, undefined, options.extensions.all); const api = createApi(options); api.on('run', plan => reporter.startRun(plan)); diff --git a/test/integration/globs.js b/test/integration/globs.js index 8be848601..d57471d4f 100644 --- a/test/integration/globs.js +++ b/test/integration/globs.js @@ -16,6 +16,19 @@ test('errors if top-level files is an empty array', t => { }); }); +test('errors if top-level helpers is an empty array', t => { + execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/helpers'}, (err, stdout, stderr) => { + t.ok(err); + + let expectedOutput = '\n'; + expectedOutput += figures.cross + ' The \'helpers\' configuration must be an array containing glob patterns.'; + expectedOutput += '\n'; + + t.is(stderr, expectedOutput); + t.end(); + }); +}); + test('errors if top-level sources is an empty array', t => { execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/sources'}, (err, stdout, stderr) => { t.ok(err); diff --git a/test/integration/watcher.js b/test/integration/watcher.js index 69a2adce9..936f3244c 100644 --- a/test/integration/watcher.js +++ b/test/integration/watcher.js @@ -156,6 +156,33 @@ test('watcher does not rerun test files when they write snapshot files', t => { }); }); +test('watcher does not rerun test files when files change that are neither tests, helpers nor sources', t => { + let killed = false; + + const child = execCli(['--verbose', '--watch'], {dirname: 'fixture/watcher/ignored-files', env: {CI: ''}}, err => { + t.ok(killed); + t.ifError(err); + t.end(); + }); + + let buffer = ''; + let passedFirst = false; + child.stdout.on('data', str => { + buffer += str; + if (buffer.includes('1 test passed') && !passedFirst) { + touch.sync(path.join(__dirname, '../fixture/watcher/ignored-files/ignored.js')); + buffer = ''; + passedFirst = true; + setTimeout(() => { + child.kill(); + killed = true; + }, 500); + } else if (passedFirst && !killed) { + t.is(buffer.replace(/\s/g, '').replace(END_MESSAGE.replace(/\s/g, ''), ''), ''); + } + }); +}); + test('watcher reruns test files when snapshot dependencies change', t => { let killed = false; diff --git a/test/watcher.js b/test/watcher.js index 5f59af522..106e36e9c 100644 --- a/test/watcher.js +++ b/test/watcher.js @@ -145,7 +145,7 @@ group('chokidar', (beforeEach, test, group) => { Subject = proxyWatcher(); }); - const start = (specificFiles, sources) => new Subject({reporter, api, files: specificFiles || [], globs: normalizeGlobs(files, sources, ['js'])}); + const start = (specificFiles, sources) => new Subject({reporter, api, files: specificFiles || [], globs: normalizeGlobs(files, undefined, sources, ['js']), resolveTestsFrom: ''}); const emitChokidar = (event, path) => { chokidarEmitter.emit('all', event, path); @@ -1086,7 +1086,7 @@ group('chokidar', (beforeEach, test, group) => { change('cannot-be-mapped.js'); return debounce().then(() => { t.ok(debug.calledThrice); - t.strictDeepEqual(debug.secondCall.args, ['ava:watcher', 'Sources remain that cannot be traced to specific tests: %O', ['cannot-be-mapped.js']]); + t.strictDeepEqual(debug.secondCall.args, ['ava:watcher', 'Helpers & sources remain that cannot be traced to specific tests: %O', ['cannot-be-mapped.js']]); t.strictDeepEqual(debug.thirdCall.args, ['ava:watcher', 'Rerunning all tests']); }); });