1 | const utils = require('./utils');
|
2 | const defaultDepth = require('./defaultDepth');
|
3 | const useFullStackTrace = require('./useFullStackTrace');
|
4 |
|
5 | const 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 |
|
17 | function 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 |
|
25 | try {
|
26 | throw base;
|
27 | } catch (err) {}
|
28 | this.stack = base.stack;
|
29 | }
|
30 |
|
31 |
|
32 |
|
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 |
|
43 | UnexpectedError.prototype = Object.create(Error.prototype);
|
44 |
|
45 | UnexpectedError.prototype.useFullStackTrace = useFullStackTrace;
|
46 |
|
47 | const missingOutputMessage =
|
48 | 'You must either provide a format or a magicpen instance';
|
49 | UnexpectedError.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 |
|
73 | UnexpectedError.prototype._isUnexpected = true;
|
74 | UnexpectedError.prototype.isUnexpected = true;
|
75 | UnexpectedError.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 |
|
90 | UnexpectedError.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 |
|
113 | UnexpectedError.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 |
|
146 | UnexpectedError.prototype.getDefaultOrNestedMessage = function(options) {
|
147 | if (this.hasDiff()) {
|
148 | return this.getDefaultErrorMessage(options);
|
149 | } else {
|
150 | return this.getNestedErrorMessage(options);
|
151 | }
|
152 | };
|
153 |
|
154 | UnexpectedError.prototype.hasDiff = function() {
|
155 | return !!this.getDiffMethod();
|
156 | };
|
157 |
|
158 | UnexpectedError.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 |
|
167 | UnexpectedError.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 |
|
176 | UnexpectedError.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 |
|
189 | UnexpectedError.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 |
|
203 | UnexpectedError.prototype.getErrorMessage = function(options) {
|
204 |
|
205 |
|
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 |
|
236 | function 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 |
|
246 | UnexpectedError.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 |
|
265 |
|
266 |
|
267 |
|
268 | const index = this.originalError.stack.indexOf(
|
269 | this.originalError.message
|
270 | );
|
271 | if (index === -1) {
|
272 |
|
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 |
|
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 |
|
323 | UnexpectedError.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 |
|
334 | UnexpectedError.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 |
|
342 | UnexpectedError.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 |
|
352 | UnexpectedError.prototype.getAllErrors = function() {
|
353 | const result = this.getParents();
|
354 | result.unshift(this);
|
355 | return result;
|
356 | };
|
357 |
|
358 | if (Object.__defineGetter__) {
|
359 | Object.defineProperty(UnexpectedError.prototype, 'htmlMessage', {
|
360 | enumerable: true,
|
361 | get() {
|
362 | return this.getErrorMessage({ format: 'html' }).toString();
|
363 | }
|
364 | });
|
365 | }
|
366 |
|
367 | module.exports = UnexpectedError;
|