UNPKG

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