From b5e71834a00af7e7394d1bbd1b33efc475d10df1 Mon Sep 17 00:00:00 2001 From: Jakob Krigovsky Date: Wed, 29 Apr 2020 11:36:12 +0200 Subject: [PATCH 1/5] Fix missing dot in name of configuration file --- docs/index.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/index.md b/docs/index.md index aeaa1c318c..6c66053d8c 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1565,7 +1565,7 @@ Node.JS native ESM support still has status: **Stability: 1 - Experimental** - [Custom reporters](#third-party-reporters) and [custom interfaces](#interfaces) can only be CommonJS files - [Required modules](#-require-module-r-module) can only be CommonJS files -- [Configuration file](#configuring-mocha-nodejs) can only be a CommonJS file (`mocharc.js` or `mocharc.cjs`) +- [Configuration file](#configuring-mocha-nodejs) can only be a CommonJS file (`.mocharc.js` or `.mocharc.cjs`) - When using module-level mocks via libs like `proxyquire`, `rewiremock` or `rewire`, hold off on using ES modules for your test files - Node.JS native ESM support does not work with [esm][npm-esm] module @@ -1732,7 +1732,7 @@ tests as shown below: Mocha supports configuration files, typical of modern command-line tools, in several formats: -- **JavaScript**: Create a `.mocharc.js` (or `mocharc.cjs` when using [`"type"="module"`](#nodejs-native-esm-support) in your `package.json`) +- **JavaScript**: Create a `.mocharc.js` (or `.mocharc.cjs` when using [`"type"="module"`](#nodejs-native-esm-support) in your `package.json`) in your project's root directory, and export an object (`module.exports = {/* ... */}`) containing your configuration. - **YAML**: Create a `.mocharc.yaml` (or `.mocharc.yml`) in your project's root directory. - **JSON**: Create a `.mocharc.json` (or `.mocharc.jsonc`) in your project's root directory. Comments — while not valid JSON — are allowed in this file, and will be ignored by Mocha. From 14da63ab4a6aa15194c4d4f797de058880663f26 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Thu, 30 Apr 2020 14:26:46 -0700 Subject: [PATCH 2/5] adds a bunch of keywords Did you know that if you click on the "testing" icon on npmjs.com, Mocha is completely absent? It wants the keyword `testing`, which we do not have. So I added that and a bunch more. Since all of the other major test frameworks seem to use keywords of others, might as well join that arms race. --- package.json | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 3861b24f18..777d26bd28 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,15 @@ "test", "bdd", "tdd", - "tap" + "tap", + "testing", + "chai", + "assertion", + "ava", + "jest", + "tape", + "jasmine", + "karma" ], "funding": { "type": "opencollective", From 9c965c910e54d588abee688da813cc0a014c6b49 Mon Sep 17 00:00:00 2001 From: Daniel0113 Date: Fri, 1 May 2020 17:50:44 -0400 Subject: [PATCH 3/5] Exposing filename in JSON, doc, and json-stream reporters (#4219) * Exposing filename in JSON reporter * Added filename to json-stream reporter * updated tests and also fixed issue with json-stream reporter * added filenames to doc reporter --- lib/reporters/doc.js | 6 ++++++ lib/reporters/json-stream.js | 1 + lib/reporters/json.js | 1 + test/reporters/doc.spec.js | 16 +++++++++++++++ test/reporters/helpers.js | 2 ++ test/reporters/json-stream.spec.js | 8 ++++++++ test/reporters/json.spec.js | 32 +++++++++++++++++++----------- 7 files changed, 54 insertions(+), 12 deletions(-) diff --git a/lib/reporters/doc.js b/lib/reporters/doc.js index 5a6af8fb42..fd6b46932d 100644 --- a/lib/reporters/doc.js +++ b/lib/reporters/doc.js @@ -62,6 +62,7 @@ function Doc(runner, options) { runner.on(EVENT_TEST_PASS, function(test) { Base.consoleLog('%s
%s
', indent(), utils.escape(test.title)); + Base.consoleLog('%s
%s
', indent(), utils.escape(test.file)); var code = utils.escape(utils.clean(test.body)); Base.consoleLog('%s
%s
', indent(), code); }); @@ -72,6 +73,11 @@ function Doc(runner, options) { indent(), utils.escape(test.title) ); + Base.consoleLog( + '%s
%s
', + indent(), + utils.escape(test.file) + ); var code = utils.escape(utils.clean(test.body)); Base.consoleLog( '%s
%s
', diff --git a/lib/reporters/json-stream.js b/lib/reporters/json-stream.js index 27282987ea..8caa8adfce 100644 --- a/lib/reporters/json-stream.js +++ b/lib/reporters/json-stream.js @@ -82,6 +82,7 @@ function clean(test) { return { title: test.title, fullTitle: test.fullTitle(), + file: test.file, duration: test.duration, currentRetry: test.currentRetry() }; diff --git a/lib/reporters/json.js b/lib/reporters/json.js index 12b6289cd7..a46776ba9c 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -87,6 +87,7 @@ function clean(test) { return { title: test.title, fullTitle: test.fullTitle(), + file: test.file, duration: test.duration, currentRetry: test.currentRetry(), err: cleanCycles(err) diff --git a/test/reporters/doc.spec.js b/test/reporters/doc.spec.js index fb2703f83c..ce84a5a1b3 100644 --- a/test/reporters/doc.spec.js +++ b/test/reporters/doc.spec.js @@ -134,9 +134,11 @@ describe('Doc reporter', function() { describe("on 'pass' event", function() { var expectedTitle = 'some tite'; + var expectedFile = 'testFile.spec.js'; var expectedBody = 'some body'; var test = { title: expectedTitle, + file: expectedFile, body: expectedBody, slow: function() { return ''; @@ -148,6 +150,7 @@ describe('Doc reporter', function() { var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', + '
' + expectedFile + '
\n', '
' + expectedBody + '
\n' ]; expect(stdout, 'to equal', expectedArray); @@ -155,18 +158,23 @@ describe('Doc reporter', function() { it('should escape title and body where necessary', function() { var unescapedTitle = '
' + expectedTitle + '
'; + var unescapedFile = '
' + expectedFile + '
'; var unescapedBody = '
' + expectedBody + '
'; test.title = unescapedTitle; + test.file = unescapedFile; test.body = unescapedBody; var expectedEscapedTitle = '<div>' + expectedTitle + '</div>'; + var expectedEscapedFile = + '<div>' + expectedFile + '</div>'; var expectedEscapedBody = '<div>' + expectedBody + '</div>'; runner = createMockRunner('pass', EVENT_TEST_PASS, null, null, test); var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedEscapedTitle + '
\n', + '
' + expectedEscapedFile + '
\n', '
' + expectedEscapedBody + '
\n' ]; expect(stdout, 'to equal', expectedArray); @@ -175,10 +183,12 @@ describe('Doc reporter', function() { describe("on 'fail' event", function() { var expectedTitle = 'some tite'; + var expectedFile = 'testFile.spec.js'; var expectedBody = 'some body'; var expectedError = 'some error'; var test = { title: expectedTitle, + file: expectedFile, body: expectedBody, slow: function() { return ''; @@ -197,6 +207,7 @@ describe('Doc reporter', function() { var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedTitle + '
\n', + '
' + expectedFile + '
\n', '
' +
             expectedBody +
             '
\n', @@ -207,13 +218,17 @@ describe('Doc reporter', function() { it('should escape title, body, and error where necessary', function() { var unescapedTitle = '
' + expectedTitle + '
'; + var unescapedFile = '
' + expectedFile + '
'; var unescapedBody = '
' + expectedBody + '
'; var unescapedError = '
' + expectedError + '
'; test.title = unescapedTitle; + test.file = unescapedFile; test.body = unescapedBody; var expectedEscapedTitle = '<div>' + expectedTitle + '</div>'; + var expectedEscapedFile = + '<div>' + expectedFile + '</div>'; var expectedEscapedBody = '<div>' + expectedBody + '</div>'; var expectedEscapedError = @@ -229,6 +244,7 @@ describe('Doc reporter', function() { var stdout = runReporter(this, runner, options); var expectedArray = [ '
' + expectedEscapedTitle + '
\n', + '
' + expectedEscapedFile + '
\n', '
' +
             expectedEscapedBody +
             '
\n', diff --git a/test/reporters/helpers.js b/test/reporters/helpers.js index 45c4d916de..f712e19e8b 100644 --- a/test/reporters/helpers.js +++ b/test/reporters/helpers.js @@ -163,6 +163,7 @@ function createElements(argObj) { function makeExpectedTest( expectedTitle, expectedFullTitle, + expectedFile, expectedDuration, currentRetry, expectedBody @@ -172,6 +173,7 @@ function makeExpectedTest( fullTitle: function() { return expectedFullTitle; }, + file: expectedFile, duration: expectedDuration, currentRetry: function() { return currentRetry; diff --git a/test/reporters/json-stream.spec.js b/test/reporters/json-stream.spec.js index de83f861b2..613b9279f8 100644 --- a/test/reporters/json-stream.spec.js +++ b/test/reporters/json-stream.spec.js @@ -20,11 +20,13 @@ describe('JSON Stream reporter', function() { var runReporter = makeRunReporter(JSONStream); var expectedTitle = 'some title'; var expectedFullTitle = 'full title'; + var expectedFile = 'someTest.spec.js'; var expectedDuration = 1000; var currentRetry = 1; var expectedTest = makeExpectedTest( expectedTitle, expectedFullTitle, + expectedFile, expectedDuration, currentRetry ); @@ -70,6 +72,8 @@ describe('JSON Stream reporter', function() { dQuote(expectedTitle) + ',"fullTitle":' + dQuote(expectedFullTitle) + + ',"file":' + + dQuote(expectedFile) + ',"duration":' + expectedDuration + ',"currentRetry":' + @@ -101,6 +105,8 @@ describe('JSON Stream reporter', function() { dQuote(expectedTitle) + ',"fullTitle":' + dQuote(expectedFullTitle) + + ',"file":' + + dQuote(expectedFile) + ',"duration":' + expectedDuration + ',"currentRetry":' + @@ -135,6 +141,8 @@ describe('JSON Stream reporter', function() { dQuote(expectedTitle) + ',"fullTitle":' + dQuote(expectedFullTitle) + + ',"file":' + + dQuote(expectedFile) + ',"duration":' + expectedDuration + ',"currentRetry":' + diff --git a/test/reporters/json.spec.js b/test/reporters/json.spec.js index f6299dd134..9aa7e7a208 100644 --- a/test/reporters/json.spec.js +++ b/test/reporters/json.spec.js @@ -11,6 +11,7 @@ describe('JSON reporter', function() { var suite; var runner; var testTitle = 'json test 1'; + var testFile = 'someTest.spec.js'; var noop = function() {}; beforeEach(function() { @@ -36,11 +37,12 @@ describe('JSON reporter', function() { it('should have 1 test failure', function(done) { var error = {message: 'oh shit'}; - suite.addTest( - new Test(testTitle, function(done) { - done(new Error(error.message)); - }) - ); + var test = new Test(testTitle, function(done) { + done(new Error(error.message)); + }); + + test.file = testFile; + suite.addTest(test); runner.run(function(failureCount) { sandbox.restore(); @@ -49,6 +51,7 @@ describe('JSON reporter', function() { failures: [ { title: testTitle, + file: testFile, err: { message: error.message } @@ -62,7 +65,9 @@ describe('JSON reporter', function() { }); it('should have 1 test pending', function(done) { - suite.addTest(new Test(testTitle)); + var test = new Test(testTitle); + test.file = testFile; + suite.addTest(test); runner.run(function(failureCount) { sandbox.restore(); @@ -70,7 +75,8 @@ describe('JSON reporter', function() { testResults: { pending: [ { - title: testTitle + title: testTitle, + file: testFile } ] } @@ -88,11 +94,12 @@ describe('JSON reporter', function() { } var error = new CircleError(); - suite.addTest( - new Test(testTitle, function(done) { - throw error; - }) - ); + var test = new Test(testTitle, function(done) { + throw error; + }); + + test.file = testFile; + suite.addTest(test); runner.run(function(failureCount) { sandbox.restore(); @@ -101,6 +108,7 @@ describe('JSON reporter', function() { failures: [ { title: testTitle, + file: testFile, err: { message: error.message } From 2509ab5f261e22bfd4bad10e59002cd8878c19d9 Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 4 May 2020 12:56:44 -0700 Subject: [PATCH 4/5] assorted test fixes & refactors (#4240) - increase default timeout for wiggle room - specify `watch-ignore` in case we run our own tests in watch mode - reformat `.mocharc.yml` - `integration/fixtures/uncaught/listeners.fixture.js`: reduce number of listeners created to avoid max listener warning - `integration/fixtures/diffs.spec.js`: do not pass `-C`; the helper already does this - `integration/options/watch.spec.js`: do not use context object; be more specific about spawn options. tweak timings - `node-unit/mocha.spec.js`: make more unit-test-like insofar as that's possible when testing w/ `require()` - `node-unit/cli/config.spec.js`: rewiremock fixes; assertion tweaks; add test for `.cjs` extension - `node-unit/cli/options.spec.js`: rewiremock fixes; increase timeout - `unit/hook-timeout.spec.js`: do not override default (configured) timeout - `unit/runner.spec.js`: leverage [unexpected-eventemitter](https://npm.im/unexpected-eventemitter) - `unit/throw.spec.js`: proper teardown: remove uncaught exception listeners - `unit/timeout.spec.js`: increase timeout to _greater than_ default (configured) value - `example/config/.mocharc.yml`: quote yaml strings BONUS - fixes a weird call to `Mocha.unloadFile()` from `Mocha#unloadFiles()` Ref: #4198 --- .mocharc.yml | 19 ++- example/config/.mocharc.yml | 22 ++-- lib/mocha.js | 4 +- test/integration/diffs.spec.js | 2 +- .../fixtures/uncaught/listeners.fixture.js | 3 +- test/integration/options/watch.spec.js | 124 ++++++++---------- test/node-unit/cli/config.spec.js | 65 +++++---- test/node-unit/cli/options.spec.js | 4 +- test/node-unit/fixtures/dumb-module.js | 0 test/node-unit/fixtures/dumber-module.js | 0 test/node-unit/mocha.spec.js | 106 ++++++++------- test/unit/hook-timeout.spec.js | 3 +- test/unit/runner.spec.js | 12 +- test/unit/throw.spec.js | 1 + test/unit/timeout.spec.js | 2 +- 15 files changed, 196 insertions(+), 171 deletions(-) create mode 100644 test/node-unit/fixtures/dumb-module.js create mode 100644 test/node-unit/fixtures/dumber-module.js diff --git a/.mocharc.yml b/.mocharc.yml index fc4c97339c..dfb82e07f6 100644 --- a/.mocharc.yml +++ b/.mocharc.yml @@ -1,7 +1,14 @@ -require: test/setup -ui: bdd +require: 'test/setup' +ui: 'bdd' global: - - okGlobalA,okGlobalB - - okGlobalC - - callback* -timeout: 300 + - 'okGlobalA,okGlobalB' + - 'okGlobalC' + - 'callback*' +timeout: 1000 +watch-ignore: + - '.*' + - 'docs/_dist/**' + - 'docs/_site/**' + - 'node_modules' + - 'coverage' + - 'cache' diff --git a/example/config/.mocharc.yml b/example/config/.mocharc.yml index da5a0a0c4f..a310525b86 100644 --- a/example/config/.mocharc.yml +++ b/example/config/.mocharc.yml @@ -8,39 +8,39 @@ delay: false diff: true exit: false # could be expressed as "no-exit: true" extension: - - js + - 'js' # fgrep and grep are mutually exclusive # fgrep: something file: - - /path/to/some/file - - /path/to/some/other/file + - '/path/to/some/file' + - '/path/to/some/other/file' forbid-only: false forbid-pending: false full-trace: false global: - - jQuery - - $ + - 'jQuery' + - '$' # fgrep and grep are mutually exclusive # grep: something growl: false ignore: - - /path/to/some/ignored/file + - '/path/to/some/ignored/file' inline-diffs: false # needs to be used with grep or fgrep # invert: false recursive: false -reporter: spec +reporter: 'spec' reporter-option: - - foo=bar - - baz=quux + - 'foo=bar' + - 'baz=quux' require: '@babel/register' retries: 1 slow: 75 sort: false -spec: test/**/*.spec.js # the positional arguments! +spec: 'test/**/*.spec.js' # the positional arguments! timeout: false # same as "no-timeout: true" or "timeout: 0" trace-warnings: true # node flags ok -ui: bdd +ui: 'bdd' v8-stack-trace-limit: 100 # V8 flags are prepended with "v8-" watch: false watch-files: diff --git a/lib/mocha.js b/lib/mocha.js index 017daa1e2c..40718d09e9 100644 --- a/lib/mocha.js +++ b/lib/mocha.js @@ -388,7 +388,9 @@ Mocha.unloadFile = function(file) { * @chainable */ Mocha.prototype.unloadFiles = function() { - this.files.forEach(Mocha.unloadFile); + this.files.forEach(function(file) { + Mocha.unloadFile(file); + }); return this; }; diff --git a/test/integration/diffs.spec.js b/test/integration/diffs.spec.js index 44b30de1ae..ac9ad18d26 100644 --- a/test/integration/diffs.spec.js +++ b/test/integration/diffs.spec.js @@ -72,7 +72,7 @@ describe('diffs', function() { var diffs, expected; before(function(done) { - run('diffs/diffs.fixture.js', ['-C'], function(err, res) { + run('diffs/diffs.fixture.js', [], function(err, res) { if (err) { done(err); return; diff --git a/test/integration/fixtures/uncaught/listeners.fixture.js b/test/integration/fixtures/uncaught/listeners.fixture.js index 3ad398cfe0..69c4059294 100644 --- a/test/integration/fixtures/uncaught/listeners.fixture.js +++ b/test/integration/fixtures/uncaught/listeners.fixture.js @@ -3,7 +3,8 @@ const assert = require('assert'); const mocha = require("../../../../lib/mocha"); -for (let i = 0; i < 15; i++) { +// keep this low to avoid warning +for (let i = 0; i < 5; i++) { const r = new mocha.Runner(new mocha.Suite("" + i, undefined)); r.run(); } diff --git a/test/integration/options/watch.spec.js b/test/integration/options/watch.spec.js index f5cd382dee..259a9416d4 100644 --- a/test/integration/options/watch.spec.js +++ b/test/integration/options/watch.spec.js @@ -7,24 +7,24 @@ const helpers = require('../helpers'); describe('--watch', function() { describe('when enabled', function() { - this.timeout(10 * 1000); - this.slow(3000); + let tempDir; + this.slow(5000); beforeEach(function() { - this.tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-')); + tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'mocha-')); }); afterEach(function() { - if (this.tempDir) { - return fs.remove(this.tempDir); + if (tempDir) { + return fs.remove(tempDir); } }); it('reruns test when watched test file is touched', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - return runMochaWatch([testFile], this.tempDir, () => { + return runMochaWatch([testFile], tempDir, () => { touchFile(testFile); }).then(results => { expect(results, 'to have length', 2); @@ -32,15 +32,15 @@ describe('--watch', function() { }); it('reruns test when file matching --watch-files changes', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'dir/file.xyz'); + const watchedFile = path.join(tempDir, 'dir/file.xyz'); touchFile(watchedFile); return runMochaWatch( [testFile, '--watch-files', 'dir/*.xyz'], - this.tempDir, + tempDir, () => { touchFile(watchedFile); } @@ -50,13 +50,13 @@ describe('--watch', function() { }); it('reruns test when file matching --watch-files is added', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'lib/file.xyz'); + const watchedFile = path.join(tempDir, 'lib/file.xyz'); return runMochaWatch( [testFile, '--watch-files', '**/*.xyz'], - this.tempDir, + tempDir, () => { touchFile(watchedFile); } @@ -66,15 +66,15 @@ describe('--watch', function() { }); it('reruns test when file matching --watch-files is removed', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'lib/file.xyz'); + const watchedFile = path.join(tempDir, 'lib/file.xyz'); touchFile(watchedFile); return runMochaWatch( [testFile, '--watch-files', 'lib/**/*.xyz'], - this.tempDir, + tempDir, () => { fs.removeSync(watchedFile); } @@ -84,15 +84,15 @@ describe('--watch', function() { }); it('does not rerun test when file not matching --watch-files is changed', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'dir/file.js'); + const watchedFile = path.join(tempDir, 'dir/file.js'); touchFile(watchedFile); return runMochaWatch( [testFile, '--watch-files', 'dir/*.xyz'], - this.tempDir, + tempDir, () => { touchFile(watchedFile); } @@ -102,14 +102,14 @@ describe('--watch', function() { }); it('picks up new test files when they are added', function() { - const testFile = path.join(this.tempDir, 'test/a.js'); + const testFile = path.join(tempDir, 'test/a.js'); copyFixture('__default__', testFile); return runMochaWatch( ['test/**/*.js', '--watch-files', 'test/**/*.js'], - this.tempDir, + tempDir, () => { - const addedTestFile = path.join(this.tempDir, 'test/b.js'); + const addedTestFile = path.join(tempDir, 'test/b.js'); copyFixture('passing', addedTestFile); } ).then(results => { @@ -120,28 +120,24 @@ describe('--watch', function() { }); it('reruns test when file matching --extension is changed', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'file.xyz'); + const watchedFile = path.join(tempDir, 'file.xyz'); touchFile(watchedFile); - return runMochaWatch( - [testFile, '--extension', 'xyz,js'], - this.tempDir, - () => { - touchFile(watchedFile); - } - ).then(results => { + return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { + touchFile(watchedFile); + }).then(results => { expect(results, 'to have length', 2); }); }); it('reruns when "rs\\n" typed', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - return runMochaWatch([testFile], this.tempDir, mochaProcess => { + return runMochaWatch([testFile], tempDir, mochaProcess => { mochaProcess.stdin.write('rs\n'); }).then(results => { expect(results, 'to have length', 2); @@ -149,54 +145,42 @@ describe('--watch', function() { }); it('reruns test when file starting with . and matching --extension is changed', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, '.file.xyz'); + const watchedFile = path.join(tempDir, '.file.xyz'); touchFile(watchedFile); - return runMochaWatch( - [testFile, '--extension', 'xyz,js'], - this.tempDir, - () => { - touchFile(watchedFile); - } - ).then(results => { + return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { + touchFile(watchedFile); + }).then(results => { expect(results, 'to have length', 2); }); }); it('ignores files in "node_modules" and ".git" by default', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const nodeModulesFile = path.join( - this.tempDir, - 'node_modules', - 'file.xyz' - ); - const gitFile = path.join(this.tempDir, '.git', 'file.xyz'); + const nodeModulesFile = path.join(tempDir, 'node_modules', 'file.xyz'); + const gitFile = path.join(tempDir, '.git', 'file.xyz'); touchFile(gitFile); touchFile(nodeModulesFile); - return runMochaWatch( - [testFile, '--extension', 'xyz,js'], - this.tempDir, - () => { - touchFile(gitFile); - touchFile(nodeModulesFile); - } - ).then(results => { + return runMochaWatch([testFile, '--extension', 'xyz,js'], tempDir, () => { + touchFile(gitFile); + touchFile(nodeModulesFile); + }).then(results => { expect(results, 'to have length', 1); }); }); it('ignores files matching --watch-ignore', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('__default__', testFile); - const watchedFile = path.join(this.tempDir, 'dir/file-to-ignore.xyz'); + const watchedFile = path.join(tempDir, 'dir/file-to-ignore.xyz'); touchFile(watchedFile); return runMochaWatch( @@ -207,7 +191,7 @@ describe('--watch', function() { '--watch-ignore', 'dir/*ignore*' ], - this.tempDir, + tempDir, () => { touchFile(watchedFile); } @@ -217,12 +201,12 @@ describe('--watch', function() { }); it('reloads test files when they change', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('options/watch/test-file-change', testFile); return runMochaWatch( [testFile, '--watch-files', '**/*.js'], - this.tempDir, + tempDir, () => { replaceFileContents( testFile, @@ -240,15 +224,15 @@ describe('--watch', function() { }); it('reloads test dependencies when they change', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('options/watch/test-with-dependency', testFile); - const dependency = path.join(this.tempDir, 'lib', 'dependency.js'); + const dependency = path.join(tempDir, 'lib', 'dependency.js'); copyFixture('options/watch/dependency', dependency); return runMochaWatch( [testFile, '--watch-files', 'lib/**/*.js'], - this.tempDir, + tempDir, () => { replaceFileContents( dependency, @@ -267,10 +251,10 @@ describe('--watch', function() { // Regression test for https://github.com/mochajs/mocha/issues/2027 it('respects --fgrep on re-runs', function() { - const testFile = path.join(this.tempDir, 'test.js'); + const testFile = path.join(tempDir, 'test.js'); copyFixture('options/grep', testFile); - return runMochaWatch([testFile, '--fgrep', 'match'], this.tempDir, () => { + return runMochaWatch([testFile, '--fgrep', 'match'], tempDir, () => { touchFile(testFile); }).then(results => { expect(results, 'to have length', 2); @@ -293,12 +277,12 @@ describe('--watch', function() { function runMochaWatch(args, cwd, change) { const [mochaProcess, resultPromise] = helpers.invokeMochaAsync( [...args, '--watch', '--reporter', 'json'], - {cwd, stdio: 'pipe'} + {cwd, stdio: ['pipe', 'pipe', 'inherit']} ); - return sleep(1000) + return sleep(2000) .then(() => change(mochaProcess)) - .then(() => sleep(1000)) + .then(() => sleep(2000)) .then(() => { mochaProcess.kill('SIGINT'); return resultPromise; diff --git a/test/node-unit/cli/config.spec.js b/test/node-unit/cli/config.spec.js index 2823cdcd24..2d49423b2b 100644 --- a/test/node-unit/cli/config.spec.js +++ b/test/node-unit/cli/config.spec.js @@ -1,12 +1,11 @@ 'use strict'; -const {loadConfig, parsers, CONFIG_FILES} = require('../../../lib/cli/config'); const {createSandbox} = require('sinon'); const rewiremock = require('rewiremock/node'); describe('cli/config', function() { let sandbox; - const config = {ok: true}; + const phonyConfigObject = {ok: true}; beforeEach(function() { sandbox = createSandbox(); @@ -17,11 +16,22 @@ describe('cli/config', function() { }); describe('loadConfig()', function() { + let parsers; + let loadConfig; + + beforeEach(function() { + const config = rewiremock.proxy( + require.resolve('../../../lib/cli/config') + ); + parsers = config.parsers; + loadConfig = config.loadConfig; + }); + describe('when parsing succeeds', function() { beforeEach(function() { - sandbox.stub(parsers, 'yaml').returns(config); - sandbox.stub(parsers, 'json').returns(config); - sandbox.stub(parsers, 'js').returns(config); + sandbox.stub(parsers, 'yaml').returns(phonyConfigObject); + sandbox.stub(parsers, 'json').returns(phonyConfigObject); + sandbox.stub(parsers, 'js').returns(phonyConfigObject); }); describe('when supplied a filepath with ".yaml" extension', function() { @@ -30,8 +40,8 @@ describe('cli/config', function() { it('should use the YAML parser', function() { loadConfig(filepath); expect(parsers.yaml, 'to have calls satisfying', [ - {args: [filepath], returned: config} - ]).and('was called times', 1); + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); }); }); @@ -41,8 +51,8 @@ describe('cli/config', function() { it('should use the YAML parser', function() { loadConfig(filepath); expect(parsers.yaml, 'to have calls satisfying', [ - {args: [filepath], returned: config} - ]).and('was called times', 1); + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); }); }); @@ -52,8 +62,19 @@ describe('cli/config', function() { it('should use the JS parser', function() { loadConfig(filepath); expect(parsers.js, 'to have calls satisfying', [ - {args: [filepath], returned: config} - ]).and('was called times', 1); + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); + }); + }); + + describe('when supplied a filepath with ".cjs" extension', function() { + const filepath = 'foo.cjs'; + + it('should use the JS parser', function() { + loadConfig(filepath); + expect(parsers.js, 'to have calls satisfying', [ + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); }); }); @@ -63,8 +84,8 @@ describe('cli/config', function() { it('should use the JSON parser', function() { loadConfig('foo.jsonc'); expect(parsers.json, 'to have calls satisfying', [ - {args: [filepath], returned: config} - ]).and('was called times', 1); + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); }); }); @@ -74,15 +95,15 @@ describe('cli/config', function() { it('should use the JSON parser', function() { loadConfig('foo.json'); expect(parsers.json, 'to have calls satisfying', [ - {args: [filepath], returned: config} - ]).and('was called times', 1); + {args: [filepath], returned: phonyConfigObject} + ]).and('was called once'); }); }); }); describe('when supplied a filepath with unsupported extension', function() { beforeEach(function() { - sandbox.stub(parsers, 'json').returns(config); + sandbox.stub(parsers, 'json').returns(phonyConfigObject); }); it('should use the JSON parser', function() { @@ -105,20 +126,18 @@ describe('cli/config', function() { describe('findConfig()', function() { let findup; let findConfig; + let CONFIG_FILES; beforeEach(function() { findup = {sync: sandbox.stub().returns('/some/path/.mocharc.js')}; - rewiremock.enable(); - findConfig = rewiremock.proxy( + const config = rewiremock.proxy( require.resolve('../../../lib/cli/config'), r => ({ 'find-up': r.by(() => findup) }) - ).findConfig; - }); - - afterEach(function() { - rewiremock.disable(); + ); + findConfig = config.findConfig; + CONFIG_FILES = config.CONFIG_FILES; }); it('should look for one of the config files using findup-sync', function() { diff --git a/test/node-unit/cli/options.spec.js b/test/node-unit/cli/options.spec.js index d60de8e268..085ba5fc71 100644 --- a/test/node-unit/cli/options.spec.js +++ b/test/node-unit/cli/options.spec.js @@ -40,12 +40,10 @@ describe('options', function() { beforeEach(function() { sandbox = createSandbox(); - rewiremock.enable(); }); afterEach(function() { sandbox.restore(); - rewiremock.disable(); }); /** @@ -58,7 +56,7 @@ describe('options', function() { describe('loadOptions()', function() { describe('when no parameter provided', function() { beforeEach(function() { - this.timeout(500); + this.timeout(1000); readFileSync = sandbox.stub(); readFileSync.onFirstCall().returns('{}'); findConfig = sandbox.stub().returns('/some/.mocharc.json'); diff --git a/test/node-unit/fixtures/dumb-module.js b/test/node-unit/fixtures/dumb-module.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/node-unit/fixtures/dumber-module.js b/test/node-unit/fixtures/dumber-module.js new file mode 100644 index 0000000000..e69de29bb2 diff --git a/test/node-unit/mocha.spec.js b/test/node-unit/mocha.spec.js index 314a012023..54f8afbc54 100644 --- a/test/node-unit/mocha.spec.js +++ b/test/node-unit/mocha.spec.js @@ -1,72 +1,82 @@ 'use strict'; -const path = require('path'); const Mocha = require('../../lib/mocha'); const utils = require('../../lib/utils'); +const {createSandbox} = require('sinon'); describe('Mocha', function() { const opts = {reporter: utils.noop}; // no output - const testFiles = [ - __filename, - path.join(__dirname, 'cli', 'config.spec.js'), - path.join(__dirname, 'cli', 'run.spec.js') - ]; - const resolvedTestFiles = testFiles.map(require.resolve); + const dumbFilepath = require.resolve('./fixtures/dumb-module'); + const dumberFilepath = require.resolve('./fixtures/dumber-module'); - describe('#addFile', function() { - it('should add the given file to the files array', function() { - const mocha = new Mocha(opts); - mocha.addFile(__filename); - expect(mocha.files, 'to have length', 1).and('to contain', __filename); - }); + let mocha; + let sandbox; - it('should be chainable', function() { - const mocha = new Mocha(opts); - expect(mocha.addFile(__filename), 'to be', mocha); - }); + beforeEach(function() { + sandbox = createSandbox(); + mocha = new Mocha(opts); + delete require.cache[dumbFilepath]; + delete require.cache[dumberFilepath]; }); - describe('#loadFiles', function() { - it('should load all files from the files array', function() { - const mocha = new Mocha(opts); + afterEach(function() { + delete require.cache[dumbFilepath]; + delete require.cache[dumberFilepath]; + sandbox.restore(); + }); - testFiles.forEach(mocha.addFile, mocha); - mocha.loadFiles(); - expect(require.cache, 'to have keys', resolvedTestFiles); - }); + describe('instance method', function() { + describe('addFile()', function() { + it('should add the given file to the files array', function() { + mocha.addFile('some-file.js'); + expect(mocha.files, 'to exhaustively satisfy', ['some-file.js']); + }); - it('should execute the optional callback if given', function() { - const mocha = new Mocha(opts); - expect(cb => { - mocha.loadFiles(cb); - }, 'to call the callback'); + it('should be chainable', function() { + expect(mocha.addFile('some-file.js'), 'to be', mocha); + }); }); - }); - describe('.unloadFile', function() { - it('should unload a specific file from cache', function() { - const resolvedFilePath = require.resolve(__filename); - require(__filename); - expect(require.cache, 'to have key', resolvedFilePath); + describe('loadFiles()', function() { + it('should load all files from the files array', function() { + this.timeout(1000); + mocha.files = [dumbFilepath, dumberFilepath]; + mocha.loadFiles(); + expect(require.cache, 'to have keys', [dumbFilepath, dumberFilepath]); + }); - Mocha.unloadFile(__filename); - expect(require.cache, 'not to have key', resolvedFilePath); + it('should execute the optional callback if given', function() { + expect(cb => { + mocha.loadFiles(cb); + }, 'to call the callback'); + }); }); - }); - describe('#unloadFiles', function() { - it('should unload all test files from cache', function() { - const mocha = new Mocha(opts); + describe('unloadFiles()', function() { + it('should delegate Mocha.unloadFile() for each item in its list of files', function() { + mocha.files = [dumbFilepath, dumberFilepath]; + sandbox.stub(Mocha, 'unloadFile'); + mocha.unloadFiles(); + expect(Mocha.unloadFile, 'to have a call exhaustively satisfying', [ + dumbFilepath + ]) + .and('to have a call exhaustively satisfying', [dumberFilepath]) + .and('was called twice'); + }); - testFiles.forEach(mocha.addFile, mocha); - mocha.loadFiles(); - mocha.unloadFiles(); - expect(require.cache, 'not to have keys', resolvedTestFiles); + it('should be chainable', function() { + expect(mocha.unloadFiles(), 'to be', mocha); + }); }); + }); - it('should be chainable', function() { - const mocha = new Mocha(opts); - expect(mocha.unloadFiles(), 'to be', mocha); + describe('static method', function() { + describe('unloadFile()', function() { + it('should unload a specific file from cache', function() { + require(dumbFilepath); + Mocha.unloadFile(dumbFilepath); + expect(require.cache, 'not to have key', dumbFilepath); + }); }); }); }); diff --git a/test/unit/hook-timeout.spec.js b/test/unit/hook-timeout.spec.js index df3605518a..8c1b1f4735 100644 --- a/test/unit/hook-timeout.spec.js +++ b/test/unit/hook-timeout.spec.js @@ -1,8 +1,7 @@ 'use strict'; before(function(done) { - this.timeout(100); - setTimeout(done, 50); + setTimeout(done, 100); }); it('should work', function(done) { diff --git a/test/unit/runner.spec.js b/test/unit/runner.spec.js index 692559f1ed..c48e2d0e8e 100644 --- a/test/unit/runner.spec.js +++ b/test/unit/runner.spec.js @@ -416,10 +416,14 @@ describe('Runner', function() { hook.parent = suite; var err = new Error('error'); suite.bail(false); - runner.on(EVENT_RUN_END, function() { - throw new Error('"end" was emit, but the bail is false'); - }); - runner.failHook(hook, err); + expect( + function() { + runner.failHook(hook, err); + }, + 'not to emit from', + hook, + EVENT_RUN_END + ); done(); }); }); diff --git a/test/unit/throw.spec.js b/test/unit/throw.spec.js index 2dc3c8a759..1e02a3a085 100644 --- a/test/unit/throw.spec.js +++ b/test/unit/throw.spec.js @@ -25,6 +25,7 @@ describe('a test that throws', function() { }); afterEach(function() { + process.removeAllListeners('uncaughtException'); uncaughtHandlers.forEach(function(listener) { process.on('uncaughtException', listener); }); diff --git a/test/unit/timeout.spec.js b/test/unit/timeout.spec.js index ce95edcb81..e96f4b5d23 100644 --- a/test/unit/timeout.spec.js +++ b/test/unit/timeout.spec.js @@ -14,7 +14,7 @@ describe('timeouts', function() { }); it('should allow overriding per-test', function(done) { - this.timeout(200); + this.timeout(1500); setTimeout(function() { done(); }, 50); From 240cb3d245c74f525b3612a56472b1d89304820a Mon Sep 17 00:00:00 2001 From: Christopher Hiller Date: Mon, 4 May 2020 12:57:30 -0700 Subject: [PATCH 5/5] test helper improvements (#4241) * test helper improvements - enables `RawResult` (the result of `invokeMocha()` or `invokeMochaAsync()`) to check the exit code via `to have code` assertion - add passed/failing/pending assertions for `RawRunResult` (the result of `runMocha()` or `runMochaAsync()`) - expose `getSummary()`, which can be used with a `RawResult` (when not failing) - reorganize the module a bit - create `runMochaAsync()` and `runMochaJSONAsync()` which are like `runMocha()` and `runMochaJSON()` except return `Promise`s - better trapping of JSON parse errors - better default handling of `STDERR` output in subprocesses (print it instead of suppress it!) - do not let the `DEBUG` env variable reach subprocesses _unless it was explicitly supplied_ - add an easily copy-paste-able `command` prop to summary - add some missing docstrings Ref: #4198 * increase timeout in watch test for CI the same code should be in PR #4240 --- test/assertions.js | 20 ++- test/integration/helpers.js | 308 +++++++++++++++++++++++------------- 2 files changed, 215 insertions(+), 113 deletions(-) diff --git a/test/assertions.js b/test/assertions.js index 7453392059..ef678ff4ea 100644 --- a/test/assertions.js +++ b/test/assertions.js @@ -118,6 +118,24 @@ exports.mixinMochaAssertions = function(expect) { }); } ) + .addAssertion( + ' [not] to have failed [test] count ', + function(expect, result, count) { + expect(result.failing, '[not] to be', count); + } + ) + .addAssertion( + ' [not] to have passed [test] count ', + function(expect, result, count) { + expect(result.passing, '[not] to be', count); + } + ) + .addAssertion( + ' [not] to have pending [test] count ', + function(expect, result, count) { + expect(result.pending, '[not] to be', count); + } + ) .addAssertion(' [not] to have test count ', function( expect, result, @@ -315,7 +333,7 @@ exports.mixinMochaAssertions = function(expect) { } ) .addAssertion( - ' to have [exit] code ', + ' to have [exit] code ', function(expect, result, code) { expect(result.code, 'to be', code); } diff --git a/test/integration/helpers.js b/test/integration/helpers.js index 799f477539..17a1acfea1 100644 --- a/test/integration/helpers.js +++ b/test/integration/helpers.js @@ -4,116 +4,13 @@ var format = require('util').format; var spawn = require('cross-spawn').spawn; var path = require('path'); var Base = require('../../lib/reporters/base'); - +var debug = require('debug')('mocha:tests:integration:helpers'); var DEFAULT_FIXTURE = resolveFixturePath('__default__'); var MOCHA_EXECUTABLE = require.resolve('../../bin/mocha'); var _MOCHA_EXECUTABLE = require.resolve('../../bin/_mocha'); module.exports = { DEFAULT_FIXTURE: DEFAULT_FIXTURE, - /** - * Invokes the mocha binary for the given fixture with color output disabled. - * Accepts an array of additional command line args to pass. The callback is - * invoked with a summary of the run, in addition to its output. The summary - * includes the number of passing, pending, and failing tests, as well as the - * exit code. Useful for testing different reporters. - * - * By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you - * want it. - * Example response: - * { - * pending: 0, - * passing: 0, - * failing: 1, - * code: 1, - * output: '...' - * } - * - * @param {string} fixturePath - Path to fixture .js file - * @param {string[]} args - Extra args to mocha executable - * @param {Function} fn - Callback - * @param {Object} [opts] - Options for `spawn()` - * @returns {ChildProcess} Mocha process - */ - runMocha: function(fixturePath, args, fn, opts) { - if (typeof args === 'function') { - opts = fn; - fn = args; - args = []; - } - - var path; - - path = resolveFixturePath(fixturePath); - args = args || []; - - return invokeSubMocha( - args.concat(['-C', path]), - function(err, res) { - if (err) { - return fn(err); - } - - fn(null, getSummary(res)); - }, - opts - ); - }, - - /** - * Invokes the mocha binary for the given fixture using the JSON reporter, - * returning the parsed 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 {*} Parsed object - */ - runMochaJSON: function(fixturePath, args, fn, opts) { - if (typeof args === 'function') { - opts = fn; - fn = args; - args = []; - } - - var path; - - path = resolveFixturePath(fixturePath); - args = (args || []).concat('--reporter', 'json', path); - - return invokeMocha( - args, - function(err, res) { - if (err) { - return fn(err); - } - - var result; - try { - // attempt to catch a JSON parsing error *only* here. - // previously, the callback was called within this `try` block, - // which would result in errors thrown from the callback - // getting caught by the `catch` block below. - result = toJSONRunResult(res); - } catch (err) { - return fn( - new Error( - format( - 'Failed to parse JSON reporter output. Error:\n%O\nResult:\n%O', - err, - res - ) - ) - ); - } - fn(null, result); - }, - opts - ); - }, /** * regular expression used for splitting lines based on new line / dot symbol. @@ -146,6 +43,8 @@ module.exports = { invokeNode: invokeNode, + getSummary: getSummary, + /** * Resolves the path to a fixture to the full path. */ @@ -160,9 +59,166 @@ module.exports = { */ escapeRegExp: function escapeRegExp(str) { return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string - } + }, + + runMocha: runMocha, + runMochaJSON: runMochaJSON, + runMochaAsync: runMochaAsync, + runMochaJSONAsync: runMochaJSONAsync }; +/** + * Invokes the mocha binary for the given fixture with color output disabled. + * Accepts an array of additional command line args to pass. The callback is + * invoked with a summary of the run, in addition to its output. The summary + * includes the number of passing, pending, and failing tests, as well as the + * exit code. Useful for testing different reporters. + * + * By default, `STDERR` is ignored. Pass `{stdio: 'pipe'}` as `opts` if you + * want it. + * Example response: + * { + * pending: 0, + * passing: 0, + * failing: 1, + * code: 1, + * output: '...' + * } + * + * @param {string} fixturePath - Path to fixture .js file + * @param {string[]} args - Extra args to mocha executable + * @param {Function} fn - Callback + * @param {Object} [opts] - Options for `spawn()` + * @returns {ChildProcess} Mocha process + */ +function runMocha(fixturePath, args, fn, opts) { + if (typeof args === 'function') { + opts = fn; + fn = args; + args = []; + } + + var path; + + path = resolveFixturePath(fixturePath); + args = args || []; + + return invokeSubMocha( + args.concat(path), + function(err, res) { + if (err) { + return fn(err); + } + + fn(null, getSummary(res)); + }, + opts + ); +} + +/** + * Invokes the mocha binary for the given fixture using the JSON reporter, + * returning the parsed 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 {*} Parsed object + */ +function runMochaJSON(fixturePath, args, fn, opts) { + if (typeof args === 'function') { + opts = fn; + fn = args; + args = []; + } + + var path; + + path = resolveFixturePath(fixturePath); + args = (args || []).concat('--reporter', 'json', path); + + return invokeMocha( + args, + function(err, res) { + if (err) { + return fn(err); + } + + var result; + try { + // attempt to catch a JSON parsing error *only* here. + // previously, the callback was called within this `try` block, + // which would result in errors thrown from the callback + // getting caught by the `catch` block below. + result = toJSONRunResult(res); + } catch (err) { + return fn( + new Error( + format( + 'Failed to parse JSON reporter output. Error:\n%O\nResult:\n%O', + err, + res + ) + ) + ); + } + fn(null, result); + }, + opts + ); +} + +/** + * + * If you need more granular control, try {@link invokeMochaAsync} instead. + * + * Like {@link runMocha}, but returns a `Promise`. + * @param {string} fixturePath - Path to (or name of, or basename of) fixture file + * @param {Options} [args] - Command-line arguments to the `mocha` executable + * @param {Object} [opts] - Options for `child_process.spawn`. + * @returns {Promise} + */ +function runMochaAsync(fixturePath, args, opts) { + return new Promise(function(resolve, reject) { + runMocha( + fixturePath, + args, + function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }, + opts + ); + }); +} + +/** + * Like {@link runMochaJSON}, but returns a `Promise`. + * @param {string} fixturePath - Path to (or name of, or basename of) fixture file + * @param {Options} [args] - Command-line args + * @param {Object} [opts] - Options for `child_process.spawn` + */ +function runMochaJSONAsync(fixturePath, args, opts) { + return new Promise(function(resolve, reject) { + runMochaJSON( + fixturePath, + args, + function(err, result) { + if (err) { + return reject(err); + } + resolve(result); + }, + opts + ); + }); +} + /** * Coerce output as returned by _spawnMochaWithListeners using JSON reporter into a JSONRunResult as * recognized by our custom unexpected assertions @@ -171,9 +227,15 @@ module.exports = { */ function toJSONRunResult(result) { var code = result.code; - result = JSON.parse(result.output); - result.code = code; - return result; + try { + result = JSON.parse(result.output); + result.code = code; + return result; + } catch (err) { + throw new Error( + `Couldn't parse JSON: ${err.message}\n\nOriginal result output: ${result.output}` + ); + } } /** @@ -267,16 +329,24 @@ function invokeSubMocha(args, fn, opts) { */ function _spawnMochaWithListeners(args, fn, opts) { var output = ''; + opts = opts || {}; if (opts === 'pipe') { - opts = {stdio: 'pipe'}; + opts = {stdio: ['inherit', 'pipe', 'pipe']}; } + var env = Object.assign({}, process.env); + // prevent DEBUG from borking STDERR when piping, unless explicitly set via `opts` + delete env.DEBUG; + opts = Object.assign( { cwd: process.cwd(), - stdio: ['ignore', 'pipe', 'ignore'] + stdio: ['inherit', 'pipe', 'inherit'], + env: env }, - opts || {} + opts ); + + debug('spawning: %s', [process.execPath].concat(args).join(' ')); var mocha = spawn(process.execPath, args, opts); var listener = function(data) { output += data; @@ -292,7 +362,8 @@ function _spawnMochaWithListeners(args, fn, opts) { fn(null, { output: output, code: code, - args: args + args: args, + command: args.join(' ') }); }); @@ -306,6 +377,11 @@ function resolveFixturePath(fixture) { return path.join('test', 'integration', 'fixtures', fixture); } +/** + * Parses some `mocha` reporter output and returns a summary based on the "epilogue" + * @param {string} res - Typically output of STDOUT from the 'spec' reporter + * @returns {Summary} + */ function getSummary(res) { return ['passing', 'pending', 'failing'].reduce(function(summary, type) { var pattern, match; @@ -317,3 +393,11 @@ function getSummary(res) { return summary; }, res); } + +/** + * A summary of a `mocha` run + * @typedef {Object} Summary + * @property {number} passing - Number of passing tests + * @property {number} pending - Number of pending tests + * @property {number} failing - Number of failing tests + */