Skip to content

Commit

Permalink
Improved watching with chokidar
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
Thomas Scholtes committed Aug 25, 2019
1 parent ea26c3d commit d683e7f
Show file tree
Hide file tree
Showing 16 changed files with 599 additions and 268 deletions.
34 changes: 27 additions & 7 deletions docs/index.md
Expand Up @@ -1087,16 +1087,36 @@ Files specified using `--file` _are not affected_ by this option.

Can be specified multiple times.

### `--extension <ext>, --watch-extensions <ext>`

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

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

Affects `--watch` behavior.

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`.

The option can be given multiple times. The option accepts a comma-delimited list: `--extension a,b` is equivalent to `--extension a --extension b`

### `--watch-files <file|directory|glob>`

> _New in v7.0.0_
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.

If the path is a directory all files and subdirectories will be watched.

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.

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`

### `--watch-ignore <file|directory|glob>`

> _New in v7.0.0_
List of paths or globs to exclude from watching. Defaults to `node_modules` and `.git`.

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.

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`

### `--file <file|directory|glob>`

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.
Expand Down Expand Up @@ -1132,9 +1152,9 @@ Sort test files (by absolute path) using [Array.prototype.sort][mdn-array-sort].

### `--watch, -w`

Executes tests on changes to JavaScript in the current working directory (and once initially).
Rerun tests on file changes.

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

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

Expand Down
4 changes: 3 additions & 1 deletion example/config/.mocharc.js
Expand Up @@ -12,5 +12,7 @@ module.exports = {
reporter: 'spec',
slow: 75,
timeout: 2000,
ui: 'bdd'
ui: 'bdd',
'watch-files': ['lib/**/*.js', 'test/**/*.js'],
'watch-ignore': ['lib/vendor']
};
4 changes: 3 additions & 1 deletion example/config/.mocharc.json
Expand Up @@ -10,5 +10,7 @@
"reporter": "spec",
"slow": 75,
"timeout": 2000,
"ui": "bdd"
"ui": "bdd",
"watch-files": ["lib/**/*.js", "test/**/*.js"],
"watch-ignore": ["lib/vendor"]
}
5 changes: 4 additions & 1 deletion example/config/.mocharc.jsonc
Expand Up @@ -10,5 +10,8 @@
"reporter": /* 📋 */ "spec",
"slow": 75,
"timeout": 2000,
"ui": "bdd"
"ui": "bdd",
// Camel-casing options are also accepted
"watchFiles": ["lib/**/*.js", "test/**/*.js"],
"watchIgnore": ["lib/vendor"]
}
5 changes: 5 additions & 0 deletions example/config/.mocharc.yml
Expand Up @@ -44,3 +44,8 @@ trace-warnings: true # node flags ok
ui: bdd
v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-"
watch: false
watch-files:
- 'lib/**/*.js'
- 'test/**/*.js'
watch-ignore:
- 'lib/vendor'
7 changes: 4 additions & 3 deletions lib/cli/run-helpers.js
Expand Up @@ -118,13 +118,14 @@ exports.runMocha = (mocha, options) => {
const {
watch = false,
extension = [],
ui = 'bdd',
exit = false,
ignore = [],
file = [],
recursive = false,
sort = false,
spec = []
spec = [],
watchFiles,
watchIgnore
} = options;

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

if (watch) {
watchRun(mocha, {ui}, fileCollectParams);
watchRun(mocha, {watchFiles, watchIgnore}, fileCollectParams);
} else {
exports.singleRun(mocha, {exit}, fileCollectParams);
}
Expand Down
7 changes: 4 additions & 3 deletions lib/cli/run-option-metadata.js
Expand Up @@ -18,9 +18,11 @@ exports.types = {
'file',
'global',
'ignore',
'require',
'reporter-option',
'spec'
'require',
'spec',
'watch-files',
'watch-ignore'
],
boolean: [
'allow-uncaught',
Expand Down Expand Up @@ -68,7 +70,6 @@ exports.aliases = {
'async-only': ['A'],
bail: ['b'],
color: ['c', 'colors'],
extension: ['watch-extensions'],
fgrep: ['f'],
global: ['globals'],
grep: ['g'],
Expand Down
15 changes: 14 additions & 1 deletion lib/cli/run.js
Expand Up @@ -88,7 +88,7 @@ exports.builder = yargs =>
extension: {
default: defaults.extension,
defaultDescription: 'js',
description: 'File extension(s) to load and/or watch',
description: 'File extension(s) to load',
group: GROUPS.FILES,
requiresArg: true,
coerce: list
Expand Down Expand Up @@ -241,6 +241,19 @@ exports.builder = yargs =>
watch: {
description: 'Watch files in the current working directory for changes',
group: GROUPS.FILES
},
'watch-files': {
description: 'List of paths or globs to watch',
group: GROUPS.FILES,
requiresArg: true,
coerce: list
},
'watch-ignore': {
description: 'List of paths or globs to exclude from watching',
group: GROUPS.FILES,
requiresArg: true,
coerce: list,
default: ['node_modules', '.git']
}
})
.positional('spec', {
Expand Down
131 changes: 102 additions & 29 deletions lib/cli/watch-run.js
@@ -1,8 +1,8 @@
'use strict';

const utils = require('../utils');
const path = require('path');
const chokidar = require('chokidar');
const Context = require('../context');
const Mocha = require('../mocha');
const collectFiles = require('./collect-files');

/**
Expand All @@ -16,15 +16,42 @@ const collectFiles = require('./collect-files');
* Run Mocha in "watch" mode
* @param {Mocha} mocha - Mocha instance
* @param {Object} opts - Options
* @param {string} opts.ui - User interface
* @param {string[]} [opts.watchFiles] - List of paths and patterns to
* watch. If not provided all files with an extension included in
* `fileColletionParams.extension` are watched. See first argument of
* `chokidar.watch`.
* @param {string[]} opts.watchIgnore - List of paths and patterns to
* exclude from watching. See `ignored` option of `chokidar`.
* @param {Object} fileCollectParams - Parameters that control test
* file collection. See `lib/cli/collect-files.js`.
* @param {string[]} fileCollectParams.extension - List of extensions to watch
* @param {string[]} fileCollectParams.extension - List of extensions
* to watch if `opts.watchFiles` is not given.
* @private
*/
module.exports = (mocha, {ui}, fileCollectParams) => {
let runner;
const files = collectFiles(fileCollectParams);
module.exports = (mocha, {watchFiles, watchIgnore}, fileCollectParams) => {
if (!watchFiles) {
watchFiles = fileCollectParams.extension.map(ext => `**/*.${ext}`);
}

const watcher = chokidar.watch(watchFiles, {
ignored: watchIgnore,
ignoreInitial: true
});

const rerunner = createRerunner(mocha, () => {
getWatchedFiles(watcher).forEach(file => {
delete require.cache[file];
});
mocha.files = collectFiles(fileCollectParams);
});

watcher.on('ready', () => {
rerunner.run();
});

watcher.on('all', () => {
rerunner.scheduleRun();
});

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

const watchFiles = utils.files(process.cwd(), fileCollectParams.extension);
let runAgain = false;
/**
* Create an object that allows you to rerun tests on the mocha
* instance. `beforeRun` is called everytime before `mocha.run()` is
* called.
*
* @param {Mocha} mocha - Mocha instance
* @param {function} beforeRun - Called just before `mocha.run()`
*/
const createRerunner = (mocha, beforeRun) => {
// Set to a `Runner` when mocha is running. Set to `null` when mocha is not
// running.
let runner = null;

let rerunScheduled = false;

const loadAndRun = () => {
const run = () => {
try {
mocha.files = files;
runAgain = false;
beforeRun();
resetMocha(mocha);
runner = mocha.run(() => {
runner = null;
if (runAgain) {
if (rerunScheduled) {
rerun();
}
});
Expand All @@ -54,29 +94,62 @@ module.exports = (mocha, {ui}, fileCollectParams) => {
}
};

const purge = () => {
watchFiles.forEach(Mocha.unloadFile);
};

loadAndRun();

const rerun = () => {
purge();
eraseLine();
mocha.suite = mocha.suite.clone();
mocha.suite.ctx = new Context();
mocha.ui(ui);
loadAndRun();
};
const scheduleRun = () => {
if (rerunScheduled) {
return;
}

utils.watch(watchFiles, () => {
runAgain = true;
rerunScheduled = true;
if (runner) {
runner.abort();
} else {
rerun();
}
};

const rerun = () => {
rerunScheduled = false;
eraseLine();
run();
};

return {
scheduleRun,
run
};
};

/**
* Return the list of absolute paths watched by a chokidar watcher.
*
* @param watcher - Instance of a chokidar watcher
* @return {string[]} - List of absolute paths
*/
const getWatchedFiles = watcher => {
const watchedDirs = watcher.getWatched();
let watchedFiles = [];
Object.keys(watchedDirs).forEach(dir => {
watchedFiles = watchedFiles.concat(
watchedDirs[dir].map(file => path.join(dir, file))
);
});
return watchedFiles;
};

/**
* Reset the internal state of the mocha instance so that tests can be rerun.
*
* @param {Mocha} mocha - Mocha instance
* @private
*/
const resetMocha = mocha => {
mocha.unloadFiles();
mocha.suite = mocha.suite.clone();
mocha.suite.ctx = new Context();
// Registers a callback on `mocha.suite` that wires new context to the DSL
// (e.g. `describe`) that is exposed as globals when the test files are
// reloaded.
mocha.ui(mocha.options.ui);
};

/**
Expand Down

0 comments on commit d683e7f

Please sign in to comment.