From 95830607c0ffbab6efd1695ed0bdc217d2f37b5b Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Sat, 21 Sep 2019 14:02:47 -0400 Subject: [PATCH] jest-diff: Add firstOrLastEmptyLineReplacement option and export 3 diffLines functions (#8955) * jest-diff: Add trimmableLineReplacement option and export 3 diffLines functions * Fix prettier lint error in README.md * Update CHANGELOG.md * Rename function and remove unneeded export * Add splitLines0 as a named export * Fix prettier error in README.md * Move edge cases and splitLines0 after diffLinesUnified * Rename trimmableLineReplacement as firstOrLastEmptyLineReplacement * Fix prettier error in diff.test.ts file * Rename option in CHANGELOG.md --- CHANGELOG.md | 1 + packages/jest-diff/README.md | 338 ++++++++++++---- packages/jest-diff/src/__tests__/diff.test.ts | 35 +- packages/jest-diff/src/diffLines.ts | 378 ++++-------------- packages/jest-diff/src/index.ts | 98 +++-- packages/jest-diff/src/joinAlignedDiffs.ts | 17 +- .../jest-diff/src/normalizeDiffOptions.ts | 1 + packages/jest-diff/src/printDiffs.ts | 186 +++++---- packages/jest-diff/src/types.ts | 2 + .../printDiffOrStringified.test.ts.snap | 4 +- 10 files changed, 536 insertions(+), 524 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c020577ef04b..8d7e33a102ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ - `[jest-diff]` Add `includeChangeCounts` and rename `Indicator` options ([#8881](https://github.com/facebook/jest/pull/8881)) - `[jest-diff]` Add `changeColor` and `patchColor` options ([#8911](https://github.com/facebook/jest/pull/8911)) - `[jest-diff]` Add `trailingSpaceFormatter` option and replace cyan with `commonColor` ([#8927](https://github.com/facebook/jest/pull/8927)) +- `[jest-diff]` Add `firstOrLastEmptyLineReplacement` option and export 3 `diffLines` functions ([#8955](https://github.com/facebook/jest/pull/8955)) - `[jest-environment-jsdom]` Add `fakeTimersLolex` ([#8925](https://github.com/facebook/jest/pull/8925)) - `[jest-environment-node]` Add `fakeTimersLolex` ([#8925](https://github.com/facebook/jest/pull/8925)) - `[@jest/fake-timers]` Add Lolex as implementation of fake timers ([#8897](https://github.com/facebook/jest/pull/8897)) diff --git a/packages/jest-diff/README.md b/packages/jest-diff/README.md index 15262adea62d..108fbd673aa6 100644 --- a/packages/jest-diff/README.md +++ b/packages/jest-diff/README.md @@ -2,13 +2,18 @@ Display differences clearly so people can review changes confidently. -The default export serializes JavaScript **values** and compares them line-by-line. +The default export serializes JavaScript **values**, compares them line-by-line, and returns a string which includes comparison lines. Two named exports compare **strings** character-by-character: -- `diffStringsUnified` returns a string which includes comparison lines. +- `diffStringsUnified` returns a string. - `diffStringsRaw` returns an array of `Diff` objects. +Three named exports compare **arrays of strings** line-by-line: + +- `diffLinesUnified` and `diffLinesUnified2` return a string. +- `diffLinesRaw` returns an array of `Diff` objects. + ## Installation To add this package as a dependency of a project, run either of the following commands: @@ -18,11 +23,11 @@ To add this package as a dependency of a project, run either of the following co ## Usage of default export -Given values and optional options, `diffDefault(a, b, options?)` does the following: +Given JavaScript **values**, `diffDefault(a, b, options?)` does the following: -- **serialize** the values as strings using the `pretty-format` package -- **compare** the strings line-by-line using the `diff-sequences` package -- **format** the changed or common lines using the `chalk` package +1. **serialize** the values as strings using the `pretty-format` package +2. **compare** the strings line-by-line using the `diff-sequences` package +3. **format** the changed or common lines using the `chalk` package To use this function, write either of the following: @@ -32,8 +37,8 @@ To use this function, write either of the following: ### Example of default export ```js -const a = ['delete', 'changed from', 'common']; -const b = ['changed to', 'insert', 'common']; +const a = ['delete', 'common', 'changed from']; +const b = ['common', 'changed to', 'insert']; const difference = diffDefault(a, b); ``` @@ -49,10 +54,10 @@ The returned **string** consists of: Array [ - "delete", + "common", - "changed from", + "changed to", + "insert", - "common", ] ``` @@ -66,11 +71,11 @@ Here are edge cases for the return value: ## Usage of diffStringsUnified -Given strings and optional options, `diffStringsUnified(a, b, options?)` does the following: +Given **strings**, `diffStringsUnified(a, b, options?)` does the following: -- **compare** the strings character-by-character using the `diff-sequences` package -- **clean up** small (often coincidental) common substrings, also known as chaff -- **format** the changed or common lines using the `chalk` package +1. **compare** the strings character-by-character using the `diff-sequences` package +2. **clean up** small (often coincidental) common substrings, also known as chaff +3. **format** the changed or common lines using the `chalk` package Although the function is mainly for **multiline** strings, it compares any strings. @@ -82,8 +87,8 @@ Write either of the following: ### Example of diffStringsUnified ```js -const a = 'changed from\ncommon'; -const b = 'changed to\ncommon'; +const a = 'common\nchanged from'; +const b = 'common\nchanged to'; const difference = diffStringsUnified(a, b); ``` @@ -91,43 +96,175 @@ const difference = diffStringsUnified(a, b); The returned **string** consists of: - annotation lines: describe the two change indicators with labels, and a blank line -- comparison lines: similar to “unified” view on GitHub, and **changed substrings** have **inverse** foreground and background colors (which the following example does not show) +- comparison lines: similar to “unified” view on GitHub, and **changed substrings** have **inverse** foreground and background colors (that is, `from` has white-on-green and `to` has white-on-red, which the following example does not show) ```diff - Expected + Received + common - changed from + changed to +``` + +### Performance of diffStringsUnified + +To get the benefit of **changed substrings** within the comparison lines, a character-by-character comparison has a higher computational cost (in time and space) than a line-by-line comparison. + +If the input strings can have **arbitrary length**, we recommend that the calling code set a limit, beyond which splits the strings, and then calls `diffLinesUnified` instead. For example, Jest falls back to line-by-line comparison if either string has length greater than 20K characters. + +## Usage of diffLinesUnified + +Given **arrays of strings**, `diffLinesUnified(aLines, bLines, options?)` does the following: + +1. **compare** the arrays line-by-line using the `diff-sequences` package +2. **format** the changed or common lines using the `chalk` package + +You might call this function when strings have been split into lines and you do not need to see changed substrings within lines. + +### Example of diffLinesUnified + +```js +const aLines = ['delete', 'common', 'changed from']; +const bLines = ['common', 'changed to', 'insert']; + +const difference = diffLinesUnified(aLines, bLines); +``` + +```diff +- Expected ++ Received + +- delete common +- changed from ++ changed to ++ insert ``` -### Edge cases of diffStringsUnified +### Edge cases of diffLinesUnified or diffStringsUnified -Here are edge cases for the return value: +Here are edge cases for arguments and return values: - both `a` and `b` are empty strings: no comparison lines - only `a` is empty string: all comparison lines have `bColor` and `bIndicator` (see Options) - only `b` is empty string: all comparison lines have `aColor` and `aIndicator` (see Options) - `a` and `b` are equal non-empty strings: all comparison lines have `commonColor` and `commonIndicator` (see Options) -### Performance of diffStringsUnified +To get the comparison lines described above from the `diffLineUnified` function, call `splitLines0(string)` instead of `string.split('\n')` -To get the benefit of **changed substrings** within the comparison lines, a character-by-character comparison has a higher computational cost (in time and space) than a line-by-line comparison. +```js +export const splitLines0 = string => + string.length === 0 ? [] : string.split('\n'); +``` -If the input strings can have **arbitrary length**, we recommend that the calling code set a limit, beyond which it calls the default export instead. For example, Jest falls back to line-by-line comparison if either string has length greater than 20K characters. +### Example of splitLines0 function -## Usage of diffStringsRaw +```js +import {diffLinesUnified, splitLines0} from 'jest-diff'; + +const a = 'multi\nline\nstring'; +const b = ''; +const options = {includeChangeCounts: true}; // see Options + +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 + +- multi +- line +- string +``` -Given strings and boolean, `diffStringsRaw(a, b, cleanup)` does the following: +### Example of split method -- **compare** the strings character-by-character using the `diff-sequences` package -- optionally **clean up** small (often coincidental) common substrings, also known as chaff +```js +const a = 'multi\nline\nstring'; +const b = ''; +const options = {includeChangeCounts: true}; // see Options + +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 + +- multi +- line +- string ++ +``` + +## Usage of diffLinesUnified2 + +Given two **pairs** of arrays of strings, `diffLinesUnified2(aLinesDisplay, bLinesDisplay, aLinesCompare, bLinesCompare, options?)` does the following: + +1. **compare** the pair of `Compare` arrays line-by-line using the `diff-sequences` package +2. **format** the corresponding lines in the pair of `Display` arrays using the `chalk` package -Write one of the following: +Jest calls this function to consider lines as common instead of changed if the only difference is indentation. -- `const {diffStringsRaw} = require('jest-diff');` in CommonJS modules -- `import {diffStringsRaw} from 'jest-diff';` in ECMAScript modules +You might call this function for case insensitive or Unicode equivalence comparison of lines. + +### Example of diffLinesUnified2 + +```js +import format from 'pretty-format'; + +const a = { + action: 'MOVE_TO', + x: 1, + y: 2, +}; +const b = { + action: 'MOVE_TO', + payload: { + x: 1, + y: 2, + }, +}; + +const difference = diffLinesUnified2( + // serialize with indentation to display lines + format(a).split('\n'), + format(b).split('\n'), + // serialize without indentation to compare lines + format(a, {indent: 0}).split('\n'), + format(b, {indent: 0}).split('\n'), +); +``` + +The `x` and `y` properties are common, because their only difference is indentation: + +```diff +- Expected ++ Received + + Object { + action: 'MOVE_TO', ++ payload: Object { + x: 1, + y: 2, ++ }, + } +``` + +The preceding example illustrates why (at least for indentation) it seems more intuitive that the function returns the common line from the `bLinesDisplay` array instead of from the `aLinesDisplay` array. + +## Usage of diffStringsRaw + +Given **strings** and a boolean option, `diffStringsRaw(a, b, cleanup)` does the following: + +1. **compare** the strings character-by-character using the `diff-sequences` package +2. optionally **clean up** small (often coincidental) common substrings, also known as chaff Because `diffStringsRaw` returns the difference as **data** instead of a string, you can format it as your application requires (for example, enclosed in HTML markup for browser instead of escape sequences for console). @@ -147,46 +284,31 @@ The value at index `1` is a substring of `a` or `b` or both. ```js const diffs = diffStringsRaw('changed from', 'changed to', true); - -/* -diffs[0][0] === 0 // DIFF_EQUAL -diffs[0][1] === 'changed ' - -diffs[1][0] === -1 // DIFF_DELETE -diffs[1][1] === 'from' - -diffs[2][0] === 1 // DIFF_INSERT -diffs[2][1] === 'to' -*/ ``` +| `i` | `diffs[i][0]` | `diffs[i][1]` | +| --: | ------------: | :------------ | +| `0` | `0` | `'changed '` | +| `1` | `-1` | `'from'` | +| `2` | `1` | `'to'` | + ### Example of diffStringsRaw without cleanup ```js const diffs = diffStringsRaw('changed from', 'changed to', false); - -/* -diffs[0][0] === 0 // DIFF_EQUAL -diffs[0][1] === 'changed ' - -diffs[1][0] === -1 // DIFF_DELETE -diffs[1][1] === 'fr' - -diffs[2][0] === 1 // DIFF_INSERT -diffs[2][1] === 't' - -// Here is a small coincidental common substring: -diffs[3][0] === 0 // DIFF_EQUAL -diffs[3][1] === 'o' - -diffs[4][0] === -1 // DIFF_DELETE -diffs[4][1] === 'm' -*/ ``` +| `i` | `diffs[i][0]` | `diffs[i][1]` | +| --: | ------------: | :------------ | +| `0` | `0` | `'changed '` | +| `1` | `-1` | `'fr'` | +| `2` | `1` | `'t'` | +| `3` | `0` | `'o'` | +| `4` | `-1` | `'m'` | + ### Advanced import for diffStringsRaw -Here are all the named imports for the `diffStringsRaw` function: +Here are all the named imports that you might need for the `diffStringsRaw` function: - `const {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff, diffStringsRaw} = require('jest-diff');` in CommonJS modules - `import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff, diffStringsRaw} from 'jest-diff';` in ECMAScript modules @@ -201,6 +323,31 @@ const diffDelete = new Diff(DIFF_DELETE, 'from'); const diffInsert = new Diff(DIFF_INSERT, 'to'); ``` +## Usage of diffLinesRaw + +Given **arrays of strings**, `diffLinesRaw(aLines, bLines)` does the following: + +- **compare** the arrays line-by-line using the `diff-sequences` package + +Because `diffLinesRaw` returns the difference as **data** instead of a string, you can format it as your application requires. + +### Example of diffLinesRaw + +```js +const aLines = ['delete', 'common', 'changed from']; +const bLines = ['common', 'changed to', 'insert']; + +const diffs = diffLinesRaw(aLines, bLines); +``` + +| `i` | `diffs[i][0]` | `diffs[i][1]` | +| --: | ------------: | :--------------- | +| `0` | `-1` | `'delete'` | +| `1` | `0` | `'common'` | +| `2` | `-1` | `'changed from'` | +| `3` | `1` | `'changed to'` | +| `4` | `1` | `'insert'` | + ## Options The default options are for the report when an assertion fails from the `expect` package used by Jest. @@ -209,26 +356,29 @@ For other applications, you can provide an options object as a third argument: - `diffDefault(a, b, options)` - `diffStringsUnified(a, b, options)` +- `diffLinesUnified(aLines, bLines, options)` +- `diffLinesUnified2(aLinesDisplay, bLinesDisplay, aLinesCompare, bLinesCompare, options)` ### Properties of options object -| name | default | -| :----------------------- | :--------------- | -| `aAnnotation` | `'Expected'` | -| `aColor` | `chalk.green` | -| `aIndicator` | `'-'` | -| `bAnnotation` | `'Received'` | -| `bColor` | `chalk.red` | -| `bIndicator` | `'+'` | -| `changeColor` | `chalk.inverse` | -| `commonColor` | `chalk.dim` | -| `commonIndicator` | `' '` | -| `contextLines` | `5` | -| `expand` | `true` | -| `includeChangeCounts` | `false` | -| `omitAnnotationLines` | `false` | -| `patchColor` | `chalk.yellow` | -| `trailingSpaceFormatter` | `chalk.bgYellow` | +| name | default | +| :-------------------------------- | :--------------- | +| `aAnnotation` | `'Expected'` | +| `aColor` | `chalk.green` | +| `aIndicator` | `'-'` | +| `bAnnotation` | `'Received'` | +| `bColor` | `chalk.red` | +| `bIndicator` | `'+'` | +| `changeColor` | `chalk.inverse` | +| `commonColor` | `chalk.dim` | +| `commonIndicator` | `' '` | +| `contextLines` | `5` | +| `expand` | `true` | +| `firstOrLastEmptyLineReplacement` | `'↵'` | +| `includeChangeCounts` | `false` | +| `omitAnnotationLines` | `false` | +| `patchColor` | `chalk.yellow` | +| `trailingSpaceFormatter` | `chalk.bgYellow` | For more information about the options, see the following examples. @@ -247,9 +397,9 @@ const options = { - Original + Modified + common - changed from + changed to - common ``` The `jest-diff` package does not assume that the 2 labels have equal length. @@ -358,8 +508,8 @@ const options = { To display the number of changed lines at the right of annotation lines: ```js -const a = ['changed from', 'common']; -const b = ['changed to', 'insert', 'common']; +const a = ['common', 'changed from']; +const b = ['common', 'changed to', 'insert']; const options = { includeChangeCounts: true, @@ -373,10 +523,10 @@ const difference = diffDefault(a, b, options); + Received 2 + Array [ + "common", - "changed from", + "changed to", + "insert", - "common", ] ``` @@ -385,8 +535,8 @@ const difference = diffDefault(a, b, options); To display only the comparison lines: ```js -const a = 'changed from\ncommon'; -const b = 'changed to\ncommon'; +const a = 'common\nchanged from'; +const b = 'common\nchanged to'; const options = { omitAnnotationLines: true, @@ -396,7 +546,31 @@ const difference = diffStringsUnified(a, b, options); ``` ```diff + common - changed from + changed to - common ``` + +### Example of option not to replace first or last empty lines + +If the **first** or **last** comparison line is **empty**, because the content is empty and the indicator is a space, you might not notice it. + +Also, because Jest trims the report when a matcher fails, it deletes an empty last line. + +The replacement is a string whose default value is `'↵'` U+21B5 downwards arrow with corner leftwards. + +To store the difference in a file without a replacement, because it could be ambiguous with the content of the arguments, provide an empty string: + +```js +const options = { + firstOrLastEmptyLineReplacement: '', +}; +``` + +If a content line is empty, then the corresponding comparison line is automatically trimmed to remove the margin space (represented as a middle dot below) for the default indicators: + +| Indicator | untrimmed | trimmed | +| ----------------: | :-------- | :------ | +| `aIndicator` | `'-·'` | `'-'` | +| `bIndicator` | `'+·'` | `'+'` | +| `commonIndicator` | `' ·'` | `''` | diff --git a/packages/jest-diff/src/__tests__/diff.test.ts b/packages/jest-diff/src/__tests__/diff.test.ts index dcea872dcdeb..d75390fc9dc3 100644 --- a/packages/jest-diff/src/__tests__/diff.test.ts +++ b/packages/jest-diff/src/__tests__/diff.test.ts @@ -699,7 +699,7 @@ describe('trailing newline in multiline string not enclosed in quotes', () => { const b = a + '\n'; describe('from less to more', () => { - const expected = [' line 1', ' line 2', ' line 3', '+ '].join('\n'); + const expected = [' line 1', ' line 2', ' line 3', '+'].join('\n'); test('(unexpanded)', () => { expect(diff(a, b, unexpandedBe)).toBe(expected); @@ -710,7 +710,7 @@ describe('trailing newline in multiline string not enclosed in quotes', () => { }); describe('from more to less', () => { - const expected = [' line 1', ' line 2', ' line 3', '- '].join('\n'); + const expected = [' line 1', ' line 2', ' line 3', '-'].join('\n'); test('(unexpanded)', () => { expect(diff(b, a, unexpandedBe)).toBe(expected); @@ -956,4 +956,35 @@ describe('options', () => { expect(diff(aTrailingSpaces, bTrailingSpaces, options)).toMatchSnapshot(); }); }); + + describe('firstOrLastEmptyLineReplacement', () => { + const noColor = (string: string) => string; + const options = { + aColor: noColor, + bColor: noColor, + changeColor: noColor, + commonColor: noColor, + firstOrLastEmptyLineReplacement: '', + omitAnnotationLines: true, + }; + + const aEmpty = '\ncommon\nchanged from\n'; + const bEmpty = '\ncommon\nchanged to\n'; + + const expected = [ + '', + ' common', + '- changed from', + '+ changed to', + '', + ].join('\n'); + + test('diffDefault', () => { + expect(diff(aEmpty, bEmpty, options)).toBe(expected); + }); + + test('diffStringsUnified', () => { + expect(diffStringsUnified(aEmpty, bEmpty, options)).toBe(expected); + }); + }); }); diff --git a/packages/jest-diff/src/diffLines.ts b/packages/jest-diff/src/diffLines.ts index 81969db24183..6e5f5b467563 100644 --- a/packages/jest-diff/src/diffLines.ts +++ b/packages/jest-diff/src/diffLines.ts @@ -5,313 +5,105 @@ * LICENSE file in the root directory of this source tree. */ -import diff, {Callbacks} from 'diff-sequences'; -import {NO_DIFF_MESSAGE} from './constants'; -import { - ChangeCounts, - createPatchMark, - formatTrailingSpaces, - printAnnotation, -} from './printDiffs'; -import {DiffOptionsNormalized} from './types'; - -type Original = { - a: string; - b: string; -}; - -type Put = (line: string) => void; - -// Given index interval in expected lines, put formatted delete lines. -const formatDelete = ( - aStart: number, - aEnd: number, - aLinesUn: Array, - aLinesIn: Array, - {aColor, aIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, - put: Put, -) => { - for (let aIndex = aStart; aIndex !== aEnd; aIndex += 1) { - const aLineUn = aLinesUn[aIndex]; - const aLineIn = aLinesIn[aIndex]; - const indentation = aLineIn.slice(0, aLineIn.length - aLineUn.length); - - put( - aColor( - aIndicator + - ' ' + - indentation + - formatTrailingSpaces(aLineUn, trailingSpaceFormatter), - ), - ); - } -}; - -// Given index interval in received lines, put formatted insert lines. -const formatInsert = ( - bStart: number, - bEnd: number, - bLinesUn: Array, - bLinesIn: Array, - {bColor, bIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, - put: Put, -) => { - for (let bIndex = bStart; bIndex !== bEnd; bIndex += 1) { - const bLineUn = bLinesUn[bIndex]; - const bLineIn = bLinesIn[bIndex]; - const indentation = bLineIn.slice(0, bLineIn.length - bLineUn.length); - - put( - bColor( - bIndicator + - ' ' + - indentation + - formatTrailingSpaces(bLineUn, trailingSpaceFormatter), - ), - ); +import diff from 'diff-sequences'; +import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from './cleanupSemantic'; +import {normalizeDiffOptions} from './normalizeDiffOptions'; +import {printDiffLines} from './printDiffs'; +import {DiffOptions} from './types'; + +// Compare two arrays of strings line-by-line. Format as comparison lines. +export const diffLinesUnified = ( + aLines: Array, + bLines: Array, + options?: DiffOptions, +): string => + printDiffLines(diffLinesRaw(aLines, bLines), normalizeDiffOptions(options)); + +// Given two pairs of arrays of strings: +// Compare the pair of comparison arrays line-by-line. +// Format the corresponding lines in the pair of displayable arrays. +export const diffLinesUnified2 = ( + aLinesDisplay: Array, + bLinesDisplay: Array, + aLinesCompare: Array, + bLinesCompare: Array, + options?: DiffOptions, +): string => { + if ( + aLinesDisplay.length !== aLinesCompare.length || + bLinesDisplay.length !== bLinesCompare.length + ) { + // Fall back to diff of display lines. + return diffLinesUnified(aLinesDisplay, bLinesDisplay, options); } -}; - -// Given the number of items and starting indexes of a common subsequence, -// put formatted common lines. -const formatCommon = ( - nCommon: number, - aCommon: number, - bCommon: number, - bLinesUn: Array, - bLinesIn: Array, - {commonColor, commonIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, - put: Put, -) => { - for (; nCommon !== 0; nCommon -= 1, aCommon += 1, bCommon += 1) { - const bLineUn = bLinesUn[bCommon]; - const bLineIn = bLinesIn[bCommon]; - const bLineInLength = bLineIn.length; - // For common lines, received indentation seems more intuitive. - const indentation = bLineIn.slice(0, bLineInLength - bLineUn.length); + const diffs = diffLinesRaw(aLinesCompare, bLinesCompare); + + // Replace comparison lines with displayable lines. + let aIndex = 0; + let bIndex = 0; + diffs.forEach((diff: Diff) => { + switch (diff[0]) { + case DIFF_DELETE: + diff[1] = aLinesDisplay[aIndex]; + aIndex += 1; + break; + + case DIFF_INSERT: + diff[1] = bLinesDisplay[bIndex]; + bIndex += 1; + break; + + default: + diff[1] = bLinesDisplay[bIndex]; + aIndex += 1; + bIndex += 1; + } + }); - put( - commonColor( - commonIndicator + - ' ' + - indentation + - formatTrailingSpaces(bLineUn, trailingSpaceFormatter), - ), - ); - } + return printDiffLines(diffs, normalizeDiffOptions(options)); }; -// jest --expand -// Return formatted diff as joined string of all lines. -const diffExpand = ( - aLinesUn: Array, - bLinesUn: Array, - aLinesIn: Array, - bLinesIn: Array, - options: DiffOptionsNormalized, -): string => { - const isCommon: Callbacks['isCommon'] = (aIndex, bIndex) => - aLinesUn[aIndex] === bLinesUn[bIndex]; - - const array: Array = []; - const put = (line: string) => { - array.push(line); - }; - - const changeCounts: ChangeCounts = { - a: 0, - b: 0, - }; - - let aStart = 0; - let bStart = 0; - - const foundSubsequence: Callbacks['foundSubsequence'] = ( - nCommon, - aCommon, - bCommon, +// Compare two arrays of strings line-by-line. +export const diffLinesRaw = ( + aLines: Array, + bLines: Array, +): Array => { + const aLength = aLines.length; + const bLength = bLines.length; + + const isCommon = (aIndex: number, bIndex: number) => + aLines[aIndex] === bLines[bIndex]; + + const diffs: Array = []; + let aIndex = 0; + let bIndex = 0; + + const foundSubsequence = ( + nCommon: number, + aCommon: number, + bCommon: number, ) => { - changeCounts.a += aCommon - aStart; - changeCounts.b += bCommon - bStart; - formatDelete(aStart, aCommon, aLinesUn, aLinesIn, options, put); - formatInsert(bStart, bCommon, bLinesUn, bLinesIn, options, put); - formatCommon(nCommon, aCommon, bCommon, bLinesUn, bLinesIn, options, put); - aStart = aCommon + nCommon; - bStart = bCommon + nCommon; - }; - - const aLength = aLinesUn.length; - const bLength = bLinesUn.length; - - diff(aLength, bLength, isCommon, foundSubsequence); - - // After the last common subsequence, format remaining change lines. - changeCounts.a += aLength - aStart; - changeCounts.b += bLength - bStart; - formatDelete(aStart, aLength, aLinesUn, aLinesIn, options, put); - formatInsert(bStart, bLength, bLinesUn, bLinesIn, options, put); - - return printAnnotation(options, changeCounts) + array.join('\n'); -}; - -// jest --no-expand -// Return joined string of formatted diff for all change lines, -// but if some common lines are omitted because there are more than the context, -// then a “patch mark” precedes each set of adjacent changed and common lines. -const diffNoExpand = ( - aLinesUn: Array, - bLinesUn: Array, - aLinesIn: Array, - bLinesIn: Array, - options: DiffOptionsNormalized, -): string => { - const isCommon: Callbacks['isCommon'] = (aIndex, bIndex) => - aLinesUn[aIndex] === bLinesUn[bIndex]; - - let iPatchMark = 0; // index of placeholder line for patch mark - const array = ['']; - const put = (line: string) => { - array.push(line); - }; - - let isAtEnd = false; - const aLength = aLinesUn.length; - const bLength = bLinesUn.length; - const nContextLines = options.contextLines; - const nContextLines2 = nContextLines + nContextLines; - const changeCounts: ChangeCounts = { - a: 0, - b: 0, - }; - - // Initialize the first patch for changes at the start, - // especially for edge case in which there is no common subsequence. - let aStart = 0; - let aEnd = 0; - let bStart = 0; - let bEnd = 0; - - // Given the number of items and starting indexes of each common subsequence, - // format any preceding change lines, and then common context lines. - const foundSubsequence: Callbacks['foundSubsequence'] = ( - nCommon, - aStartCommon, - bStartCommon, - ) => { - const aEndCommon = aStartCommon + nCommon; - const bEndCommon = bStartCommon + nCommon; - isAtEnd = aEndCommon === aLength && bEndCommon === bLength; - - // If common subsequence is at start, re-initialize the first patch. - if (aStartCommon === 0 && bStartCommon === 0) { - const nLines = nContextLines < nCommon ? nContextLines : nCommon; - aStart = aEndCommon - nLines; - bStart = bEndCommon - nLines; - - formatCommon(nLines, aStart, bStart, bLinesUn, bLinesIn, options, put); - aEnd = aEndCommon; - bEnd = bEndCommon; - return; + for (; aIndex !== aCommon; aIndex += 1) { + diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])); } - - // Format preceding change lines. - changeCounts.a += aStartCommon - aEnd; - changeCounts.b += bStartCommon - bEnd; - formatDelete(aEnd, aStartCommon, aLinesUn, aLinesIn, options, put); - formatInsert(bEnd, bStartCommon, bLinesUn, bLinesIn, options, put); - aEnd = aStartCommon; - bEnd = bStartCommon; - - // If common subsequence is at end, then context follows preceding changes; - // else context follows preceding changes AND precedes following changes. - const maxContextLines = isAtEnd ? nContextLines : nContextLines2; - - if (nCommon <= maxContextLines) { - // The patch includes all lines in the common subsequence. - formatCommon(nCommon, aEnd, bEnd, bLinesUn, bLinesIn, options, put); - aEnd += nCommon; - bEnd += nCommon; - return; + for (; bIndex !== bCommon; bIndex += 1) { + diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])); } - - // The patch ends because context is less than number of common lines. - formatCommon(nContextLines, aEnd, bEnd, bLinesUn, bLinesIn, options, put); - aEnd += nContextLines; - bEnd += nContextLines; - - array[iPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd, options); - - // If common subsequence is not at end, another patch follows it. - if (!isAtEnd) { - iPatchMark = array.length; // index of placeholder line - array[iPatchMark] = ''; - - const nLines = nContextLines < nCommon ? nContextLines : nCommon; - aStart = aEndCommon - nLines; - bStart = bEndCommon - nLines; - - formatCommon(nLines, aStart, bStart, bLinesUn, bLinesIn, options, put); - aEnd = aEndCommon; - bEnd = bEndCommon; + for (; nCommon !== 0; nCommon -= 1, aIndex += 1, bIndex += 1) { + diffs.push(new Diff(DIFF_EQUAL, bLines[bIndex])); } }; diff(aLength, bLength, isCommon, foundSubsequence); - // If no common subsequence or last was not at end, format remaining change lines. - if (!isAtEnd) { - changeCounts.a += aLength - aEnd; - changeCounts.b += bLength - bEnd; - formatDelete(aEnd, aLength, aLinesUn, aLinesIn, options, put); - formatInsert(bEnd, bLength, bLinesUn, bLinesIn, options, put); - aEnd = aLength; - bEnd = bLength; - } - - if (aStart === 0 && aEnd === aLength && bStart === 0 && bEnd === bLength) { - array.splice(0, 1); // delete placeholder line for patch mark - } else { - array[iPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd, options); - } - - return printAnnotation(options, changeCounts) + array.join('\n'); -}; - -export default ( - a: string, - b: string, - options: DiffOptionsNormalized, - original?: Original, -): string => { - if (a === b) { - return NO_DIFF_MESSAGE; + // After the last common subsequence, push remaining change items. + for (; aIndex !== aLength; aIndex += 1) { + diffs.push(new Diff(DIFF_DELETE, aLines[aIndex])); } - - let aLinesUn = a.split('\n'); - let bLinesUn = b.split('\n'); - - // Indentation is unknown if expected value is snapshot or multiline string. - let aLinesIn = aLinesUn; - let bLinesIn = bLinesUn; - - if (original) { - // Indentation is known if expected value is data structure: - // Compare lines without indentation and format lines with indentation. - aLinesIn = original.a.split('\n'); - bLinesIn = original.b.split('\n'); - - if ( - aLinesUn.length !== aLinesIn.length || - bLinesUn.length !== bLinesIn.length - ) { - // Fall back if unindented and indented lines are inconsistent. - aLinesUn = aLinesIn; - bLinesUn = bLinesIn; - } + for (; bIndex !== bLength; bIndex += 1) { + diffs.push(new Diff(DIFF_INSERT, bLines[bIndex])); } - return options.expand - ? diffExpand(aLinesUn, bLinesUn, aLinesIn, bLinesIn, options) - : diffNoExpand(aLinesUn, bLinesUn, aLinesIn, bLinesIn, options); + return diffs; }; diff --git a/packages/jest-diff/src/index.ts b/packages/jest-diff/src/index.ts index 76cbb7c34dae..5ccf52c6d9ea 100644 --- a/packages/jest-diff/src/index.ts +++ b/packages/jest-diff/src/index.ts @@ -9,15 +9,15 @@ import prettyFormat = require('pretty-format'); import chalk from 'chalk'; import getType = require('jest-get-type'); import {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from './cleanupSemantic'; -import diffLines from './diffLines'; -import {normalizeDiffOptions} from './normalizeDiffOptions'; -import {diffStringsRaw, diffStringsUnified} from './printDiffs'; +import {diffLinesRaw, diffLinesUnified, diffLinesUnified2} from './diffLines'; +import {diffStringsRaw, diffStringsUnified, splitLines0} from './printDiffs'; import {NO_DIFF_MESSAGE, SIMILAR_MESSAGE} from './constants'; -import {DiffOptionsNormalized, DiffOptions as ImportDiffOptions} from './types'; +import {DiffOptions as ImportDiffOptions} from './types'; export type DiffOptions = ImportDiffOptions; -export {diffStringsRaw, diffStringsUnified}; +export {diffLinesRaw, diffLinesUnified, diffLinesUnified2}; +export {diffStringsRaw, diffStringsUnified, splitLines0}; export {DIFF_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff}; const { @@ -85,32 +85,31 @@ function diff(a: any, b: any, options?: DiffOptions): string | null { return null; } - const optionsNormalized = normalizeDiffOptions(options); switch (aType) { case 'string': - return diffLines(a, b, optionsNormalized); + return diffLinesUnified(splitLines0(a), splitLines0(b), options); case 'boolean': case 'number': - return comparePrimitive(a, b, optionsNormalized); + return comparePrimitive(a, b, options); case 'map': - return compareObjects(sortMap(a), sortMap(b), optionsNormalized); + return compareObjects(sortMap(a), sortMap(b), options); case 'set': - return compareObjects(sortSet(a), sortSet(b), optionsNormalized); + return compareObjects(sortSet(a), sortSet(b), options); default: - return compareObjects(a, b, optionsNormalized); + return compareObjects(a, b, options); } } function comparePrimitive( a: number | boolean, b: number | boolean, - options: DiffOptionsNormalized, + options?: DiffOptions, ) { - return diffLines( - prettyFormat(a, FORMAT_OPTIONS), - prettyFormat(b, FORMAT_OPTIONS), - options, - ); + const aFormat = prettyFormat(a, FORMAT_OPTIONS); + const bFormat = prettyFormat(b, FORMAT_OPTIONS); + return aFormat === bFormat + ? NO_DIFF_MESSAGE + : diffLinesUnified(splitLines0(aFormat), splitLines0(bFormat), options); } function sortMap(map: Map) { @@ -124,43 +123,60 @@ function sortSet(set: Set) { function compareObjects( a: Record, b: Record, - options: DiffOptionsNormalized, + options?: DiffOptions, ) { - let diffMessage; + let difference; let hasThrown = false; try { - diffMessage = diffLines( - prettyFormat(a, FORMAT_OPTIONS_0), - prettyFormat(b, FORMAT_OPTIONS_0), - options, - { - a: prettyFormat(a, FORMAT_OPTIONS), - b: prettyFormat(b, FORMAT_OPTIONS), - }, - ); + const aCompare = prettyFormat(a, FORMAT_OPTIONS_0); + const bCompare = prettyFormat(b, FORMAT_OPTIONS_0); + + if (aCompare === bCompare) { + difference = NO_DIFF_MESSAGE; + } else { + const aDisplay = prettyFormat(a, FORMAT_OPTIONS); + const bDisplay = prettyFormat(b, FORMAT_OPTIONS); + + difference = diffLinesUnified2( + aDisplay.split('\n'), + bDisplay.split('\n'), + aCompare.split('\n'), + bCompare.split('\n'), + options, + ); + } } catch (e) { hasThrown = true; } // If the comparison yields no results, compare again but this time // without calling `toJSON`. It's also possible that toJSON might throw. - if (!diffMessage || diffMessage === NO_DIFF_MESSAGE) { - diffMessage = diffLines( - prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0), - prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0), - options, - { - a: prettyFormat(a, FALLBACK_FORMAT_OPTIONS), - b: prettyFormat(b, FALLBACK_FORMAT_OPTIONS), - }, - ); - if (diffMessage !== NO_DIFF_MESSAGE && !hasThrown) { - diffMessage = SIMILAR_MESSAGE + '\n\n' + diffMessage; + if (difference === undefined || difference === NO_DIFF_MESSAGE) { + const aCompare = prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0); + const bCompare = prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0); + + if (aCompare === bCompare) { + difference = NO_DIFF_MESSAGE; + } else { + const aDisplay = prettyFormat(a, FALLBACK_FORMAT_OPTIONS); + const bDisplay = prettyFormat(b, FALLBACK_FORMAT_OPTIONS); + + difference = diffLinesUnified2( + aDisplay.split('\n'), + bDisplay.split('\n'), + aCompare.split('\n'), + bCompare.split('\n'), + options, + ); + } + + if (difference !== NO_DIFF_MESSAGE && !hasThrown) { + difference = SIMILAR_MESSAGE + '\n\n' + difference; } } - return diffMessage; + return difference; } export default diff; diff --git a/packages/jest-diff/src/joinAlignedDiffs.ts b/packages/jest-diff/src/joinAlignedDiffs.ts index 7ae7a9b41ff4..ebfb69078c18 100644 --- a/packages/jest-diff/src/joinAlignedDiffs.ts +++ b/packages/jest-diff/src/joinAlignedDiffs.ts @@ -95,12 +95,14 @@ export const joinAlignedDiffsNoExpand = ( }; const pushDeleteLine = (line: string): void => { - lines.push(printDeleteLine(line, options)); + const j = lines.length; + lines.push(printDeleteLine(line, j === 0 || j === jLast, options)); aEnd += 1; }; const pushInsertLine = (line: string): void => { - lines.push(printInsertLine(line, options)); + const j = lines.length; + lines.push(printInsertLine(line, j === 0 || j === jLast, options)); bEnd += 1; }; @@ -200,20 +202,17 @@ export const joinAlignedDiffsExpand = ( diffs .map((diff: Diff, i: number, diffs: Array): string => { const line = diff[1]; + const isFirstOrLast = i === 0 || i === diffs.length - 1; switch (diff[0]) { case DIFF_DELETE: - return printDeleteLine(line, options); + return printDeleteLine(line, isFirstOrLast, options); case DIFF_INSERT: - return printInsertLine(line, options); + return printInsertLine(line, isFirstOrLast, options); default: - return printCommonLine( - line, - i === 0 || i === diffs.length - 1, - options, - ); + return printCommonLine(line, isFirstOrLast, options); } }) .join('\n'); diff --git a/packages/jest-diff/src/normalizeDiffOptions.ts b/packages/jest-diff/src/normalizeDiffOptions.ts index 8ee795dcfb71..410a18029507 100644 --- a/packages/jest-diff/src/normalizeDiffOptions.ts +++ b/packages/jest-diff/src/normalizeDiffOptions.ts @@ -23,6 +23,7 @@ const OPTIONS_DEFAULT: DiffOptionsNormalized = { commonIndicator: ' ', contextLines: DIFF_CONTEXT_DEFAULT, expand: true, + firstOrLastEmptyLineReplacement: '\u{21B5}', // downwards arrow with corner leftwards includeChangeCounts: false, omitAnnotationLines: false, patchColor: chalk.yellow, diff --git a/packages/jest-diff/src/printDiffs.ts b/packages/jest-diff/src/printDiffs.ts index cc5ed53c562c..1c3c975f15d7 100644 --- a/packages/jest-diff/src/printDiffs.ts +++ b/packages/jest-diff/src/printDiffs.ts @@ -12,7 +12,7 @@ import { Diff, cleanupSemantic, } from './cleanupSemantic'; -import diffLines from './diffLines'; +import {diffLinesUnified} from './diffLines'; import diffStrings from './diffStrings'; import getAlignedDiffs from './getAlignedDiffs'; import { @@ -22,48 +22,85 @@ import { import {normalizeDiffOptions} from './normalizeDiffOptions'; import {DiffOptions, DiffOptionsColor, DiffOptionsNormalized} from './types'; -const NEWLINE_SYMBOL = '\u{21B5}'; // downwards arrow with corner leftwards - -export const formatTrailingSpaces = ( +const formatTrailingSpaces = ( line: string, trailingSpaceFormatter: DiffOptionsColor, ): string => line.replace(/\s+$/, match => trailingSpaceFormatter(match)); +const printDiffLine = ( + line: string, + isFirstOrLast: boolean, + color: DiffOptionsColor, + indicator: string, + firstOrLastEmptyLineReplacement: string, + trailingSpaceFormatter: DiffOptionsColor, +): string => + line.length !== 0 + ? color( + indicator + ' ' + formatTrailingSpaces(line, trailingSpaceFormatter), + ) + : indicator !== ' ' + ? color(indicator) + : isFirstOrLast && firstOrLastEmptyLineReplacement.length !== 0 + ? color(indicator + ' ' + firstOrLastEmptyLineReplacement) + : ''; + export const printDeleteLine = ( line: string, - {aColor, aIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, + isFirstOrLast: boolean, + { + aColor, + aIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, + }: DiffOptionsNormalized, ): string => - aColor( - line.length !== 0 - ? aIndicator + ' ' + formatTrailingSpaces(line, trailingSpaceFormatter) - : aIndicator, + printDiffLine( + line, + isFirstOrLast, + aColor, + aIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, ); export const printInsertLine = ( line: string, - {bColor, bIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, + isFirstOrLast: boolean, + { + bColor, + bIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, + }: DiffOptionsNormalized, ): string => - bColor( - line.length !== 0 - ? bIndicator + ' ' + formatTrailingSpaces(line, trailingSpaceFormatter) - : bIndicator, + printDiffLine( + line, + isFirstOrLast, + bColor, + bIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, ); -// Prevent visually ambiguous empty line as the first or the last. export const printCommonLine = ( line: string, isFirstOrLast: boolean, - {commonColor, commonIndicator, trailingSpaceFormatter}: DiffOptionsNormalized, + { + commonColor, + commonIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, + }: DiffOptionsNormalized, ): string => - line.length !== 0 - ? commonColor( - commonIndicator + - ' ' + - formatTrailingSpaces(line, trailingSpaceFormatter), - ) - : isFirstOrLast - ? commonColor(commonIndicator + ' ' + NEWLINE_SYMBOL) - : ''; + printDiffLine( + line, + isFirstOrLast, + commonColor, + commonIndicator, + firstOrLastEmptyLineReplacement, + trailingSpaceFormatter, + ); export const hasCommonDiff = (diffs: Array, isMultiline: boolean) => { if (isMultiline) { @@ -82,7 +119,7 @@ export type ChangeCounts = { b: number; }; -const countChanges = (diffs: Array): ChangeCounts => { +export const countChanges = (diffs: Array): ChangeCounts => { let a = 0; let b = 0; @@ -145,6 +182,15 @@ export const printAnnotation = ( ); }; +export const printDiffLines = ( + diffs: Array, + options: DiffOptionsNormalized, +): string => + printAnnotation(options, countChanges(diffs)) + + (options.expand + ? joinAlignedDiffsExpand(diffs, options) + : joinAlignedDiffsNoExpand(diffs, options)); + // In GNU diff format, indexes are one-based instead of zero-based. export const createPatchMark = ( aStart: number, @@ -157,89 +203,39 @@ export const createPatchMark = ( `@@ -${aStart + 1},${aEnd - aStart} +${bStart + 1},${bEnd - bStart} @@`, ); -// Given two string arguments, compare them character-by-character. +export const splitLines0 = (string: string) => + string.length === 0 ? [] : string.split('\n'); + +// Compare two strings character-by-character. // Format as comparison lines in which changed substrings have inverse colors. export const diffStringsUnified = ( a: string, b: string, options?: DiffOptions, ): string => { - const optionsNormalized = normalizeDiffOptions(options); - - if (a.length === 0 || b.length === 0) { - const lines: Array = []; - const changeCounts: ChangeCounts = { - a: 0, - b: 0, - }; - - if (a.length !== 0) { - // All comparison lines have aColor and aIndicator. - a.split('\n').forEach(line => { - lines.push(printDeleteLine(line, optionsNormalized)); - }); - changeCounts.a = lines.length; - } - - if (b.length !== 0) { - // All comparison lines have bColor and bIndicator. - b.split('\n').forEach(line => { - lines.push(printInsertLine(line, optionsNormalized)); - }); - changeCounts.b = lines.length; - } - - // Else if both are empty strings, there are no comparison lines. - - return printAnnotation(optionsNormalized, changeCounts) + lines.join('\n'); - } - - if (a === b) { - const lines = a.split('\n'); - const iLast = lines.length - 1; - const changeCounts = { - a: 0, - b: 0, - }; - - // All comparison lines have commonColor and commonIndicator. - return ( - printAnnotation(optionsNormalized, changeCounts) + - lines - .map((line, i) => - printCommonLine(line, i === 0 || i === iLast, optionsNormalized), - ) - .join('\n') + if (a !== b && a.length !== 0 && b.length !== 0) { + const isMultiline = a.includes('\n') || b.includes('\n'); + + // getAlignedDiffs assumes that a newline was appended to the strings. + const diffs = diffStringsRaw( + isMultiline ? a + '\n' : a, + isMultiline ? b + '\n' : b, + true, // cleanupSemantic ); - } - const isMultiline = a.includes('\n') || b.includes('\n'); - - // getAlignedDiffs assumes that a newline was appended to the strings. - const diffs = diffStringsRaw( - isMultiline ? a + '\n' : a, - isMultiline ? b + '\n' : b, - true, // cleanupSemantic - ); - - if (hasCommonDiff(diffs, isMultiline)) { - const lines = getAlignedDiffs(diffs, optionsNormalized.changeColor); - return ( - printAnnotation(optionsNormalized, countChanges(lines)) + - (optionsNormalized.expand - ? joinAlignedDiffsExpand(lines, optionsNormalized) - : joinAlignedDiffsNoExpand(lines, optionsNormalized)) - ); + if (hasCommonDiff(diffs, isMultiline)) { + const optionsNormalized = normalizeDiffOptions(options); + const lines = getAlignedDiffs(diffs, optionsNormalized.changeColor); + return printDiffLines(lines, optionsNormalized); + } } // Fall back to line-by-line diff. - // Given strings, it returns a string, not null. - return diffLines(a, b, optionsNormalized) as string; + return diffLinesUnified(splitLines0(a), splitLines0(b), options); }; -// Given two string arguments, compare them character-by-character. +// Compare two strings character-by-character. // Optionally clean up small common substrings, also known as chaff. -// Return an array of diff objects. export const diffStringsRaw = ( a: string, b: string, diff --git a/packages/jest-diff/src/types.ts b/packages/jest-diff/src/types.ts index 60ae940c7992..7c54ad591211 100644 --- a/packages/jest-diff/src/types.ts +++ b/packages/jest-diff/src/types.ts @@ -23,6 +23,7 @@ export type DiffOptions = { omitAnnotationLines?: boolean; patchColor?: DiffOptionsColor; trailingSpaceFormatter?: DiffOptionsColor; + firstOrLastEmptyLineReplacement?: string; }; export type DiffOptionsNormalized = { @@ -41,4 +42,5 @@ export type DiffOptionsNormalized = { omitAnnotationLines: boolean; patchColor: DiffOptionsColor; trailingSpaceFormatter: DiffOptionsColor; + firstOrLastEmptyLineReplacement: string; }; 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 6115bed35c90..d26c80eb8b56 100644 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap @@ -97,11 +97,11 @@ exports[`fallback to line diff 1`] = ` [...a, ...b,]; [...a, ...b]; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ ++ + =====================================output===================================== [...a, ...b]; [...a, ...b]; - + + ================================================================================ `;