UNPKG

17.7 kBJavaScriptView Raw
1const utils = require('./utils');
2const stringDiff = require('diff');
3const specialCharRegExp = require('./specialCharRegExp');
4
5module.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 // eslint-disable-next-line no-control-regex
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 // Intended to be redefined by a plugin that offers syntax highlighting:
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 // We haven't yet come up with a good styling for partial matches in text mode
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 // As long as there's at least one pen with a line left:
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};