From 6fbcd9ece849718a401b9765280cdbeebf0a3422 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 18 Jun 2019 17:24:07 -0400 Subject: [PATCH] jest-snapshot: Highlight substring differences when matcher fails, part 3 (#8569) * expect: Highlight substring differences when matcher fails, part 3 * Delete duplicate line comment slashes * Update CHANGELOG.md * Remove backslash escape preceding backslash * Replace expect with jest-snapshot in CHANGELOG.md * Delete unnecessary isExpand function * Factor out printDiffOrStringified into added print.ts file * Add unit tests for printDiffOrStringified function * Add Facebook comment to 2 added files * Fix two edge cases * Fall back to line diff if either serialization has newline * Also display diff if strings have application-specific serialization * Add test without serialize * Edit comments for consistent use of serialization versus stringified * Call getStringDiff when strings have custom serialization * Edit comment * Add tests for backtick and isLineDiffable true single-multi --- CHANGELOG.md | 1 + .../__snapshots__/failures.test.ts.snap | 14 +- .../watchModeUpdateSnapshot.test.ts.snap | 6 +- e2e/__tests__/toMatchSnapshot.test.ts | 2 +- packages/jest-snapshot/package.json | 1 + .../printDiffOrStringified.test.ts.snap | 230 ++++++++++ .../__tests__/printDiffOrStringified.test.ts | 394 ++++++++++++++++++ packages/jest-snapshot/src/index.ts | 26 +- packages/jest-snapshot/src/print.ts | 130 ++++++ packages/jest-snapshot/tsconfig.json | 1 + 10 files changed, 778 insertions(+), 27 deletions(-) create mode 100644 packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap create mode 100644 packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts create mode 100644 packages/jest-snapshot/src/print.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fabc33277c85..1d775180362e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,7 @@ - `[expect]` Highlight substring differences when matcher fails, part 1 ([#8448](https://github.com/facebook/jest/pull/8448)) - `[expect]` Highlight substring differences when matcher fails, part 2 ([#8528](https://github.com/facebook/jest/pull/8528)) +- `[jest-snapshot]` Highlight substring differences when matcher fails, part 3 ([#8569](https://github.com/facebook/jest/pull/8569)) - `[jest-cli]` Improve chai support (with detailed output, to match jest exceptions) ([#8454](https://github.com/facebook/jest/pull/8454)) - `[*]` Manage the global timeout with `--testTimeout` command line argument. ([#8456](https://github.com/facebook/jest/pull/8456)) - `[pretty-format]` Render custom displayName of memoized components diff --git a/e2e/__tests__/__snapshots__/failures.test.ts.snap b/e2e/__tests__/__snapshots__/failures.test.ts.snap index f527fca5e837..6e2ea6c5dafd 100644 --- a/e2e/__tests__/__snapshots__/failures.test.ts.snap +++ b/e2e/__tests__/__snapshots__/failures.test.ts.snap @@ -790,11 +790,8 @@ FAIL __tests__/snapshot.test.js Snapshot name: \`failing snapshot 1\` - - Snapshot - + Received - - - "bar" - + "foo" + Snapshot: "bar" + Received: "foo" 9 | 10 | test('failing snapshot', () => { @@ -819,11 +816,8 @@ FAIL __tests__/snapshotWithHint.test.js Snapshot name: \`failing snapshot with hint: descriptive hint 1\` - - Snapshot - + Received - - - "bar" - + "foo" + Snapshot: "bar" + Received: "foo" 9 | 10 | test('failing snapshot with hint', () => { diff --git a/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap index db4e6ef9b129..2ad72b2cb373 100644 --- a/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap @@ -6,10 +6,8 @@ FAIL __tests__/bar.spec.js ● bar expect(received).toMatchSnapshot() Snapshot name: \`bar 1\` - - Snapshot - + Received - - "foo" - + "bar" + Snapshot: "foo" + Received: "bar" 1 | > 2 | test('bar', () => { expect('bar').toMatchSnapshot(); }); | ^ diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index 9ee514bcd5a0..803c3ae70387 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -108,7 +108,7 @@ test('first snapshot fails, second passes', () => { writeFiles(TESTS_DIR, {[filename]: template([`'kiwi'`, `'banana'`])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Snapshot name: `snapshots 1`'); - expect(stderr).toMatch('- "apple"\n + "kiwi"'); + expect(stderr).toMatch('Snapshot: "apple"\n Received: "kiwi"'); expect(stderr).not.toMatch('1 obsolete snapshot found'); expect(status).toBe(1); } diff --git a/packages/jest-snapshot/package.json b/packages/jest-snapshot/package.json index df97bb2bd40a..2a300d51b6ff 100644 --- a/packages/jest-snapshot/package.json +++ b/packages/jest-snapshot/package.json @@ -15,6 +15,7 @@ "chalk": "^2.0.1", "expect": "^24.8.0", "jest-diff": "^24.8.0", + "jest-get-type": "^24.8.0", "jest-matcher-utils": "^24.8.0", "jest-message-util": "^24.8.0", "jest-resolve": "^24.8.0", diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap new file mode 100644 index 000000000000..8c7ff72d1c25 --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap @@ -0,0 +1,230 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`backtick single line expected and received 1`] = ` +Snapshot: "var foo = \`backtick\`;" +Received: "var foo = tag\`backtick\`;" +`; + +exports[`empty string expected and received single line 1`] = ` +Snapshot: "" +Received: "single line string" +`; + +exports[`empty string received and expected multi line 1`] = ` +Snapshot: "multi +line +string" +Received: "" +`; + +exports[`escape backslash in multi line string 1`] = ` +- Snapshot ++ Received + +- Forward / slash and back \\ slash ++ Forward / slash ++ Back \\ slash +`; + +exports[`escape backslash in single line string 1`] = ` +Snapshot: "forward / slash and back \\\\ slash" +Received: "Forward / slash and back \\\\ slash" +`; + +exports[`escape double quote marks in string 1`] = ` +Snapshot: "What does \\"oobleck\\" mean?" +Received: "What does \\"ewbleck\\" mean?" +`; + +exports[`escape regexp 1`] = ` +Snapshot: /\\\\\\\\\\("\\)/g +Received: /\\\\\\\\\\("\\)/ +`; + +exports[`expand false 1`] = ` +- Snapshot ++ Received + +@@ -12,7 +12,9 @@ + ? "number" + : T extends boolean + ? "boolean" + : T extends undefined + ? "undefined" +- : T extends Function ? "function" : "object"; ++ : T extends Function ++ ? "function" ++ : "object"; + ↵ +`; + +exports[`expand true 1`] = ` +- Snapshot ++ Received + + type TypeName = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : + T extends undefined ? "undefined" : + T extends Function ? "function" : + "object"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + type TypeName = T extends string + ? "string" + : T extends number + ? "number" + : T extends boolean + ? "boolean" + : T extends undefined + ? "undefined" +- : T extends Function ? "function" : "object"; ++ : T extends Function ++ ? "function" ++ : "object"; + ↵ +`; + +exports[`fallback to line diff 1`] = ` +- Snapshot ++ Received + ++ ====================================options===================================== ++ parsers: ["flow", "typescript"] ++ printWidth: 80 ++ | printWidth ++ =====================================input====================================== + [...a, ...b,]; + [...a, ...b]; +- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++ ++ =====================================output===================================== + [...a, ...b]; + [...a, ...b]; + ++ ================================================================================ +`; + +exports[`isLineDiffable false boolean 1`] = ` +Snapshot: true +Received: false +`; + +exports[`isLineDiffable false number 1`] = ` +Snapshot: -0 +Received: NaN +`; + +exports[`isLineDiffable true array 1`] = ` +- Snapshot ++ Received + + Array [ + Object { ++ "_id": "b14680dec683e744ada1f2fe08614086", + "code": 4011, + "weight": 2.13, + }, + Object { ++ "_id": "7fc63ff01769c4fa7d9279e97e307829", + "code": 4019, + "count": 4, + }, + ] +`; + +exports[`isLineDiffable true object 1`] = ` +- Snapshot ++ Received + + Object { + "props": Object { +- "className": "logo", +- "src": "/img/jest.png", ++ "alt": "Jest logo", ++ "class": "logo", ++ "src": "/img/jest.svg", + }, + "type": "img", + } +`; + +exports[`isLineDiffable true single line expected and multi line received 1`] = ` +- Snapshot ++ Received + +- Array [] ++ Array [ ++ 0, ++ ] +`; + +exports[`isLineDiffable true single line expected and received 1`] = ` +Snapshot: Array [] +Received: Object {} +`; + +exports[`multi line small change in one line and other is unchanged 1`] = ` +- Snapshot ++ Received + +- There is no route defined for key 'Settings'. ++ There is no route defined for key Settings. + Must be one of: 'Home' +`; + +exports[`multi line small changes 1`] = ` +- Snapshot ++ Received + +- 69 |· ++ 68 |· +- 70 | test('assert.doesNotThrow', () => { ++ 69 | test('assert.doesNotThrow', () => { +- > 71 | assert.doesNotThrow(() => { ++ > 70 | assert.doesNotThrow(() => { + | ^ +- 72 | throw Error('err!'); ++ 71 | throw Error('err!'); +- 73 | }); ++ 72 | }); +- 74 | }); ++ 73 | }); +- at Object.doesNotThrow (__tests__/assertionError.test.js:71:10) ++ at Object.doesNotThrow (__tests__/assertionError.test.js:70:10) +`; + +exports[`single line large changes 1`] = ` +Snapshot: "Array length must be a finite positive integer" +Received: "Invalid array length" +`; + +exports[`without serialize backtick single line expected and multi line received 1`] = ` +- Snapshot ++ Received + +- var foo = \`backtick\`; ++ var foo = \`back ++ tick\`; +`; + +exports[`without serialize backtick single line expected and received 1`] = ` +Snapshot: var foo = \`backtick\`; +Received: var foo = \`back\${x}tick\`; +`; + +exports[`without serialize prettier/pull/5590 1`] = ` +- Snapshot ++ Received + +@@ -4,8 +4,8 @@ + | printWidth + =====================================input====================================== + John "ShotGun" Nelson + + =====================================output===================================== +- <i"John "ShotGun" Nelson" /> ++ <i'John "ShotGun" Nelson' /> + + ================================================================================ +`; diff --git a/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts b/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts new file mode 100644 index 000000000000..3907dc1b9564 --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts @@ -0,0 +1,394 @@ +/** + * 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 ansiRegex from 'ansi-regex'; +import style from 'ansi-styles'; +import {printDiffOrStringified} from '../print'; +import {serialize, unescape} from '../utils'; + +// This is an experiment to read snapshots more easily: +// * to avoid first line misaligned because of opening double quote mark, +// return string without calling print function to serialize it, +// which also reduces extra escape sequences which is a subject of the tests! +// * to align lines, return tags which have same width +// * to see inline markup, return matching and tags +// * to see unexpected escape codes, do not return empty string as default + +const convertStyles = (val: string): string => + val.replace(ansiRegex(), match => { + switch (match) { + case style.inverse.open: + return ''; + case style.inverse.close: + return ''; + + case style.dim.open: + return ''; + case style.green.open: + return ''; + case style.red.open: + return ''; + case style.yellow.open: + return ''; + + case style.dim.close: + case style.green.close: + case style.red.close: + case style.yellow.close: + return ''; + + default: + return match; + } + }); + +expect.addSnapshotSerializer({ + serialize(val: string): string { + return val; + }, + test(val: any): val is string { + return typeof val === 'string'; + }, +}); + +const testWithSerialize = ( + expected: any, + received: any, + expand: boolean, +): string => { + // Simulate serializing the expected value as a snapshot, + // and then returning actual and expected when match function fails. + // Assume that the caller of printDiffOrStringified trims the strings. + const expectedSerializedTrimmed = unescape(serialize(expected)).trim(); + const receivedSerializedTrimmed = unescape(serialize(received)).trim(); + + return convertStyles( + printDiffOrStringified( + expectedSerializedTrimmed, + receivedSerializedTrimmed, + received, + 'Snapshot', + 'Received', + expand, + ), + ); +}; + +const testWithoutSerialize = ( + expected: string, + received: string, + expand: boolean, +): string => + convertStyles( + printDiffOrStringified( + expected, + received, + received, + 'Snapshot', + 'Received', + expand, + ), + ); + +describe('backtick', () => { + test('single line expected and received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = tag`backtick`;'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); +}); + +describe('empty string', () => { + test('expected and received single line', () => { + const expected = ''; + const received = 'single line string'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('received and expected multi line', () => { + const expected = 'multi\nline\nstring'; + const received = ''; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); +}); + +describe('escape', () => { + test('double quote marks in string', () => { + const expected = 'What does "oobleck" mean?'; + const received = 'What does "ewbleck" mean?'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('backslash in multi line string', () => { + const expected = 'Forward / slash and back \\ slash'; + const received = 'Forward / slash\nBack \\ slash'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('backslash in single line string', () => { + const expected = 'forward / slash and back \\ slash'; + const received = 'Forward / slash and back \\ slash'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('regexp', () => { + const expected = /\\(")/g; + const received = /\\(")/; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); +}); + +describe('expand', () => { + // prettier/pull/5272 + const expected = [ + 'type TypeName =', + 'T extends string ? "string" :', + 'T extends number ? "number" :', + 'T extends boolean ? "boolean" :', + 'T extends undefined ? "undefined" :', + 'T extends Function ? "function" :', + '"object";', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + 'type TypeName = T extends string', + '? "string"', + ': T extends number', + '? "number"', + ': T extends boolean', + '? "boolean"', + ': T extends undefined', + '? "undefined"', + ': T extends Function ? "function" : "object";', + '', + ].join('\n'); + const received = [ + 'type TypeName =', + 'T extends string ? "string" :', + 'T extends number ? "number" :', + 'T extends boolean ? "boolean" :', + 'T extends undefined ? "undefined" :', + 'T extends Function ? "function" :', + '"object";', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + 'type TypeName = T extends string', + '? "string"', + ': T extends number', + '? "number"', + ': T extends boolean', + '? "boolean"', + ': T extends undefined', + '? "undefined"', + ': T extends Function', + '? "function"', + ': "object";', + '', + ].join('\n'); + + test('false', () => { + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('true', () => { + expect(testWithSerialize(expected, received, true)).toMatchSnapshot(); + }); +}); + +test('fallback to line diff', () => { + const expected = [ + '[...a, ...b,];', + '[...a, ...b];', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + '[...a, ...b];', + '[...a, ...b];', + '', + ].join('\n'); + const received = [ + '====================================options=====================================', + 'parsers: ["flow", "typescript"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + '[...a, ...b,];', + '[...a, ...b];', + '', + '=====================================output=====================================', + '[...a, ...b];', + '[...a, ...b];', + '', + '================================================================================', + ].join('\n'); + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); +}); + +describe('isLineDiffable', () => { + describe('false', () => { + test('boolean', () => { + const expected = true; + const received = false; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('number', () => { + const expected = -0; + const received = NaN; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('true', () => { + test('array', () => { + const expected0 = { + code: 4011, + weight: 2.13, + }; + const expected1 = { + code: 4019, + count: 4, + }; + + const expected = [expected0, expected1]; + const received = [ + {_id: 'b14680dec683e744ada1f2fe08614086', ...expected0}, + {_id: '7fc63ff01769c4fa7d9279e97e307829', ...expected1}, + ]; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('object', () => { + const type = 'img'; + const expected = { + props: { + className: 'logo', + src: '/img/jest.png', + }, + type, + }; + const received = { + props: { + alt: 'Jest logo', + class: 'logo', + src: '/img/jest.svg', + }, + type, + }; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('single line expected and received', () => { + const expected = []; + const received = {}; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('single line expected and multi line received', () => { + const expected = []; + const received = [0]; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + }); + }); +}); + +test('multi line small change in one line and other is unchanged', () => { + const expected = + "There is no route defined for key 'Settings'.\nMust be one of: 'Home'"; + const received = + "There is no route defined for key Settings.\nMust be one of: 'Home'"; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); +}); + +test('multi line small changes', () => { + const expected = [ + ' 69 | ', + " 70 | test('assert.doesNotThrow', () => {", + ' > 71 | assert.doesNotThrow(() => {', + ' | ^', + " 72 | throw Error('err!');", + ' 73 | });', + ' 74 | });', + ' at Object.doesNotThrow (__tests__/assertionError.test.js:71:10)', + ].join('\n'); + const received = [ + ' 68 | ', + " 69 | test('assert.doesNotThrow', () => {", + ' > 70 | assert.doesNotThrow(() => {', + ' | ^', + " 71 | throw Error('err!');", + ' 72 | });', + ' 73 | });', + ' at Object.doesNotThrow (__tests__/assertionError.test.js:70:10)', + ].join('\n'); + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); +}); + +test('single line large changes', () => { + const expected = 'Array length must be a finite positive integer'; + const received = 'Invalid array length'; + + expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); +}); + +describe('without serialize', () => { + test('backtick single line expected and received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = `back${x}tick`;'; + + expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('backtick single line expected and multi line received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = `back\ntick`;'; + + expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + }); + + test('prettier/pull/5590', () => { + const expected = [ + '====================================options=====================================', + 'parsers: ["html"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + `John "ShotGun" Nelson`, + '', + '=====================================output=====================================', + 'John "ShotGun" Nelson', + '', + '================================================================================', + ].join('\n'); + const received = [ + '====================================options=====================================', + 'parsers: ["html"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + `John "ShotGun" Nelson`, + '', + '=====================================output=====================================', + `John "ShotGun" Nelson`, + '', + '================================================================================', + ].join('\n'); + + expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + }); +}); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index b579e279e7a8..dd9978d1acb5 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -10,10 +10,8 @@ import {Config} from '@jest/types'; import {FS as HasteFS} from 'jest-haste-map'; // eslint-disable-line import/no-extraneous-dependencies import {MatcherState} from 'expect'; -import diff from 'jest-diff'; import { BOLD_WEIGHT, - EXPECTED_COLOR, matcherHint, MatcherHintOptions, RECEIVED_COLOR, @@ -26,6 +24,7 @@ import { } from './snapshot_resolver'; import SnapshotState from './State'; import {addSerializer, getSerializers} from './plugins'; +import {printDiffOrStringified} from './print'; import * as utils from './utils'; type Context = MatcherState & { @@ -327,19 +326,22 @@ const _toMatchSnapshot = ({ } else { expected = (expected || '').trim(); actual = (actual || '').trim(); - const diffMessage = diff(expected, actual, { - aAnnotation: 'Snapshot', - bAnnotation: 'Received', - expand: snapshotState.expand, - }); + + // Assign to local variable because of declaration let expected: + // TypeScript thinks it could change before report function is called. + const printed = printDiffOrStringified( + expected, + actual, + received, + 'Snapshot', + 'Received', + snapshotState.expand, + ); report = () => - `Snapshot name: ${printName(currentTestName, hint, count)}\n\n` + - (diffMessage || - EXPECTED_COLOR('- ' + (expected || '')) + - '\n' + - RECEIVED_COLOR('+ ' + actual)); + `Snapshot name: ${printName(currentTestName, hint, count)}\n\n` + printed; } + // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message diff --git a/packages/jest-snapshot/src/print.ts b/packages/jest-snapshot/src/print.ts new file mode 100644 index 000000000000..bd4aacd7cf38 --- /dev/null +++ b/packages/jest-snapshot/src/print.ts @@ -0,0 +1,130 @@ +/** + * 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 diff, {getStringDiff} from 'jest-diff'; +import getType, {isPrimitive} from 'jest-get-type'; +import { + EXPECTED_COLOR, + RECEIVED_COLOR, + getLabelPrinter, + printDiffOrStringify, +} from 'jest-matcher-utils'; +import prettyFormat from 'pretty-format'; +import {unescape} from './utils'; + +const isLineDiffable = (received: any): boolean => { + const receivedType = getType(received); + + if (isPrimitive(received)) { + return typeof received === 'string' && received.includes('\n'); + } + + 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 printDiffOrStringified = ( + expectedSerializedTrimmed: string, + receivedSerializedTrimmed: string, + received: unknown, + expectedLabel: string, + receivedLabel: string, + expand: boolean, // CLI options: true if `--expand` or false if `--no-expand` +): string => { + if (typeof received === 'string') { + if ( + expectedSerializedTrimmed.length >= 2 && + expectedSerializedTrimmed.startsWith('"') && + expectedSerializedTrimmed.endsWith('"') && + receivedSerializedTrimmed === unescape(prettyFormat(received)) + ) { + // The expected snapshot looks like a stringified string. + // The received serialization is default stringified string. + + // Undo default serialization of expected snapshot: + // Remove enclosing double quote marks. + // Remove backslash escape preceding backslash here, + // because unescape replaced it only preceding double quote mark. + return printDiffOrStringify( + expectedSerializedTrimmed.slice(1, -1).replace(/\\\\/g, '\\'), + received, + expectedLabel, + receivedLabel, + expand, + ); + } + + // Display substring highlight even when strings have custom serialization. + const result = getStringDiff( + expectedSerializedTrimmed, + receivedSerializedTrimmed, + { + aAnnotation: expectedLabel, + bAnnotation: receivedLabel, + expand, + }, + ); + + if (result !== null) { + if (result.isMultiline) { + return result.annotatedDiff; + } + + // Because not default stringify, call EXPECTED_COLOR and RECEIVED_COLOR + // This is reason to call getStringDiff instead of printDiffOrStringify + // Because there is no closing double quote mark at end of single lines, + // future improvement is to call replaceSpacesAtEnd if it becomes public. + const printLabel = getLabelPrinter(expectedLabel, receivedLabel); + return ( + printLabel(expectedLabel) + + EXPECTED_COLOR(result.a) + + '\n' + + printLabel(receivedLabel) + + RECEIVED_COLOR(result.b) + ); + } + } + + if ( + (expectedSerializedTrimmed.includes('\n') || + receivedSerializedTrimmed.includes('\n')) && + isLineDiffable(received) + ) { + return diff(expectedSerializedTrimmed, receivedSerializedTrimmed, { + aAnnotation: expectedLabel, + bAnnotation: receivedLabel, + expand, + }) as string; + } + + const printLabel = getLabelPrinter(expectedLabel, receivedLabel); + return ( + printLabel(expectedLabel) + + EXPECTED_COLOR(expectedSerializedTrimmed) + + '\n' + + printLabel(receivedLabel) + + RECEIVED_COLOR(receivedSerializedTrimmed) + ); +}; diff --git a/packages/jest-snapshot/tsconfig.json b/packages/jest-snapshot/tsconfig.json index dba6475fc19c..f76821875718 100644 --- a/packages/jest-snapshot/tsconfig.json +++ b/packages/jest-snapshot/tsconfig.json @@ -7,6 +7,7 @@ "references": [ {"path": "../expect"}, {"path": "../jest-diff"}, + {"path": "../jest-get-type"}, {"path": "../jest-haste-map"}, {"path": "../jest-matcher-utils"}, {"path": "../jest-message-util"},