diff --git a/docs/index.md b/docs/index.md index 7300faf9e9..94fbe29089 100644 --- a/docs/index.md +++ b/docs/index.md @@ -1933,6 +1933,8 @@ Alias: `JSON`, `json` The JSON reporter outputs a single large JSON object when the tests have completed (failures or not). +By default, it will output to the console. To write directly to a file, use `--reporter-option output=filename.json`. + ![json reporter](images/reporter-json.png?withoutEnlargement&resize=920,9999){:class="screenshot" loading="lazy"} ### JSON Stream diff --git a/lib/reporters/json.js b/lib/reporters/json.js index a314cd3805..ead181b4e8 100644 --- a/lib/reporters/json.js +++ b/lib/reporters/json.js @@ -7,6 +7,10 @@ */ var Base = require('./base'); +var fs = require('fs'); +var path = require('path'); +var errors = require('../errors'); +var createUnsupportedError = errors.createUnsupportedError; var constants = require('../runner').constants; var EVENT_TEST_PASS = constants.EVENT_TEST_PASS; var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL; @@ -38,6 +42,14 @@ function JSONReporter(runner, options) { var pending = []; var failures = []; var passes = []; + var output; + + if (options && options.reporterOptions && options.reporterOptions.output) { + if (!fs || !fs.writeFileSync) { + throw createUnsupportedError('file output not supported in browser'); + } + output = options.reporterOptions.output; + } runner.on(EVENT_TEST_END, function(test) { tests.push(test); @@ -66,7 +78,13 @@ function JSONReporter(runner, options) { runner.testResults = obj; - process.stdout.write(JSON.stringify(obj, null, 2)); + var json = JSON.stringify(obj, null, 2); + if (output) { + fs.mkdirSync(path.dirname(output), {recursive: true}); + fs.writeFileSync(output, json); + } else { + process.stdout.write(json); + } }); } diff --git a/test/reporters/json.spec.js b/test/reporters/json.spec.js index 3eca1e9175..97f9019732 100644 --- a/test/reporters/json.spec.js +++ b/test/reporters/json.spec.js @@ -1,12 +1,15 @@ 'use strict'; +var fs = require('fs'); var sinon = require('sinon'); +var JSONReporter = require('../../lib/reporters/json'); var Mocha = require('../../'); var Suite = Mocha.Suite; var Runner = Mocha.Runner; var Test = Mocha.Test; describe('JSON reporter', function() { + var mocha; var suite; var runner; var testTitle = 'json test 1'; @@ -14,131 +17,188 @@ describe('JSON reporter', function() { var noop = function() {}; beforeEach(function() { - var mocha = new Mocha({ + mocha = new Mocha({ reporter: 'json' }); suite = new Suite('JSON suite', 'root'); runner = new Runner(suite); - var options = {}; - /* eslint no-unused-vars: off */ - var mochaReporter = new mocha._reporter(runner, options); - }); - - beforeEach(function() { - sinon.stub(process.stdout, 'write').callsFake(noop); }); afterEach(function() { sinon.restore(); }); - it('should have 1 test failure', function(done) { - var error = {message: 'oh shit'}; + describe('test results', function() { + beforeEach(function() { + var options = {}; + /* eslint no-unused-vars: off */ + var mochaReporter = new mocha._reporter(runner, options); + }); - var test = new Test(testTitle, function(done) { - done(new Error(error.message)); + beforeEach(function() { + sinon.stub(process.stdout, 'write').callsFake(noop); }); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - failures: [ - { - title: testTitle, - file: testFile, - err: { - message: error.message + it('should have 1 test failure', function(done) { + var error = {message: 'oh shit'}; + + var test = new Test(testTitle, function(done) { + done(new Error(error.message)); + }); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + failures: [ + { + title: testTitle, + file: testFile, + err: { + message: error.message + } } - } - ] - } + ] + } + }); + expect(failureCount, 'to be', 1); + done(); }); - expect(failureCount, 'to be', 1); - done(); }); - }); - it('should have 1 test pending', function(done) { - var test = new Test(testTitle); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - pending: [ - { - title: testTitle, - file: testFile - } - ] - } + it('should have 1 test pending', function(done) { + var test = new Test(testTitle); + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + pending: [ + { + title: testTitle, + file: testFile + } + ] + } + }); + expect(failureCount, 'to be', 0); + done(); }); - expect(failureCount, 'to be', 0); - done(); }); - }); - it('should have 1 test pass', function(done) { - const test = new Test(testTitle, () => {}); - - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - expect(runner, 'to satisfy', { - testResults: { - passes: [ - { - title: testTitle, - file: testFile, - speed: /(slow|medium|fast)/ - } - ] - } + it('should have 1 test pass', function(done) { + const test = new Test(testTitle, () => {}); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + expect(runner, 'to satisfy', { + testResults: { + passes: [ + { + title: testTitle, + file: testFile, + speed: /(slow|medium|fast)/ + } + ] + } + }); + expect(failureCount, 'to be', 0); + done(); + }); + }); + + it('should handle circular objects in errors', function(done) { + var testTitle = 'json test 1'; + function CircleError() { + this.message = 'oh shit'; + this.circular = this; + } + var error = new CircleError(); + + var test = new Test(testTitle, function(done) { + throw error; + }); + + test.file = testFile; + suite.addTest(test); + + runner.run(function(failureCount) { + sinon.restore(); + expect(runner, 'to satisfy', { + testResults: { + failures: [ + { + title: testTitle, + file: testFile, + err: { + message: error.message + } + } + ] + } + }); + expect(failureCount, 'to be', 1); + done(); }); - expect(failureCount, 'to be', 0); - done(); }); }); - it('should handle circular objects in errors', function(done) { - var testTitle = 'json test 1'; - function CircleError() { - this.message = 'oh shit'; - this.circular = this; - } - var error = new CircleError(); + describe("when 'reporterOptions.output' is provided", function() { + var expectedDirName = 'reports'; + var expectedFileName = 'reports/test-results.json'; + var options = { + reporterOptions: { + output: expectedFileName + } + }; + + beforeEach(function() { + /* eslint no-unused-vars: off */ + var mochaReporter = new mocha._reporter(runner, options); + }); - var test = new Test(testTitle, function(done) { - throw error; + beforeEach(function() { + // Add one test to suite to avoid assertions against empty test results + var test = new Test(testTitle, () => {}); + test.file = testFile; + suite.addTest(test); }); - test.file = testFile; - suite.addTest(test); - - runner.run(function(failureCount) { - sinon.restore(); - expect(runner, 'to satisfy', { - testResults: { - failures: [ - { - title: testTitle, - file: testFile, - err: { - message: error.message - } - } - ] - } + describe('when file can be created', function() { + it('should write test results to file', function(done) { + var fsMkdirSync = sinon.stub(fs, 'mkdirSync'); + var fsWriteFileSync = sinon.stub(fs, 'writeFileSync'); + + fsWriteFileSync.callsFake(function(filename, content) { + var expectedJson = JSON.stringify(runner.testResults, null, 2); + expect(expectedFileName, 'to be', filename); + expect(content, 'to be', expectedJson); + }); + + runner.run(function() { + fsMkdirSync.calledWith(expectedDirName, {recursive: true}); + expect(fsWriteFileSync.calledOnce, 'to be true'); + done(); + }); + }); + }); + + describe('when run in browser', function() { + it('should throw unsupported error', function() { + sinon.stub(fs, 'writeFileSync').value(false); + expect( + () => new JSONReporter(runner, options), + 'to throw', + 'file output not supported in browser' + ); }); - expect(failureCount, 'to be', 1); - done(); }); }); });