From c3a01676b06e9b2372ef7d883256c6745ba3c25c Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Sat, 2 Mar 2019 11:22:45 -0500 Subject: [PATCH] expect: Improve report when matcher fails, part 11 (#8008) --- CHANGELOG.md | 1 + .../__snapshots__/matchers.test.js.snap | 174 ++++++------ packages/expect/src/matchers.ts | 251 +++++++++++------- packages/expect/src/print.ts | 61 +++++ packages/jest-matcher-utils/src/index.ts | 1 + 5 files changed, 299 insertions(+), 189 deletions(-) create mode 100644 packages/expect/src/print.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 10517c26bc96..1a6bbff7d4e2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,7 @@ - `[expect]`: Improve report when matcher fails, part 8 ([#7876](https://github.com/facebook/jest/pull/7876)) - `[expect]`: Improve report when matcher fails, part 9 ([#7940](https://github.com/facebook/jest/pull/7940)) - `[expect]`: Improve report when matcher fails, part 10 ([#7960](https://github.com/facebook/jest/pull/7960)) +- `[expect]`: Improve report when matcher fails, part 11 ([#8008](https://github.com/facebook/jest/pull/8008)) - `[pretty-format]` Support `React.memo` ([#7891](https://github.com/facebook/jest/pull/7891)) - `[jest-config]` Print error information on preset normalization error ([#7935](https://github.com/facebook/jest/pull/7935)) - `[jest-haste-map]` Add `skipPackageJson` option ([#7778](https://github.com/facebook/jest/pull/7778)) diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index f236cb1e4902..beea9f37fb31 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -1624,49 +1624,49 @@ Expected has value: null" `; exports[`.toContain(), .toContainEqual() '"11112111"' contains '"2"' 1`] = ` -"expect(string).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: \\"2\\" -Received string: \\"11112111\\"" +Expected substring: not \\"2\\" +Received string: \\"11112111\\"" `; exports[`.toContain(), .toContainEqual() '"abcdef"' contains '"abc"' 1`] = ` -"expect(string).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: \\"abc\\" -Received string: \\"abcdef\\"" +Expected substring: not \\"abc\\" +Received string: \\"abcdef\\"" `; exports[`.toContain(), .toContainEqual() '["a", "b", "c", "d"]' contains '"a"' 1`] = ` -"expect(array).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: \\"a\\" -Received array: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"]" +Expected value: not \\"a\\" +Received array: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"]" `; exports[`.toContain(), .toContainEqual() '["a", "b", "c", "d"]' contains a value equal to '"a"' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: \\"a\\" -Received array: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"]" +Expected value: not \\"a\\" +Received array: [\\"a\\", \\"b\\", \\"c\\", \\"d\\"]" `; exports[`.toContain(), .toContainEqual() '[{"a": "b"}, {"a": "c"}]' contains a value equal to '{"a": "b"}' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: {\\"a\\": \\"b\\"} -Received array: [{\\"a\\": \\"b\\"}, {\\"a\\": \\"c\\"}]" +Expected value: not {\\"a\\": \\"b\\"} +Received array: [{\\"a\\": \\"b\\"}, {\\"a\\": \\"c\\"}]" `; exports[`.toContain(), .toContainEqual() '[{"a": "b"}, {"a": "c"}]' does not contain a value equal to'{"a": "d"}' 1`] = ` -"expect(array).toContainEqual(value) // deep equality +"expect(received).toContainEqual(expected) // deep equality Expected value: {\\"a\\": \\"d\\"} Received array: [{\\"a\\": \\"b\\"}, {\\"a\\": \\"c\\"}]" `; exports[`.toContain(), .toContainEqual() '[{}, []]' does not contain '[]' 1`] = ` -"expect(array).toContain(value) // indexOf +"expect(received).toContain(expected) // indexOf Expected value: [] Received array: [{}, []] @@ -1675,7 +1675,7 @@ Received array: [{}, []] `; exports[`.toContain(), .toContainEqual() '[{}, []]' does not contain '{}' 1`] = ` -"expect(array).toContain(value) // indexOf +"expect(received).toContain(expected) // indexOf Expected value: {} Received array: [{}, []] @@ -1684,105 +1684,105 @@ Received array: [{}, []] `; exports[`.toContain(), .toContainEqual() '[0, 1]' contains '1' 1`] = ` -"expect(object).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: 1 -Received object: [0, 1]" +Expected value: not 1 +Received object: [0, 1]" `; exports[`.toContain(), .toContainEqual() '[0, 1]' contains a value equal to '1' 1`] = ` -"expect(object).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: 1 -Received object: [0, 1]" +Expected value: not 1 +Received object: [0, 1]" `; exports[`.toContain(), .toContainEqual() '[1, 2, 3, 4]' contains '1' 1`] = ` -"expect(array).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: 1 -Received array: [1, 2, 3, 4]" +Expected value: not 1 +Received array: [1, 2, 3, 4]" `; exports[`.toContain(), .toContainEqual() '[1, 2, 3, 4]' contains a value equal to '1' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: 1 -Received array: [1, 2, 3, 4]" +Expected value: not 1 +Received array: [1, 2, 3, 4]" `; exports[`.toContain(), .toContainEqual() '[1, 2, 3]' does not contain '4' 1`] = ` -"expect(array).toContain(value) // indexOf +"expect(received).toContain(expected) // indexOf Expected value: 4 Received array: [1, 2, 3]" `; exports[`.toContain(), .toContainEqual() '[Symbol(a)]' contains 'Symbol(a)' 1`] = ` -"expect(array).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: Symbol(a) -Received array: [Symbol(a)]" +Expected value: not Symbol(a) +Received array: [Symbol(a)]" `; exports[`.toContain(), .toContainEqual() '[Symbol(a)]' contains a value equal to 'Symbol(a)' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: Symbol(a) -Received array: [Symbol(a)]" +Expected value: not Symbol(a) +Received array: [Symbol(a)]" `; exports[`.toContain(), .toContainEqual() '[null, undefined]' does not contain '1' 1`] = ` -"expect(array).toContain(value) // indexOf +"expect(received).toContain(expected) // indexOf Expected value: 1 Received array: [null, undefined]" `; exports[`.toContain(), .toContainEqual() '[undefined, null]' contains 'null' 1`] = ` -"expect(array).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: null -Received array: [undefined, null]" +Expected value: not null +Received array: [undefined, null]" `; exports[`.toContain(), .toContainEqual() '[undefined, null]' contains 'undefined' 1`] = ` -"expect(array).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: undefined -Received array: [undefined, null]" +Expected value: not undefined +Received array: [undefined, null]" `; exports[`.toContain(), .toContainEqual() '[undefined, null]' contains a value equal to 'null' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: null -Received array: [undefined, null]" +Expected value: not null +Received array: [undefined, null]" `; exports[`.toContain(), .toContainEqual() '[undefined, null]' contains a value equal to 'undefined' 1`] = ` -"expect(array).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: undefined -Received array: [undefined, null]" +Expected value: not undefined +Received array: [undefined, null]" `; exports[`.toContain(), .toContainEqual() 'Set {"abc", "def"}' contains '"abc"' 1`] = ` -"expect(set).not.toContain(value) // indexOf +"expect(received).not.toContain(expected) // indexOf -Expected value: \\"abc\\" -Received set: Set {\\"abc\\", \\"def\\"}" +Expected value: not \\"abc\\" +Received set: Set {\\"abc\\", \\"def\\"}" `; exports[`.toContain(), .toContainEqual() 'Set {1, 2, 3, 4}' contains a value equal to '1' 1`] = ` -"expect(set).not.toContainEqual(value) // deep equality +"expect(received).not.toContainEqual(expected) // deep equality -Expected value: 1 -Received set: Set {1, 2, 3, 4}" +Expected value: not 1 +Received set: Set {1, 2, 3, 4}" `; exports[`.toContain(), .toContainEqual() error cases 1`] = ` -"expect(received).toContain(expected) +"expect(received).toContain(expected) // indexOf Matcher error: received value must not be null nor undefined @@ -1790,7 +1790,7 @@ Received has value: null" `; exports[`.toContain(), .toContainEqual() error cases for toContainEqual 1`] = ` -"expect(received).toContainEqual(expected) +"expect(received).toContainEqual(expected) // deep equality Matcher error: received value must not be null nor undefined @@ -3442,25 +3442,21 @@ With a value of: `; exports[`.toMatch() {pass: true} expect(Foo bar).toMatch(/^foo/i) 1`] = ` -"expect(received).not.toMatch(expected) +"expect(received).not.toMatch(expected) -Expected value not to match: - /^foo/i -Received: - \\"Foo bar\\"" +Expected pattern: not /^foo/i +Received string: \\"Foo bar\\"" `; exports[`.toMatch() {pass: true} expect(foo).toMatch(foo) 1`] = ` -"expect(received).not.toMatch(expected) +"expect(received).not.toMatch(expected) -Expected value not to match: - \\"foo\\" -Received: - \\"foo\\"" +Expected substring: not \\"foo\\" +Received string: \\"foo\\"" `; exports[`.toMatch() throws if non String actual value passed: [/foo/i, "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3469,7 +3465,7 @@ Received has value: /foo/i" `; exports[`.toMatch() throws if non String actual value passed: [[], "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3478,7 +3474,7 @@ Received has value: []" `; exports[`.toMatch() throws if non String actual value passed: [[Function anonymous], "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3487,7 +3483,7 @@ Received has value: [Function anonymous]" `; exports[`.toMatch() throws if non String actual value passed: [{}, "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3496,7 +3492,7 @@ Received has value: {}" `; exports[`.toMatch() throws if non String actual value passed: [1, "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3505,7 +3501,7 @@ Received has value: 1" `; exports[`.toMatch() throws if non String actual value passed: [true, "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3514,7 +3510,7 @@ Received has value: true" `; exports[`.toMatch() throws if non String actual value passed: [undefined, "foo"] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: received value must be a string @@ -3522,7 +3518,7 @@ Received has value: undefined" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", []] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3531,7 +3527,7 @@ Expected has value: []" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", [Function anonymous]] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3540,7 +3536,7 @@ Expected has value: [Function anonymous]" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", {}] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3549,7 +3545,7 @@ Expected has value: {}" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", 1] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3558,7 +3554,7 @@ Expected has value: 1" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", true] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3567,7 +3563,7 @@ Expected has value: true" `; exports[`.toMatch() throws if non String/RegExp expected value passed: ["foo", undefined] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) Matcher error: expected value must be a string or regular expression @@ -3575,21 +3571,17 @@ Expected has value: undefined" `; exports[`.toMatch() throws: [bar, /foo/] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) -Expected value to match: - /foo/ -Received: - \\"bar\\"" +Expected pattern: /foo/ +Received string: \\"bar\\"" `; exports[`.toMatch() throws: [bar, foo] 1`] = ` -"expect(received).toMatch(expected) +"expect(received).toMatch(expected) -Expected value to match: - \\"foo\\" -Received: - \\"bar\\"" +Expected substring: \\"foo\\" +Received string: \\"bar\\"" `; exports[`.toStrictEqual() matches the expected snapshot when it fails 1`] = ` diff --git a/packages/expect/src/matchers.ts b/packages/expect/src/matchers.ts index dbf2ddb80ea9..7adb37cb497f 100644 --- a/packages/expect/src/matchers.ts +++ b/packages/expect/src/matchers.ts @@ -7,7 +7,6 @@ */ import getType from 'jest-get-type'; -import {escapeStrForRegex} from 'jest-regex-util'; import { EXPECTED_COLOR, RECEIVED_COLOR, @@ -26,6 +25,11 @@ import { MatcherHintOptions, } from 'jest-matcher-utils'; import {MatchersObject, MatcherState} from './types'; +import { + printReceivedArrayContainExpectedItem, + printReceivedStringContainExpectedResult, + printReceivedStringContainExpectedSubstring, +} from './print'; import { getObjectSubset, getPath, @@ -368,57 +372,84 @@ const matchers: MatchersObject = { toContain( this: MatcherState, - collection: ContainIterable | string, - value: unknown, + received: ContainIterable | string, + expected: unknown, ) { - const collectionType = getType(collection); + const isNot = this.isNot; + const options: MatcherHintOptions = { + comment: 'indexOf', + isNot, + promise: this.promise, + }; - let converted: any = null; - if (Array.isArray(collection) || typeof collection === 'string') { - // strings have `indexOf` so we don't need to convert - // arrays have `indexOf` and we don't want to make a copy - converted = collection; - } else { - try { - converted = Array.from(collection); - } catch (e) { - throw new Error( - matcherErrorMessage( - matcherHint('.toContain', undefined, undefined, { - isNot: this.isNot, - }), - `${RECEIVED_COLOR( - 'received', - )} value must not be null nor undefined`, - printWithType('Received', collection, printReceived), - ), + if (received == null) { + throw new Error( + matcherErrorMessage( + matcherHint('toContain', undefined, undefined, options), + `${RECEIVED_COLOR('received')} value must not be null nor undefined`, + printWithType('Received', received, printReceived), + ), + ); + } + + if (typeof received === 'string') { + const index = received.indexOf(String(expected)); + const pass = index !== -1; + + const message = () => { + const labelExpected = `Expected ${ + typeof expected === 'string' ? 'substring' : 'value' + }`; + const labelReceived = 'Received string'; + const printLabel = getLabelPrinter(labelExpected, labelReceived); + + return ( + matcherHint('toContain', undefined, undefined, options) + + '\n\n' + + `${printLabel(labelExpected)}${isNot ? 'not ' : ''}${printExpected( + expected, + )}\n` + + `${printLabel(labelReceived)}${isNot ? ' ' : ''}${ + isNot + ? printReceivedStringContainExpectedSubstring( + received, + index, + String(expected).length, + ) + : printReceived(received) + }` ); - } + }; + + return {message, pass}; } - // At this point, we're either a string or an Array, - // which was converted from an array-like structure. - const pass = converted.indexOf(value) != -1; + + const indexable = Array.from(received); + const index = indexable.indexOf(expected); + const pass = index !== -1; + const message = () => { - const stringExpected = 'Expected value'; - const stringReceived = `Received ${collectionType}`; - const printLabel = getLabelPrinter(stringExpected, stringReceived); - const suggestToContainEqual = - !pass && - converted !== null && - typeof converted !== 'string' && - converted instanceof Array && - converted.findIndex(item => equals(item, value, [iterableEquality])) !== - -1; + const labelExpected = 'Expected value'; + const labelReceived = `Received ${getType(received)}`; + const printLabel = getLabelPrinter(labelExpected, labelReceived); return ( - matcherHint('.toContain', collectionType, 'value', { - comment: 'indexOf', - isNot: this.isNot, - }) + + matcherHint('toContain', undefined, undefined, options) + '\n\n' + - `${printLabel(stringExpected)}${printExpected(value)}\n` + - `${printLabel(stringReceived)}${printReceived(collection)}` + - (suggestToContainEqual ? `\n\n${SUGGEST_TO_CONTAIN_EQUAL}` : '') + `${printLabel(labelExpected)}${isNot ? 'not ' : ''}${printExpected( + expected, + )}\n` + + `${printLabel(labelReceived)}${isNot ? ' ' : ''}${ + isNot && Array.isArray(received) + ? printReceivedArrayContainExpectedItem(received, index) + : printReceived(received) + }` + + (!isNot && + indexable.findIndex(item => + equals(item, expected, [iterableEquality]), + ) !== -1 + ? `\n\n${SUGGEST_TO_CONTAIN_EQUAL}` + : '') ); }; @@ -427,47 +458,47 @@ const matchers: MatchersObject = { toContainEqual( this: MatcherState, - collection: ContainIterable, - value: unknown, + received: ContainIterable, + expected: unknown, ) { - const collectionType = getType(collection); - let converted = null; - if (Array.isArray(collection)) { - converted = collection; - } else { - try { - converted = Array.from(collection); - } catch (e) { - throw new Error( - matcherErrorMessage( - matcherHint('.toContainEqual', undefined, undefined, { - isNot: this.isNot, - }), - `${RECEIVED_COLOR( - 'received', - )} value must not be null nor undefined`, - printWithType('Received', collection, printReceived), - ), - ); - } + const isNot = this.isNot; + const options: MatcherHintOptions = { + comment: 'deep equality', + isNot, + promise: this.promise, + }; + + if (received == null) { + throw new Error( + matcherErrorMessage( + matcherHint('toContainEqual', undefined, undefined, options), + `${RECEIVED_COLOR('received')} value must not be null nor undefined`, + printWithType('Received', received, printReceived), + ), + ); } - const pass = - converted.findIndex(item => equals(item, value, [iterableEquality])) !== - -1; + const index = Array.from(received).findIndex(item => + equals(item, expected, [iterableEquality]), + ); + const pass = index !== -1; + const message = () => { - const stringExpected = 'Expected value'; - const stringReceived = `Received ${collectionType}`; - const printLabel = getLabelPrinter(stringExpected, stringReceived); + const labelExpected = 'Expected value'; + const labelReceived = `Received ${getType(received)}`; + const printLabel = getLabelPrinter(labelExpected, labelReceived); return ( - matcherHint('.toContainEqual', collectionType, 'value', { - comment: 'deep equality', - isNot: this.isNot, - }) + + matcherHint('toContainEqual', undefined, undefined, options) + '\n\n' + - `${printLabel(stringExpected)}${printExpected(value)}\n` + - `${printLabel(stringReceived)}${printReceived(collection)}` + `${printLabel(labelExpected)}${isNot ? 'not ' : ''}${printExpected( + expected, + )}\n` + + `${printLabel(labelReceived)}${isNot ? ' ' : ''}${ + isNot && Array.isArray(received) + ? printReceivedArrayContainExpectedItem(received, index) + : printReceived(received) + }` ); }; @@ -655,12 +686,15 @@ const matchers: MatchersObject = { }, toMatch(this: MatcherState, received: string, expected: string | RegExp) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + if (typeof received !== 'string') { throw new Error( matcherErrorMessage( - matcherHint('.toMatch', undefined, undefined, { - isNot: this.isNot, - }), + matcherHint('toMatch', undefined, undefined, options), `${RECEIVED_COLOR('received')} value must be a string`, printWithType('Received', received, printReceived), ), @@ -673,9 +707,7 @@ const matchers: MatchersObject = { ) { throw new Error( matcherErrorMessage( - matcherHint('.toMatch', undefined, undefined, { - isNot: this.isNot, - }), + matcherHint('toMatch', undefined, undefined, options), `${EXPECTED_COLOR( 'expected', )} value must be a string or regular expression`, @@ -684,22 +716,45 @@ const matchers: MatchersObject = { ); } - const pass = new RegExp( - typeof expected === 'string' ? escapeStrForRegex(expected) : expected, - ).test(received); + const pass = + typeof expected === 'string' + ? received.includes(expected) + : expected.test(received); + const message = pass ? () => - matcherHint('.not.toMatch') + - `\n\nExpected value not to match:\n` + - ` ${printExpected(expected)}` + - `\nReceived:\n` + - ` ${printReceived(received)}` - : () => - matcherHint('.toMatch') + - `\n\nExpected value to match:\n` + - ` ${printExpected(expected)}` + - `\nReceived:\n` + - ` ${printReceived(received)}`; + typeof expected === 'string' + ? matcherHint('toMatch', undefined, undefined, options) + + '\n\n' + + `Expected substring: not ${printExpected(expected)}\n` + + `Received string: ${printReceivedStringContainExpectedSubstring( + received, + received.indexOf(expected), + expected.length, + )}` + : matcherHint('toMatch', undefined, undefined, options) + + '\n\n' + + `Expected pattern: not ${printExpected(expected)}\n` + + `Received string: ${printReceivedStringContainExpectedResult( + received, + typeof expected.exec === 'function' + ? expected.exec(received) + : null, + )}` + : () => { + const labelExpected = `Expected ${ + typeof expected === 'string' ? 'substring' : 'pattern' + }`; + const labelReceived = 'Received string'; + const printLabel = getLabelPrinter(labelExpected, labelReceived); + + return ( + matcherHint('toMatch', undefined, undefined, options) + + '\n\n' + + `${printLabel(labelExpected)}${printExpected(expected)}\n` + + `${printLabel(labelReceived)}${printReceived(received)}` + ); + }; return {message, pass}; }, diff --git a/packages/expect/src/print.ts b/packages/expect/src/print.ts new file mode 100644 index 000000000000..a8c50038a225 --- /dev/null +++ b/packages/expect/src/print.ts @@ -0,0 +1,61 @@ +/** + * 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 { + INVERTED_COLOR, + RECEIVED_COLOR, + printReceived, + stringify, +} from 'jest-matcher-utils'; + +// Format substring but do not enclose in double quote marks. +// The replacement is compatible with pretty-format package. +const printSubstring = (val: string): string => val.replace(/"|\\/g, '\\$&'); + +export const printReceivedStringContainExpectedSubstring = ( + received: string, + start: number, + length: number, // not end +): string => + RECEIVED_COLOR( + '"' + + printSubstring(received.slice(0, start)) + + INVERTED_COLOR(printSubstring(received.slice(start, start + length))) + + printSubstring(received.slice(start + length)) + + '"', + ); + +export const printReceivedStringContainExpectedResult = ( + received: string, + result: RegExpExecArray | null, +): string => + result === null + ? printReceived(received) + : printReceivedStringContainExpectedSubstring( + received, + result.index, + result[0].length, + ); + +// The serialized array is compatible with pretty-format package min option. +// However, items have default stringify depth (instead of depth - 1) +// so expected item looks consistent by itself and enclosed in the array. +export const printReceivedArrayContainExpectedItem = ( + received: Array, + index: number, +): string => + RECEIVED_COLOR( + '[' + + received + .map((item, i) => { + const stringified = stringify(item); + return i === index ? INVERTED_COLOR(stringified) : stringified; + }) + .join(', ') + + ']', + ); diff --git a/packages/jest-matcher-utils/src/index.ts b/packages/jest-matcher-utils/src/index.ts index 6207960d5af7..69c681ffb345 100644 --- a/packages/jest-matcher-utils/src/index.ts +++ b/packages/jest-matcher-utils/src/index.ts @@ -37,6 +37,7 @@ export type MatcherHintOptions = { export const EXPECTED_COLOR = chalk.green; export const RECEIVED_COLOR = chalk.red; +export const INVERTED_COLOR = chalk.inverse; const DIM_COLOR = chalk.dim; const NUMBERS = [