tape-six 0.9.0

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.
@@ -0,0 +1,270 @@
1
+ import {stringRep, normalizeBox, padBox, padBoxLeft, drawBox, stackHorizontally} from './utils/box.js';
2
+ import {formatNumber, formatTime} from './utils/formatters.js';
3
+
4
+ // colors
5
+
6
+ const join = (...args) => args.filter(value => value).join('');
7
+ const to6 = x => Math.min(5, Math.round((Math.max(0, Math.min(255, x)) / 255) * 6));
8
+ const buildColor = (r, g, b) => 16 + 36 * to6(r) + 6 * to6(g) + to6(b);
9
+
10
+ const successStyle = `\x1B[48;5;${buildColor(0, 32, 0)};1;97m`,
11
+ failureStyle = `\x1B[48;5;${buildColor(64, 0, 0)};1;97m`,
12
+ skippedStyle = `\x1B[48;5;${buildColor(0, 0, 64)};1;97m`,
13
+ reset = '\x1B[0m';
14
+
15
+ // main
16
+
17
+ class TTYReporter {
18
+ constructor({
19
+ output = process.stdout,
20
+ renumberAsserts = false,
21
+ failureOnly = false,
22
+ showBanner = true,
23
+ showTime = true,
24
+ showData = false,
25
+ showAssertNumber = false
26
+ } = {}) {
27
+ if (!output || !output.isTTY) throw Error('Module TTYReporter works only with TTY output streams.');
28
+
29
+ this.output = output;
30
+ this.hasColors = this.output.hasColors(256);
31
+ this.renumberAsserts = renumberAsserts;
32
+ this.failureOnly = failureOnly;
33
+ this.showBanner = showBanner;
34
+ this.showTime = showTime;
35
+ this.showData = showData;
36
+ this.showAssertNumber = showAssertNumber;
37
+
38
+ this.depth = this.assertCounter = this.failedAsserts = this.successfulAsserts = this.skippedAsserts = this.todoAsserts = 0;
39
+ this.testCounter = -1;
40
+ this.technicalDepth = 0;
41
+
42
+ this.lines = 0;
43
+ this.testStack = [];
44
+
45
+ // colors
46
+ this.red = this.paint('\x1B[31m');
47
+ this.green = this.paint('\x1B[92m');
48
+ this.blue = this.paint('\x1B[94m');
49
+ this.yellow = this.paint('\x1B[93m');
50
+ this.blackBg = this.paint('\x1B[40m', '\x1B[49m');
51
+ this.lowWhite = this.paint('\x1B[2;37m', '\x1B[22;39m');
52
+ this.brightWhite = this.paint('\x1B[1;97m', '\x1B[22;39m');
53
+ this.brightYellow = this.paint('\x1B[1;93m', '\x1B[22;39m');
54
+ this.brightRed = this.paint('\x1B[91m');
55
+ this.italic = this.paint('\x1B[3m', '\x1B[23m');
56
+ this.warning = this.paint('\x1B[41;1;37m', '\x1B[22;39;49m');
57
+ this.success = this.paint(successStyle, reset);
58
+ this.failure = this.paint(failureStyle, reset);
59
+ this.skipped = this.paint(skippedStyle, reset);
60
+
61
+ this.out('');
62
+ }
63
+ paint(prefix, suffix = '\x1B[39m') {
64
+ return this.hasColors ? text => join(prefix, text, suffix) : text => text;
65
+ }
66
+ formatValue(value) {
67
+ if (typeof value == 'string') return value;
68
+ if (typeof value.type == 'string') return this.blue(JSON.stringify(value));
69
+ return this.red(this.italic(JSON.stringify(value)));
70
+ }
71
+ out(text) {
72
+ if (this.depth < 2 + this.technicalDepth) {
73
+ this.output.write(text + '\n');
74
+ } else {
75
+ this.output.write(stringRep(this.depth - 1 - this.technicalDepth, ' ') + text + '\n');
76
+ }
77
+ ++this.lines;
78
+ return this;
79
+ }
80
+ report(event) {
81
+ this.output.moveCursor(0, -1);
82
+ this.output.clearLine(0);
83
+ let text;
84
+ switch (event.type) {
85
+ case 'test':
86
+ this.depth > this.technicalDepth && !this.failureOnly && this.out('\u25CB ' + (event.name || 'anonymous test'));
87
+ ++this.depth;
88
+ ++this.testCounter;
89
+ this.testStack.push({name: event.name, lines: this.lines, fail: false});
90
+ break;
91
+ case 'end':
92
+ this.testStack.pop();
93
+ --this.depth;
94
+ if (this.depth > this.technicalDepth) {
95
+ if (this.failureOnly) break;
96
+ text = (event.fail ? '✗' : '✓') + ' ' + (event.name || this.italic('anonymous test'));
97
+ text = event.fail ? this.brightRed(text) : this.green(text);
98
+ text += this.makeState(event.data);
99
+ this.showTime && (text += this.lowWhite(' - ' + formatTime(event.diffTime)));
100
+ this.out(text);
101
+ break;
102
+ }
103
+ if (this.depth) break;
104
+
105
+ // summary
106
+ {
107
+ const state = event.data,
108
+ total = state.asserts - state.skipped,
109
+ success = total - state.failed;
110
+
111
+ if (!this.showBanner) {
112
+ this.out(
113
+ this.blackBg(
114
+ ' ' +
115
+ (event.fail ? '⛔' : '♥️') +
116
+ ' ' +
117
+ this.brightWhite('tests: ' + formatNumber(this.testCounter)) +
118
+ ', ' +
119
+ ('asserts: ' + formatNumber(state.asserts)) +
120
+ ', ' +
121
+ this.green('passed: ' + formatNumber(success)) +
122
+ ', ' +
123
+ this.red('failed: ' + formatNumber(state.failed)) +
124
+ ', ' +
125
+ this.blue('skipped: ' + formatNumber(state.skipped)) +
126
+ ', ' +
127
+ this.brightYellow('todo: ' + formatNumber(this.todoAsserts)) +
128
+ ', ' +
129
+ this.lowWhite('time: ' + formatTime(event.diffTime)) +
130
+ ' '
131
+ )
132
+ );
133
+ return;
134
+ }
135
+
136
+ const paintMethod = event.fail ? 'failure' : 'success';
137
+ let box1 = [event.fail ? 'Need work' : 'All good!'];
138
+ box1 = padBox(box1, 0, 2);
139
+ box1 = drawBox(box1);
140
+ box1 = padBox(box1, 0, 3);
141
+ box1 = normalizeBox(
142
+ [...box1, '', 'Passed: ' + (event.fail ? formatNumber((total > 0 ? success / total : 1) * 100, 1) + '%' : '100%')],
143
+ ' ',
144
+ 'center'
145
+ );
146
+ box1 = padBox(box1, 2, 0);
147
+ box1 = box1.map(s => this[paintMethod](s));
148
+ box1 = padBoxLeft(box1, 2);
149
+
150
+ let box2 = normalizeBox(
151
+ [
152
+ formatNumber(this.testCounter),
153
+ formatNumber(state.asserts),
154
+ formatNumber(success),
155
+ formatNumber(state.failed),
156
+ formatNumber(state.skipped),
157
+ formatNumber(this.todoAsserts),
158
+ formatTime(event.diffTime)
159
+ ],
160
+ ' ',
161
+ 'left'
162
+ );
163
+ box2 = padBoxLeft(box2, 1);
164
+ box2 = stackHorizontally(normalizeBox(['tests:', 'asserts:', ' passed:', ' failed:', ' skipped:', ' todo:', 'time:']), box2);
165
+
166
+ box2[0] = this.brightWhite(box2[0]);
167
+ // box2[1] = this.brightYellow(box2[1]);
168
+ box2[2] = this.green(box2[2]);
169
+ box2[3] = this.red(box2[3]);
170
+ box2[4] = this.blue(box2[4]);
171
+ box2[5] = this.brightYellow(box2[5]);
172
+ box2[6] = this.lowWhite(box2[6]);
173
+
174
+ box2 = padBox(box2, 1, 3);
175
+ box2 = box2.map(s => this.blackBg(s));
176
+
177
+ const box = stackHorizontally(box1, box2);
178
+ this.out('');
179
+ box.forEach(s => this.out(s));
180
+ this.out('');
181
+ }
182
+ return;
183
+ case 'comment':
184
+ !this.failureOnly && this.out(this.blue(this.italic(event.name || 'empty comment')));
185
+ break;
186
+ case 'bail-out':
187
+ {
188
+ text = 'Bail out!';
189
+ event.name && (text += ' ' + event.name);
190
+ let box = [text];
191
+ box = padBox(box, 0, 1);
192
+ box = drawBox(box);
193
+ box = padBox(box, 0, 1);
194
+
195
+ const currentDepth = this.depth;
196
+ this.depth = 0;
197
+ box.forEach(s => this.out(this.warning(s)));
198
+ this.depth = currentDepth;
199
+ }
200
+ break;
201
+ case 'assert':
202
+ const lastTest = this.testStack[this.testStack.length - 1],
203
+ isFailed = event.fail && !event.skip && !event.todo;
204
+ isFailed ? ++this.failedAsserts : ++this.successfulAsserts;
205
+ event.skip && ++this.skippedAsserts;
206
+ event.todo && ++this.todoAsserts;
207
+ if (!isFailed && this.failureOnly) break;
208
+ text = (event.fail ? '✗' : '✓');
209
+ if (this.showAssertNumber) {
210
+ text += ' ' + (this.renumberAsserts ? ++this.assertCounter : event.id);
211
+ }
212
+ if (event.skip) {
213
+ text += ' SKIP';
214
+ } else if (event.todo) {
215
+ text += ' ' + this.brightYellow('TODO');
216
+ }
217
+ event.name && (text += ' ' + event.name);
218
+ if (event.skip) {
219
+ text = this.blue(text);
220
+ } else {
221
+ text = isFailed ? this.red(text) : this.green(text);
222
+ }
223
+ this.showTime && (text += this.lowWhite(' - ' + formatTime(event.diffTime)));
224
+ event.fail && event.at && (text += this.lowWhite(' - ' + event.at));
225
+ if (this.failureOnly && !lastTest.fail) {
226
+ lastTest.fail = true;
227
+ --this.depth;
228
+ this.out(this.brightRed('✗ ' + (lastTest.name || 'anonymous test')));
229
+ ++this.depth;
230
+ }
231
+ this.out(text);
232
+
233
+ if (!event.fail || event.skip || !this.showData) break;
234
+
235
+ this.out(this.lowWhite(' operator: ') + event.operator);
236
+ if (event.hasOwnProperty('expected')) {
237
+ this.out(this.lowWhite(' expected: ') + this.formatValue(event.expected));
238
+ }
239
+ if (event.hasOwnProperty('actual')) {
240
+ this.out(this.lowWhite(' actual: ') + this.formatValue(event.actual));
241
+ }
242
+ const stack = event.actual && event.actual.type === 'Error' && typeof event.actual.stack == 'string' ? event.actual.stack : event.marker.stack;
243
+ if (typeof stack == 'string') {
244
+ this.out(this.lowWhite(' stack: |-'));
245
+ stack.split('\n').forEach(line => this.out(this.lowWhite(' ' + line)));
246
+ }
247
+ break;
248
+ }
249
+ this.showScore();
250
+ }
251
+ showScore() {
252
+ this.out(
253
+ this.success(' ' + this.successfulAsserts + ' ') +
254
+ this.failure(' ' + this.failedAsserts + ' ') +
255
+ (this.skippedAsserts ? this.skipped(' ' + this.skippedAsserts + ' ') : '')
256
+ );
257
+ }
258
+ makeState(state) {
259
+ const success = state.asserts - state.skipped - state.failed;
260
+ if (!success && !state.failed && !state.skipped) return '';
261
+ return (
262
+ ' ' +
263
+ this[success ? 'success' : 'blackBg'](' ' + formatNumber(success) + ' ') +
264
+ this[state.failed ? 'failure' : 'blackBg'](' ' + formatNumber(state.failed) + ' ') +
265
+ (state.skipped ? this.blackBg(this.blue(' ' + formatNumber(state.skipped) + ' ')) : '')
266
+ );
267
+ }
268
+ }
269
+
270
+ export default TTYReporter;
@@ -0,0 +1,126 @@
1
+ import yamlFormatter from './utils/yamlFormatter.js';
2
+
3
+ const formatterOptions = {offset: 2};
4
+
5
+ const styles = {
6
+ info: 'color: #44f; font-style: italic;',
7
+ success: 'color: green;',
8
+ failure: 'color: red;',
9
+ summary: 'font-weight: bold;',
10
+ 'summary-info': 'font-weight: bold; color: #44f; font-style: italic;',
11
+ 'summary-success': 'font-weight: bold; color: green;',
12
+ 'summary-failure': 'font-weight: bold; color: red;',
13
+ 'summary-result-success': 'font-weight: bold; color: white; background-color: green; padding: 0.5em 1em;',
14
+ 'summary-result-failure': 'font-weight: bold; color: white; background-color: red; padding: 0.5em 1em;',
15
+ 'bail-out': 'color: white; background-color: red; font-weight: bold; padding: 0.5em 1em;'
16
+ };
17
+
18
+ const logger = (text, style) => {
19
+ const css = typeof style == 'string' && styles[style];
20
+ if (css) {
21
+ console.log('%c' + text, css);
22
+ } else {
23
+ console.log(text);
24
+ }
25
+ };
26
+
27
+ const formatValue = value => {
28
+ if (typeof value == 'string') return value;
29
+ return JSON.stringify(value);
30
+ };
31
+
32
+ class TapReporter {
33
+ constructor({write = logger, useJson = false, renumberAsserts = false} = {}) {
34
+ this.write = write;
35
+ this.renumberAsserts = renumberAsserts;
36
+ this.useJson = useJson;
37
+ this.depth = 0;
38
+ this.assertCounter = 0;
39
+ this.opened = false;
40
+ }
41
+ open() {
42
+ if (!this.opened) {
43
+ this.write('TAP version 13');
44
+ this.opened = true;
45
+ }
46
+ }
47
+ report(event) {
48
+ let text;
49
+ switch (event.type) {
50
+ case 'test':
51
+ this.open();
52
+ event.name && this.write('# start: ' + event.name, 'info');
53
+ ++this.depth;
54
+ break;
55
+ case 'comment':
56
+ this.open();
57
+ this.write('# ' + event.name);
58
+ break;
59
+ case 'end':
60
+ --this.depth;
61
+ event.name && this.write('# finish: ' + event.name + ' # time=' + event.diffTime.toFixed(3) + 'ms', 'info');
62
+ if (this.depth) break;
63
+ const state = event.data,
64
+ success = state.asserts - state.failed - state.skipped;
65
+ this.write('1..' + state.asserts, 'summary');
66
+ this.write('# tests ' + state.asserts, 'summary');
67
+ state.skipped && this.write('# skip ' + state.skipped, 'summary-info');
68
+ success && this.write('# pass ' + success, 'summary-success');
69
+ state.failed && this.write('# fail ' + state.failed, 'summary-failure');
70
+ this.write('# ' + (event.fail ? 'not ok' : 'ok'), event.fail ? 'summary-result-failure' : 'summary-result-success');
71
+ this.write('# time=' + event.diffTime.toFixed(3) + 'ms', 'summary-info');
72
+ break;
73
+ case 'bail-out':
74
+ this.open();
75
+ text = 'Bail out!';
76
+ event.name && (text += ' ' + event.name);
77
+ this.write(text, 'bail-out');
78
+ break;
79
+ case 'assert':
80
+ this.open();
81
+ text = (event.fail ? 'not ok' : 'ok') + ' ' + (this.renumberAsserts ? ++this.assertCounter : event.id);
82
+ if (event.skip) {
83
+ text += ' # SKIP';
84
+ } else if (event.todo) {
85
+ text += ' # TODO';
86
+ }
87
+ event.name && (text += ' ' + event.name);
88
+ text += ' # time=' + event.diffTime.toFixed(3) + 'ms';
89
+ this.write(text, event.fail ? 'failure' : 'success');
90
+ if (event.fail) {
91
+ this.write(' ---', 'yaml');
92
+ if (this.useJson) {
93
+ this.write(' operator: ' + event.operator, 'yaml');
94
+ if (event.hasOwnProperty('expected')) {
95
+ this.write(' expected: ' + formatValue(event.expected), 'yaml');
96
+ }
97
+ if (event.hasOwnProperty('actual')) {
98
+ this.write(' actual: ' + formatValue(event.actual), 'yaml');
99
+ }
100
+ event.at && this.write(' at: ' + event.at, 'yaml');
101
+ } else {
102
+ yamlFormatter({operator: event.operator}, formatterOptions).forEach(line => this.write(line, 'yaml'));
103
+ if (event.data) {
104
+ yamlFormatter(
105
+ {
106
+ expected: event.data.expected,
107
+ actual: event.data.actual
108
+ },
109
+ formatterOptions
110
+ ).forEach(line => this.write(line, 'yaml'));
111
+ }
112
+ yamlFormatter({at: event.at}, formatterOptions).forEach(line => this.write(line, 'yaml'));
113
+ }
114
+ const stack = event.actual && event.actual.type === 'Error' && typeof event.actual.stack == 'string' ? event.actual.stack : event.marker.stack;
115
+ if (typeof stack == 'string') {
116
+ this.write(' stack: |-', 'yaml');
117
+ stack.split('\n').forEach(line => this.write(' ' + line, 'yaml'));
118
+ }
119
+ this.write(' ...', 'yaml');
120
+ }
121
+ break;
122
+ }
123
+ }
124
+ }
125
+
126
+ export default TapReporter;