ava 0.16.0 → 0.18.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,308 +1,311 @@
1
1
  'use strict';
2
- var StringDecoder = require('string_decoder').StringDecoder;
3
- var cliCursor = require('cli-cursor');
4
- var lastLineTracker = require('last-line-stream/tracker');
5
- var plur = require('plur');
6
- var spinners = require('cli-spinners');
7
- var chalk = require('chalk');
8
- var cliTruncate = require('cli-truncate');
9
- var cross = require('figures').cross;
10
- var repeating = require('repeating');
11
- var objectAssign = require('object-assign');
12
- var colors = require('../colors');
13
-
14
- chalk.enabled = true;
15
- Object.keys(colors).forEach(function (key) {
16
- colors[key].enabled = true;
17
- });
18
-
19
- function MiniReporter(options) {
20
- if (!(this instanceof MiniReporter)) {
21
- return new MiniReporter(options);
22
- }
23
-
24
- var spinnerDef = spinners[process.platform === 'win32' ? 'line' : 'dots'];
25
- this.spinnerFrames = spinnerDef.frames.map(function (c) {
26
- return chalk.gray.dim(c);
27
- });
28
- this.spinnerInterval = spinnerDef.interval;
29
-
30
- this.options = objectAssign({}, options);
2
+ const StringDecoder = require('string_decoder').StringDecoder;
3
+ const cliCursor = require('cli-cursor');
4
+ const lastLineTracker = require('last-line-stream/tracker');
5
+ const plur = require('plur');
6
+ const spinners = require('cli-spinners');
7
+ const chalk = require('chalk');
8
+ const cliTruncate = require('cli-truncate');
9
+ const cross = require('figures').cross;
10
+ const indentString = require('indent-string');
11
+ const formatAssertError = require('../format-assert-error');
12
+ const extractStack = require('../extract-stack');
13
+ const codeExcerpt = require('../code-excerpt');
14
+ const colors = require('../colors');
31
15
 
32
- this.reset();
33
- this.stream = process.stderr;
34
- this.stringDecoder = new StringDecoder();
35
- }
16
+ // TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution.
17
+ const CSI = '\u001b[';
18
+ const ERASE_LINE = CSI + '2K';
19
+ const CURSOR_TO_COLUMN_0 = CSI + '0G';
20
+ const CURSOR_UP = CSI + '1A';
36
21
 
37
- module.exports = MiniReporter;
22
+ // Returns a string that will erase `count` lines from the end of the terminal.
23
+ function eraseLines(count) {
24
+ let clear = '';
38
25
 
39
- MiniReporter.prototype.start = function () {
40
- var self = this;
41
-
42
- this.interval = setInterval(function () {
43
- self.spinnerIndex = (self.spinnerIndex + 1) % self.spinnerFrames.length;
44
- self.write(self.prefix());
45
- }, this.spinnerInterval);
46
-
47
- return this.prefix('');
48
- };
49
-
50
- MiniReporter.prototype.reset = function () {
51
- this.clearInterval();
52
- this.passCount = 0;
53
- this.knownFailureCount = 0;
54
- this.failCount = 0;
55
- this.skipCount = 0;
56
- this.todoCount = 0;
57
- this.rejectionCount = 0;
58
- this.exceptionCount = 0;
59
- this.currentStatus = '';
60
- this.currentTest = '';
61
- this.statusLineCount = 0;
62
- this.spinnerIndex = 0;
63
- this.lastLineTracker = lastLineTracker();
64
- };
65
-
66
- MiniReporter.prototype.spinnerChar = function () {
67
- return this.spinnerFrames[this.spinnerIndex];
68
- };
69
-
70
- MiniReporter.prototype.clearInterval = function () {
71
- clearInterval(this.interval);
72
- this.interval = null;
73
- };
74
-
75
- MiniReporter.prototype.test = function (test) {
76
- if (test.todo) {
77
- this.todoCount++;
78
- } else if (test.skip) {
79
- this.skipCount++;
80
- } else if (test.error) {
81
- this.failCount++;
82
- } else {
83
- this.passCount++;
84
- if (test.failing) {
85
- this.knownFailureCount++;
86
- }
26
+ for (let i = 0; i < count; i++) {
27
+ clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : '');
87
28
  }
88
29
 
89
- if (test.todo || test.skip) {
90
- return;
30
+ if (count) {
31
+ clear += CURSOR_TO_COLUMN_0;
91
32
  }
92
33
 
93
- return this.prefix(this._test(test));
94
- };
34
+ return clear;
35
+ }
95
36
 
96
- MiniReporter.prototype.prefix = function (str) {
97
- str = str || this.currentTest;
98
- this.currentTest = str;
37
+ class MiniReporter {
38
+ constructor(options) {
39
+ this.options = Object.assign({}, options);
99
40
 
100
- // The space before the newline is required for proper formatting. (Not sure why).
101
- return ' \n ' + this.spinnerChar() + ' ' + str;
102
- };
41
+ chalk.enabled = this.options.color;
42
+ for (const key of Object.keys(colors)) {
43
+ colors[key].enabled = this.options.color;
44
+ }
103
45
 
104
- MiniReporter.prototype._test = function (test) {
105
- var SPINNER_WIDTH = 3;
106
- var PADDING = 1;
107
- var title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING);
46
+ const spinnerDef = spinners[process.platform === 'win32' ? 'line' : 'dots'];
47
+ this.spinnerFrames = spinnerDef.frames.map(c => chalk.gray.dim(c));
48
+ this.spinnerInterval = spinnerDef.interval;
108
49
 
109
- if (test.error || test.failing) {
110
- title = colors.error(test.title);
50
+ this.reset();
51
+ this.stream = process.stderr;
52
+ this.stringDecoder = new StringDecoder();
111
53
  }
54
+ start() {
55
+ this.interval = setInterval(() => {
56
+ this.spinnerIndex = (this.spinnerIndex + 1) % this.spinnerFrames.length;
57
+ this.write(this.prefix());
58
+ }, this.spinnerInterval);
112
59
 
113
- return title + '\n' + this.reportCounts();
114
- };
115
-
116
- MiniReporter.prototype.unhandledError = function (err) {
117
- if (err.type === 'exception') {
118
- this.exceptionCount++;
119
- } else {
120
- this.rejectionCount++;
60
+ return this.prefix('');
61
+ }
62
+ reset() {
63
+ this.clearInterval();
64
+ this.passCount = 0;
65
+ this.knownFailureCount = 0;
66
+ this.failCount = 0;
67
+ this.skipCount = 0;
68
+ this.todoCount = 0;
69
+ this.rejectionCount = 0;
70
+ this.exceptionCount = 0;
71
+ this.currentStatus = '';
72
+ this.currentTest = '';
73
+ this.statusLineCount = 0;
74
+ this.spinnerIndex = 0;
75
+ this.lastLineTracker = lastLineTracker();
121
76
  }
122
- };
123
-
124
- MiniReporter.prototype.reportCounts = function (time) {
125
- var lines = [
126
- this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '',
127
- this.knownFailureCount > 0 ? '\n ' + colors.error(this.knownFailureCount, plur('known failure', this.knownFailureCount)) : '',
128
- this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '',
129
- this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '',
130
- this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : ''
131
- ].filter(Boolean);
132
-
133
- if (time && lines.length > 0) {
134
- lines[0] += ' ' + time;
77
+ spinnerChar() {
78
+ return this.spinnerFrames[this.spinnerIndex];
135
79
  }
80
+ clearInterval() {
81
+ clearInterval(this.interval);
82
+ this.interval = null;
83
+ }
84
+ test(test) {
85
+ if (test.todo) {
86
+ this.todoCount++;
87
+ } else if (test.skip) {
88
+ this.skipCount++;
89
+ } else if (test.error) {
90
+ this.failCount++;
91
+ } else {
92
+ this.passCount++;
93
+ if (test.failing) {
94
+ this.knownFailureCount++;
95
+ }
96
+ }
136
97
 
137
- return lines.join('');
138
- };
98
+ if (test.todo || test.skip) {
99
+ return;
100
+ }
139
101
 
140
- MiniReporter.prototype.finish = function (runStatus) {
141
- this.clearInterval();
142
- var time;
102
+ return this.prefix(this._test(test));
103
+ }
104
+ prefix(str) {
105
+ str = str || this.currentTest;
106
+ this.currentTest = str;
143
107
 
144
- if (this.options.watching) {
145
- time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
108
+ // The space before the newline is required for proper formatting
109
+ // TODO(jamestalmage): Figure out why it's needed and document it here
110
+ return ` \n ${this.spinnerChar()} ${str}`;
146
111
  }
112
+ _test(test) {
113
+ const SPINNER_WIDTH = 3;
114
+ const PADDING = 1;
115
+ let title = cliTruncate(test.title, process.stdout.columns - SPINNER_WIDTH - PADDING);
147
116
 
148
- var status = this.reportCounts(time);
117
+ if (test.error || test.failing) {
118
+ title = colors.error(test.title);
119
+ }
149
120
 
150
- if (this.rejectionCount > 0) {
151
- status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
121
+ return title + '\n' + this.reportCounts();
152
122
  }
153
-
154
- if (this.exceptionCount > 0) {
155
- status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount));
123
+ unhandledError(err) {
124
+ if (err.type === 'exception') {
125
+ this.exceptionCount++;
126
+ } else {
127
+ this.rejectionCount++;
128
+ }
156
129
  }
130
+ reportCounts(time) {
131
+ const lines = [
132
+ this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '',
133
+ this.knownFailureCount > 0 ? '\n ' + colors.error(this.knownFailureCount, plur('known failure', this.knownFailureCount)) : '',
134
+ this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '',
135
+ this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '',
136
+ this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : ''
137
+ ].filter(Boolean);
138
+
139
+ if (time && lines.length > 0) {
140
+ lines[0] += ' ' + time;
141
+ }
157
142
 
158
- if (runStatus.previousFailCount > 0) {
159
- status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun');
143
+ return lines.join('');
160
144
  }
145
+ finish(runStatus) {
146
+ this.clearInterval();
147
+ let time;
161
148
 
162
- var i = 0;
163
-
164
- if (this.knownFailureCount > 0) {
165
- runStatus.knownFailures.forEach(function (test) {
166
- i++;
167
-
168
- var title = test.title;
149
+ if (this.options.watching) {
150
+ time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
151
+ }
169
152
 
170
- status += '\n\n\n ' + colors.error(i + '.', title);
171
- // TODO output description with link
172
- // status += colors.stack(description);
173
- });
174
- }
153
+ let status = this.reportCounts(time);
175
154
 
176
- if (this.failCount > 0) {
177
- runStatus.errors.forEach(function (test) {
178
- if (!test.error || !test.error.message) {
179
- return;
180
- }
155
+ if (this.rejectionCount > 0) {
156
+ status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
157
+ }
181
158
 
182
- i++;
159
+ if (this.exceptionCount > 0) {
160
+ status += '\n ' + colors.error(this.exceptionCount, plur('exception', this.exceptionCount));
161
+ }
183
162
 
184
- var title = test.error ? test.title : 'Unhandled Error';
185
- var description;
163
+ if (runStatus.previousFailCount > 0) {
164
+ status += '\n ' + colors.error(runStatus.previousFailCount, 'previous', plur('failure', runStatus.previousFailCount), 'in test files that were not rerun');
165
+ }
186
166
 
187
- if (test.error) {
188
- description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack).trimRight();
189
- } else {
190
- description = JSON.stringify(test);
167
+ if (this.knownFailureCount > 0) {
168
+ for (const test of runStatus.knownFailures) {
169
+ const title = test.title;
170
+ status += '\n\n ' + colors.title(title);
171
+ // TODO: Output description with link
172
+ // status += colors.stack(description);
191
173
  }
174
+ }
192
175
 
193
- status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
194
- status += colors.stack(description);
195
- });
196
- }
176
+ if (this.failCount > 0) {
177
+ runStatus.errors.forEach((test, index) => {
178
+ if (!test.error) {
179
+ return;
180
+ }
181
+
182
+ const title = test.error ? test.title : 'Unhandled Error';
183
+ const beforeSpacing = index === 0 ? '\n\n' : '\n\n\n\n';
184
+
185
+ status += beforeSpacing + ' ' + colors.title(title) + '\n';
186
+ if (test.error.source) {
187
+ status += ' ' + colors.errorSource(test.error.source.file + ':' + test.error.source.line) + '\n';
188
+
189
+ const excerpt = codeExcerpt(test.error.source, {maxWidth: process.stdout.columns});
190
+ if (excerpt) {
191
+ status += '\n' + indentString(excerpt, 2) + '\n';
192
+ }
193
+ }
194
+
195
+ if (test.error.showOutput) {
196
+ status += '\n' + indentString(formatAssertError(test.error), 2);
197
+ }
198
+
199
+ // `.trim()` is needed, because default `err.message` is ' ' (see lib/assert.js)
200
+ if (test.error.message.trim()) {
201
+ status += '\n' + indentString(test.error.message, 2) + '\n';
202
+ }
203
+
204
+ if (test.error.stack) {
205
+ const extracted = extractStack(test.error.stack);
206
+ if (extracted.includes('\n')) {
207
+ status += '\n' + indentString(colors.errorStack(extracted), 2);
208
+ }
209
+ }
210
+ });
211
+ }
197
212
 
198
- if (this.rejectionCount > 0 || this.exceptionCount > 0) {
199
- runStatus.errors.forEach(function (err) {
200
- if (err.title) {
201
- return;
202
- }
213
+ if (this.rejectionCount > 0 || this.exceptionCount > 0) {
214
+ // TODO(sindresorhus): Figure out why this causes a test failure when switched to a for-of loop
215
+ runStatus.errors.forEach(err => {
216
+ if (err.title) {
217
+ return;
218
+ }
219
+
220
+ if (err.type === 'exception' && err.name === 'AvaError') {
221
+ status += '\n\n ' + colors.error(cross + ' ' + err.message);
222
+ } else {
223
+ const title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
224
+ let description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
225
+ description = description.split('\n');
226
+ const errorTitle = err.name ? description[0] : 'Threw non-error: ' + description[0];
227
+ const errorStack = description.slice(1).join('\n');
228
+
229
+ status += '\n\n ' + colors.title(title) + '\n';
230
+ status += ' ' + colors.stack(errorTitle) + '\n';
231
+ status += colors.errorStack(errorStack);
232
+ }
233
+ });
234
+ }
203
235
 
204
- i++;
236
+ if (runStatus.failFastEnabled === true && runStatus.remainingCount > 0 && runStatus.failCount > 0) {
237
+ const remaining = 'At least ' + runStatus.remainingCount + ' ' + plur('test was', 'tests were', runStatus.remainingCount) + ' skipped.';
238
+ status += '\n\n ' + colors.information('`--fail-fast` is on. ' + remaining);
239
+ }
205
240
 
206
- if (err.type === 'exception' && err.name === 'AvaError') {
207
- status += '\n\n\n ' + colors.error(cross + ' ' + err.message);
208
- } else {
209
- var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
210
- var description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
241
+ if (runStatus.hasExclusive === true && runStatus.remainingCount > 0) {
242
+ status += '\n\n ' + colors.information('The .only() modifier is used in some tests.', runStatus.remainingCount, plur('test', runStatus.remainingCount), plur('was', 'were', runStatus.remainingCount), 'not run');
243
+ }
211
244
 
212
- status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
213
- status += ' ' + colors.stack(description);
214
- }
215
- });
245
+ return status + '\n\n';
216
246
  }
217
-
218
- return status + '\n';
219
- };
220
-
221
- MiniReporter.prototype.section = function () {
222
- return '\n' + chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80));
223
- };
224
-
225
- MiniReporter.prototype.clear = function () {
226
- return '';
227
- };
228
-
229
- MiniReporter.prototype.write = function (str) {
230
- cliCursor.hide();
231
- this.currentStatus = str;
232
- this._update();
233
- this.statusLineCount = this.currentStatus.split('\n').length;
234
- };
235
-
236
- MiniReporter.prototype.stdout = MiniReporter.prototype.stderr = function (data) {
237
- this._update(data);
238
- };
239
-
240
- MiniReporter.prototype._update = function (data) {
241
- var str = '';
242
- var ct = this.statusLineCount;
243
- var columns = process.stdout.columns;
244
- var lastLine = this.lastLineTracker.lastLine();
245
-
246
- // Terminals automatically wrap text. We only need the last log line as seen on the screen.
247
- lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns));
248
-
249
- // Don't delete the last log line if it's completely empty.
250
- if (lastLine.length) {
251
- ct++;
247
+ section() {
248
+ return '\n' + chalk.gray.dim('\u2500'.repeat(process.stdout.columns || 80));
252
249
  }
253
-
254
- // Erase the existing status message, plus the last log line.
255
- str += eraseLines(ct);
256
-
257
- // Rewrite the last log line.
258
- str += lastLine;
259
-
260
- if (str.length) {
261
- this.stream.write(str);
250
+ clear() {
251
+ return '';
262
252
  }
263
-
264
- if (data) {
265
- // send new log data to the terminal, and update the last line status.
266
- this.lastLineTracker.update(this.stringDecoder.write(data));
267
- this.stream.write(data);
253
+ write(str) {
254
+ cliCursor.hide();
255
+ this.currentStatus = str;
256
+ this._update();
257
+ this.statusLineCount = this.currentStatus.split('\n').length;
258
+ }
259
+ stdout(data) {
260
+ this._update(data);
261
+ }
262
+ stderr(data) {
263
+ this._update(data);
268
264
  }
265
+ _update(data) {
266
+ let str = '';
267
+ let ct = this.statusLineCount;
268
+ const columns = process.stdout.columns;
269
+ let lastLine = this.lastLineTracker.lastLine();
270
+
271
+ // Terminals automatically wrap text. We only need the last log line as seen on the screen.
272
+ lastLine = lastLine.substring(lastLine.length - (lastLine.length % columns));
273
+
274
+ // Don't delete the last log line if it's completely empty.
275
+ if (lastLine.length > 0) {
276
+ ct++;
277
+ }
269
278
 
270
- var currentStatus = this.currentStatus;
279
+ // Erase the existing status message, plus the last log line.
280
+ str += eraseLines(ct);
271
281
 
272
- if (currentStatus.length) {
273
- lastLine = this.lastLineTracker.lastLine();
274
- // We need a newline at the end of the last log line, before the status message.
275
- // However, if the last log line is the exact width of the terminal a newline is implied,
276
- // and adding a second will cause problems.
277
- if (lastLine.length % columns) {
278
- currentStatus = '\n' + currentStatus;
279
- }
280
- // rewrite the status message.
281
- this.stream.write(currentStatus);
282
- }
283
- };
282
+ // Rewrite the last log line.
283
+ str += lastLine;
284
284
 
285
- // TODO(@jamestalamge): This should be fixed in log-update and ansi-escapes once we are confident it's a good solution.
286
- var CSI = '\u001b[';
287
- var ERASE_LINE = CSI + '2K';
288
- var CURSOR_TO_COLUMN_0 = CSI + '0G';
289
- var CURSOR_UP = CSI + '1A';
285
+ if (str.length > 0) {
286
+ this.stream.write(str);
287
+ }
290
288
 
291
- // Returns a string that will erase `count` lines from the end of the terminal.
292
- function eraseLines(count) {
293
- var clear = '';
289
+ if (data) {
290
+ // Send new log data to the terminal, and update the last line status.
291
+ this.lastLineTracker.update(this.stringDecoder.write(data));
292
+ this.stream.write(data);
293
+ }
294
294
 
295
- for (var i = 0; i < count; i++) {
296
- clear += ERASE_LINE + (i < count - 1 ? CURSOR_UP : '');
297
- }
295
+ let currentStatus = this.currentStatus;
298
296
 
299
- if (count) {
300
- clear += CURSOR_TO_COLUMN_0;
297
+ if (currentStatus.length > 0) {
298
+ lastLine = this.lastLineTracker.lastLine();
299
+ // We need a newline at the end of the last log line, before the status message.
300
+ // However, if the last log line is the exact width of the terminal a newline is implied,
301
+ // and adding a second will cause problems.
302
+ if (lastLine.length % columns) {
303
+ currentStatus = '\n' + currentStatus;
304
+ }
305
+ // Rewrite the status message.
306
+ this.stream.write(currentStatus);
307
+ }
301
308
  }
302
-
303
- return clear;
304
309
  }
305
310
 
306
- function stripFirstLine(message) {
307
- return message.replace(/^[^\n]*\n/, '');
308
- }
311
+ module.exports = MiniReporter;