Skip to content

Commit fe1abd1

Browse files
author
Thomas Scholtes
committed
Improved watching with chokidar
This change improves the file watching behavior and fixes #3912. * We introduce the `--watch-files` command line option. This option allows control over which files are and is separate and more powerful than `--extension`. Fixes #2702. * We introduce the `--watch-ignore` command line option that allows control over which files are not watched. Before this was hardcoded to `node_modules` and `.git`. See #2554. * The `chokidar` package now handles file watching. (We’re using version `2.1.6` instead of `3.x` because the latter dropped support for Node v6.) * New test files are picked up by the file watcher and run. Fixes #2176.
1 parent ea26c3d commit fe1abd1

File tree

16 files changed

+619
-268
lines changed

16 files changed

+619
-268
lines changed

docs/index.md

Lines changed: 27 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1087,16 +1087,36 @@ Files specified using `--file` _are not affected_ by this option.
10871087

10881088
Can be specified multiple times.
10891089

1090-
### `--extension <ext>, --watch-extensions <ext>`
1091-
1092-
> _Updated in v6.0.0. Previously `--watch-extensions`, but now expanded to affect general test file loading behavior. `--watch-extensions` is now an alias_
1090+
### `--extension <ext>`
10931091

10941092
Files having this extension will be considered test files. Defaults to `js`.
10951093

1096-
Affects `--watch` behavior.
1097-
10981094
Specifying `--extension` will _remove_ `.js` as a test file extension; use `--extension js` to re-add it. For example, to load `.mjs` and `.js` test files, you must supply `--extension mjs --extension js`.
10991095

1096+
The option can be given multiple times. The option accepts a comma-delimited list: `--extension a,b` is equivalent to `--extension a --extension b`
1097+
1098+
### `--watch-files <file|directory|glob>`
1099+
1100+
> _New in v7.0.0_
1101+
1102+
List of paths or globs to watch when `--watch` is set. If a file matching the given glob changes or is added or removed mocha will rerun all tests.
1103+
1104+
If the path is a directory all files and subdirectories will be watched.
1105+
1106+
By default all files in the current directory having one of the extensions provided by `--extension` and not contained in the `node_modules` or `.git` folders are watched.
1107+
1108+
The option can be given multiple times. The option accepts a comma-delimited list: `--watch-files a,b` is equivalent to `--watch-files a --watch-files b`
1109+
1110+
### `--watch-ignore <file|directory|glob>`
1111+
1112+
> _New in v7.0.0_
1113+
1114+
List of paths or globs to exclude from watching. Defaults to `node_modules` and `.git`.
1115+
1116+
To exclude all files in a directory it is preferable to use `foo/bar` instead of `foo/bar/**/*`. The latter will still watch the directory `foo/bar` but will ignore all changes to the content of that directory.
1117+
1118+
The option can be given multiple times. The option accepts a comma-delimited list: `--watch-ignore a,b` is equivalent to `--watch-ignore a --watch-ignore b`
1119+
11001120
### `--file <file|directory|glob>`
11011121

11021122
Explicitly _include_ a test file to be loaded before other test files files. Multiple uses of `--file` are allowed, and will be loaded in order given.
@@ -1132,9 +1152,9 @@ Sort test files (by absolute path) using [Array.prototype.sort][mdn-array-sort].
11321152

11331153
### `--watch, -w`
11341154

1135-
Executes tests on changes to JavaScript in the current working directory (and once initially).
1155+
Rerun tests on file changes.
11361156

1137-
By default, only files with extension `.js` are watched. Use `--extension` to change this behavior.
1157+
The `--watch-files` and `--watch-ignore` options can be used to control which files are watched for changes.
11381158

11391159
### `--fgrep <string>, -f <string>`
11401160

example/config/.mocharc.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,5 +12,7 @@ module.exports = {
1212
reporter: 'spec',
1313
slow: 75,
1414
timeout: 2000,
15-
ui: 'bdd'
15+
ui: 'bdd',
16+
'watch-files': ['lib/**/*.js', 'test/**/*.js'],
17+
'watch-ignore': ['lib/vendor']
1618
};

example/config/.mocharc.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,7 @@
1010
"reporter": "spec",
1111
"slow": 75,
1212
"timeout": 2000,
13-
"ui": "bdd"
13+
"ui": "bdd",
14+
"watch-files": ["lib/**/*.js", "test/**/*.js"],
15+
"watch-ignore": ["lib/vendor"]
1416
}

example/config/.mocharc.jsonc

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,5 +10,8 @@
1010
"reporter": /* 📋 */ "spec",
1111
"slow": 75,
1212
"timeout": 2000,
13-
"ui": "bdd"
13+
"ui": "bdd",
14+
// Camel-casing options are also accepted
15+
"watchFiles": ["lib/**/*.js", "test/**/*.js"],
16+
"watchIgnore": ["lib/vendor"]
1417
}

example/config/.mocharc.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,8 @@ trace-warnings: true # node flags ok
4444
ui: bdd
4545
v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-"
4646
watch: false
47+
watch-files:
48+
- 'lib/**/*.js'
49+
- 'test/**/*.js'
50+
watch-ignore:
51+
- 'lib/vendor'

lib/cli/run-helpers.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -118,13 +118,14 @@ exports.runMocha = (mocha, options) => {
118118
const {
119119
watch = false,
120120
extension = [],
121-
ui = 'bdd',
122121
exit = false,
123122
ignore = [],
124123
file = [],
125124
recursive = false,
126125
sort = false,
127-
spec = []
126+
spec = [],
127+
watchFiles,
128+
watchIgnore
128129
} = options;
129130

130131
const fileCollectParams = {
@@ -137,7 +138,7 @@ exports.runMocha = (mocha, options) => {
137138
};
138139

139140
if (watch) {
140-
watchRun(mocha, {ui}, fileCollectParams);
141+
watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
141142
} else {
142143
exports.singleRun(mocha, {exit}, fileCollectParams);
143144
}

lib/cli/run-option-metadata.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,11 @@ exports.types = {
1818
'file',
1919
'global',
2020
'ignore',
21-
'require',
2221
'reporter-option',
23-
'spec'
22+
'require',
23+
'spec',
24+
'watch-files',
25+
'watch-ignore'
2426
],
2527
boolean: [
2628
'allow-uncaught',
@@ -68,7 +70,6 @@ exports.aliases = {
6870
'async-only': ['A'],
6971
bail: ['b'],
7072
color: ['c', 'colors'],
71-
extension: ['watch-extensions'],
7273
fgrep: ['f'],
7374
global: ['globals'],
7475
grep: ['g'],

lib/cli/run.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,7 @@ exports.builder = yargs =>
8888
extension: {
8989
default: defaults.extension,
9090
defaultDescription: 'js',
91-
description: 'File extension(s) to load and/or watch',
91+
description: 'File extension(s) to load',
9292
group: GROUPS.FILES,
9393
requiresArg: true,
9494
coerce: list
@@ -241,6 +241,19 @@ exports.builder = yargs =>
241241
watch: {
242242
description: 'Watch files in the current working directory for changes',
243243
group: GROUPS.FILES
244+
},
245+
'watch-files': {
246+
description: 'List of paths or globs to watch',
247+
group: GROUPS.FILES,
248+
requiresArg: true,
249+
coerce: list
250+
},
251+
'watch-ignore': {
252+
description: 'List of paths or globs to exclude from watching',
253+
group: GROUPS.FILES,
254+
requiresArg: true,
255+
coerce: list,
256+
default: ['node_modules', '.git']
244257
}
245258
})
246259
.positional('spec', {

lib/cli/watch-run.js

Lines changed: 102 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
'use strict';
22

3-
const utils = require('../utils');
3+
const path = require('path');
4+
const chokidar = require('chokidar');
45
const Context = require('../context');
5-
const Mocha = require('../mocha');
66
const collectFiles = require('./collect-files');
77

88
/**
@@ -16,15 +16,42 @@ const collectFiles = require('./collect-files');
1616
* Run Mocha in "watch" mode
1717
* @param {Mocha} mocha - Mocha instance
1818
* @param {Object} opts - Options
19-
* @param {string} opts.ui - User interface
19+
* @param {string[]} [opts.watchFiles] - List of paths and patterns to
20+
* watch. If not provided all files with an extension included in
21+
* `fileColletionParams.extension` are watched. See first argument of
22+
* `chokidar.watch`.
23+
* @param {string[]} opts.watchIgnore - List of paths and patterns to
24+
* exclude from watching. See `ignored` option of `chokidar`.
2025
* @param {Object} fileCollectParams - Parameters that control test
2126
* file collection. See `lib/cli/collect-files.js`.
22-
* @param {string[]} fileCollectParams.extension - List of extensions to watch
27+
* @param {string[]} fileCollectParams.extension - List of extensions
28+
* to watch if `opts.watchFiles` is not given.
2329
* @private
2430
*/
25-
module.exports = (mocha, {ui}, fileCollectParams) => {
26-
let runner;
27-
const files = collectFiles(fileCollectParams);
31+
module.exports = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => {
32+
if (!watchFiles) {
33+
watchFiles = fileCollectParams.extension.map(ext => `**/*.${ext}`);
34+
}
35+
36+
const watcher = chokidar.watch(watchFiles, {
37+
ignored: watchIgnore,
38+
ignoreInitial: true
39+
});
40+
41+
const rerunner = createRerunner(mocha, () => {
42+
getWatchedFiles(watcher).forEach(file => {
43+
delete require.cache[file];
44+
});
45+
mocha.files = collectFiles(fileCollectParams);
46+
});
47+
48+
watcher.on('ready', () => {
49+
rerunner.run();
50+
});
51+
52+
watcher.on('all', () => {
53+
rerunner.scheduleRun();
54+
});
2855

2956
console.log();
3057
hideCursor();
@@ -35,17 +62,30 @@ module.exports = (mocha, {ui}, fileCollectParams) => {
3562
// killed by SIGINT which has portable number 2.
3663
process.exit(128 + 2);
3764
});
65+
};
3866

39-
const watchFiles = utils.files(process.cwd(), fileCollectParams.extension);
40-
let runAgain = false;
67+
/**
68+
* Create an object that allows you to rerun tests on the mocha
69+
* instance. `beforeRun` is called everytime before `mocha.run()` is
70+
* called.
71+
*
72+
* @param {Mocha} mocha - Mocha instance
73+
* @param {function} beforeRun - Called just before `mocha.run()`
74+
*/
75+
const createRerunner = (mocha, beforeRun) => {
76+
// Set to a `Runner` when mocha is running. Set to `null` when mocha is not
77+
// running.
78+
let runner = null;
79+
80+
let rerunScheduled = false;
4181

42-
const loadAndRun = () => {
82+
const run = () => {
4383
try {
44-
mocha.files = files;
45-
runAgain = false;
84+
beforeRun();
85+
resetMocha(mocha);
4686
runner = mocha.run(() => {
4787
runner = null;
48-
if (runAgain) {
88+
if (rerunScheduled) {
4989
rerun();
5090
}
5191
});
@@ -54,29 +94,62 @@ module.exports = (mocha, {ui}, fileCollectParams) => {
5494
}
5595
};
5696

57-
const purge = () => {
58-
watchFiles.forEach(Mocha.unloadFile);
59-
};
60-
61-
loadAndRun();
62-
63-
const rerun = () => {
64-
purge();
65-
eraseLine();
66-
mocha.suite = mocha.suite.clone();
67-
mocha.suite.ctx = new Context();
68-
mocha.ui(ui);
69-
loadAndRun();
70-
};
97+
const scheduleRun = () => {
98+
if (rerunScheduled) {
99+
return;
100+
}
71101

72-
utils.watch(watchFiles, () => {
73-
runAgain = true;
102+
rerunScheduled = true;
74103
if (runner) {
75104
runner.abort();
76105
} else {
77106
rerun();
78107
}
108+
};
109+
110+
const rerun = () => {
111+
rerunScheduled = false;
112+
eraseLine();
113+
run();
114+
};
115+
116+
return {
117+
scheduleRun,
118+
run
119+
};
120+
};
121+
122+
/**
123+
* Return the list of absolute paths watched by a chokidar watcher.
124+
*
125+
* @param watcher - Instance of a chokidar watcher
126+
* @return {string[]} - List of absolute paths
127+
*/
128+
const getWatchedFiles = watcher => {
129+
const watchedDirs = watcher.getWatched();
130+
let watchedFiles = [];
131+
Object.keys(watchedDirs).forEach(dir => {
132+
watchedFiles = watchedFiles.concat(
133+
watchedDirs[dir].map(file => path.join(dir, file))
134+
);
79135
});
136+
return watchedFiles;
137+
};
138+
139+
/**
140+
* Reset the internal state of the mocha instance so that tests can be rerun.
141+
*
142+
* @param {Mocha} mocha - Mocha instance
143+
* @private
144+
*/
145+
const resetMocha = mocha => {
146+
mocha.unloadFiles();
147+
mocha.suite = mocha.suite.clone();
148+
mocha.suite.ctx = new Context();
149+
// Registers a callback on `mocha.suite` that wires new context to the DSL
150+
// (e.g. `describe`) that is exposed as globals when the test files are
151+
// reloaded.
152+
mocha.ui(mocha.options.ui);
80153
};
81154

82155
/**

0 commit comments

Comments
 (0)