'use strict'; const path = require('path'); const chokidar = require('chokidar'); const Context = require('../context'); const collectFiles = require('./collect-files'); /** * Exports the `watchRun` function that runs mocha in "watch" mode. * @see module:lib/cli/run-helpers * @module * @private */ /** * Run Mocha in "watch" mode * @param {Mocha} mocha - Mocha instance * @param {Object} opts - Options * @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 if `opts.watchFiles` is not given. * @private */ 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(); process.on('exit', () => { showCursor(); }); process.on('SIGINT', () => { showCursor(); console.log('\n'); process.exit(128 + 2); }); // Keyboard shortcut for restarting when "rs\n" is typed (ala Nodemon) process.stdin.resume(); process.stdin.setEncoding('utf8'); process.stdin.on('data', data => { const str = data .toString() .trim() .toLowerCase(); if (str === 'rs') rerunner.scheduleRun(); }); }; /** * 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 run = () => { try { beforeRun(); resetMocha(mocha); runner = mocha.run(() => { runner = null; if (rerunScheduled) { rerun(); } }); } catch (e) { console.log(e.stack); } }; const scheduleRun = () => { if (rerunScheduled) { return; } 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); }; /** * Hide the cursor. * @ignore * @private */ const hideCursor = () => { process.stdout.write('\u001b[?25l'); }; /** * Show the cursor. * @ignore * @private */ const showCursor = () => { process.stdout.write('\u001b[?25h'); }; /** * Erases the line on stdout * @private */ const eraseLine = () => { process.stdout.write('\u001b[2K'); };