diff --git a/CHANGELOG.md b/CHANGELOG.md index c1bf053cde4a..a654ea7f9591 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -20,6 +20,7 @@ - `[jest-get-type]` Add `BigInt` support. ([#8382](https://github.com/facebook/jest/pull/8382)) - `[jest-matcher-utils]` Add `BigInt` support to `ensureNumbers` `ensureActualIsNumber`, `ensureExpectedIsNumber` ([#8382](https://github.com/facebook/jest/pull/8382)) - `[jest-runner]` Warn if a worker had to be force exited ([#8206](https://github.com/facebook/jest/pull/8206)) +- `[jest-snapshot]` Display change counts in annotation lines ([#8982](https://github.com/facebook/jest/pull/8982)) - `[@jest/test-result]` Create method to create empty `TestResult` ([#8867](https://github.com/facebook/jest/pull/8867)) - `[jest-worker]` [**BREAKING**] Return a promise from `end()`, resolving with the information whether workers exited gracefully ([#8206](https://github.com/facebook/jest/pull/8206)) diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index e2c3f7fb1f2e..a051fd99db72 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -108,7 +108,9 @@ test('first snapshot fails, second passes', () => { writeFiles(TESTS_DIR, {[filename]: template([`'kiwi'`, `'banana'`])}); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch('Snapshot name: `snapshots 1`'); - expect(stderr).toMatch('Snapshot: "apple"\n Received: "kiwi"'); + // Match lines separately because empty line has been replaced with space: + expect(stderr).toMatch('Snapshot: "apple"'); + expect(stderr).toMatch('Received: "kiwi"'); expect(stderr).not.toMatch('1 obsolete snapshot found'); expect(exitCode).toBe(1); } diff --git a/packages/jest-diff/README.md b/packages/jest-diff/README.md index 108fbd673aa6..a1c9d1c68861 100644 --- a/packages/jest-diff/README.md +++ b/packages/jest-diff/README.md @@ -173,8 +173,8 @@ const difference = diffLinesUnified(splitLines0(a), splitLines0(b), options); Given an empty string, `splitLines0(b)` returns `[]` an empty array, formatted as no `Received` lines: ```diff -- Expected 3 -+ Received 0 +- Expected - 3 ++ Received + 0 - multi - line @@ -194,8 +194,8 @@ const difference = diffLinesUnified(a.split('\n'), b.split('\n'), options); Given an empty string, `b.split('\n')` returns `['']` an array that contains an empty string, formatted as one empty `Received` line, which is **ambiguous** with an empty line: ```diff -- Expected 3 -+ Received 1 +- Expected - 3 ++ Received + 1 - multi - line @@ -220,16 +220,16 @@ You might call this function for case insensitive or Unicode equivalence compari import format from 'pretty-format'; const a = { - action: 'MOVE_TO', - x: 1, - y: 2, + text: 'Ignore indentation in serialized object', + time: '2019-09-19T12:34:56.000Z', + type: 'CREATE_ITEM', }; const b = { - action: 'MOVE_TO', payload: { - x: 1, - y: 2, + text: 'Ignore indentation in serialized object', + time: '2019-09-19T12:34:56.000Z', }, + type: 'CREATE_ITEM', }; const difference = diffLinesUnified2( @@ -242,18 +242,18 @@ const difference = diffLinesUnified2( ); ``` -The `x` and `y` properties are common, because their only difference is indentation: +The `text` and `time` properties are common, because their only difference is indentation: ```diff - Expected + Received Object { - action: 'MOVE_TO', + payload: Object { - x: 1, - y: 2, + text: 'Ignore indentation in serialized object', + time: '2019-09-19T12:34:56.000Z', + }, + type: 'CREATE_ITEM', } ``` @@ -519,8 +519,8 @@ const difference = diffDefault(a, b, options); ``` ```diff -- Expected 1 - -+ Received 2 + +- Expected - 1 ++ Received + 2 Array [ "common", diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.ts.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.ts.snap index 981f946af1cc..7cc376692267 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.ts.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.ts.snap @@ -34,8 +34,8 @@ exports[`color of text (expanded) 1`] = ` `; exports[`context number of lines: -1 (5 default) 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -6,9 +6,9 @@ 4, @@ -51,8 +51,8 @@ exports[`context number of lines: -1 (5 default) 1`] = ` `; exports[`context number of lines: 0 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -11,1 +11,0 @@ - 9, @@ -61,8 +61,8 @@ exports[`context number of lines: 0 1`] = ` `; exports[`context number of lines: 1 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -10,4 +10,4 @@ 8, @@ -73,8 +73,8 @@ exports[`context number of lines: 1 1`] = ` `; exports[`context number of lines: 2 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -9,6 +9,6 @@ 7, @@ -87,8 +87,8 @@ exports[`context number of lines: 2 1`] = ` `; exports[`context number of lines: 3.1 (5 default) 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -6,9 +6,9 @@ 4, @@ -104,8 +104,8 @@ exports[`context number of lines: 3.1 (5 default) 1`] = ` `; exports[`context number of lines: undefined (5 default) 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 @@ -6,9 +6,9 @@ 4, @@ -121,36 +121,36 @@ exports[`context number of lines: undefined (5 default) 1`] = ` `; exports[`diffStringsUnified edge cases empty both a and b 1`] = ` -- Expected 0 - -+ Received 0 + +- Expected - 0 ++ Received + 0 `; exports[`diffStringsUnified edge cases empty only a 1`] = ` -- Expected 0 - -+ Received 1 + +- Expected - 0 ++ Received + 1 + one-line string `; exports[`diffStringsUnified edge cases empty only b 1`] = ` -- Expected 1 - -+ Received 0 + +- Expected - 1 ++ Received + 0 - one-line string `; exports[`diffStringsUnified edge cases equal both non-empty 1`] = ` -- Expected 0 - -+ Received 0 + +- Expected - 0 ++ Received + 0 one-line string `; exports[`diffStringsUnified edge cases multiline has no common after clean up chaff 1`] = ` -- Expected 2 - -+ Received 2 + +- Expected - 2 ++ Received + 2 - delete - two @@ -159,16 +159,16 @@ exports[`diffStringsUnified edge cases multiline has no common after clean up ch `; exports[`diffStringsUnified edge cases one-line has no common after clean up chaff 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 - delete + insert `; exports[`falls back to not call toJSON if it throws and then objects have differences 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 Object { - "line": 1, @@ -181,8 +181,8 @@ exports[`falls back to not call toJSON if serialization has no differences but t Compared values serialize to the same structure. Printing internal object structure without calling \`toJSON\` instead. -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 Object { - "line": 1, @@ -192,24 +192,24 @@ exports[`falls back to not call toJSON if serialization has no differences but t `; exports[`oneline strings 1`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 - ab + aa `; exports[`oneline strings 2`] = ` -- Expected 1 - -+ Received 1 + +- Expected - 1 ++ Received + 1 - 123456789 + 234567890 `; exports[`oneline strings 3`] = ` -- Expected 1 - -+ Received 2 + +- Expected - 1 ++ Received + 2 - oneline + multi @@ -217,8 +217,8 @@ exports[`oneline strings 3`] = ` `; exports[`oneline strings 4`] = ` -- Expected 2 - -+ Received 1 + +- Expected - 2 ++ Received + 1 - multi - line @@ -301,8 +301,8 @@ exports[`options includeChangeCounts false diffStringsUnified 1`] = ` `; exports[`options includeChangeCounts true padding diffLinesUnified a has 2 digits 1`] = ` -- Before 10 - -+ After 1 + +- Before - 10 ++ After + 1 common - a @@ -319,8 +319,8 @@ exports[`options includeChangeCounts true padding diffLinesUnified a has 2 digit `; exports[`options includeChangeCounts true padding diffLinesUnified b has 2 digits 1`] = ` -- Before 1 - -+ After 10 + +- Before - 1 ++ After + 10 common - a @@ -337,8 +337,8 @@ exports[`options includeChangeCounts true padding diffLinesUnified b has 2 digit `; exports[`options includeChangeCounts true padding diffStringsUnified 1`] = ` -- Before 1 - -+ After 1 + +- Before - 1 ++ After + 1 - change from + change to diff --git a/packages/jest-diff/src/printDiffs.ts b/packages/jest-diff/src/printDiffs.ts index 1c3c975f15d7..f694b36fcb0b 100644 --- a/packages/jest-diff/src/printDiffs.ts +++ b/packages/jest-diff/src/printDiffs.ts @@ -162,16 +162,20 @@ export const printAnnotation = ( const aCount = String(changeCounts.a); const bCount = String(changeCounts.b); - const aPadding = - Math.max(bAnnotation.length - aAnnotation.length, 0) + - Math.max(bCount.length - aCount.length, 0); - const bPadding = - Math.max(aAnnotation.length - bAnnotation.length, 0) + - Math.max(aCount.length - bCount.length, 0); - - // Separate annotation from count by padding plus margin of 2 spaces. - aRest = ' '.repeat(aPadding + 2) + aCount + ' ' + aIndicator; - bRest = ' '.repeat(bPadding + 2) + bCount + ' ' + bIndicator; + // Padding right aligns the ends of the annotations. + const baAnnotationLengthDiff = bAnnotation.length - aAnnotation.length; + const aAnnotationPadding = ' '.repeat(Math.max(0, baAnnotationLengthDiff)); + const bAnnotationPadding = ' '.repeat(Math.max(0, -baAnnotationLengthDiff)); + + // Padding left aligns the ends of the counts. + const baCountLengthDiff = bCount.length - aCount.length; + const aCountPadding = ' '.repeat(Math.max(0, baCountLengthDiff)); + const bCountPadding = ' '.repeat(Math.max(0, -baCountLengthDiff)); + + aRest = + aAnnotationPadding + ' ' + aIndicator + ' ' + aCountPadding + aCount; + bRest = + bAnnotationPadding + ' ' + bIndicator + ' ' + bCountPadding + bCount; } return ( diff --git a/packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts b/packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts index adaf7d7608ea..df1bf6051714 100644 --- a/packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts +++ b/packages/jest-matcher-utils/src/__tests__/printDiffOrStringify.test.ts @@ -6,7 +6,7 @@ */ import {alignedAnsiStyleSerializer} from '@jest/test-utils'; -import {EXPECTED_COLOR, INVERTED_COLOR, printDiffOrStringify} from '../index'; +import {INVERTED_COLOR, printDiffOrStringify} from '../index'; expect.addSnapshotSerializer(alignedAnsiStyleSerializer); @@ -50,16 +50,40 @@ describe('printDiffOrStringify', () => { expect(testDiffOrStringify(expected, received)).toMatchSnapshot(); }); - test('received is multiline longer than max', () => { - const expected = 'multi\nline'; - const received = 'multi' + '\n123456789'.repeat(2000); // 5 + 20K chars + describe('MAX_DIFF_STRING_LENGTH', () => { + const lessChange = INVERTED_COLOR('single '); + const less = 'single line'; + const more = 'multi line' + '\n123456789'.repeat(2000); // 10 + 20K chars + + test('both are less', () => { + const difference = testDiffOrStringify('multi\nline', less); + + expect(difference).toMatch('- multi'); + expect(difference).toMatch('- line'); + + // diffStringsUnified has substring change + expect(difference).not.toMatch('+ single line'); + expect(difference).toMatch(lessChange); + }); + + test('expected is more', () => { + const difference = testDiffOrStringify(more, less); + + expect(difference).toMatch('- multi line'); + expect(difference).toMatch('+ single line'); + + // diffLinesUnified does not have substring change + expect(difference).not.toMatch(lessChange); + }); - const test = testDiffOrStringify(expected, received); + test('received is more', () => { + const difference = testDiffOrStringify(less, more); - // It is a generic line diff: - expect(test).toContain(EXPECTED_COLOR('- line')); + expect(difference).toMatch('- single line'); + expect(difference).toMatch('+ multi line'); - // It is not a specific substring diff - expect(test).not.toContain(EXPECTED_COLOR('- ' + INVERTED_COLOR('line'))); + // diffLinesUnified does not have substring change + expect(difference).not.toMatch(lessChange); + }); }); }); diff --git a/packages/jest-snapshot/src/State.ts b/packages/jest-snapshot/src/State.ts index db7868b03cb7..ea0e886d9a0f 100644 --- a/packages/jest-snapshot/src/State.ts +++ b/packages/jest-snapshot/src/State.ts @@ -12,10 +12,10 @@ import {getStackTraceLines, getTopFrame} from 'jest-message-util'; import { getSnapshotData, keyToTestName, + removeExtraLineBreaks, saveSnapshotFile, serialize, testNameToKey, - unescape, } from './utils'; import {InlineSnapshot, saveInlineSnapshots} from './inline_snapshots'; import {SnapshotData} from './types'; @@ -29,7 +29,7 @@ export type SnapshotStateOptions = { export type SnapshotMatchOptions = { testName: string; - received: any; + received: unknown; key?: string; inlineSnapshot?: string; isInline: boolean; @@ -253,9 +253,12 @@ export default class SnapshotState { if (!pass) { this.unmatched++; return { - actual: unescape(receivedSerialized), + actual: removeExtraLineBreaks(receivedSerialized), count, - expected: expected !== undefined ? unescape(expected) : undefined, + expected: + expected !== undefined + ? removeExtraLineBreaks(expected) + : undefined, key, pass: false, }; diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap index d26c80eb8b56..1a5d74cefabb 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap @@ -11,15 +11,17 @@ Received: "single line string" `; exports[`empty string received and expected multi line 1`] = ` -Snapshot: "multi -line -string" -Received: "" +- Snapshot - 3 ++ Received + 0 + +- multi +- line +- string `; exports[`escape backslash in multi line string 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 2 - Forward / slash and back \\ slash + Forward / slash @@ -42,8 +44,8 @@ Received: /\\\\\\\\\\("\\)/ `; exports[`expand false 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 3 @@ -12,7 +12,9 @@ ? "number" @@ -59,8 +61,8 @@ exports[`expand false 1`] = ` `; exports[`expand true 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 3 type TypeName = T extends string ? "string" : @@ -86,8 +88,8 @@ exports[`expand true 1`] = ` `; exports[`fallback to line diff 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 8 + ====================================options===================================== + parsers: ["flow", "typescript"] @@ -106,8 +108,8 @@ exports[`fallback to line diff 1`] = ` `; exports[`has no common after clean up chaff array 1`] = ` -- Snapshot -+ Received +- Snapshot - 2 ++ Received + 2 Array [ - "delete", @@ -122,19 +124,41 @@ Snapshot: "delete" Received: "insert" `; +exports[`isLineDiffable false asymmetric matcher 1`] = ` +Snapshot: null +Received: Object { + "asymmetricMatch": [Function], +} +`; + exports[`isLineDiffable false boolean 1`] = ` Snapshot: true Received: false `; +exports[`isLineDiffable false date 1`] = ` +Snapshot: 2019-09-19T00:00:00.000Z +Received: 2019-09-20T00:00:00.000Z +`; + +exports[`isLineDiffable false error 1`] = ` +Snapshot: [Error: Cannot spread fragment "NameAndAppearances" within itself.] +Received: [Error: Cannot spread fragment "NameAndAppearancesAndFriends" within itself.] +`; + +exports[`isLineDiffable false function 1`] = ` +Snapshot: undefined +Received: [Function] +`; + exports[`isLineDiffable false number 1`] = ` Snapshot: -0 Received: NaN `; exports[`isLineDiffable true array 1`] = ` -- Snapshot -+ Received +- Snapshot - 0 ++ Received + 2 Array [ Object { @@ -151,8 +175,8 @@ exports[`isLineDiffable true array 1`] = ` `; exports[`isLineDiffable true object 1`] = ` -- Snapshot -+ Received +- Snapshot - 2 ++ Received + 3 Object { "props": Object { @@ -167,8 +191,8 @@ exports[`isLineDiffable true object 1`] = ` `; exports[`isLineDiffable true single line expected and multi line received 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 3 - Array [] + Array [ @@ -177,13 +201,16 @@ exports[`isLineDiffable true single line expected and multi line received 1`] = `; exports[`isLineDiffable true single line expected and received 1`] = ` -Snapshot: Array [] -Received: Object {} +- Snapshot - 1 ++ Received + 1 + +- Array [] ++ Object {} `; exports[`multi line small change in one line and other is unchanged 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 1 - There is no route defined for key 'Settings'. + There is no route defined for key Settings. @@ -191,8 +218,8 @@ exports[`multi line small change in one line and other is unchanged 1`] = ` `; exports[`multi line small changes 1`] = ` -- Snapshot -+ Received +- Snapshot - 7 ++ Received + 7 - 69 | + 68 | @@ -217,8 +244,8 @@ Received: "Invalid array length" `; exports[`without serialize backtick single line expected and multi line received 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 2 - var foo = \`backtick\`; + var foo = \`back @@ -226,16 +253,16 @@ exports[`without serialize backtick single line expected and multi line received `; exports[`without serialize backtick single line expected and received 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 1 - var foo = \`backtick\`; + var foo = \`back\${x}tick\`; `; exports[`without serialize has no common after clean up chaff multi line 1`] = ` -- Snapshot -+ Received +- Snapshot - 2 ++ Received + 2 - delete - two @@ -244,16 +271,16 @@ exports[`without serialize has no common after clean up chaff multi line 1`] = ` `; exports[`without serialize has no common after clean up chaff single line 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 1 - delete + insert `; exports[`without serialize prettier/pull/5590 1`] = ` -- Snapshot -+ Received +- Snapshot - 1 ++ Received + 1 @@ -4,8 +4,8 @@ | printWidth diff --git a/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts b/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts index 862e5a52e9a7..3651bd201834 100644 --- a/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts +++ b/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts @@ -7,8 +7,9 @@ import ansiRegex = require('ansi-regex'); import * as style from 'ansi-styles'; +import chalk from 'chalk'; import {printDiffOrStringified} from '../print'; -import {serialize, unescape} from '../utils'; +import {stringify} from '../utils'; // This is an experiment to read snapshots more easily: // * to avoid first line misaligned because of opening double quote mark, @@ -58,51 +59,35 @@ expect.addSnapshotSerializer({ }, }); -const testWithSerialize = ( - expected: any, - received: any, +// Simulate default serialization. +const testWithStringify = ( + expected: unknown, + received: unknown, 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( +): string => + convertStyles( printDiffOrStringified( - expectedSerializedTrimmed, - receivedSerializedTrimmed, + stringify(expected), + stringify(received), received, - 'Snapshot', - 'Received', expand, ), ); -}; -const testWithoutSerialize = ( +// Simulate custom raw string serialization. +const testWithoutStringify = ( expected: string, received: string, expand: boolean, ): string => - convertStyles( - printDiffOrStringified( - expected, - received, - received, - 'Snapshot', - 'Received', - expand, - ), - ); + convertStyles(printDiffOrStringified(expected, received, 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(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); }); @@ -111,14 +96,14 @@ describe('empty string', () => { const expected = ''; const received = 'single line string'; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('received and expected multi line', () => { const expected = 'multi\nline\nstring'; const received = ''; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); }); @@ -127,28 +112,28 @@ describe('escape', () => { const expected = 'What does "oobleck" mean?'; const received = 'What does "ewbleck" mean?'; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(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(); + expect(testWithStringify(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(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('regexp', () => { const expected = /\\(")/g; const received = /\\(")/; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); }); @@ -198,11 +183,11 @@ describe('expand', () => { ].join('\n'); test('false', () => { - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('true', () => { - expect(testWithSerialize(expected, received, true)).toMatchSnapshot(); + expect(testWithStringify(expected, received, true)).toMatchSnapshot(); }); }); @@ -231,7 +216,7 @@ test('fallback to line diff', () => { '================================================================================', ].join('\n'); - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); describe('has no common after clean up chaff', () => { @@ -239,31 +224,149 @@ describe('has no common after clean up chaff', () => { const expected = ['delete', 'two']; const received = ['insert', '2']; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('string single line', () => { const expected = 'delete'; const received = 'insert'; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); +}); + +describe('MAX_DIFF_STRING_LENGTH', () => { + describe('unquoted', () => { + // Do not call diffStringsUnified if either string is longer than max. + const lessChange = chalk.inverse('single '); + const less = 'single line'; + const more = 'multi line' + '\n123456789'.repeat(2000); // 10 + 20K chars + + test('both are less', () => { + const less2 = 'multi\nline'; + const difference = printDiffOrStringified(less2, less, less, true); + + expect(difference).toMatch('- multi'); + expect(difference).toMatch('- line'); + expect(difference).toMatch(lessChange); + expect(difference).not.toMatch('+ single line'); + }); + + test('expected is more', () => { + const difference = printDiffOrStringified(more, less, less, true); + + expect(difference).toMatch('- multi line'); + expect(difference).toMatch('+ single line'); + expect(difference).not.toMatch(lessChange); + }); + + test('received is more', () => { + const difference = printDiffOrStringified(less, more, more, true); + + expect(difference).toMatch('- single line'); + expect(difference).toMatch('+ multi line'); + expect(difference).not.toMatch(lessChange); + }); + }); + + describe('quoted', () => { + // Do not call diffStringsRaw if either string is longer than max. + const lessChange = chalk.inverse('no'); + const less = 'no numbers'; + const more = 'many numbers' + ' 123456789'.repeat(2000); // 12 + 20K chars + const lessQuoted = '"' + less + '"'; + const moreQuoted = '"' + more + '"'; + + test('both are less', () => { + const lessQuoted2 = '"0 numbers"'; + const stringified = printDiffOrStringified( + lessQuoted2, + lessQuoted, + less, + true, + ); + + expect(stringified).toMatch('Received:'); + expect(stringified).toMatch(lessChange); + expect(stringified).not.toMatch('+ Received'); + }); + + test('expected is more', () => { + const stringified = printDiffOrStringified( + moreQuoted, + lessQuoted, + less, + true, + ); + + expect(stringified).toMatch('Received:'); + expect(stringified).toMatch(less); + expect(stringified).not.toMatch('+ Received'); + expect(stringified).not.toMatch(lessChange); + }); + + test('received is more', () => { + const stringified = printDiffOrStringified( + lessQuoted, + moreQuoted, + more, + true, + ); + + expect(stringified).toMatch('Snapshot:'); + expect(stringified).toMatch(less); + expect(stringified).not.toMatch('- Snapshot'); + expect(stringified).not.toMatch(lessChange); + }); }); }); describe('isLineDiffable', () => { describe('false', () => { + test('asymmetric matcher', () => { + const expected = null; + const received = {asymmetricMatch: () => {}}; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + test('boolean', () => { const expected = true; const received = false; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('date', () => { + const expected = new Date('2019-09-19'); + const received = new Date('2019-09-20'); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('error', () => { + const expected = new Error( + 'Cannot spread fragment "NameAndAppearances" within itself.', + ); + const received = new Error( + 'Cannot spread fragment "NameAndAppearancesAndFriends" within itself.', + ); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('function', () => { + const expected = undefined; + const received = () => {}; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('number', () => { const expected = -0; const received = NaN; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); }); @@ -284,7 +387,7 @@ describe('isLineDiffable', () => { {_id: '7fc63ff01769c4fa7d9279e97e307829', ...expected1}, ]; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('object', () => { @@ -305,21 +408,21 @@ describe('isLineDiffable', () => { type, }; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('single line expected and received', () => { const expected = []; const received = {}; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('single line expected and multi line received', () => { const expected = []; const received = [0]; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); }); }); @@ -330,7 +433,7 @@ test('multi line small change in one line and other is unchanged', () => { const received = "There is no route defined for key Settings.\nMust be one of: 'Home'"; - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); test('multi line small changes', () => { @@ -355,14 +458,14 @@ test('multi line small changes', () => { ' at Object.doesNotThrow (__tests__/assertionError.test.js:70:10)', ].join('\n'); - expect(testWithSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithStringify(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(); + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); }); describe('without serialize', () => { @@ -370,28 +473,28 @@ describe('without serialize', () => { const expected = 'var foo = `backtick`;'; const received = 'var foo = `back${x}tick`;'; - expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithoutStringify(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(); + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); }); test('has no common after clean up chaff multi line', () => { const expected = 'delete\ntwo'; const received = 'insert\n2'; - expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); }); test('has no common after clean up chaff single line', () => { const expected = 'delete'; const received = 'insert'; - expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); }); test('prettier/pull/5590', () => { @@ -422,6 +525,6 @@ describe('without serialize', () => { '================================================================================', ].join('\n'); - expect(testWithoutSerialize(expected, received, false)).toMatchSnapshot(); + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); }); }); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index e898b94630a9..3e587f1a89b3 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -343,7 +343,7 @@ const _toMatchSnapshot = ({ testName: fullTestName, }); const {count, pass} = result; - let {actual, expected} = result; + const {actual, expected} = result; let report: () => string; if (pass) { @@ -357,22 +357,9 @@ const _toMatchSnapshot = ({ `${RECEIVED_COLOR('Received value')} ` + `${actual}`; } else { - expected = utils.removeExtraLineBreaks(expected); - actual = utils.removeExtraLineBreaks(actual); - - // 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` + printed; + `Snapshot name: ${printName(currentTestName, hint, count)}\n\n` + + printDiffOrStringified(expected, actual, received, snapshotState.expand); } // Passing the actual and expected objects so that a custom reporter diff --git a/packages/jest-snapshot/src/print.ts b/packages/jest-snapshot/src/print.ts index c59df18106f5..b277c5fd51a8 100644 --- a/packages/jest-snapshot/src/print.ts +++ b/packages/jest-snapshot/src/print.ts @@ -5,22 +5,54 @@ * LICENSE file in the root directory of this source tree. */ -import diff, {diffStringsUnified} from 'jest-diff'; +import { + DIFF_DELETE, + DIFF_EQUAL, + DIFF_INSERT, + Diff, + diffLinesUnified, + diffStringsRaw, + diffStringsUnified, + splitLines0, +} from 'jest-diff'; import getType = require('jest-get-type'); import { EXPECTED_COLOR, + INVERTED_COLOR, RECEIVED_COLOR, getLabelPrinter, - printDiffOrStringify, } from 'jest-matcher-utils'; import prettyFormat = require('pretty-format'); -import {unescape} from './utils'; +import {unstringifyString} from './utils'; + +// 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, + 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: any): boolean => { const receivedType = getType(received); if (getType.isPrimitive(received)) { - return typeof received === 'string' && received.includes('\n'); + return typeof received === 'string'; } if ( @@ -48,74 +80,84 @@ const isLineDiffable = (received: any): boolean => { const MAX_DIFF_STRING_LENGTH = 20000; export const printDiffOrStringified = ( - expectedSerializedTrimmed: string, - receivedSerializedTrimmed: string, + a: string, // snapshot without extra line breaks + b: string, // received serialized but without extra line breaks received: unknown, - expectedLabel: string, - receivedLabel: string, expand: boolean, // CLI options: true if `--expand` or false if `--no-expand` ): string => { + const aAnnotation = 'Snapshot'; + const bAnnotation = 'Received'; + const aColor = EXPECTED_COLOR; + const bColor = RECEIVED_COLOR; + const options = { + aAnnotation, + aColor, + bAnnotation, + bColor, + expand, + includeChangeCounts: true, + }; + if (typeof received === 'string') { if ( - expectedSerializedTrimmed.length >= 2 && - expectedSerializedTrimmed.startsWith('"') && - expectedSerializedTrimmed.endsWith('"') && - receivedSerializedTrimmed === unescape(prettyFormat(received)) + a.length >= 2 && + a.startsWith('"') && + a.endsWith('"') && + b === 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, - ); - } + // If snapshot looks like default serialization of a string + // and received is string which has default serialization. - // Display substring highlight even when strings have custom serialization. - if ( - expectedSerializedTrimmed.length !== 0 && - receivedSerializedTrimmed.length !== 0 && - expectedSerializedTrimmed.length <= MAX_DIFF_STRING_LENGTH && - receivedSerializedTrimmed.length <= MAX_DIFF_STRING_LENGTH && - expectedSerializedTrimmed !== receivedSerializedTrimmed - ) { - return diffStringsUnified( - expectedSerializedTrimmed, - receivedSerializedTrimmed, - { - aAnnotation: expectedLabel, - bAnnotation: receivedLabel, - expand, - }, - ); + 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 = unstringifyString(a); // hypothetical unserialized 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(splitLines0(a), splitLines0(b), options); } - if ( - (expectedSerializedTrimmed.includes('\n') || - receivedSerializedTrimmed.includes('\n')) && - isLineDiffable(received) - ) { - return diff(expectedSerializedTrimmed, receivedSerializedTrimmed, { - aAnnotation: expectedLabel, - bAnnotation: receivedLabel, - expand, - }) as string; + if (isLineDiffable(received)) { + // TODO future PR will replace with diffLinesUnified2 to ignore indentation + return diffLinesUnified(splitLines0(a), splitLines0(b), options); } - const printLabel = getLabelPrinter(expectedLabel, receivedLabel); + const printLabel = getLabelPrinter(aAnnotation, bAnnotation); return ( - printLabel(expectedLabel) + - EXPECTED_COLOR(expectedSerializedTrimmed) + + printLabel(aAnnotation) + + aColor(a) + '\n' + - printLabel(receivedLabel) + - RECEIVED_COLOR(receivedSerializedTrimmed) + printLabel(bAnnotation) + + bColor(b) ); }; diff --git a/packages/jest-snapshot/src/utils.ts b/packages/jest-snapshot/src/utils.ts index 3449492afe8c..eb97da5c3edc 100644 --- a/packages/jest-snapshot/src/utils.ts +++ b/packages/jest-snapshot/src/utils.ts @@ -136,19 +136,21 @@ export const removeExtraLineBreaks = (string: string): string => ? string.slice(1, -1) : string; -export const serialize = (data: string): string => - addExtraLineBreaks( - normalizeNewlines( - prettyFormat(data, { - escapeRegex: true, - plugins: getSerializers(), - printFunctionName: false, - }), - ), +export const serialize = (val: unknown): string => + addExtraLineBreaks(stringify(val)); + +export const stringify = (val: unknown): string => + normalizeNewlines( + prettyFormat(val, { + escapeRegex: true, + plugins: getSerializers(), + printFunctionName: false, + }), ); -// unescape double quotes -export const unescape = (data: string): string => data.replace(/\\(")/g, '$1'); +// Remove double quote marks and unescape double quotes and backslashes. +export const unstringifyString = (stringified: string): string => + stringified.slice(1, -1).replace(/\\("|\\)/g, '$1'); export const escapeBacktickString = (str: string): string => str.replace(/`|\\|\${/g, '\\$&');