Skip to content

Commit 98a4fbf

Browse files
levithomasondignifiedquire
authored andcommitted
feat(reporter): add config formatError function
Allows a `formatError` config function. The function receives the entire error message as its only argument. It returns the new error message. Fixes #2119.
1 parent 96f8f14 commit 98a4fbf

File tree

9 files changed

+116
-12
lines changed

9 files changed

+116
-12
lines changed

docs/config/01-configuration-file.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -588,6 +588,23 @@ Additional reporters, such as `growl`, `junit`, `teamcity` or `coverage` can be
588588
Note: Just about all additional reporters in Karma (other than progress) require an additional library to be installed (via NPM).
589589

590590

591+
## formatError
592+
**Type:** Function
593+
594+
**Default:** `undefined`
595+
596+
**CLI:** `--format-error ./path/to/formatFunction.js`
597+
598+
**Arguments:**
599+
600+
* `msg` - The entire assertion error and stack trace as a string.
601+
602+
**Returns:** A new error message string.
603+
604+
**Description:** Format assertion errors and stack traces. Useful for removing vendors and compiled sources.
605+
606+
The CLI option should be a path to a file that exports the format function. This can be a function exported at the root of the module or an export named `formatError`.
607+
591608
## restartOnFileChange
592609
**Type:** Boolean
593610

lib/cli.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,20 @@ var processArgs = function (argv, options, fs, path) {
4141
options.failOnEmptyTestSuite = options.failOnEmptyTestSuite === 'true'
4242
}
4343

44+
if (helper.isString(options.formatError)) {
45+
try {
46+
var required = require(options.formatError)
47+
} catch (err) {
48+
console.error('Could not require formatError: ' + options.formatError, err)
49+
}
50+
// support exports.formatError and module.exports = function
51+
options.formatError = required.formatError || required
52+
if (!helper.isFunction(options.formatError)) {
53+
console.error('Format error must be a function, got: ' + typeof options.formatError)
54+
process.exit(1)
55+
}
56+
}
57+
4458
if (helper.isString(options.logLevel)) {
4559
var logConstant = constant['LOG_' + options.logLevel.toUpperCase()]
4660
if (helper.isDefined(logConstant)) {

lib/config.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,10 @@ var normalizeConfig = function (config, configFilePath) {
165165
throw new TypeError('Invalid configuration: browsers option must be an array')
166166
}
167167

168+
if (config.formatError && !helper.isFunction(config.formatError)) {
169+
throw new TypeError('Invalid configuration: formatError option must be a function.')
170+
}
171+
168172
var defaultClient = config.defaultClient || {}
169173
Object.keys(defaultClient).forEach(function (key) {
170174
var option = config.client[key]

lib/reporter.js

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ var log = require('./logger').create('reporter')
88
var MultiReporter = require('./reporters/multi')
99
var baseReporterDecoratorFactory = require('./reporters/base').decoratorFactory
1010

11-
var createErrorFormatter = function (basePath, emitter, SourceMapConsumer) {
11+
var createErrorFormatter = function (config, emitter, SourceMapConsumer) {
12+
var basePath = config.basePath
1213
var lastServedFiles = []
1314

1415
emitter.on('file_list_modified', function (files) {
@@ -92,12 +93,17 @@ var createErrorFormatter = function (basePath, emitter, SourceMapConsumer) {
9293
msg = indentation + msg.replace(/\n/g, '\n' + indentation)
9394
}
9495

96+
// allow the user to format the error
97+
if (config.formatError) {
98+
msg = config.formatError(msg)
99+
}
100+
95101
return msg + '\n'
96102
}
97103
}
98104

99105
var createReporters = function (names, config, emitter, injector) {
100-
var errorFormatter = createErrorFormatter(config.basePath, emitter, SourceMapConsumer)
106+
var errorFormatter = createErrorFormatter(config, emitter, SourceMapConsumer)
101107
var reporters = []
102108

103109
// TODO(vojta): instantiate all reporters through DI

test/unit/cli.spec.js

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,18 @@ describe('cli', () => {
133133
expect(mockery.process.exit).to.have.been.calledWith(1)
134134
})
135135

136+
it('should parse format-error into a function', () => {
137+
// root export
138+
var options = processArgs(['--format-error', '../../test/unit/fixtures/format-error-root'])
139+
var formatErrorRoot = require('../../test/unit/fixtures/format-error-root')
140+
expect(options.formatError).to.equal(formatErrorRoot)
141+
142+
// property export
143+
options = processArgs(['--format-error', '../../test/unit/fixtures/format-error-property'])
144+
var formatErrorProperty = require('../../test/unit/fixtures/format-error-property').formatError
145+
expect(options.formatError).to.equal(formatErrorProperty)
146+
})
147+
136148
it('should parse browsers into an array', () => {
137149
var options = processArgs(['--browsers', 'Chrome,ChromeCanary,Firefox'])
138150
expect(options.browsers).to.deep.equal(['Chrome', 'ChromeCanary', 'Firefox'])

test/unit/config.spec.js

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -331,6 +331,16 @@ describe('config', () => {
331331

332332
expect(invalid).to.throw('Invalid configuration: browsers option must be an array')
333333
})
334+
335+
it('should validate that the formatError option is a function', () => {
336+
var invalid = function () {
337+
normalizeConfigWithDefaults({
338+
formatError: 'lodash/identity'
339+
})
340+
}
341+
342+
expect(invalid).to.throw('Invalid configuration: formatError option must be a function.')
343+
})
334344
})
335345

336346
describe('createPatternObject', () => {
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
exports.formatError = function formatErrorProperty (msg) {
2+
return msg
3+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// a valid --format-error file
2+
module.exports = function formatErrorRoot (msg) {
3+
return msg
4+
}

test/unit/reporter.spec.js

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {EventEmitter} from 'events'
22
import {loadFile} from 'mocks'
33
import path from 'path'
44
import _ from 'lodash'
5+
import sinon from 'sinon'
56

67
import File from '../../lib/file'
78

@@ -15,10 +16,43 @@ describe('reporter', () => {
1516
describe('formatError', () => {
1617
var emitter
1718
var formatError = emitter = null
19+
var sandbox
1820

1921
beforeEach(() => {
2022
emitter = new EventEmitter()
21-
formatError = m.createErrorFormatter('', emitter)
23+
formatError = m.createErrorFormatter({ basePath: '' }, emitter)
24+
sandbox = sinon.sandbox.create()
25+
})
26+
27+
it('should call config.formatError if defined', () => {
28+
var spy = sandbox.spy()
29+
formatError = m.createErrorFormatter({ basePath: '', formatError: spy }, emitter)
30+
formatError()
31+
32+
expect(spy).to.have.been.calledOnce
33+
})
34+
35+
it('should not call config.formatError if not defined', () => {
36+
var spy = sandbox.spy()
37+
formatError()
38+
39+
expect(spy).not.to.have.been.calledOnce
40+
})
41+
42+
it('should pass the error message as the first config.formatError argument', () => {
43+
var ERROR = 'foo bar'
44+
var spy = sandbox.spy()
45+
formatError = m.createErrorFormatter({ basePath: '', formatError: spy }, emitter)
46+
formatError(ERROR)
47+
48+
expect(spy.firstCall.args[0]).to.equal(ERROR)
49+
})
50+
51+
it('should display the error returned by config.formatError', () => {
52+
var formattedError = 'A new error'
53+
formatError = m.createErrorFormatter({ basePath: '', formatError: () => formattedError }, emitter)
54+
55+
expect(formatError('Something', '\t')).to.equal(formattedError + '\n')
2256
})
2357

2458
it('should indent', () => {
@@ -51,7 +85,7 @@ describe('reporter', () => {
5185

5286
// TODO(vojta): enable once we serve source under urlRoot
5387
it.skip('should handle non default karma service folders', () => {
54-
formatError = m.createErrorFormatter('', '/_karma_/')
88+
formatError = m.createErrorFormatter({ basePath: '' }, '/_karma_/')
5589
expect(formatError('file http://localhost:8080/_karma_/base/usr/a.js and http://127.0.0.1:8080/_karma_/base/home/b.js')).to.be.equal('file usr/a.js and home/b.js\n')
5690
})
5791

@@ -65,7 +99,7 @@ describe('reporter', () => {
6599
})
66100

67101
it('should restore base paths', () => {
68-
formatError = m.createErrorFormatter('/some/base', emitter)
102+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter)
69103
expect(formatError('at http://localhost:123/base/a.js?123')).to.equal('at a.js\n')
70104
})
71105

@@ -121,7 +155,7 @@ describe('reporter', () => {
121155
MockSourceMapConsumer.LEAST_UPPER_BOUND = 2
122156

123157
it('should rewrite stack traces', (done) => {
124-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
158+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
125159
var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')]
126160
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'}
127161
servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'}
@@ -136,7 +170,7 @@ describe('reporter', () => {
136170
})
137171

138172
it('should rewrite stack traces to the first column when no column is given', (done) => {
139-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
173+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
140174
var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')]
141175
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'}
142176
servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'}
@@ -151,7 +185,7 @@ describe('reporter', () => {
151185
})
152186

153187
it('should rewrite relative url stack traces', (done) => {
154-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
188+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
155189
var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')]
156190
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'}
157191
servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'}
@@ -167,7 +201,7 @@ describe('reporter', () => {
167201

168202
it('should resolve relative urls from source maps', (done) => {
169203
sourceMappingPath = 'original/' // Note: relative path.
170-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
204+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
171205
var servedFiles = [new File('/some/base/path/a.js')]
172206
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.fancyjs'}
173207

@@ -181,7 +215,7 @@ describe('reporter', () => {
181215
})
182216

183217
it('should fall back to non-source-map format if originalPositionFor throws', (done) => {
184-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
218+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
185219
var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')]
186220
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'}
187221
servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'}
@@ -196,7 +230,7 @@ describe('reporter', () => {
196230
})
197231

198232
it('should not try to use source maps when no line is given', (done) => {
199-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
233+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
200234
var servedFiles = [new File('/some/base/a.js'), new File('/some/base/b.js')]
201235
servedFiles[0].sourceMap = {content: 'SOURCE MAP a.js'}
202236
servedFiles[1].sourceMap = {content: 'SOURCE MAP b.js'}
@@ -216,7 +250,7 @@ describe('reporter', () => {
216250
var servedFiles = null
217251

218252
beforeEach(() => {
219-
formatError = m.createErrorFormatter('/some/base', emitter, MockSourceMapConsumer)
253+
formatError = m.createErrorFormatter({ basePath: '/some/base' }, emitter, MockSourceMapConsumer)
220254
servedFiles = [new File('C:/a/b/c.js')]
221255
servedFiles[0].sourceMap = {content: 'SOURCE MAP b.js'}
222256
})

0 commit comments

Comments
 (0)