UNPKG

10.2 kBJavaScriptView Raw
1var utils = require('./utils');
2var defaultDepth = require('./defaultDepth');
3var useFullStackTrace = require('./useFullStackTrace');
4
5var errorMethodBlacklist = [
6 'message',
7 'line',
8 'sourceId',
9 'sourceURL',
10 'stack',
11 'stackArray'
12].reduce(function (result, prop) {
13 result[prop] = true;
14 return result;
15}, {});
16
17function UnexpectedError(expect, parent) {
18 this.errorMode = (expect && expect.errorMode) || 'default';
19 var base = Error.call(this, '');
20
21 if (Error.captureStackTrace) {
22 Error.captureStackTrace(this, UnexpectedError);
23 } else {
24 // Throw the error to make sure it has its stack serialized:
25 try {
26 throw base;
27 } catch (err) {}
28 this.stack = base.stack;
29 }
30
31 // Prevent flooding the screen in node-tap
32 // https://github.com/unexpectedjs/unexpected/issues/582
33 Object.defineProperties(this, {
34 expect: {
35 enumerable: false,
36 value: expect
37 }
38 });
39 this.parent = parent || null;
40 this.name = 'UnexpectedError';
41}
42
43UnexpectedError.prototype = Object.create(Error.prototype);
44
45UnexpectedError.prototype.useFullStackTrace = useFullStackTrace;
46
47var missingOutputMessage =
48 'You must either provide a format or a magicpen instance';
49UnexpectedError.prototype.outputFromOptions = function(options) {
50 if (!options) {
51 throw new Error(missingOutputMessage);
52 }
53
54 if (typeof options === 'string') {
55 return this.expect.createOutput(options);
56 }
57
58 if (options.isMagicPen) {
59 return options.clone();
60 }
61
62 if (options.output) {
63 return options.output.clone();
64 }
65
66 if (options.format) {
67 return this.expect.createOutput(options.format);
68 }
69
70 throw new Error(missingOutputMessage);
71};
72
73UnexpectedError.prototype._isUnexpected = true;
74UnexpectedError.prototype.isUnexpected = true;
75UnexpectedError.prototype.buildDiff = function(options) {
76 var output = this.outputFromOptions(options);
77 var expect = this.expect;
78 return (
79 this.createDiff &&
80 this.createDiff(
81 output,
82 function (actual, expected) { return expect.diff(actual, expected, output.clone()); },
83 function (v, depth) { return output.clone().appendInspected(v, (depth || defaultDepth) - 1); },
84 function (actual, expected) { return expect.equal(actual, expected); }
85 )
86 );
87};
88
89UnexpectedError.prototype.getDefaultErrorMessage = function(options) {
90 var output = this.outputFromOptions(options);
91 if (this.expect.testDescription) {
92 output.append(this.expect.standardErrorMessage(output.clone(), options));
93 } else if (typeof this.output === 'function') {
94 this.output.call(output, output);
95 }
96
97 var errorWithDiff = this;
98 while (!errorWithDiff.createDiff && errorWithDiff.parent) {
99 errorWithDiff = errorWithDiff.parent;
100 }
101
102 if (errorWithDiff && errorWithDiff.createDiff) {
103 var comparison = errorWithDiff.buildDiff(options);
104 if (comparison) {
105 output.nl(2).append(comparison);
106 }
107 }
108
109 return output;
110};
111
112UnexpectedError.prototype.getNestedErrorMessage = function(options) {
113 var output = this.outputFromOptions(options);
114 if (this.expect.testDescription) {
115 output.append(this.expect.standardErrorMessage(output.clone(), options));
116 } else if (typeof this.output === 'function') {
117 this.output.call(output, output);
118 }
119
120 var parent = this.parent;
121 while (parent.getErrorMode() === 'bubble') {
122 parent = parent.parent;
123 }
124
125 if (typeof options === 'string') {
126 options = { format: options };
127 } else if (options && options.isMagicPen) {
128 options = { output: options };
129 }
130
131 output
132 .nl()
133 .indentLines()
134 .i()
135 .block(
136 parent.getErrorMessage(
137 utils.extend({}, options || {}, {
138 compact: this.expect.subject === parent.expect.subject
139 })
140 )
141 );
142 return output;
143};
144
145UnexpectedError.prototype.getDefaultOrNestedMessage = function(options) {
146 if (this.hasDiff()) {
147 return this.getDefaultErrorMessage(options);
148 } else {
149 return this.getNestedErrorMessage(options);
150 }
151};
152
153UnexpectedError.prototype.hasDiff = function() {
154 return !!this.getDiffMethod();
155};
156
157UnexpectedError.prototype.getDiffMethod = function() {
158 var errorWithDiff = this;
159 while (!errorWithDiff.createDiff && errorWithDiff.parent) {
160 errorWithDiff = errorWithDiff.parent;
161 }
162
163 return (errorWithDiff && errorWithDiff.createDiff) || null;
164};
165
166UnexpectedError.prototype.getDiff = function(options) {
167 var errorWithDiff = this;
168 while (!errorWithDiff.createDiff && errorWithDiff.parent) {
169 errorWithDiff = errorWithDiff.parent;
170 }
171
172 return errorWithDiff && errorWithDiff.buildDiff(options);
173};
174
175UnexpectedError.prototype.getDiffMessage = function(options) {
176 var output = this.outputFromOptions(options);
177 var comparison = this.getDiff(options);
178 if (comparison) {
179 output.append(comparison);
180 } else if (this.expect.testDescription) {
181 output.append(this.expect.standardErrorMessage(output.clone(), options));
182 } else if (typeof this.output === 'function') {
183 this.output.call(output, output);
184 }
185 return output;
186};
187
188UnexpectedError.prototype.getErrorMode = function() {
189 if (!this.parent) {
190 switch (this.errorMode) {
191 case 'default':
192 case 'bubbleThrough':
193 return this.errorMode;
194 default:
195 return 'default';
196 }
197 } else {
198 return this.errorMode;
199 }
200};
201
202UnexpectedError.prototype.getErrorMessage = function(options) {
203 // Search for any parent error that has an error mode of 'bubbleThrough' through on the
204 // error these should be bubbled to the top
205 var errorWithBubbleThrough = this.parent;
206 while (
207 errorWithBubbleThrough &&
208 errorWithBubbleThrough.getErrorMode() !== 'bubbleThrough'
209 ) {
210 errorWithBubbleThrough = errorWithBubbleThrough.parent;
211 }
212 if (errorWithBubbleThrough) {
213 return errorWithBubbleThrough.getErrorMessage(options);
214 }
215
216 var errorMode = this.getErrorMode();
217 switch (errorMode) {
218 case 'nested':
219 return this.getNestedErrorMessage(options);
220 case 'default':
221 return this.getDefaultErrorMessage(options);
222 case 'bubbleThrough':
223 return this.getDefaultErrorMessage(options);
224 case 'bubble':
225 return this.parent.getErrorMessage(options);
226 case 'diff':
227 return this.getDiffMessage(options);
228 case 'defaultOrNested':
229 return this.getDefaultOrNestedMessage(options);
230 default:
231 throw new Error(("Unknown error mode: '" + errorMode + "'"));
232 }
233};
234
235function findStackStart(lines) {
236 for (var i = lines.length - 1; i >= 0; i -= 1) {
237 if (lines[i] === '') {
238 return i + 1;
239 }
240 }
241
242 return -1;
243}
244
245UnexpectedError.prototype.serializeMessage = function(outputFormat) {
246 if (!this._hasSerializedErrorMessage) {
247 var htmlFormat = outputFormat === 'html';
248 if (htmlFormat) {
249 if (!('htmlMessage' in this)) {
250 this.htmlMessage = this.getErrorMessage({ format: 'html' }).toString();
251 }
252 }
253
254 this.message = "\n" + (this.getErrorMessage({
255 format: htmlFormat ? 'text' : outputFormat
256 }).toString()) + "\n";
257
258 if (
259 this.originalError &&
260 this.originalError instanceof Error &&
261 typeof this.originalError.stack === 'string'
262 ) {
263 // The stack of the original error looks like this:
264 // <constructor name>: <error message>\n<actual stack trace>
265 // Try to get hold of <actual stack trace> and append it
266 // to the error message of this error:
267 var index = this.originalError.stack.indexOf(
268 this.originalError.message
269 );
270 if (index === -1) {
271 // Phantom.js doesn't include the error message in the stack property
272 this.stack = (this.message) + "\n" + (this.originalError.stack);
273 } else {
274 this.stack =
275 this.message +
276 this.originalError.stack.substr(
277 index + this.originalError.message.length
278 );
279 }
280 } else if (/^(Unexpected)?Error:?\n/.test(this.stack)) {
281 // Fix for Jest that does not seem to capture the error message
282 var matchErrorName = /^(?:Unexpected)?Error:?\n/.exec(this.stack);
283 if (matchErrorName) {
284 this.stack = this.message + this.stack.substr(matchErrorName[0].length);
285 }
286 }
287
288 if (this.stack && !this.useFullStackTrace) {
289 var lines = this.stack.split(/\n/);
290 var stackStart = findStackStart(lines);
291
292 var newStack = lines.filter(
293 function (line, i) { return i < stackStart ||
294 (!/node_modules[/\\]unexpected(?:-[^/\\]+)?[/\\]/.test(line) &&
295 !/executeExpect.*node_modules[/\\]unexpected[/\\]/.test(
296 lines[i + 1]
297 )); }
298 );
299
300 if (newStack.length !== lines.length) {
301 var indentation = /^(\s*)/.exec(lines[lines.length - 1])[1];
302
303 if (outputFormat === 'html') {
304 newStack.push(
305 (indentation + "set the query parameter full-trace=true to see the full stack trace")
306 );
307 } else {
308 newStack.push(
309 (indentation + "set UNEXPECTED_FULL_TRACE=true to see the full stack trace")
310 );
311 }
312 }
313
314 this.stack = newStack.join('\n');
315 }
316
317 this._hasSerializedErrorMessage = true;
318 }
319};
320
321UnexpectedError.prototype.clone = function() {
322 var that = this;
323 var newError = new UnexpectedError(this.expect);
324 Object.keys(that).forEach(function (key) {
325 if (!errorMethodBlacklist[key]) {
326 newError[key] = that[key];
327 }
328 });
329 return newError;
330};
331
332UnexpectedError.prototype.getLabel = function() {
333 var currentError = this;
334 while (currentError && !currentError.label) {
335 currentError = currentError.parent;
336 }
337 return (currentError && currentError.label) || null;
338};
339
340UnexpectedError.prototype.getParents = function() {
341 var result = [];
342 var parent = this.parent;
343 while (parent) {
344 result.push(parent);
345 parent = parent.parent;
346 }
347 return result;
348};
349
350UnexpectedError.prototype.getAllErrors = function() {
351 var result = this.getParents();
352 result.unshift(this);
353 return result;
354};
355
356if (Object.__defineGetter__) {
357 Object.defineProperty(UnexpectedError.prototype, 'htmlMessage', {
358 enumerable: true,
359 get: function get() {
360 return this.getErrorMessage({ format: 'html' }).toString();
361 }
362 });
363}
364
365module.exports = UnexpectedError;