Skip to content

Commit 171e211

Browse files
dornyjuergba
andauthoredAug 20, 2021
feat(reporter): add output option to 'JSON' (#4607)
Co-authored-by: Juerg B <44573692+juergba@users.noreply.github.com>
1 parent bbf0c11 commit 171e211

File tree

6 files changed

+219
-103
lines changed

6 files changed

+219
-103
lines changed
 

‎docs/index.md

+2
Original file line numberDiff line numberDiff line change
@@ -1933,6 +1933,8 @@ Alias: `JSON`, `json`
19331933

19341934
The JSON reporter outputs a single large JSON object when the tests have completed (failures or not).
19351935

1936+
By default, it will output to the console. To write directly to a file, use `--reporter-option output=filename.json`.
1937+
19361938
![json reporter](images/reporter-json.png?withoutEnlargement&resize=920,9999){:class="screenshot" loading="lazy"}
19371939

19381940
### JSON Stream

‎example/config/.mocharc.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ module.exports = {
3333
parallel: false,
3434
recursive: false,
3535
reporter: 'spec',
36-
'reporter-option': ['foo=bar', 'baz=quux'],
36+
'reporter-option': ['foo=bar', 'baz=quux'], // array, not object
3737
require: '@babel/register',
3838
retries: 1,
3939
slow: '75',

‎example/config/.mocharc.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ package: './package.json'
3535
parallel: false
3636
recursive: false
3737
reporter: 'spec'
38-
reporter-option:
38+
reporter-option: # array, not object
3939
- 'foo=bar'
4040
- 'baz=quux'
4141
require: '@babel/register'

‎lib/reporters/base.js

+1-1
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ exports.colors = {
9090

9191
exports.symbols = {
9292
ok: symbols.success,
93-
err: symbols.err,
93+
err: symbols.error,
9494
dot: '.',
9595
comma: ',',
9696
bang: '!'

‎lib/reporters/json.js

+28-3
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,16 @@
77
*/
88

99
var Base = require('./base');
10+
var fs = require('fs');
11+
var path = require('path');
12+
const createUnsupportedError = require('../errors').createUnsupportedError;
13+
const utils = require('../utils');
1014
var constants = require('../runner').constants;
1115
var EVENT_TEST_PASS = constants.EVENT_TEST_PASS;
16+
var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
1217
var EVENT_TEST_FAIL = constants.EVENT_TEST_FAIL;
1318
var EVENT_TEST_END = constants.EVENT_TEST_END;
1419
var EVENT_RUN_END = constants.EVENT_RUN_END;
15-
var EVENT_TEST_PENDING = constants.EVENT_TEST_PENDING;
1620

1721
/**
1822
* Expose `JSON`.
@@ -30,14 +34,22 @@ exports = module.exports = JSONReporter;
3034
* @param {Runner} runner - Instance triggers reporter actions.
3135
* @param {Object} [options] - runner options
3236
*/
33-
function JSONReporter(runner, options) {
37+
function JSONReporter(runner, options = {}) {
3438
Base.call(this, runner, options);
3539

3640
var self = this;
3741
var tests = [];
3842
var pending = [];
3943
var failures = [];
4044
var passes = [];
45+
var output;
46+
47+
if (options.reporterOption && options.reporterOption.output) {
48+
if (utils.isBrowser()) {
49+
throw createUnsupportedError('file output not supported in browser');
50+
}
51+
output = options.reporterOption.output;
52+
}
4153

4254
runner.on(EVENT_TEST_END, function(test) {
4355
tests.push(test);
@@ -66,7 +78,20 @@ function JSONReporter(runner, options) {
6678

6779
runner.testResults = obj;
6880

69-
process.stdout.write(JSON.stringify(obj, null, 2));
81+
var json = JSON.stringify(obj, null, 2);
82+
if (output) {
83+
try {
84+
fs.mkdirSync(path.dirname(output), {recursive: true});
85+
fs.writeFileSync(output, json);
86+
} catch (err) {
87+
console.error(
88+
`${Base.symbols.err} [mocha] writing output to "${output}" failed: ${err.message}\n`
89+
);
90+
process.stdout.write(json);
91+
}
92+
} else {
93+
process.stdout.write(json);
94+
}
7095
});
7196
}
7297

‎test/reporters/json.spec.js

+186-97
Original file line numberDiff line numberDiff line change
@@ -1,144 +1,233 @@
11
'use strict';
22

3+
var fs = require('fs');
34
var sinon = require('sinon');
5+
var JSONReporter = require('../../lib/reporters/json');
6+
var utils = require('../../lib/utils');
47
var Mocha = require('../../');
58
var Suite = Mocha.Suite;
69
var Runner = Mocha.Runner;
710
var Test = Mocha.Test;
811

912
describe('JSON reporter', function() {
13+
var mocha;
1014
var suite;
1115
var runner;
1216
var testTitle = 'json test 1';
1317
var testFile = 'someTest.spec.js';
1418
var noop = function() {};
1519

1620
beforeEach(function() {
17-
var mocha = new Mocha({
21+
mocha = new Mocha({
1822
reporter: 'json'
1923
});
2024
suite = new Suite('JSON suite', 'root');
2125
runner = new Runner(suite);
22-
var options = {};
23-
/* eslint no-unused-vars: off */
24-
var mochaReporter = new mocha._reporter(runner, options);
25-
});
26-
27-
beforeEach(function() {
28-
sinon.stub(process.stdout, 'write').callsFake(noop);
2926
});
3027

3128
afterEach(function() {
3229
sinon.restore();
3330
});
3431

35-
it('should have 1 test failure', function(done) {
36-
var error = {message: 'oh shit'};
32+
describe('test results', function() {
33+
beforeEach(function() {
34+
var options = {};
35+
/* eslint no-unused-vars: off */
36+
var mochaReporter = new mocha._reporter(runner, options);
37+
});
38+
39+
beforeEach(function() {
40+
sinon.stub(process.stdout, 'write').callsFake(noop);
41+
});
42+
43+
it('should have 1 test failure', function(done) {
44+
var error = {message: 'oh shit'};
3745

38-
var test = new Test(testTitle, function(done) {
39-
done(new Error(error.message));
46+
var test = new Test(testTitle, function(done) {
47+
done(new Error(error.message));
48+
});
49+
50+
test.file = testFile;
51+
suite.addTest(test);
52+
53+
runner.run(function(failureCount) {
54+
sinon.restore();
55+
expect(runner, 'to satisfy', {
56+
testResults: {
57+
failures: [
58+
{
59+
title: testTitle,
60+
file: testFile,
61+
err: {
62+
message: error.message
63+
}
64+
}
65+
]
66+
}
67+
});
68+
expect(failureCount, 'to be', 1);
69+
done();
70+
});
4071
});
4172

42-
test.file = testFile;
43-
suite.addTest(test);
44-
45-
runner.run(function(failureCount) {
46-
sinon.restore();
47-
expect(runner, 'to satisfy', {
48-
testResults: {
49-
failures: [
50-
{
51-
title: testTitle,
52-
file: testFile,
53-
err: {
54-
message: error.message
73+
it('should have 1 test pending', function(done) {
74+
var test = new Test(testTitle);
75+
test.file = testFile;
76+
suite.addTest(test);
77+
78+
runner.run(function(failureCount) {
79+
sinon.restore();
80+
expect(runner, 'to satisfy', {
81+
testResults: {
82+
pending: [
83+
{
84+
title: testTitle,
85+
file: testFile
5586
}
56-
}
57-
]
58-
}
87+
]
88+
}
89+
});
90+
expect(failureCount, 'to be', 0);
91+
done();
5992
});
60-
expect(failureCount, 'to be', 1);
61-
done();
6293
});
63-
});
6494

65-
it('should have 1 test pending', function(done) {
66-
var test = new Test(testTitle);
67-
test.file = testFile;
68-
suite.addTest(test);
69-
70-
runner.run(function(failureCount) {
71-
sinon.restore();
72-
expect(runner, 'to satisfy', {
73-
testResults: {
74-
pending: [
75-
{
76-
title: testTitle,
77-
file: testFile
78-
}
79-
]
80-
}
95+
it('should have 1 test pass', function(done) {
96+
const test = new Test(testTitle, () => {});
97+
98+
test.file = testFile;
99+
suite.addTest(test);
100+
101+
runner.run(function(failureCount) {
102+
sinon.restore();
103+
expect(runner, 'to satisfy', {
104+
testResults: {
105+
passes: [
106+
{
107+
title: testTitle,
108+
file: testFile,
109+
speed: /(slow|medium|fast)/
110+
}
111+
]
112+
}
113+
});
114+
expect(failureCount, 'to be', 0);
115+
done();
81116
});
82-
expect(failureCount, 'to be', 0);
83-
done();
84117
});
85-
});
86118

87-
it('should have 1 test pass', function(done) {
88-
const test = new Test(testTitle, () => {});
89-
90-
test.file = testFile;
91-
suite.addTest(test);
92-
93-
runner.run(function(failureCount) {
94-
expect(runner, 'to satisfy', {
95-
testResults: {
96-
passes: [
97-
{
98-
title: testTitle,
99-
file: testFile,
100-
speed: /(slow|medium|fast)/
101-
}
102-
]
103-
}
119+
it('should handle circular objects in errors', function(done) {
120+
var testTitle = 'json test 1';
121+
function CircleError() {
122+
this.message = 'oh shit';
123+
this.circular = this;
124+
}
125+
var error = new CircleError();
126+
127+
var test = new Test(testTitle, function(done) {
128+
throw error;
129+
});
130+
131+
test.file = testFile;
132+
suite.addTest(test);
133+
134+
runner.run(function(failureCount) {
135+
sinon.restore();
136+
expect(runner, 'to satisfy', {
137+
testResults: {
138+
failures: [
139+
{
140+
title: testTitle,
141+
file: testFile,
142+
err: {
143+
message: error.message
144+
}
145+
}
146+
]
147+
}
148+
});
149+
expect(failureCount, 'to be', 1);
150+
done();
104151
});
105-
expect(failureCount, 'to be', 0);
106-
done();
107152
});
108153
});
109154

110-
it('should handle circular objects in errors', function(done) {
111-
var testTitle = 'json test 1';
112-
function CircleError() {
113-
this.message = 'oh shit';
114-
this.circular = this;
115-
}
116-
var error = new CircleError();
155+
describe('when "reporterOption.output" is provided', function() {
156+
var expectedDirName = 'reports';
157+
var expectedFileName = 'reports/test-results.json';
158+
var options = {
159+
reporterOption: {
160+
output: expectedFileName
161+
}
162+
};
163+
164+
beforeEach(function() {
165+
/* eslint no-unused-vars: off */
166+
var mochaReporter = new mocha._reporter(runner, options);
167+
});
117168

118-
var test = new Test(testTitle, function(done) {
119-
throw error;
169+
beforeEach(function() {
170+
// Add one test to suite to avoid assertions against empty test results
171+
var test = new Test(testTitle, () => {});
172+
test.file = testFile;
173+
suite.addTest(test);
120174
});
121175

122-
test.file = testFile;
123-
suite.addTest(test);
124-
125-
runner.run(function(failureCount) {
126-
sinon.restore();
127-
expect(runner, 'to satisfy', {
128-
testResults: {
129-
failures: [
130-
{
131-
title: testTitle,
132-
file: testFile,
133-
err: {
134-
message: error.message
135-
}
136-
}
137-
]
138-
}
176+
it('should write test results to file', function(done) {
177+
const fsMkdirSync = sinon.stub(fs, 'mkdirSync');
178+
const fsWriteFileSync = sinon.stub(fs, 'writeFileSync');
179+
180+
fsWriteFileSync.callsFake(function(filename, content) {
181+
const expectedJson = JSON.stringify(runner.testResults, null, 2);
182+
expect(expectedFileName, 'to be', filename);
183+
expect(content, 'to be', expectedJson);
139184
});
140-
expect(failureCount, 'to be', 1);
141-
done();
185+
186+
runner.run(function() {
187+
expect(
188+
fsMkdirSync.calledWith(expectedDirName, {recursive: true}),
189+
'to be true'
190+
);
191+
expect(fsWriteFileSync.calledOnce, 'to be true');
192+
done();
193+
});
194+
});
195+
196+
it('should warn and write test results to console', function(done) {
197+
const fsMkdirSync = sinon.stub(fs, 'mkdirSync');
198+
const fsWriteFileSync = sinon.stub(fs, 'writeFileSync');
199+
200+
fsWriteFileSync.throws('unable to write file');
201+
202+
const outLog = [];
203+
const fake = chunk => outLog.push(chunk);
204+
sinon.stub(process.stderr, 'write').callsFake(fake);
205+
sinon.stub(process.stdout, 'write').callsFake(fake);
206+
207+
runner.run(function() {
208+
sinon.restore();
209+
expect(
210+
fsMkdirSync.calledWith(expectedDirName, {recursive: true}),
211+
'to be true'
212+
);
213+
expect(fsWriteFileSync.calledOnce, 'to be true');
214+
expect(
215+
outLog[0],
216+
'to contain',
217+
`[mocha] writing output to "${expectedFileName}" failed:`
218+
);
219+
expect(outLog[1], 'to match', /"fullTitle": "JSON suite json test 1"/);
220+
done();
221+
});
222+
});
223+
224+
it('should throw "unsupported error" in browser', function() {
225+
sinon.stub(utils, 'isBrowser').callsFake(() => true);
226+
expect(
227+
() => new JSONReporter(runner, options),
228+
'to throw',
229+
'file output not supported in browser'
230+
);
142231
});
143232
});
144233
});

0 commit comments

Comments
 (0)
Please sign in to comment.