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 mochajs#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 mochajs#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 mochajs#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 mochajs#2176.
  • Loading branch information
Thomas Scholtes committed Aug 6, 2019
1 parent ad4860e commit abf266f
Show file tree
Hide file tree
Showing 14 changed files with 348 additions and 334 deletions.
20 changes: 18 additions & 2 deletions docs/index.md
Expand Up @@ -1093,10 +1093,26 @@ Can be specified multiple times.
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`.

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

> _New in v6.3.0_
List of comma separated paths or globs to watch when `--watch` is set. If a file matching the given If the argument is a glob a change to any file matching the glob will rerun the tests.

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

By default all files in the current directory having on of the extensions provided by `--extensions` and not contained in the `node_modules` or `.git` folders are watched.

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

> _New in v6.3.0_
List of comma separated 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.

### `--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
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',
watchFiles: ['lib/**/*.js', 'test/**/*.js'],
watchIgnore: ['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",
"watchFiles": ["lib/**/*.js", "test/**/*.js"],
"watchIgnore": ["lib/vendor"]
}
4 changes: 3 additions & 1 deletion example/config/.mocharc.jsonc
Expand Up @@ -10,5 +10,7 @@
"reporter": /* 📋 */ "spec",
"slow": 75,
"timeout": 2000,
"ui": "bdd"
"ui": "bdd",
"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
watchFiles:
- 'lib/**/*.js'
- 'test/**/*.js'
watchIgnore:
- 'lib/vendor'
6 changes: 4 additions & 2 deletions lib/cli/run-helpers.js
Expand Up @@ -124,7 +124,9 @@ exports.runMocha = (mocha, options) => {
file = [],
recursive = false,
sort = false,
spec = []
spec = [],
watchFiles,
watchIgnore
} = options;

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

if (watch) {
watchRun(mocha, {ui}, fileCollectParams);
watchRun(mocha, {ui, watchFiles, watchIgnore}, fileCollectParams);
} else {
exports.singleRun(mocha, {exit}, fileCollectParams);
}
Expand Down
14 changes: 14 additions & 0 deletions lib/cli/run.js
Expand Up @@ -241,6 +241,20 @@ exports.builder = yargs =>
watch: {
description: 'Watch files in the current working directory for changes',
group: GROUPS.FILES
},
'watch-files': {
description: 'Comma separated list of paths or globs to watch',
group: GROUPS.FILES,
requiresArg: true,
coerce: list
},
'watch-ignore': {
description:
'Comma separated list of paths or globs exclude from watching',
group: GROUPS.FILES,
requiresArg: true,
coerce: list,
default: ['node_modules', '.git']
}
})
.positional('spec', {
Expand Down
111 changes: 82 additions & 29 deletions lib/cli/watch-run.js
@@ -1,8 +1,7 @@
'use strict';

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

/**
Expand All @@ -17,14 +16,42 @@ const collectFiles = require('./collect-files');
* @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, {ui, watchFiles, watchIgnore}, fileCollectParams) => {
if (!watchFiles) {
watchFiles = fileCollectParams.extension.map(ext => `**/*.${ext}`);
}

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

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

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,42 @@ 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
};
};

/**
* 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();
mocha.ui(mocha.options.ui);
};

/**
Expand Down
21 changes: 0 additions & 21 deletions lib/utils.js
Expand Up @@ -53,27 +53,6 @@ exports.isString = function(obj) {
return typeof obj === 'string';
};

/**
* Watch the given `files` for changes
* and invoke `fn(file)` on modification.
*
* @private
* @param {Array} files
* @param {Function} fn
*/
exports.watch = function(files, fn) {
var options = {interval: 100};
var debug = require('debug')('mocha:watch');
files.forEach(function(file) {
debug('file %s', file);
fs.watchFile(file, options, function(curr, prev) {
if (prev.mtime < curr.mtime) {
fn(file);
}
});
});
};

/**
* Predicate to screen `pathname` for further consideration.
*
Expand Down

0 comments on commit abf266f

Please sign in to comment.