1 | const utils = require('./utils');
|
2 | const stringDiff = require('diff');
|
3 | const specialCharRegExp = require('./specialCharRegExp');
|
4 |
|
5 | module.exports = expect => {
|
6 | expect.installTheme({
|
7 | styles: {
|
8 | jsBoolean: 'jsPrimitive',
|
9 | jsNumber: 'jsPrimitive',
|
10 | error: ['red', 'bold'],
|
11 | success: ['green', 'bold'],
|
12 | diffAddedLine: 'green',
|
13 | diffAddedHighlight: ['bgGreen', 'white'],
|
14 | diffAddedSpecialChar: ['bgGreen', 'cyan', 'bold'],
|
15 | diffRemovedLine: 'red',
|
16 | diffRemovedHighlight: ['bgRed', 'white'],
|
17 | diffRemovedSpecialChar: ['bgRed', 'cyan', 'bold'],
|
18 | partialMatchHighlight: ['bgYellow']
|
19 | }
|
20 | });
|
21 |
|
22 | expect.installTheme('html', {
|
23 | palette: [
|
24 | '#993333',
|
25 | '#669933',
|
26 | '#314575',
|
27 | '#337777',
|
28 | '#710071',
|
29 | '#319916',
|
30 | '#BB1A53',
|
31 | '#999933',
|
32 | '#4311C2',
|
33 | '#996633',
|
34 | '#993399',
|
35 | '#333399',
|
36 | '#228842',
|
37 | '#C24747',
|
38 | '#336699',
|
39 | '#663399'
|
40 | ],
|
41 | styles: {
|
42 | jsComment: '#969896',
|
43 | jsFunctionName: '#795da3',
|
44 | jsKeyword: '#a71d5d',
|
45 | jsPrimitive: '#0086b3',
|
46 | jsRegexp: '#183691',
|
47 | jsString: '#df5000',
|
48 | jsKey: '#555'
|
49 | }
|
50 | });
|
51 |
|
52 | expect.installTheme('ansi', {
|
53 | palette: [
|
54 | '#FF1A53',
|
55 | '#E494FF',
|
56 | '#1A53FF',
|
57 | '#FF1AC6',
|
58 | '#1AFF53',
|
59 | '#D557FF',
|
60 | '#81FF57',
|
61 | '#C6FF1A',
|
62 | '#531AFF',
|
63 | '#AFFF94',
|
64 | '#C61AFF',
|
65 | '#53FF1A',
|
66 | '#FF531A',
|
67 | '#1AFFC6',
|
68 | '#FFC61A',
|
69 | '#1AC6FF'
|
70 | ],
|
71 | styles: {
|
72 | jsComment: 'gray',
|
73 | jsFunctionName: 'jsKeyword',
|
74 | jsKeyword: 'magenta',
|
75 | jsNumber: [],
|
76 | jsPrimitive: 'cyan',
|
77 | jsRegexp: 'green',
|
78 | jsString: 'cyan',
|
79 | jsKey: '#666',
|
80 | diffAddedHighlight: ['bgGreen', 'black'],
|
81 | diffRemovedHighlight: ['bgRed', 'black'],
|
82 | partialMatchHighlight: ['bgYellow', 'black']
|
83 | }
|
84 | });
|
85 |
|
86 | expect.addStyle('colorByIndex', function(content, index) {
|
87 | const palette = this.theme().palette;
|
88 | if (palette) {
|
89 | const color = palette[index % palette.length];
|
90 | this.text(content, color);
|
91 | } else {
|
92 | this.text(content);
|
93 | }
|
94 | });
|
95 |
|
96 | expect.addStyle('singleQuotedString', function(content) {
|
97 | content = String(content);
|
98 | this.jsString("'")
|
99 | .jsString(
|
100 |
|
101 | content.replace(/[\\\x00-\x1f']/g, $0 => {
|
102 | if ($0 === '\n') {
|
103 | return '\\n';
|
104 | } else if ($0 === '\r') {
|
105 | return '\\r';
|
106 | } else if ($0 === "'") {
|
107 | return "\\'";
|
108 | } else if ($0 === '\\') {
|
109 | return '\\\\';
|
110 | } else if ($0 === '\t') {
|
111 | return '\\t';
|
112 | } else if ($0 === '\b') {
|
113 | return '\\b';
|
114 | } else if ($0 === '\f') {
|
115 | return '\\f';
|
116 | } else {
|
117 | const charCode = $0.charCodeAt(0);
|
118 | return `\\x${charCode < 16 ? '0' : ''}${charCode.toString(16)}`;
|
119 | }
|
120 | })
|
121 | )
|
122 | .jsString("'");
|
123 | });
|
124 |
|
125 | expect.addStyle('propertyForObject', function(
|
126 | key,
|
127 | inspectedValue,
|
128 | isArrayLike
|
129 | ) {
|
130 | let keyOmitted = false;
|
131 | const isSymbol = typeof key === 'symbol';
|
132 | if (isSymbol) {
|
133 | this.text('[')
|
134 | .appendInspected(key)
|
135 | .text(']')
|
136 | .text(':');
|
137 | } else {
|
138 | key = String(key);
|
139 | if (/^[a-z$_][a-z0-9$_]*$/i.test(key)) {
|
140 | this.text(key, 'jsKey').text(':');
|
141 | } else if (/^(?:0|[1-9][0-9]*)$/.test(key)) {
|
142 | if (isArrayLike) {
|
143 | keyOmitted = true;
|
144 | } else {
|
145 | this.jsNumber(key).text(':');
|
146 | }
|
147 | } else {
|
148 | this.singleQuotedString(key).text(':');
|
149 | }
|
150 | }
|
151 |
|
152 | if (!inspectedValue.isEmpty()) {
|
153 | if (!keyOmitted) {
|
154 | if (
|
155 | key.length > 5 &&
|
156 | inspectedValue.isBlock() &&
|
157 | inspectedValue.isMultiline()
|
158 | ) {
|
159 | this.indentLines();
|
160 | this.nl().i();
|
161 | } else {
|
162 | this.sp();
|
163 | }
|
164 | }
|
165 | this.append(inspectedValue);
|
166 | }
|
167 | });
|
168 |
|
169 |
|
170 | expect.addStyle('code', function(content, language) {
|
171 | this.text(content);
|
172 | });
|
173 |
|
174 | expect.addStyle('annotationBlock', function(...args) {
|
175 | const pen = this.getContentFromArguments(args);
|
176 | const height = pen.size().height;
|
177 |
|
178 | this.block(function() {
|
179 | for (let i = 0; i < height; i += 1) {
|
180 | if (i > 0) {
|
181 | this.nl();
|
182 | }
|
183 | this.error('//');
|
184 | }
|
185 | });
|
186 | this.sp().block(pen);
|
187 | });
|
188 |
|
189 | expect.addStyle('commentBlock', function(...args) {
|
190 | const pen = this.getContentFromArguments(args);
|
191 | const height = pen.size().height;
|
192 |
|
193 | this.block(function() {
|
194 | for (let i = 0; i < height; i += 1) {
|
195 | if (i > 0) {
|
196 | this.nl();
|
197 | }
|
198 | this.jsComment('//');
|
199 | }
|
200 | });
|
201 | this.sp().block(pen);
|
202 | });
|
203 |
|
204 | expect.addStyle('diffLinesOmitted', function(lineCount) {
|
205 | this.jsComment(`... ${lineCount} lines omitted ...`);
|
206 | });
|
207 |
|
208 | expect.addStyle('removedHighlight', function(content) {
|
209 | this.alt({
|
210 | text() {
|
211 | content.split(/(\n)/).forEach(function(fragment) {
|
212 | this.block(function() {
|
213 | this.text(fragment.replace(/\n/g, '\\n'))
|
214 | .nl()
|
215 | .text(fragment.replace(/[\s\S]/g, '^'));
|
216 | });
|
217 | if (fragment === '\n') {
|
218 | this.nl();
|
219 | }
|
220 | }, this);
|
221 | },
|
222 | fallback() {
|
223 | content.split(/(\n)/).forEach(fragment => {
|
224 | if (fragment === '\n') {
|
225 | this.diffRemovedSpecialChar('\\n').nl();
|
226 | } else {
|
227 | this.diffRemovedHighlight(fragment);
|
228 | }
|
229 | });
|
230 | }
|
231 | });
|
232 | });
|
233 |
|
234 | expect.addStyle('match', function(content) {
|
235 | this.alt({
|
236 | text() {
|
237 | content.split(/(\n)/).forEach(function(fragment) {
|
238 | if (fragment === '\n') {
|
239 | this.nl();
|
240 | } else {
|
241 | this.block(function() {
|
242 | this.text(fragment)
|
243 | .nl()
|
244 | .text(fragment.replace(/[\s\S]/g, '^'));
|
245 | });
|
246 | }
|
247 | }, this);
|
248 | },
|
249 | fallback() {
|
250 | this.diffAddedHighlight(content);
|
251 | }
|
252 | });
|
253 | });
|
254 |
|
255 | expect.addStyle('partialMatch', function(content) {
|
256 | this.alt({
|
257 | text() {
|
258 |
|
259 | this.match(content);
|
260 | },
|
261 | fallback() {
|
262 | this.partialMatchHighlight(content);
|
263 | }
|
264 | });
|
265 | });
|
266 |
|
267 | expect.addStyle('shouldEqualError', function(expected) {
|
268 | this.error(typeof expected === 'undefined' ? 'should be' : 'should equal')
|
269 | .sp()
|
270 | .block(function() {
|
271 | this.appendInspected(expected);
|
272 | });
|
273 | });
|
274 |
|
275 | expect.addStyle('errorName', function({ name, constructor }) {
|
276 | if (typeof name === 'string' && name !== 'Error') {
|
277 | this.text(name);
|
278 | } else if (constructor && typeof constructor.name === 'string') {
|
279 | this.text(constructor.name);
|
280 | } else {
|
281 | this.text('Error');
|
282 | }
|
283 | });
|
284 |
|
285 | expect.addStyle('appendErrorMessage', function(error, options) {
|
286 | if (error && error.isUnexpected) {
|
287 | this.append(
|
288 | error.getErrorMessage(utils.extend({ output: this }, options))
|
289 | );
|
290 | } else {
|
291 | this.appendInspected(error);
|
292 | }
|
293 | });
|
294 |
|
295 | expect.addStyle('appendItems', function(items, separator) {
|
296 | const that = this;
|
297 | separator = separator || '';
|
298 | items.forEach((item, index) => {
|
299 | if (index > 0) {
|
300 | that.append(separator);
|
301 | }
|
302 | that.appendInspected(item);
|
303 | });
|
304 | });
|
305 |
|
306 | expect.addStyle('stringDiffFragment', function(
|
307 | ch,
|
308 | text,
|
309 | baseStyle,
|
310 | markUpSpecialCharacters,
|
311 | isAtEol
|
312 | ) {
|
313 | const lines = text.split(/\n/);
|
314 | lines.forEach(function(line, i) {
|
315 | if (this.isAtStartOfLine()) {
|
316 | this.alt({
|
317 | text: ch,
|
318 | fallback() {
|
319 | if (
|
320 | line === '' &&
|
321 | ch !== ' ' &&
|
322 | (i === 0 || i !== lines.length - 1)
|
323 | ) {
|
324 | this[
|
325 | ch === '+' ? 'diffAddedSpecialChar' : 'diffRemovedSpecialChar'
|
326 | ]('\\n');
|
327 | }
|
328 | }
|
329 | });
|
330 | }
|
331 | const matchTrailingSpace =
|
332 | (isAtEol || i < lines.length - 1) && line.match(/^(.*[^ ])?( +)$/);
|
333 | if (matchTrailingSpace) {
|
334 | line = matchTrailingSpace[1] || '';
|
335 | }
|
336 |
|
337 | if (markUpSpecialCharacters) {
|
338 | line.split(specialCharRegExp).forEach(function(part) {
|
339 | if (specialCharRegExp.test(part)) {
|
340 | this[
|
341 | { '+': 'diffAddedSpecialChar', '-': 'diffRemovedSpecialChar' }[
|
342 | ch
|
343 | ] || baseStyle
|
344 | ](utils.escapeChar(part));
|
345 | } else {
|
346 | this[baseStyle](part);
|
347 | }
|
348 | }, this);
|
349 | } else {
|
350 | this[baseStyle](line);
|
351 | }
|
352 | if (matchTrailingSpace) {
|
353 | this[
|
354 | { '+': 'diffAddedHighlight', '-': 'diffRemovedHighlight' }[ch] ||
|
355 | baseStyle
|
356 | ](matchTrailingSpace[2]);
|
357 | }
|
358 | if (i !== lines.length - 1) {
|
359 | this.nl();
|
360 | }
|
361 | }, this);
|
362 | });
|
363 |
|
364 | expect.addStyle('stringDiff', function(actual, expected, options = {}) {
|
365 | const type = options.type || 'WordsWithSpace';
|
366 | const diffLines = [];
|
367 | let lastPart;
|
368 | stringDiff.diffLines(actual, expected).forEach(part => {
|
369 | if (lastPart && lastPart.removed && part.added) {
|
370 | diffLines.push({
|
371 | oldValue: lastPart.value,
|
372 | newValue: part.value,
|
373 | replaced: true
|
374 | });
|
375 | lastPart = null;
|
376 | } else {
|
377 | if (lastPart) {
|
378 | diffLines.push(lastPart);
|
379 | }
|
380 | lastPart = part;
|
381 | }
|
382 | });
|
383 | if (lastPart) {
|
384 | diffLines.push(lastPart);
|
385 | }
|
386 |
|
387 | diffLines.forEach(function(part, index) {
|
388 | if (part.replaced) {
|
389 | let oldValue = part.oldValue;
|
390 | let newValue = part.newValue;
|
391 | const newLine = this.clone();
|
392 | const oldEndsWithNewline = oldValue.slice(-1) === '\n';
|
393 | const newEndsWithNewline = newValue.slice(-1) === '\n';
|
394 | if (oldEndsWithNewline) {
|
395 | oldValue = oldValue.slice(0, -1);
|
396 | }
|
397 | if (newEndsWithNewline) {
|
398 | newValue = newValue.slice(0, -1);
|
399 | }
|
400 | stringDiff[`diff${type}`](oldValue, newValue).forEach(function(
|
401 | { added, value, removed },
|
402 | i,
|
403 | lines
|
404 | ) {
|
405 | const isAtEol = i === lines.length - 1;
|
406 | if (added) {
|
407 | newLine.stringDiffFragment(
|
408 | '+',
|
409 | value,
|
410 | 'diffAddedHighlight',
|
411 | options.markUpSpecialCharacters,
|
412 | isAtEol
|
413 | );
|
414 | } else if (removed) {
|
415 | this.stringDiffFragment(
|
416 | '-',
|
417 | value,
|
418 | 'diffRemovedHighlight',
|
419 | options.markUpSpecialCharacters,
|
420 | isAtEol
|
421 | );
|
422 | } else {
|
423 | newLine.stringDiffFragment(
|
424 | '+',
|
425 | value,
|
426 | 'diffAddedLine',
|
427 | undefined,
|
428 | isAtEol
|
429 | );
|
430 | this.stringDiffFragment(
|
431 | '-',
|
432 | value,
|
433 | 'diffRemovedLine',
|
434 | undefined,
|
435 | isAtEol
|
436 | );
|
437 | }
|
438 | },
|
439 | this);
|
440 | if (newEndsWithNewline && !oldEndsWithNewline) {
|
441 | newLine.diffAddedSpecialChar('\\n');
|
442 | }
|
443 |
|
444 | if (oldEndsWithNewline && !newEndsWithNewline) {
|
445 | this.diffRemovedSpecialChar('\\n');
|
446 | }
|
447 | this.nl()
|
448 | .append(newLine)
|
449 | .nl(oldEndsWithNewline && index < diffLines.length - 1 ? 1 : 0);
|
450 | } else {
|
451 | const endsWithNewline = /\n$/.test(part.value);
|
452 | const value = endsWithNewline ? part.value.slice(0, -1) : part.value;
|
453 | if (part.added) {
|
454 | this.stringDiffFragment(
|
455 | '+',
|
456 | value,
|
457 | 'diffAddedLine',
|
458 | options.markUpSpecialCharacters,
|
459 | endsWithNewline || index === diffLines.length - 1
|
460 | );
|
461 | } else if (part.removed) {
|
462 | this.stringDiffFragment(
|
463 | '-',
|
464 | value,
|
465 | 'diffRemovedLine',
|
466 | options.markUpSpecialCharacters,
|
467 | endsWithNewline || index === diffLines.length - 1
|
468 | );
|
469 | } else {
|
470 | const horizon = 3;
|
471 | if (index === diffLines.length - 1 && part.count > horizon + 1) {
|
472 | this.stringDiffFragment(
|
473 | ' ',
|
474 | value
|
475 | .split('\n')
|
476 | .slice(0, horizon)
|
477 | .join('\n'),
|
478 | 'text'
|
479 | )
|
480 | .nl()
|
481 | .diffLinesOmitted(part.count - horizon);
|
482 | } else if (index === 0 && part.count > horizon + 1) {
|
483 | this.diffLinesOmitted(part.count - horizon)
|
484 | .nl()
|
485 | .stringDiffFragment(
|
486 | ' ',
|
487 | value
|
488 | .split('\n')
|
489 | .slice(-horizon)
|
490 | .join('\n'),
|
491 | 'text'
|
492 | );
|
493 | } else if (part.count > 2 * horizon + 1) {
|
494 | this.stringDiffFragment(
|
495 | ' ',
|
496 | value
|
497 | .split('\n')
|
498 | .slice(0, horizon)
|
499 | .join('\n'),
|
500 | 'text'
|
501 | )
|
502 | .nl()
|
503 | .diffLinesOmitted(part.count - 2 * horizon)
|
504 | .nl()
|
505 | .stringDiffFragment(
|
506 | ' ',
|
507 | value
|
508 | .split('\n')
|
509 | .slice(-horizon)
|
510 | .join('\n'),
|
511 | 'text'
|
512 | );
|
513 | } else {
|
514 | this.stringDiffFragment(' ', value, 'text');
|
515 | }
|
516 | }
|
517 | if (endsWithNewline) {
|
518 | this.nl();
|
519 | }
|
520 | }
|
521 | }, this);
|
522 | });
|
523 |
|
524 | expect.addStyle('arrow', function(options = {}) {
|
525 | const styles = options.styles || [];
|
526 | let i;
|
527 | this.nl(options.top || 0)
|
528 | .sp(options.left || 0)
|
529 | .text('┌', styles);
|
530 | for (i = 1; i < options.width; i += 1) {
|
531 | this.text(
|
532 | i === options.width - 1 && options.direction === 'up' ? '▷' : '─',
|
533 | styles
|
534 | );
|
535 | }
|
536 | this.nl();
|
537 | for (i = 1; i < options.height - 1; i += 1) {
|
538 | this.sp(options.left || 0)
|
539 | .text('│', styles)
|
540 | .nl();
|
541 | }
|
542 | this.sp(options.left || 0).text('└', styles);
|
543 | for (i = 1; i < options.width; i += 1) {
|
544 | this.text(
|
545 | i === options.width - 1 && options.direction === 'down' ? '▷' : '─',
|
546 | styles
|
547 | );
|
548 | }
|
549 | });
|
550 |
|
551 | const flattenBlocksInLines = require('magicpen/lib/flattenBlocksInLines');
|
552 | expect.addStyle('merge', function(pens) {
|
553 | const flattenedPens = pens
|
554 | .map(({ output }) => flattenBlocksInLines(output))
|
555 | .reverse();
|
556 | const maxHeight = flattenedPens.reduce(
|
557 | (maxHeight, { length }) => Math.max(maxHeight, length),
|
558 | 0
|
559 | );
|
560 | const blockNumbers = new Array(flattenedPens.length);
|
561 | const blockOffsets = new Array(flattenedPens.length);
|
562 |
|
563 | for (let lineNumber = 0; lineNumber < maxHeight; lineNumber += 1) {
|
564 | if (lineNumber > 0) {
|
565 | this.nl();
|
566 | }
|
567 | let i;
|
568 | for (i = 0; i < blockNumbers.length; i += 1) {
|
569 | blockNumbers[i] = 0;
|
570 | blockOffsets[i] = 0;
|
571 | }
|
572 | let contentLeft;
|
573 | do {
|
574 | contentLeft = false;
|
575 | let hasOutputChar = false;
|
576 | for (i = 0; i < flattenedPens.length; i += 1) {
|
577 | const currentLine = flattenedPens[i][lineNumber];
|
578 | if (currentLine) {
|
579 | while (
|
580 | currentLine[blockNumbers[i]] &&
|
581 | blockOffsets[i] >=
|
582 | currentLine[blockNumbers[i]].args.content.length
|
583 | ) {
|
584 | blockNumbers[i] += 1;
|
585 | blockOffsets[i] = 0;
|
586 | }
|
587 | const currentBlock = currentLine[blockNumbers[i]];
|
588 | if (currentBlock) {
|
589 | contentLeft = true;
|
590 | if (!hasOutputChar) {
|
591 | const ch = currentBlock.args.content.charAt(blockOffsets[i]);
|
592 | if (ch !== ' ') {
|
593 | this.text(ch, currentBlock.args.styles);
|
594 | hasOutputChar = true;
|
595 | }
|
596 | }
|
597 | blockOffsets[i] += 1;
|
598 | }
|
599 | }
|
600 | }
|
601 | if (!hasOutputChar && contentLeft) {
|
602 | this.sp();
|
603 | }
|
604 | } while (contentLeft);
|
605 | }
|
606 | });
|
607 |
|
608 | expect.addStyle('arrowsAlongsideChangeOutputs', function(
|
609 | packing,
|
610 | changeOutputs
|
611 | ) {
|
612 | if (packing) {
|
613 | const topByChangeNumber = {};
|
614 | let top = 0;
|
615 | changeOutputs.forEach((changeOutput, index) => {
|
616 | topByChangeNumber[index] = top;
|
617 | top += changeOutput.size().height;
|
618 | });
|
619 | const that = this;
|
620 |
|
621 | const arrows = [];
|
622 | packing.forEach((columnSet, i, { length }) => {
|
623 | columnSet.forEach(({ start, end, direction }) => {
|
624 | arrows.push(
|
625 | that.clone().arrow({
|
626 | left: i * 2,
|
627 | top: topByChangeNumber[start],
|
628 | width: 1 + (length - i) * 2,
|
629 | height: topByChangeNumber[end] - topByChangeNumber[start] + 1,
|
630 | direction
|
631 | })
|
632 | );
|
633 | });
|
634 | });
|
635 |
|
636 | if (arrows.length === 1) {
|
637 | this.block(arrows[0]);
|
638 | } else if (arrows.length > 1) {
|
639 | this.block(function() {
|
640 | this.merge(arrows);
|
641 | });
|
642 | }
|
643 | } else {
|
644 | this.i();
|
645 | }
|
646 |
|
647 | this.block(function() {
|
648 | changeOutputs.forEach(function(changeOutput, index) {
|
649 | this.nl(index > 0 ? 1 : 0);
|
650 | if (!changeOutput.isEmpty()) {
|
651 | this.sp(packing ? 1 : 0).append(changeOutput);
|
652 | }
|
653 | }, this);
|
654 | });
|
655 | });
|
656 | };
|