packages/jest-snapshot/src/printSnapshot.ts (261 lines of code) (raw):
/**
* Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved.
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*/
import chalk = require('chalk');
import {getObjectSubset} from '@jest/expect-utils';
import {
DIFF_DELETE,
DIFF_EQUAL,
DIFF_INSERT,
Diff,
DiffOptionsColor,
diffLinesUnified,
diffLinesUnified2,
diffStringsRaw,
diffStringsUnified,
} from 'jest-diff';
import {getType, isPrimitive} from 'jest-get-type';
import {
BOLD_WEIGHT,
EXPECTED_COLOR,
INVERTED_COLOR,
MatcherHintOptions,
RECEIVED_COLOR,
getLabelPrinter,
matcherHint,
} from 'jest-matcher-utils';
import {format as prettyFormat} from 'pretty-format';
import {
aBackground2,
aBackground3,
aForeground2,
aForeground3,
bBackground2,
bBackground3,
bForeground2,
bForeground3,
} from './colors';
import {dedentLines} from './dedentLines';
import type {MatchSnapshotConfig} from './types';
import {deserializeString, minify, serialize} from './utils';
type Chalk = chalk.Chalk;
export const getSnapshotColorForChalkInstance = (
chalkInstance: Chalk,
): DiffOptionsColor => {
const level = chalkInstance.level;
if (level === 3) {
return chalkInstance
.rgb(aForeground3[0], aForeground3[1], aForeground3[2])
.bgRgb(aBackground3[0], aBackground3[1], aBackground3[2]);
}
if (level === 2) {
return chalkInstance.ansi256(aForeground2).bgAnsi256(aBackground2);
}
return chalkInstance.magenta.bgYellowBright;
};
export const getReceivedColorForChalkInstance = (
chalkInstance: Chalk,
): DiffOptionsColor => {
const level = chalkInstance.level;
if (level === 3) {
return chalkInstance
.rgb(bForeground3[0], bForeground3[1], bForeground3[2])
.bgRgb(bBackground3[0], bBackground3[1], bBackground3[2]);
}
if (level === 2) {
return chalkInstance.ansi256(bForeground2).bgAnsi256(bBackground2);
}
return chalkInstance.cyan.bgWhiteBright; // also known as teal
};
export const aSnapshotColor = getSnapshotColorForChalkInstance(chalk);
export const bReceivedColor = getReceivedColorForChalkInstance(chalk);
export const noColor = (string: string): string => string;
export const HINT_ARG = 'hint';
export const SNAPSHOT_ARG = 'snapshot';
export const PROPERTIES_ARG = 'properties';
export const matcherHintFromConfig = (
{
context: {isNot, promise},
hint,
inlineSnapshot,
matcherName,
properties,
}: MatchSnapshotConfig,
isUpdatable: boolean,
): string => {
const options: MatcherHintOptions = {isNot, promise};
if (isUpdatable) {
options.receivedColor = bReceivedColor;
}
let expectedArgument = '';
if (typeof properties === 'object') {
expectedArgument = PROPERTIES_ARG;
if (isUpdatable) {
options.expectedColor = noColor;
}
if (typeof hint === 'string' && hint.length !== 0) {
options.secondArgument = HINT_ARG;
options.secondArgumentColor = BOLD_WEIGHT;
} else if (typeof inlineSnapshot === 'string') {
options.secondArgument = SNAPSHOT_ARG;
if (isUpdatable) {
options.secondArgumentColor = aSnapshotColor;
} else {
options.secondArgumentColor = noColor;
}
}
} else {
if (typeof hint === 'string' && hint.length !== 0) {
expectedArgument = HINT_ARG;
options.expectedColor = BOLD_WEIGHT;
} else if (typeof inlineSnapshot === 'string') {
expectedArgument = SNAPSHOT_ARG;
if (isUpdatable) {
options.expectedColor = aSnapshotColor;
}
}
}
return matcherHint(matcherName, undefined, expectedArgument, options);
};
// Given array of diffs, return string:
// * include common substrings
// * exclude change substrings which have opposite op
// * include change substrings which have argument op
// with change color only if there is a common substring
const joinDiffs = (
diffs: Array<Diff>,
op: number,
hasCommon: boolean,
): string =>
diffs.reduce(
(reduced: string, diff: Diff): string =>
reduced +
(diff[0] === DIFF_EQUAL
? diff[1]
: diff[0] !== op
? ''
: hasCommon
? INVERTED_COLOR(diff[1])
: diff[1]),
'',
);
const isLineDiffable = (received: unknown): boolean => {
const receivedType = getType(received);
if (isPrimitive(received)) {
return typeof received === 'string';
}
if (
receivedType === 'date' ||
receivedType === 'function' ||
receivedType === 'regexp'
) {
return false;
}
if (received instanceof Error) {
return false;
}
if (
receivedType === 'object' &&
typeof (received as any).asymmetricMatch === 'function'
) {
return false;
}
return true;
};
export const printExpected = (val: unknown): string =>
EXPECTED_COLOR(minify(val));
export const printReceived = (val: unknown): string =>
RECEIVED_COLOR(minify(val));
export const printPropertiesAndReceived = (
properties: object,
received: object,
expand: boolean, // CLI options: true if `--expand` or false if `--no-expand`
): string => {
const aAnnotation = 'Expected properties';
const bAnnotation = 'Received value';
if (isLineDiffable(properties) && isLineDiffable(received)) {
return diffLinesUnified(
serialize(properties).split('\n'),
serialize(getObjectSubset(received, properties)).split('\n'),
{
aAnnotation,
aColor: EXPECTED_COLOR,
bAnnotation,
bColor: RECEIVED_COLOR,
changeLineTrailingSpaceColor: chalk.bgYellow,
commonLineTrailingSpaceColor: chalk.bgYellow,
emptyFirstOrLastLinePlaceholder: '↵', // U+21B5
expand,
includeChangeCounts: true,
},
);
}
const printLabel = getLabelPrinter(aAnnotation, bAnnotation);
return `${printLabel(aAnnotation) + printExpected(properties)}\n${printLabel(
bAnnotation,
)}${printReceived(received)}`;
};
const MAX_DIFF_STRING_LENGTH = 20000;
export const printSnapshotAndReceived = (
a: string, // snapshot without extra line breaks
b: string, // received serialized but without extra line breaks
received: unknown,
expand: boolean, // CLI options: true if `--expand` or false if `--no-expand`
): string => {
const aAnnotation = 'Snapshot';
const bAnnotation = 'Received';
const aColor = aSnapshotColor;
const bColor = bReceivedColor;
const options = {
aAnnotation,
aColor,
bAnnotation,
bColor,
changeLineTrailingSpaceColor: noColor,
commonLineTrailingSpaceColor: chalk.bgYellow,
emptyFirstOrLastLinePlaceholder: '↵', // U+21B5
expand,
includeChangeCounts: true,
};
if (typeof received === 'string') {
if (
a.length >= 2 &&
a.startsWith('"') &&
a.endsWith('"') &&
b === prettyFormat(received)
) {
// If snapshot looks like default serialization of a string
// and received is string which has default serialization.
if (!a.includes('\n') && !b.includes('\n')) {
// If neither string is multiline,
// display as labels and quoted strings.
let aQuoted = a;
let bQuoted = b;
if (
a.length - 2 <= MAX_DIFF_STRING_LENGTH &&
b.length - 2 <= MAX_DIFF_STRING_LENGTH
) {
const diffs = diffStringsRaw(a.slice(1, -1), b.slice(1, -1), true);
const hasCommon = diffs.some(diff => diff[0] === DIFF_EQUAL);
aQuoted = `"${joinDiffs(diffs, DIFF_DELETE, hasCommon)}"`;
bQuoted = `"${joinDiffs(diffs, DIFF_INSERT, hasCommon)}"`;
}
const printLabel = getLabelPrinter(aAnnotation, bAnnotation);
return `${printLabel(aAnnotation) + aColor(aQuoted)}\n${printLabel(
bAnnotation,
)}${bColor(bQuoted)}`;
}
// Else either string is multiline, so display as unquoted strings.
a = deserializeString(a); // hypothetical expected string
b = received; // not serialized
}
// Else expected had custom serialization or was not a string
// or received has custom serialization.
return a.length <= MAX_DIFF_STRING_LENGTH &&
b.length <= MAX_DIFF_STRING_LENGTH
? diffStringsUnified(a, b, options)
: diffLinesUnified(a.split('\n'), b.split('\n'), options);
}
if (isLineDiffable(received)) {
const aLines2 = a.split('\n');
const bLines2 = b.split('\n');
// Fall through to fix a regression for custom serializers
// like jest-snapshot-serializer-raw that ignore the indent option.
const b0 = serialize(received, 0);
if (b0 !== b) {
const aLines0 = dedentLines(aLines2);
if (aLines0 !== null) {
// Compare lines without indentation.
const bLines0 = b0.split('\n');
return diffLinesUnified2(aLines2, bLines2, aLines0, bLines0, options);
}
}
// Fall back because:
// * props include a multiline string
// * text has more than one adjacent line
// * markup does not close
return diffLinesUnified(aLines2, bLines2, options);
}
const printLabel = getLabelPrinter(aAnnotation, bAnnotation);
return `${printLabel(aAnnotation) + aColor(a)}\n${printLabel(
bAnnotation,
)}${bColor(b)}`;
};