From c66e6a4cd4d9d08f4377f5e5291a97acd24d71f2 Mon Sep 17 00:00:00 2001 From: Thomas Scholtes Date: Tue, 14 May 2019 17:38:51 +0200 Subject: [PATCH] Extend tests for `--watch` options This commit adds two test cases to test the `--watch` option. We check that touching a test file reruns the tests and we test that touching a file that has a correct extensions reruns the test. This commit adds `fs-extra` as a new dev dependency. --- .eslintrc.yml | 2 + package-lock.json | 46 +++++- package.json | 1 + test/integration/helpers.js | 41 +++--- test/integration/options/watch.spec.js | 189 ++++++++++++++++++++----- 5 files changed, 222 insertions(+), 57 deletions(-) diff --git a/.eslintrc.yml b/.eslintrc.yml index 3fe7399ed0..f696b45ca8 100644 --- a/.eslintrc.yml +++ b/.eslintrc.yml @@ -25,6 +25,8 @@ overrides: - bin/* - lib/cli/**/*.js - test/node-unit/**/*.js + - test/integration/options/watch.spec.js + - test/integration/helpers.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..a705a2fe9b 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -114,32 +114,37 @@ module.exports = { ); }, /** - * Invokes the mocha binary for the given fixture using the JSON reporter, - * returning the **raw** string output, as well as exit code. + * Invokes the mocha binary with the given arguments fixture using + * the JSON reporter. Returns 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__ + * want it as part of the result output. + * * @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(args, opts) { 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, '--reporter', 'json'], + function(err, resRaw) { + if (err) { + reject(err); + } else { + resolve(resRaw); + } + }, + opts + ); + }); - fn(null, resRaw); - }, - opts - ); + return [childProcess, resultPromise]; }, /** diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js index 1df65fa342..1fb187a1e7 100644 --- a/test/integration/options/watch.spec.js +++ b/test/integration/options/watch.spec.js @@ -1,56 +1,173 @@ '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; + +const sigintExitCode = 130; describe('--watch', function() { - var args = []; + describe('when enabled', function() { + this.timeout(10 * 1000); + this.slow(3000); - before(function() { - args = ['--watch']; - }); + beforeEach(function() { + this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-')); - describe('when enabled', function() { - before(function() { + const fixtureSource = 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() { // Feature works but SIMULATING the signal (ctrl+c) via child process // does not work due to lack of POSIX signal compliance on Windows. if (process.platform === 'win32') { this.skip(); } - }); - 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'; + 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 expect( + sleep(1000) + .then(() => { + touchFile(this.testFile); + return sleep(1000); + }) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }), + 'when fulfilled', + 'to have length', + 2 ); + }); - setTimeout(function() { - // Kill the child process - mocha.kill('SIGINT'); - }, 1000); + 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 + } + ); + + return expect( + sleep(1000) + .then(() => { + touchFile(watchedFile); + return sleep(1000); + }) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }), + 'when fulfilled', + '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 expect( + sleep(1000) + .then(() => { + touchFile(gitFile); + touchFile(nodeModulesFile); + }) + .then(() => sleep(1000)) + .then(() => { + mocha.kill('SIGINT'); + return outputPromise; + }), + 'when fulfilled', + '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. + */ +function runMochaJSONWatchAsync(args, spawnOpts) { + args = [...args, '--watch']; + const [mocha, mochaDone] = runMochaJSONRawAsync(args, spawnOpts); + const testResults = mochaDone.then(data => { + 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]; +} + +/** + * Synchronously touch a file by appending a space to the end. Creates + * the file and all its parent directories if necessary. + */ +function touchFile(file) { + fs.ensureDirSync(path.dirname(file)); + fs.appendFileSync(file, ' '); +} + +function sleep(time) { + return new Promise(resolve => { + setTimeout(resolve, time); + }); +}