Skip to content

Commit 8ca00a2

Browse files
committed
Support helper glob configuration
Fixes #2105.
1 parent 501572c commit 8ca00a2

File tree

13 files changed

+169
-46
lines changed

13 files changed

+169
-46
lines changed

docs/06-configuration.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@ To ignore files, prefix the pattern with an `!` (exclamation mark).
1616
"!test/exclude-files-in-this-directory/*",
1717
"!**/exclude-files-with-this-name.*"
1818
],
19+
"helpers": [
20+
"**/helpers/**/*"
21+
],
1922
"sources": [
2023
"src/**/*",
2124
"!dist/**/*"
@@ -49,6 +52,7 @@ Arguments passed to the CLI will always take precedence over the CLI options con
4952
## Options
5053

5154
- `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
55+
- `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
5256
- `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)
5357
- `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)
5458
- `cache`: cache compiled test and helper files under `node_modules/.cache/ava`. If `false`, files are cached in a temporary directory instead

lib/api.js

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,8 @@ class Api extends Emittery {
110110
const precompiler = await this._setupPrecompiler();
111111
let helpers = [];
112112
if (files.length === 0 || precompiler.enabled) {
113-
const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs});
113+
const helperPatterns = precompiler.enabled ? apiOptions.globs.helperPatterns : [];
114+
const found = await globs.findHelpersAndTests({cwd: this.options.resolveTestsFrom, ...apiOptions.globs, helperPatterns});
114115
if (files.length === 0) {
115116
({tests: files} = found);
116117
}

lib/cli.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -180,7 +180,7 @@ exports.run = async () => { // eslint-disable-line complexity
180180

181181
let globs;
182182
try {
183-
globs = normalizeGlobs(conf.files, conf.sources, extensions.all);
183+
globs = normalizeGlobs(conf.files, conf.helpers, conf.sources, extensions.all);
184184
} catch (error) {
185185
exit(error.message);
186186
}

lib/globs.js

Lines changed: 59 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -28,11 +28,15 @@ const normalizePatterns = patterns => {
2828
});
2929
};
3030

31-
function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
31+
function normalizeGlobs(testPatterns, helperPatterns, sourcePatterns, extensions) {
3232
if (typeof testPatterns !== 'undefined' && (!Array.isArray(testPatterns) || testPatterns.length === 0)) {
3333
throw new Error('The \'files\' configuration must be an array containing glob patterns.');
3434
}
3535

36+
if (typeof helperPatterns !== 'undefined' && (!Array.isArray(helperPatterns) || helperPatterns.length === 0)) {
37+
throw new Error('The \'helpers\' configuration must be an array containing glob patterns.');
38+
}
39+
3640
if (sourcePatterns && (!Array.isArray(sourcePatterns) || sourcePatterns.length === 0)) {
3741
throw new Error('The \'sources\' configuration must be an array containing glob patterns.');
3842
}
@@ -58,6 +62,12 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
5862
testPatterns = defaultTestPatterns;
5963
}
6064

65+
if (helperPatterns) {
66+
helperPatterns = normalizePatterns(helperPatterns);
67+
} else {
68+
helperPatterns = [];
69+
}
70+
6171
const defaultSourcePatterns = [
6272
'**/*.snap',
6373
'ava.config.js',
@@ -75,11 +85,13 @@ function normalizeGlobs(testPatterns, sourcePatterns, extensions) {
7585
sourcePatterns = defaultSourcePatterns;
7686
}
7787

78-
return {extensions, testPatterns, sourcePatterns};
88+
return {extensions, testPatterns, helperPatterns, sourcePatterns};
7989
}
8090

8191
exports.normalizeGlobs = normalizeGlobs;
8292

93+
const hasExtension = (extensions, file) => extensions.includes(path.extname(file).slice(1));
94+
8395
const findFiles = async (cwd, patterns) => {
8496
const files = await globby(patterns, {
8597
absolute: true,
@@ -108,22 +120,35 @@ const findFiles = async (cwd, patterns) => {
108120
return files;
109121
};
110122

111-
async function findHelpersAndTests({cwd, extensions, testPatterns}) {
112-
const helpers = [];
123+
async function findHelpersAndTests({cwd, extensions, testPatterns, helperPatterns}) {
124+
// Search for tests concurrently with finding helpers.
125+
const findingTests = findFiles(cwd, testPatterns);
126+
127+
const uniqueHelpers = new Set();
128+
if (helperPatterns.length > 0) {
129+
for (const file of await findFiles(cwd, helperPatterns)) {
130+
if (!hasExtension(extensions, file)) {
131+
continue;
132+
}
133+
134+
uniqueHelpers.add(file);
135+
}
136+
}
137+
113138
const tests = [];
114-
for (const file of await findFiles(cwd, testPatterns)) {
115-
if (!extensions.includes(path.extname(file).slice(1))) {
139+
for (const file of await findingTests) {
140+
if (!hasExtension(extensions, file)) {
116141
continue;
117142
}
118143

119144
if (path.basename(file).startsWith('_')) {
120-
helpers.push(file);
121-
} else {
145+
uniqueHelpers.add(file);
146+
} else if (!uniqueHelpers.has(file)) { // Helpers cannot be tests.
122147
tests.push(file);
123148
}
124149
}
125150

126-
return {helpers, tests};
151+
return {helpers: [...uniqueHelpers], tests};
127152
}
128153

129154
exports.findHelpersAndTests = findHelpersAndTests;
@@ -175,10 +200,31 @@ const matches = (file, patterns) => {
175200
return micromatch.some(file, patterns, {ignore});
176201
};
177202

178-
function classify(file, {testPatterns, sourcePatterns}) {
179-
const isHelper = path.basename(file).startsWith('_');
180-
const isTest = !isHelper && matches(file, testPatterns);
181-
const isSource = !isHelper && !isTest && matches(file, sourcePatterns);
203+
const NOT_IGNORED = ['**/*'];
204+
205+
function classify(file, {extensions, helperPatterns, testPatterns, sourcePatterns}) {
206+
let isHelper = false;
207+
let isTest = false;
208+
let isSource = false;
209+
210+
if (hasExtension(extensions, file)) {
211+
if (path.basename(file).startsWith('_')) {
212+
isHelper = matches(file, NOT_IGNORED);
213+
} else {
214+
isHelper = matches(file, helperPatterns);
215+
216+
if (!isHelper) {
217+
isTest = matches(file, testPatterns);
218+
219+
if (!isTest) {
220+
isSource = matches(file, sourcePatterns);
221+
}
222+
}
223+
}
224+
} else {
225+
isSource = matches(file, sourcePatterns);
226+
}
227+
182228
return {isHelper, isTest, isSource};
183229
}
184230

test/api.js

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ function apiCreator(options = {}) {
3232
options.babelConfig = babelPipeline.validate(options.babelConfig);
3333
options.concurrency = 2;
3434
options.extensions = options.extensions || {all: ['js'], enhancementsOnly: [], full: ['js']};
35-
options.globs = normalizeGlobs(options.files, options.sources, options.extensions.all);
35+
options.globs = normalizeGlobs(options.files, options.helpers, options.sources, options.extensions.all);
3636
options.projectDir = options.projectDir || ROOT_DIR;
3737
options.resolveTestsFrom = options.resolveTestsFrom || options.projectDir;
3838
const instance = new Api(options);
@@ -579,16 +579,15 @@ test('test file in node_modules is ignored', t => {
579579
});
580580
});
581581

582-
// TODO: Re-enable to test helpers patterns.
583-
// test('test file in helpers is ignored', t => {
584-
// t.plan(1);
585-
//
586-
// const api = apiCreator();
587-
// return api.run([path.join(__dirname, 'fixture/ignored-dirs/helpers/test.js')])
588-
// .then(runStatus => {
589-
// t.is(runStatus.stats.declaredTests, 0);
590-
// });
591-
// });
582+
test('test file in helpers is ignored', t => {
583+
t.plan(1);
584+
585+
const api = apiCreator({helpers: ['**/helpers/*'], projectDir: path.join(__dirname, 'fixture/ignored-dirs')});
586+
return api.run()
587+
.then(runStatus => {
588+
t.is(runStatus.stats.declaredTests, 1);
589+
});
590+
});
592591

593592
test('Node.js-style --require CLI argument', t => {
594593
const requirePath = './' + path.relative('.', path.join(__dirname, 'fixture/install-global.js')).replace(/\\/g, '/');
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
// Empty

test/fixture/ignored-dirs/fixtures/test.js

Lines changed: 0 additions & 5 deletions
This file was deleted.

test/fixture/ignored-dirs/test.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import test from '../../..';
2+
3+
test('pass', t => t.pass());
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
{
2+
"ava": {
3+
"helpers": []
4+
}
5+
}

test/globs.js

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function fixture(...args) {
1717
}
1818

1919
test('ignores relativeness in patterns', t => {
20-
const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, ['js']);
20+
const {testPatterns} = globs.normalizeGlobs(['./foo.js', '!./bar'], undefined, undefined, ['js']);
2121
t.deepEqual(testPatterns, ['foo.js', '!bar']);
2222
t.end();
2323
});
@@ -26,6 +26,7 @@ test('isTest', t => {
2626
const options = globs.normalizeGlobs(
2727
['**/foo*.js', '**/foo*/**/*.js', '!**/fixtures', '!**/helpers'],
2828
undefined,
29+
undefined,
2930
['js']
3031
);
3132

@@ -55,7 +56,7 @@ test('isTest', t => {
5556
});
5657

5758
test('isSource with defaults', t => {
58-
const options = globs.normalizeGlobs(undefined, undefined, ['js']);
59+
const options = globs.normalizeGlobs(undefined, undefined, undefined, ['js']);
5960

6061
function isSource(file) {
6162
t.true(globs.classify(file, options).isSource, `${file} should be a source`);
@@ -92,6 +93,7 @@ test('isSource with defaults', t => {
9293
test('isSource with negation negation patterns', t => {
9394
const options = globs.normalizeGlobs(
9495
['**/foo*'],
96+
undefined,
9597
['!**/bar*'],
9698
['js']
9799
);
@@ -102,6 +104,62 @@ test('isSource with negation negation patterns', t => {
102104
t.end();
103105
});
104106

107+
test('isHelper (prefixed only)', t => {
108+
const options = globs.normalizeGlobs(undefined, undefined, undefined, ['js']);
109+
110+
function isHelper(file) {
111+
t.true(globs.classify(file, options).isHelper, `${file} should be a helper`);
112+
}
113+
114+
function notHelper(file) {
115+
t.false(globs.classify(file, options).isHelper, `${file} should not be a helper`);
116+
}
117+
118+
notHelper('foo.js');
119+
notHelper('bar/foo.js');
120+
121+
isHelper('_foo.js');
122+
isHelper('foo/_foo.js');
123+
notHelper('fixtures/foo.js');
124+
notHelper('helpers/foo.js');
125+
isHelper('helpers/_foo.js');
126+
127+
notHelper('snapshots/foo.js.snap');
128+
129+
notHelper('foo.json');
130+
notHelper('foo.coffee');
131+
notHelper('node_modules/_foo.js');
132+
t.end();
133+
});
134+
135+
test('isHelper (with patterns)', t => {
136+
const options = globs.normalizeGlobs(undefined, ['**/f*.*'], undefined, ['js']);
137+
138+
function isHelper(file) {
139+
t.true(globs.classify(file, options).isHelper, `${file} should be a helper`);
140+
}
141+
142+
function notHelper(file) {
143+
t.false(globs.classify(file, options).isHelper, `${file} should not be a helper`);
144+
}
145+
146+
isHelper('foo.js');
147+
notHelper('foo/bar.js');
148+
isHelper('bar/foo.js');
149+
150+
isHelper('_foo.js');
151+
isHelper('foo/_foo.js');
152+
isHelper('fixtures/foo.js');
153+
isHelper('helpers/foo.js');
154+
155+
notHelper('snapshots/foo.js.snap');
156+
157+
notHelper('foo.json');
158+
notHelper('foo.coffee');
159+
notHelper('node_modules/foo.js');
160+
t.end();
161+
});
162+
105163
test('findHelpersAndTests finds tests (just .js)', async t => {
106164
const fixtureDir = fixture('default-patterns');
107165
process.chdir(fixtureDir);
@@ -118,7 +176,7 @@ test('findHelpersAndTests finds tests (just .js)', async t => {
118176

119177
const {tests: actual} = await globs.findHelpersAndTests({
120178
cwd: fixtureDir,
121-
...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, ['js'])
179+
...globs.normalizeGlobs(['!**/fixtures/*.*', '!**/helpers/*.*'], undefined, undefined, ['js'])
122180
});
123181
actual.sort();
124182
t.deepEqual(actual, expected);
@@ -136,7 +194,7 @@ test('findHelpersAndTests finds tests (.js, .jsx)', async t => {
136194

137195
const {tests: actual} = await globs.findHelpersAndTests({
138196
cwd: fixtureDir,
139-
...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, ['js', 'jsx'])
197+
...globs.normalizeGlobs(['!**/fixtures/*', '!**/helpers/*'], undefined, undefined, ['js', 'jsx'])
140198
});
141199
actual.sort();
142200
t.deepEqual(actual, expected);
@@ -146,17 +204,16 @@ test('findHelpersAndTests finds helpers (just .js)', async t => {
146204
const fixtureDir = fixture('default-patterns');
147205
process.chdir(fixtureDir);
148206

149-
// TODO: Support pattern to match helpers directories.
150207
const expected = [
151-
// 'sub/directory/__tests__/helpers/foo.js',
208+
'sub/directory/__tests__/helpers/foo.js',
152209
'sub/directory/__tests__/_foo.js',
153-
// 'test/helpers/test.js',
210+
'test/helpers/test.js',
154211
'test/_foo-help.js'
155212
].sort().map(file => path.join(fixtureDir, file));
156213

157214
const {helpers: actual} = await globs.findHelpersAndTests({
158215
cwd: fixtureDir,
159-
...globs.normalizeGlobs(undefined, undefined, ['js'])
216+
...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js'])
160217
});
161218
actual.sort();
162219
t.deepEqual(actual, expected);
@@ -166,16 +223,15 @@ test('findHelpersAndTests finds helpers (.js and .jsx)', async t => {
166223
const fixtureDir = fixture('custom-extension');
167224
process.chdir(fixtureDir);
168225

169-
// TODO: Support pattern to match helpers directories.
170226
const expected = [
171-
'test/sub/_helper.jsx'
172-
// 'test/helpers/a.jsx',
173-
// 'test/helpers/b.js'
227+
'test/sub/_helper.jsx',
228+
'test/helpers/a.jsx',
229+
'test/helpers/b.js'
174230
].sort().map(file => path.join(fixtureDir, file));
175231

176232
const {helpers: actual} = await globs.findHelpersAndTests({
177233
cwd: fixtureDir,
178-
...globs.normalizeGlobs(undefined, undefined, ['js', 'jsx'])
234+
...globs.normalizeGlobs(undefined, ['**/helpers/*'], undefined, ['js', 'jsx'])
179235
});
180236
actual.sort();
181237
t.deepEqual(actual, expected);

test/helper/report.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ const run = (type, reporter, match = []) => {
9494
pattern = '*.ts';
9595
}
9696

97-
options.globs = normalizeGlobs(undefined, undefined, options.extensions.all);
97+
options.globs = normalizeGlobs(undefined, undefined, undefined, options.extensions.all);
9898

9999
const api = createApi(options);
100100
api.on('run', plan => reporter.startRun(plan));

test/integration/globs.js

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ test('errors if top-level files is an empty array', t => {
1616
});
1717
});
1818

19+
test('errors if top-level helpers is an empty array', t => {
20+
execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/helpers'}, (err, stdout, stderr) => {
21+
t.ok(err);
22+
23+
let expectedOutput = '\n';
24+
expectedOutput += figures.cross + ' The \'helpers\' configuration must be an array containing glob patterns.';
25+
expectedOutput += '\n';
26+
27+
t.is(stderr, expectedOutput);
28+
t.end();
29+
});
30+
});
31+
1932
test('errors if top-level sources is an empty array', t => {
2033
execCli(['es2015.js'], {dirname: 'fixture/invalid-globs/sources'}, (err, stdout, stderr) => {
2134
t.ok(err);

0 commit comments

Comments
 (0)