You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
590 lines
16 KiB
590 lines
16 KiB
'use strict'; |
|
|
|
Object.defineProperty(exports, '__esModule', { |
|
value: true |
|
}); |
|
exports.stringify = |
|
exports.printWithType = |
|
exports.printReceived = |
|
exports.printExpected = |
|
exports.printDiffOrStringify = |
|
exports.pluralize = |
|
exports.matcherHint = |
|
exports.matcherErrorMessage = |
|
exports.highlightTrailingWhitespace = |
|
exports.getLabelPrinter = |
|
exports.ensureNumbers = |
|
exports.ensureNoExpected = |
|
exports.ensureExpectedIsNumber = |
|
exports.ensureExpectedIsNonNegativeInteger = |
|
exports.ensureActualIsNumber = |
|
exports.diff = |
|
exports.SUGGEST_TO_CONTAIN_EQUAL = |
|
exports.RECEIVED_COLOR = |
|
exports.INVERTED_COLOR = |
|
exports.EXPECTED_COLOR = |
|
exports.DIM_COLOR = |
|
exports.BOLD_WEIGHT = |
|
void 0; |
|
|
|
var _chalk = _interopRequireDefault(require('chalk')); |
|
|
|
var _jestDiff = require('jest-diff'); |
|
|
|
var _jestGetType = require('jest-get-type'); |
|
|
|
var _prettyFormat = require('pretty-format'); |
|
|
|
var _Replaceable = _interopRequireDefault(require('./Replaceable')); |
|
|
|
var _deepCyclicCopyReplaceable = _interopRequireDefault( |
|
require('./deepCyclicCopyReplaceable') |
|
); |
|
|
|
function _interopRequireDefault(obj) { |
|
return obj && obj.__esModule ? obj : {default: obj}; |
|
} |
|
|
|
/** |
|
* 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. |
|
*/ |
|
|
|
/* eslint-disable local/ban-types-eventually */ |
|
const { |
|
AsymmetricMatcher, |
|
DOMCollection, |
|
DOMElement, |
|
Immutable, |
|
ReactElement, |
|
ReactTestComponent |
|
} = _prettyFormat.plugins; |
|
const PLUGINS = [ |
|
ReactTestComponent, |
|
ReactElement, |
|
DOMElement, |
|
DOMCollection, |
|
Immutable, |
|
AsymmetricMatcher |
|
]; |
|
const EXPECTED_COLOR = _chalk.default.green; |
|
exports.EXPECTED_COLOR = EXPECTED_COLOR; |
|
const RECEIVED_COLOR = _chalk.default.red; |
|
exports.RECEIVED_COLOR = RECEIVED_COLOR; |
|
const INVERTED_COLOR = _chalk.default.inverse; |
|
exports.INVERTED_COLOR = INVERTED_COLOR; |
|
const BOLD_WEIGHT = _chalk.default.bold; |
|
exports.BOLD_WEIGHT = BOLD_WEIGHT; |
|
const DIM_COLOR = _chalk.default.dim; |
|
exports.DIM_COLOR = DIM_COLOR; |
|
const MULTILINE_REGEXP = /\n/; |
|
const SPACE_SYMBOL = '\u{00B7}'; // middle dot |
|
|
|
const NUMBERS = [ |
|
'zero', |
|
'one', |
|
'two', |
|
'three', |
|
'four', |
|
'five', |
|
'six', |
|
'seven', |
|
'eight', |
|
'nine', |
|
'ten', |
|
'eleven', |
|
'twelve', |
|
'thirteen' |
|
]; |
|
|
|
const SUGGEST_TO_CONTAIN_EQUAL = _chalk.default.dim( |
|
'Looks like you wanted to test for object/array equality with the stricter `toContain` matcher. You probably need to use `toContainEqual` instead.' |
|
); |
|
|
|
exports.SUGGEST_TO_CONTAIN_EQUAL = SUGGEST_TO_CONTAIN_EQUAL; |
|
|
|
const stringify = (object, maxDepth = 10) => { |
|
const MAX_LENGTH = 10000; |
|
let result; |
|
|
|
try { |
|
result = (0, _prettyFormat.format)(object, { |
|
maxDepth, |
|
min: true, |
|
plugins: PLUGINS |
|
}); |
|
} catch { |
|
result = (0, _prettyFormat.format)(object, { |
|
callToJSON: false, |
|
maxDepth, |
|
min: true, |
|
plugins: PLUGINS |
|
}); |
|
} |
|
|
|
return result.length >= MAX_LENGTH && maxDepth > 1 |
|
? stringify(object, Math.floor(maxDepth / 2)) |
|
: result; |
|
}; |
|
|
|
exports.stringify = stringify; |
|
|
|
const highlightTrailingWhitespace = text => |
|
text.replace(/\s+$/gm, _chalk.default.inverse('$&')); // Instead of inverse highlight which now implies a change, |
|
// replace common spaces with middle dot at the end of any line. |
|
|
|
exports.highlightTrailingWhitespace = highlightTrailingWhitespace; |
|
|
|
const replaceTrailingSpaces = text => |
|
text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length)); |
|
|
|
const printReceived = object => |
|
RECEIVED_COLOR(replaceTrailingSpaces(stringify(object))); |
|
|
|
exports.printReceived = printReceived; |
|
|
|
const printExpected = value => |
|
EXPECTED_COLOR(replaceTrailingSpaces(stringify(value))); |
|
|
|
exports.printExpected = printExpected; |
|
|
|
const printWithType = ( |
|
name, |
|
value, |
|
print // printExpected or printReceived |
|
) => { |
|
const type = (0, _jestGetType.getType)(value); |
|
const hasType = |
|
type !== 'null' && type !== 'undefined' |
|
? `${name} has type: ${type}\n` |
|
: ''; |
|
const hasValue = `${name} has value: ${print(value)}`; |
|
return hasType + hasValue; |
|
}; |
|
|
|
exports.printWithType = printWithType; |
|
|
|
const ensureNoExpected = (expected, matcherName, options) => { |
|
if (typeof expected !== 'undefined') { |
|
// Prepend maybe not only for backward compatibility. |
|
const matcherString = (options ? '' : '[.not]') + matcherName; |
|
throw new Error( |
|
matcherErrorMessage( |
|
matcherHint(matcherString, undefined, '', options), // Because expected is omitted in hint above, |
|
// expected is black instead of green in message below. |
|
'this matcher must not have an expected argument', |
|
printWithType('Expected', expected, printExpected) |
|
) |
|
); |
|
} |
|
}; |
|
/** |
|
* Ensures that `actual` is of type `number | bigint` |
|
*/ |
|
|
|
exports.ensureNoExpected = ensureNoExpected; |
|
|
|
const ensureActualIsNumber = (actual, matcherName, options) => { |
|
if (typeof actual !== 'number' && typeof actual !== 'bigint') { |
|
// Prepend maybe not only for backward compatibility. |
|
const matcherString = (options ? '' : '[.not]') + matcherName; |
|
throw new Error( |
|
matcherErrorMessage( |
|
matcherHint(matcherString, undefined, undefined, options), |
|
`${RECEIVED_COLOR('received')} value must be a number or bigint`, |
|
printWithType('Received', actual, printReceived) |
|
) |
|
); |
|
} |
|
}; |
|
/** |
|
* Ensures that `expected` is of type `number | bigint` |
|
*/ |
|
|
|
exports.ensureActualIsNumber = ensureActualIsNumber; |
|
|
|
const ensureExpectedIsNumber = (expected, matcherName, options) => { |
|
if (typeof expected !== 'number' && typeof expected !== 'bigint') { |
|
// Prepend maybe not only for backward compatibility. |
|
const matcherString = (options ? '' : '[.not]') + matcherName; |
|
throw new Error( |
|
matcherErrorMessage( |
|
matcherHint(matcherString, undefined, undefined, options), |
|
`${EXPECTED_COLOR('expected')} value must be a number or bigint`, |
|
printWithType('Expected', expected, printExpected) |
|
) |
|
); |
|
} |
|
}; |
|
/** |
|
* Ensures that `actual` & `expected` are of type `number | bigint` |
|
*/ |
|
|
|
exports.ensureExpectedIsNumber = ensureExpectedIsNumber; |
|
|
|
const ensureNumbers = (actual, expected, matcherName, options) => { |
|
ensureActualIsNumber(actual, matcherName, options); |
|
ensureExpectedIsNumber(expected, matcherName, options); |
|
}; |
|
|
|
exports.ensureNumbers = ensureNumbers; |
|
|
|
const ensureExpectedIsNonNegativeInteger = (expected, matcherName, options) => { |
|
if ( |
|
typeof expected !== 'number' || |
|
!Number.isSafeInteger(expected) || |
|
expected < 0 |
|
) { |
|
// Prepend maybe not only for backward compatibility. |
|
const matcherString = (options ? '' : '[.not]') + matcherName; |
|
throw new Error( |
|
matcherErrorMessage( |
|
matcherHint(matcherString, undefined, undefined, options), |
|
`${EXPECTED_COLOR('expected')} value must be a non-negative integer`, |
|
printWithType('Expected', expected, printExpected) |
|
) |
|
); |
|
} |
|
}; // Given array of diffs, return concatenated string: |
|
// * include common substrings |
|
// * exclude change substrings which have opposite op |
|
// * include change substrings which have argument op |
|
// with inverse highlight only if there is a common substring |
|
|
|
exports.ensureExpectedIsNonNegativeInteger = ensureExpectedIsNonNegativeInteger; |
|
|
|
const getCommonAndChangedSubstrings = (diffs, op, hasCommonDiff) => |
|
diffs.reduce( |
|
(reduced, diff) => |
|
reduced + |
|
(diff[0] === _jestDiff.DIFF_EQUAL |
|
? diff[1] |
|
: diff[0] !== op |
|
? '' |
|
: hasCommonDiff |
|
? INVERTED_COLOR(diff[1]) |
|
: diff[1]), |
|
'' |
|
); |
|
|
|
const isLineDiffable = (expected, received) => { |
|
const expectedType = (0, _jestGetType.getType)(expected); |
|
const receivedType = (0, _jestGetType.getType)(received); |
|
|
|
if (expectedType !== receivedType) { |
|
return false; |
|
} |
|
|
|
if ((0, _jestGetType.isPrimitive)(expected)) { |
|
// Print generic line diff for strings only: |
|
// * if neither string is empty |
|
// * if either string has more than one line |
|
return ( |
|
typeof expected === 'string' && |
|
typeof received === 'string' && |
|
expected.length !== 0 && |
|
received.length !== 0 && |
|
(MULTILINE_REGEXP.test(expected) || MULTILINE_REGEXP.test(received)) |
|
); |
|
} |
|
|
|
if ( |
|
expectedType === 'date' || |
|
expectedType === 'function' || |
|
expectedType === 'regexp' |
|
) { |
|
return false; |
|
} |
|
|
|
if (expected instanceof Error && received instanceof Error) { |
|
return false; |
|
} |
|
|
|
if ( |
|
receivedType === 'object' && |
|
typeof received.asymmetricMatch === 'function' |
|
) { |
|
return false; |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
const MAX_DIFF_STRING_LENGTH = 20000; |
|
|
|
const printDiffOrStringify = ( |
|
expected, |
|
received, |
|
expectedLabel, |
|
receivedLabel, |
|
expand // CLI options: true if `--expand` or false if `--no-expand` |
|
) => { |
|
if ( |
|
typeof expected === 'string' && |
|
typeof received === 'string' && |
|
expected.length !== 0 && |
|
received.length !== 0 && |
|
expected.length <= MAX_DIFF_STRING_LENGTH && |
|
received.length <= MAX_DIFF_STRING_LENGTH && |
|
expected !== received |
|
) { |
|
if (expected.includes('\n') || received.includes('\n')) { |
|
return (0, _jestDiff.diffStringsUnified)(expected, received, { |
|
aAnnotation: expectedLabel, |
|
bAnnotation: receivedLabel, |
|
changeLineTrailingSpaceColor: _chalk.default.bgYellow, |
|
commonLineTrailingSpaceColor: _chalk.default.bgYellow, |
|
emptyFirstOrLastLinePlaceholder: '↵', |
|
// U+21B5 |
|
expand, |
|
includeChangeCounts: true |
|
}); |
|
} |
|
|
|
const diffs = (0, _jestDiff.diffStringsRaw)(expected, received, true); |
|
const hasCommonDiff = diffs.some(diff => diff[0] === _jestDiff.DIFF_EQUAL); |
|
const printLabel = getLabelPrinter(expectedLabel, receivedLabel); |
|
const expectedLine = |
|
printLabel(expectedLabel) + |
|
printExpected( |
|
getCommonAndChangedSubstrings( |
|
diffs, |
|
_jestDiff.DIFF_DELETE, |
|
hasCommonDiff |
|
) |
|
); |
|
const receivedLine = |
|
printLabel(receivedLabel) + |
|
printReceived( |
|
getCommonAndChangedSubstrings( |
|
diffs, |
|
_jestDiff.DIFF_INSERT, |
|
hasCommonDiff |
|
) |
|
); |
|
return expectedLine + '\n' + receivedLine; |
|
} |
|
|
|
if (isLineDiffable(expected, received)) { |
|
const {replacedExpected, replacedReceived} = |
|
replaceMatchedToAsymmetricMatcher( |
|
(0, _deepCyclicCopyReplaceable.default)(expected), |
|
(0, _deepCyclicCopyReplaceable.default)(received), |
|
[], |
|
[] |
|
); |
|
const difference = (0, _jestDiff.diff)(replacedExpected, replacedReceived, { |
|
aAnnotation: expectedLabel, |
|
bAnnotation: receivedLabel, |
|
expand, |
|
includeChangeCounts: true |
|
}); |
|
|
|
if ( |
|
typeof difference === 'string' && |
|
difference.includes('- ' + expectedLabel) && |
|
difference.includes('+ ' + receivedLabel) |
|
) { |
|
return difference; |
|
} |
|
} |
|
|
|
const printLabel = getLabelPrinter(expectedLabel, receivedLabel); |
|
const expectedLine = printLabel(expectedLabel) + printExpected(expected); |
|
const receivedLine = |
|
printLabel(receivedLabel) + |
|
(stringify(expected) === stringify(received) |
|
? 'serializes to the same string' |
|
: printReceived(received)); |
|
return expectedLine + '\n' + receivedLine; |
|
}; // Sometimes, e.g. when comparing two numbers, the output from jest-diff |
|
// does not contain more information than the `Expected:` / `Received:` already gives. |
|
// In those cases, we do not print a diff to make the output shorter and not redundant. |
|
|
|
exports.printDiffOrStringify = printDiffOrStringify; |
|
|
|
const shouldPrintDiff = (actual, expected) => { |
|
if (typeof actual === 'number' && typeof expected === 'number') { |
|
return false; |
|
} |
|
|
|
if (typeof actual === 'bigint' && typeof expected === 'bigint') { |
|
return false; |
|
} |
|
|
|
if (typeof actual === 'boolean' && typeof expected === 'boolean') { |
|
return false; |
|
} |
|
|
|
return true; |
|
}; |
|
|
|
function replaceMatchedToAsymmetricMatcher( |
|
replacedExpected, |
|
replacedReceived, |
|
expectedCycles, |
|
receivedCycles |
|
) { |
|
if (!_Replaceable.default.isReplaceable(replacedExpected, replacedReceived)) { |
|
return { |
|
replacedExpected, |
|
replacedReceived |
|
}; |
|
} |
|
|
|
if ( |
|
expectedCycles.includes(replacedExpected) || |
|
receivedCycles.includes(replacedReceived) |
|
) { |
|
return { |
|
replacedExpected, |
|
replacedReceived |
|
}; |
|
} |
|
|
|
expectedCycles.push(replacedExpected); |
|
receivedCycles.push(replacedReceived); |
|
const expectedReplaceable = new _Replaceable.default(replacedExpected); |
|
const receivedReplaceable = new _Replaceable.default(replacedReceived); |
|
expectedReplaceable.forEach((expectedValue, key) => { |
|
const receivedValue = receivedReplaceable.get(key); |
|
|
|
if (isAsymmetricMatcher(expectedValue)) { |
|
if (expectedValue.asymmetricMatch(receivedValue)) { |
|
receivedReplaceable.set(key, expectedValue); |
|
} |
|
} else if (isAsymmetricMatcher(receivedValue)) { |
|
if (receivedValue.asymmetricMatch(expectedValue)) { |
|
expectedReplaceable.set(key, receivedValue); |
|
} |
|
} else if ( |
|
_Replaceable.default.isReplaceable(expectedValue, receivedValue) |
|
) { |
|
const replaced = replaceMatchedToAsymmetricMatcher( |
|
expectedValue, |
|
receivedValue, |
|
expectedCycles, |
|
receivedCycles |
|
); |
|
expectedReplaceable.set(key, replaced.replacedExpected); |
|
receivedReplaceable.set(key, replaced.replacedReceived); |
|
} |
|
}); |
|
return { |
|
replacedExpected: expectedReplaceable.object, |
|
replacedReceived: receivedReplaceable.object |
|
}; |
|
} |
|
|
|
function isAsymmetricMatcher(data) { |
|
const type = (0, _jestGetType.getType)(data); |
|
return type === 'object' && typeof data.asymmetricMatch === 'function'; |
|
} |
|
|
|
const diff = (a, b, options) => |
|
shouldPrintDiff(a, b) ? (0, _jestDiff.diff)(a, b, options) : null; |
|
|
|
exports.diff = diff; |
|
|
|
const pluralize = (word, count) => |
|
(NUMBERS[count] || count) + ' ' + word + (count === 1 ? '' : 's'); // To display lines of labeled values as two columns with monospace alignment: |
|
// given the strings which will describe the values, |
|
// return function which given each string, returns the label: |
|
// string, colon, space, and enough padding spaces to align the value. |
|
|
|
exports.pluralize = pluralize; |
|
|
|
const getLabelPrinter = (...strings) => { |
|
const maxLength = strings.reduce( |
|
(max, string) => (string.length > max ? string.length : max), |
|
0 |
|
); |
|
return string => `${string}: ${' '.repeat(maxLength - string.length)}`; |
|
}; |
|
|
|
exports.getLabelPrinter = getLabelPrinter; |
|
|
|
const matcherErrorMessage = ( |
|
hint, |
|
generic, |
|
specific // incorrect value returned from call to printWithType |
|
) => |
|
`${hint}\n\n${_chalk.default.bold('Matcher error')}: ${generic}${ |
|
typeof specific === 'string' ? '\n\n' + specific : '' |
|
}`; // Display assertion for the report when a test fails. |
|
// New format: rejects/resolves, not, and matcher name have black color |
|
// Old format: matcher name has dim color |
|
|
|
exports.matcherErrorMessage = matcherErrorMessage; |
|
|
|
const matcherHint = ( |
|
matcherName, |
|
received = 'received', |
|
expected = 'expected', |
|
options = {} |
|
) => { |
|
const { |
|
comment = '', |
|
expectedColor = EXPECTED_COLOR, |
|
isDirectExpectCall = false, |
|
// seems redundant with received === '' |
|
isNot = false, |
|
promise = '', |
|
receivedColor = RECEIVED_COLOR, |
|
secondArgument = '', |
|
secondArgumentColor = EXPECTED_COLOR |
|
} = options; |
|
let hint = ''; |
|
let dimString = 'expect'; // concatenate adjacent dim substrings |
|
|
|
if (!isDirectExpectCall && received !== '') { |
|
hint += DIM_COLOR(dimString + '(') + receivedColor(received); |
|
dimString = ')'; |
|
} |
|
|
|
if (promise !== '') { |
|
hint += DIM_COLOR(dimString + '.') + promise; |
|
dimString = ''; |
|
} |
|
|
|
if (isNot) { |
|
hint += DIM_COLOR(dimString + '.') + 'not'; |
|
dimString = ''; |
|
} |
|
|
|
if (matcherName.includes('.')) { |
|
// Old format: for backward compatibility, |
|
// especially without promise or isNot options |
|
dimString += matcherName; |
|
} else { |
|
// New format: omit period from matcherName arg |
|
hint += DIM_COLOR(dimString + '.') + matcherName; |
|
dimString = ''; |
|
} |
|
|
|
if (expected === '') { |
|
dimString += '()'; |
|
} else { |
|
hint += DIM_COLOR(dimString + '(') + expectedColor(expected); |
|
|
|
if (secondArgument) { |
|
hint += DIM_COLOR(', ') + secondArgumentColor(secondArgument); |
|
} |
|
|
|
dimString = ')'; |
|
} |
|
|
|
if (comment !== '') { |
|
dimString += ' // ' + comment; |
|
} |
|
|
|
if (dimString !== '') { |
|
hint += DIM_COLOR(dimString); |
|
} |
|
|
|
return hint; |
|
}; |
|
|
|
exports.matcherHint = matcherHint;
|
|
|