diff --git a/.eslintrc.yml b/.eslintrc.yml index 3fe7399ed0..3257d70654 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -25,6 +25,7 @@ overrides: - bin/* - lib/cli/**/*.js - test/node-unit/**/*.js + - test/integration/**/*.js - lib/growl.js parserOptions: ecmaVersion: 6 diff --git a/package-lock.json b/package-lock.json index 2aeadf2b21..359f0ee697 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,6 +52,26 @@ "ms": "^2.1.1" } }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } + }, "minimist": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", @@ -6051,9 +6071,9 @@ "dev": true }, "fs-extra": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", - "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.0.1.tgz", + "integrity": "sha512-W+XLrggcDzlle47X/XnS7FXrXu9sDo+Ze9zpndeBxdgv88FHLm1HtmkhEwavruS6koanBjp098rUpHs65EmG7A==", "dev": true, "requires": { "graceful-fs": "^4.1.2", @@ -16182,6 +16202,26 @@ "requires": { "lodash": "^4.17.11" } + }, + "fs-extra": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-7.0.1.tgz", + "integrity": "sha512-YJDaCJZEnBmcbw13fvdAM9AwNOJwOzrE4pqMqBq5nFiEqXUqHwlK4B+3pUw6JNvfSPtX05xFHtYy/1ni01eGCw==", + "dev": true, + "requires": { + "graceful-fs": "^4.1.2", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + } + }, + "jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", + "dev": true, + "requires": { + "graceful-fs": "^4.1.6" + } } } }, diff --git a/package.json b/package.json index 67f1aae4f0..b8fc469780 100644 --- a/package.json +++ b/package.json @@ -550,6 +550,7 @@ "eslint-plugin-prettier": "^3.0.1", "eslint-plugin-promise": "^4.0.1", "eslint-plugin-standard": "^4.0.0", + "fs-extra": "^8.0.1", "husky": "^1.3.1", "jsdoc": "^3.5.5", "karma": "^4.0.1", diff --git a/test/integration/helpers.js b/test/integration/helpers.js index b774bb7890..6db7833447 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -115,31 +115,36 @@ module.exports = { }, /** * Invokes the mocha binary for the given fixture using the JSON reporter, - * returning the **raw** string output, as well as exit code. + * returning the child process and a promise for the results of running the + * command. The result includes the **raw** string output, as well as exit code. * * By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you * want it. * @param {string} fixturePath - Path from __dirname__ * @param {string[]} args - Array of args - * @param {Function} fn - Callback * @param {Object} [opts] - Opts for `spawn()` - * @returns {string} Raw output + * @returns {[ChildProcess|Promise]} */ - runMochaJSONRaw: function(fixturePath, args, fn, opts) { - var path; - - path = resolveFixturePath(fixturePath); + runMochaJSONRawAsync: function(fixturePath, args, opts) { + const path = resolveFixturePath(fixturePath); args = args || []; - return invokeSubMocha( - args.concat(['--reporter', 'json', path]), - function(err, resRaw) { - if (err) return fn(err); + let childProcess; + const resultPromise = new Promise((resolve, reject) => { + childProcess = invokeSubMocha( + args.concat(['--reporter', 'json', path]), + function(err, resRaw) { + if (err) { + reject(err); + } else { + resolve(resRaw); + } + }, + opts + ); + }); - fn(null, resRaw); - }, - opts - ); + return [childProcess, resultPromise]; }, /** @@ -282,7 +287,7 @@ function resolveFixturePath(fixture) { if (path.extname(fixture) !== '.js') { fixture += '.fixture.js'; } - return path.join('test', 'integration', 'fixtures', fixture); + return path.resolve('test', 'integration', 'fixtures', fixture); } function getSummary(res) { diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js index 1df65fa342..74c4fd0f94 100644 --- a/test/integration/options/watch.spec.js +++ b/test/integration/options/watch.spec.js @@ -1,16 +1,18 @@ 'use strict'; -var helpers = require('../helpers'); -var runMochaJSONRaw = helpers.runMochaJSONRaw; +const fs = require('fs-extra'); +const os = require('os'); +const path = require('path'); +const helpers = require('../helpers'); +const runMochaJSONRawAsync = helpers.runMochaJSONRawAsync; -describe('--watch', function() { - var args = []; - - before(function() { - args = ['--watch']; - }); +const sigintExitCode = 130; +describe('--watch', function() { describe('when enabled', function() { + this.timeout(10 * 1000); + this.slow(3000); + before(function() { // Feature works but SIMULATING the signal (ctrl+c) via child process // does not work due to lack of POSIX signal compliance on Windows. @@ -19,38 +21,156 @@ describe('--watch', function() { } }); - it('should show the cursor and signal correct exit code, when watch process is terminated', function(done) { - this.timeout(0); - this.slow(3000); - - var fixture = 'exit.fixture.js'; - var spawnOpts = {stdio: 'pipe'}; - var mocha = runMochaJSONRaw( - fixture, - args, - function postmortem(err, data) { - if (err) { - return done(err); - } - - var expectedCloseCursor = '\u001b[?25h'; + beforeEach(function() { + this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-')); + + var fixtureSource = helpers.resolveFixturePath(helpers.DEFAULT_FIXTURE); + + this.testFile = path.join(this.tempDir, 'test.js'); + fs.copySync(fixtureSource, this.testFile); + }); + + afterEach(function() { + if (this.tempDir) { + return fs.remove(this.tempDir); + } + }); + + it('should show the cursor and signal correct exit code, when watch process is terminated', function() { + const [mocha, resultPromise] = runMochaJSONRawAsync( + helpers.DEFAULT_FIXTURE, + ['--watch'] + ); + + return sleep(1000) + .then(() => { + mocha.kill('SIGINT'); + return resultPromise; + }) + .then(data => { + const expectedCloseCursor = '\u001b[?25h'; expect(data.output, 'to contain', expectedCloseCursor); - function exitStatusBySignal(sig) { - return 128 + sig; - } + expect(data.code, 'to be', sigintExitCode); + }); + }); + + it('reruns test when watched test file is touched', function() { + const [mocha, outputPromise] = runMochaJSONWatchAsync(this.testFile, [], { + cwd: this.tempDir + }); - var sigint = 2; - expect(data.code, 'to be', exitStatusBySignal(sigint)); - done(); - }, - spawnOpts + return sleep(1000) + .then(() => { + touchFile(this.testFile); + return sleep(1000); + }) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }) + .then(results => { + expect(results, 'to have length', 2); + }); + }); + + it('reruns test when file matching extension is touched', function() { + const watchedFile = path.join(this.tempDir, 'file.xyz'); + touchFile(watchedFile); + const [mocha, outputPromise] = runMochaJSONWatchAsync( + this.testFile, + ['--extension', 'xyz,js'], + { + cwd: this.tempDir + } ); - setTimeout(function() { - // Kill the child process - mocha.kill('SIGINT'); - }, 1000); + return sleep(1000) + .then(() => { + touchFile(watchedFile); + return sleep(1000); + }) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }) + .then(results => { + expect(results, 'to have length', 2); + }); + }); + + it('ignores files in "node_modules" and ".git"', function() { + const nodeModulesFile = path.join( + this.tempDir, + 'node_modules', + 'file.xyz' + ); + const gitFile = path.join(this.tempDir, '.git', 'file.xyz'); + + touchFile(gitFile); + touchFile(nodeModulesFile); + + const [mocha, outputPromise] = runMochaJSONWatchAsync( + this.testFile, + ['--extension', 'xyz,js'], + { + cwd: this.tempDir + } + ); + + return sleep(2000) + .then(() => { + touchFile(gitFile); + touchFile(nodeModulesFile); + }) + .then(() => sleep(2000)) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }) + .then(results => { + expect(results, 'to have length', 1); + }); }); }); }); + +/** + * Invokes the mocha binary with the `--watch` argument for the given fixture. + * + * Returns child process and a promise for the test results. The test results + * are an array of JSON objects generated by the JSON reporter. + * + * Checks that the exit code of the mocha command is 130, i.e. mocha was killed + * by SIGINT. + */ +function runMochaJSONWatchAsync(fixture, args, spawnOpts) { + args = ['--watch'].concat(args); + const [mocha, mochaDone] = runMochaJSONRawAsync(fixture, args, spawnOpts); + const testResults = mochaDone.then(data => { + expect(data.code, 'to be', sigintExitCode); + + const testResults = data.output + // eslint-disable-next-line no-control-regex + .replace(/\u001b\[\?25./g, '') + .split('\u001b[2K') + .map(x => JSON.parse(x)); + return testResults; + }); + return [mocha, testResults]; +} + +/** + * Touch a file by appending a space to the end. Returns a promise that resolves + * when the file has been touched. + */ +function touchFile(file) { + fs.ensureDirSync(path.dirname(file)); + fs.appendFileSync(file, ' '); +} + +function sleep(time) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +}