expect: Highlight substring differences when matcher fails, part 2 (#8528)

This commit is contained in:
Mark Pedrotti
2019-06-13 23:25:40 +02:00
committed by Simen Bekkhus
parent 6da622fb31
commit 00df15ea32
27 changed files with 1811 additions and 487 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ bin/
flow-typed/**
packages/*/build/**
packages/*/build-es5/**
packages/jest-matcher-utils/src/cleanupSemantic.ts
packages/jest-diff/src/cleanupSemantic.ts
website/blog
website/build
website/node_modules
+1 -1
View File
@@ -1,4 +1,4 @@
fixtures/failing-jsons/
packages/jest-matcher-utils/src/cleanupSemantic.ts
packages/jest-diff/src/cleanupSemantic.ts
packages/jest-config/src/__tests__/jest-preset.json
packages/pretty-format/perf/world.geo.json
+1
View File
@@ -3,6 +3,7 @@
### Features
- `[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-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
@@ -303,9 +303,9 @@ string" 1`] = `
<green>- Expected</>
<red>+ Received</>
<green>- 3</>
<red>+ four</>
<red>+ 4</>
<green>- <inverse>3</></>
<red>+ <inverse>four</></>
<red>+ <inverse>4</></>
<dim> line</>
<dim> string</>"
`;
@@ -321,9 +321,12 @@ exports[`.toBe() fails for: "with
trailing space" and "without trailing space" 1`] = `
"<dim>expect(</><red>received</><dim>).</>toBe<dim>(</><green>expected</><dim>) // Object.is equality</>
Expected: <green>\\"without trailing space\\"</>
Received: <red>\\"with<inverse> </></>
<red>trailing space\\"</>"
<green>- Expected</>
<red>+ Received</>
<green>- with<inverse>out</> trailing space</>
<red>+ with·</>
<red>+ trailing space</>"
`;
exports[`.toBe() fails for: /received/ and /expected/ 1`] = `
@@ -1971,6 +1974,20 @@ Expected: <green>\\"apple\\"</>
Received: <red>\\"banana\\"</>"
`;
exports[`.toEqual() {pass: false} expect("type TypeName<T> = T extends Function ? \\"function\\" : \\"object\\";").toEqual("type TypeName<T> = T extends Function
? \\"function\\"
: \\"object\\";") 1`] = `
"<dim>expect(</><red>received</><dim>).</>toEqual<dim>(</><green>expected</><dim>) // deep equality</>
<green>- Expected</>
<red>+ Received</>
<green>- type TypeName<T> = T extends Function</>
<green>- ? \\"function\\"</>
<green>- : \\"object\\";</>
<red>+ type TypeName<T> = T extends Function<inverse> </>? \\"function\\"<inverse> </>: \\"object\\";</>"
`;
exports[`.toEqual() {pass: false} expect([1, 2]).toEqual([2, 1]) 1`] = `
"<dim>expect(</><red>received</><dim>).</>toEqual<dim>(</><green>expected</><dim>) // deep equality</>
@@ -3080,6 +3097,26 @@ Expected value: <green>\\"\\\\\\"That <inverse>cat </>cartoon\\\\\\"\\"</>
Received value: <red>\\"\\\\\\"That cartoon\\\\\\"\\"</>"
`;
exports[`.toHaveProperty() {pass: false} expect({"children": ["Roses are red.
Violets are blue.
Testing with Jest is good for you."], "props": null, "type": "pre"}).toHaveProperty('children,0', "Roses are red, violets are blue.
Testing with Jest
Is good for you.") 1`] = `
"<dim>expect(</><red>received</><dim>).</>toHaveProperty<dim>(</><green>path</><dim>, </><green>value</><dim>)</>
Expected path: <green>[\\"children\\", 0]</>
<green>- Expected value</>
<red>+ Received value</>
<green>- Roses are red<inverse>, v</>iolets are blue.</>
<red>+ Roses are red<inverse>.</></>
<red>+ <inverse>V</>iolets are blue.</>
<green>- Testing with Jest</>
<green>- <inverse>I</>s good for you.</>
<red>+ Testing with Jest<inverse> i</>s good for you.</>"
`;
exports[`.toHaveProperty() {pass: false} expect({"key": 1}).toHaveProperty('not') 1`] = `
"<dim>expect(</><red>received</><dim>).</>toHaveProperty<dim>(</><green>path</><dim>)</>
@@ -3527,6 +3564,29 @@ Expected: <green>\\"<inverse>Another caveat is that</> Jest will not typecheck y
Received: <red>\\"<inverse>Because TypeScript support in Babel is just transpilation,</> Jest will not type<inverse>-</>check your tests<inverse> as they run</>.\\"</>"
`;
exports[`.toStrictEqual() displays substring diff for multiple lines 1`] = `
"<dim>expect(</><red>received</><dim>).</>toStrictEqual<dim>(</><green>expected</><dim>) // deep equality</>
<green>- Expected</>
<red>+ Received</>
<green>- 6<inverse>9</> |·</>
<red>+ 6<inverse>8</> |·</>
<green>- <inverse>70</> | test('assert.doesNotThrow', () => {</>
<red>+ <inverse>69</> | test('assert.doesNotThrow', () => {</>
<green>- > 7<inverse>1</> | assert.doesNotThrow(() => {</>
<red>+ > 7<inverse>0</> | assert.doesNotThrow(() => {</>
<dim> | ^</>
<green>- 7<inverse>2</> | throw Error('err!');</>
<red>+ 7<inverse>1</> | throw Error('err!');</>
<green>- 7<inverse>3</> | });</>
<red>+ 7<inverse>2</> | });</>
<green>- 7<inverse>4</> | });</>
<red>+ 7<inverse>3</> | });</>
<green>- at Object.doesNotThrow (__tests__/assertionError.test.js:7<inverse>1</>:10)</>
<red>+ at Object.doesNotThrow (__tests__/assertionError.test.js:7<inverse>0</>:10)</>"
`;
exports[`.toStrictEqual() matches the expected snapshot when it fails 1`] = `
"<dim>expect(</><red>received</><dim>).</>toStrictEqual<dim>(</><green>expected</><dim>) // deep equality</>
@@ -161,6 +161,18 @@ Expected message: not <green>\\"Invalid array length\\"</>
"
`;
exports[`toThrow error-message fail multiline diff highlight incorrect expected space 1`] = `
"<dim>expect(</><red>received</><dim>).</>toThrow<dim>(</><green>expected</><dim>)</>
<green>- Expected message</>
<red>+ Received message</>
<green>- There is no route defined for key Settings.<inverse> </></>
<red>+ There is no route defined for key Settings.</>
<dim> Must be one of: 'Home'</>
"
`;
exports[`toThrow expected is undefined threw, but should not have (non-error falsey) 1`] = `
"<dim>expect(</><red>received</><dim>).</>not<dim>.</>toThrow<dim>()</>
@@ -457,6 +469,18 @@ Expected message: not <green>\\"Invalid array length\\"</>
"
`;
exports[`toThrowError error-message fail multiline diff highlight incorrect expected space 1`] = `
"<dim>expect(</><red>received</><dim>).</>toThrowError<dim>(</><green>expected</><dim>)</>
<green>- Expected message</>
<red>+ Received message</>
<green>- There is no route defined for key Settings.<inverse> </></>
<red>+ There is no route defined for key Settings.</>
<dim> Must be one of: 'Home'</>
"
`;
exports[`toThrowError expected is undefined threw, but should not have (non-error falsey) 1`] = `
"<dim>expect(</><red>received</><dim>).</>not<dim>.</>toThrowError<dim>()</>
+46 -5
View File
@@ -3,7 +3,6 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
const {stringify} = require('jest-matcher-utils');
@@ -330,6 +329,32 @@ describe('.toStrictEqual()', () => {
).toThrowErrorMatchingSnapshot();
});
it('displays substring diff for multiple lines', () => {
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(() =>
jestExpect(received).toStrictEqual(expected),
).toThrowErrorMatchingSnapshot();
});
it('does not pass for different types', () => {
expect({
test: new TestClassA(1, 2),
@@ -371,6 +396,10 @@ describe('.toEqual()', () => {
[{a: 5}, {b: 6}],
['banana', 'apple'],
['1\u{00A0}234,57\u{00A0}$', '1 234,57 $'], // issues/6881
[
'type TypeName<T> = T extends Function ? "function" : "object";',
'type TypeName<T> = T extends Function\n? "function"\n: "object";',
],
[null, undefined],
[[1], [2]],
[[1, 2], [2, 1]],
@@ -1361,13 +1390,24 @@ describe('.toHaveProperty()', () => {
const memoized = function() {};
memoized.memo = [];
const receivedDiff = {
const pathDiff = ['children', 0];
const receivedDiffSingle = {
children: ['"That cartoon"'],
props: null,
type: 'p',
};
const pathDiff = ['children', 0];
const valueDiff = '"That cat cartoon"';
const valueDiffSingle = '"That cat cartoon"';
const receivedDiffMultiple = {
children: [
'Roses are red.\nViolets are blue.\nTesting with Jest is good for you.',
],
props: null,
type: 'pre',
};
const valueDiffMultiple =
'Roses are red, violets are blue.\nTesting with Jest\nIs good for you.';
[
[{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 1],
@@ -1405,7 +1445,8 @@ describe('.toHaveProperty()', () => {
[{a: {b: {c: {d: 1}}}}, 'a.b.c.d', 2],
[{'a.b.c.d': 1}, 'a.b.c.d', 2],
[{'a.b.c.d': 1}, ['a.b.c.d'], 2],
[receivedDiff, pathDiff, valueDiff],
[receivedDiffSingle, pathDiff, valueDiffSingle],
[receivedDiffMultiple, pathDiff, valueDiffMultiple],
[{a: {b: {c: {d: 1}}}}, ['a', 'b', 'c', 'd'], 2],
[{a: {b: {c: {}}}}, 'a.b.c.d', 1],
[{a: 1}, 'a.b.c.d', 5],
@@ -3,7 +3,6 @@
*
* This source code is licensed under the MIT license found in the
* LICENSE file in the root directory of this source tree.
*
*/
'use strict';
@@ -266,6 +265,19 @@ class customError extends Error {
}).not[toThrow]({message}),
).toThrowErrorMatchingSnapshot();
});
test('multiline diff highlight incorrect expected space', () => {
// jest/issues/2673
const a =
"There is no route defined for key Settings. \nMust be one of: 'Home'";
const b =
"There is no route defined for key Settings.\nMust be one of: 'Home'";
expect(() =>
jestExpect(() => {
throw new ErrorMessage(b);
})[toThrow]({message: a}),
).toThrowErrorMatchingSnapshot();
});
});
});
+14 -4
View File
@@ -12,6 +12,7 @@ import {
RECEIVED_COLOR,
matcherErrorMessage,
matcherHint,
printDiffOrStringify,
printExpected,
printReceived,
printWithType,
@@ -231,13 +232,22 @@ const toThrowExpectedObject = (
: () =>
matcherHint(matcherName, undefined, undefined, options) +
'\n\n' +
formatExpected('Expected message: ', expected.message) +
(thrown === null
? '\n' + DID_NOT_THROW
? formatExpected('Expected message: ', expected.message) +
'\n' +
DID_NOT_THROW
: thrown.hasMessage
? formatReceived('Received message: ', thrown, 'message') +
? printDiffOrStringify(
expected.message,
thrown.message,
'Expected message',
'Received message',
true,
) +
'\n' +
formatStack(thrown)
: formatReceived('Received value: ', thrown, 'value'));
: formatExpected('Expected message: ', expected.message) +
formatReceived('Received value: ', thrown, 'value'));
return {message, pass};
};
@@ -0,0 +1,160 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`getAlignedDiffs lines change preceding and following common 1`] = `
"<green>- <inverse>delete</></>
<red>+ <inverse>insert</></>
<dim> common between changes</>
<green>- <inverse>prev</></>
<red>+ <inverse>next</></>"
`;
exports[`getAlignedDiffs lines common at end when both current change lines are empty 1`] = `
"<green>- <inverse>delete</></>
<dim> common at end</>"
`;
exports[`getAlignedDiffs lines common between delete and insert 1`] = `
"<green>- <inverse>delete</></>
<dim> common between changes</>
<red>+ <inverse>insert</></>"
`;
exports[`getAlignedDiffs lines common between insert and delete 1`] = `
"<red>+ <inverse>insert</></>
<dim> common between changes</>
<green>- <inverse>delete</></>"
`;
exports[`getAlignedDiffs lines common preceding and following change 1`] = `
"<dim> common preceding</>
<green>- <inverse>delete</></>
<red>+ <inverse>insert</></>
<dim> common following</>"
`;
exports[`getAlignedDiffs newline change from space 1`] = `
"<green>- preceding<inverse> </>following</>
<red>+ preceding</>
<red>+ following</>"
`;
exports[`getAlignedDiffs newline change to space 1`] = `
"<green>- preceding</>
<green>- following</>
<red>+ preceding<inverse> </>following</>"
`;
exports[`getAlignedDiffs newline delete only 1`] = `
"<green>- preceding</>
<green>- following</>
<red>+ precedingfollowing</>"
`;
exports[`getAlignedDiffs newline delete with adjacent change 1`] = `
"<green>- preced<inverse>ing</></>
<green>- following</>
<red>+ preced<inverse>ed</>following</>"
`;
exports[`getAlignedDiffs newline insert only 1`] = `
"<green>- precedingfollowing</>
<red>+ preceding</>
<red>+ following</>"
`;
exports[`getAlignedDiffs newline insert with adjacent changes 1`] = `
"<green>- preced<inverse>edf</>ollowing</>
<red>+ preced<inverse>ing</></>
<red>+ <inverse>F</>ollowing</>"
`;
exports[`getAlignedDiffs strings change at start and delete or insert at end 1`] = `
"<green>- <inverse>prev</> change common<inverse> delete</></>
<red>+ <inverse>next</> change common</>
<dim> unchanged</>
<green>- <inverse>expect</>ed change common</>
<red>+ <inverse>receiv</>ed change common<inverse> insert</></>"
`;
exports[`getAlignedDiffs strings delete or insert at start and change at end 1`] = `
"<green>- common change <inverse>prev</></>
<red>+ <inverse>insert </>common change <inverse>next</></>
<dim> unchanged</>
<green>- <inverse>delete </>common change th<inverse>is</></>
<red>+ common change th<inverse>at</></>"
`;
exports[`getAlignedDiffs substrings first common when both current change lines are empty 1`] = `
"<red>+ <inverse>insert</></>
<dim> first</>
<dim> middle</>
<green>- last <inverse>prev</></>
<red>+ last <inverse>next</></>"
`;
exports[`getAlignedDiffs substrings first common when either current change line is non-empty 1`] = `
"<green>- <inverse>expected </>first</>
<red>+ first</>
<dim> last</>"
`;
exports[`getAlignedDiffs substrings first delete completes the current line 1`] = `
"<green>- common preceding <inverse>first</></>
<green>- <inverse>middle</></>
<green>- <inverse>last </>and following</>
<red>+ common preceding and following</>"
`;
exports[`getAlignedDiffs substrings first insert completes the current line 1`] = `
"<green>- common preceding</>
<red>+ common preceding<inverse> first</></>
<red>+ <inverse>middle</></>
<red>+</>"
`;
exports[`getAlignedDiffs substrings last is empty in delete at end 1`] = `
"<green>- common string preceding <inverse>prev</></>
<green>-</>
<red>+ common string preceding <inverse>next</></>"
`;
exports[`getAlignedDiffs substrings last is empty in insert at end 1`] = `
"<green>- common string preceding <inverse>prev</></>
<red>+ common string preceding <inverse>next</></>
<red>+</>"
`;
exports[`getAlignedDiffs substrings last is non-empty in common not at end 1`] = `
"<dim> common first</>
<green>- last <inverse>expect</>ed</>
<red>+ last <inverse>receiv</>ed</>"
`;
exports[`getAlignedDiffs substrings middle is empty in delete between common 1`] = `
"<green>- common at start precedes <inverse>delete</></>
<green>-</>
<green>- <inverse>expect</>ed common at end</>
<red>+ common at start precedes <inverse>receiv</>ed common at end</>"
`;
exports[`getAlignedDiffs substrings middle is empty in insert at start 1`] = `
"<green>- <inverse>expect</>ed common at end</>
<red>+ <inverse>insert line</></>
<red>+</>
<red>+ <inverse>receiv</>ed common at end</>"
`;
exports[`getAlignedDiffs substrings middle is non-empty in delete at end 1`] = `
"<green>- common at start precedes <inverse>delete</></>
<green>- <inverse>non-empty line</></>
<green>- <inverse>next</></>
<red>+ common at start precedes <inverse>prev</></>"
`;
exports[`getAlignedDiffs substrings middle is non-empty in insert between common 1`] = `
"<green>- common at start precedes <inverse>delete expect</>ed</>
<red>+ common at start precedes <inverse>insert</></>
<red>+ <inverse>non-empty</></>
<red>+ <inverse>receiv</>ed</>"
`;
@@ -0,0 +1,160 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`joinAlignedDiffsExpand first line is empty common 1`] = `
"<dim> ↵</>
<dim> common 2 preceding A</>
<dim> common 1 preceding A</>
<green>- delete line</>
<green>- change <inverse>expect</>ed A</>
<red>+ change <inverse>receiv</>ed A</>
<dim> common 1 following A</>
<dim> common 2 following A</>
<dim> common 3 following A</>
<dim> common 4 following A</>
<dim> common 4 preceding B</>
<dim> common 3 preceding B</>
<dim> common 2 preceding B</>
<dim> common 1 preceding B</>
<green>- change <inverse>expect</>ed B</>
<red>+ change <inverse>receiv</>ed B</>
<red>+ insert line</>
<dim> common 1 following B</>
<dim> common 2 following B</>
<dim> common 3 between B and C</>
<dim> common 2 preceding C</>
<dim> common 1 preceding C</>
<green>- change <inverse>expect</>ed C</>
<red>+ change <inverse>receiv</>ed C</>
<dim> common 1 following C</>
<dim> common 2 following C</>
<dim> common 3 following C</>
<dim> common 5 following C</>"
`;
exports[`joinAlignedDiffsNoExpand patch 0 with context 1 and change at start and end 1`] = `
"<green>- delete</>
<dim> common following delete</>
<dim> common preceding insert</>
<red>+ insert</>"
`;
exports[`joinAlignedDiffsNoExpand patch 0 with context 5 and first line is empty common 1`] = `
"<dim> ↵</>
<dim> common 2 preceding A</>
<dim> common 1 preceding A</>
<green>- delete line</>
<green>- change <inverse>expect</>ed A</>
<red>+ change <inverse>receiv</>ed A</>
<dim> common 1 following A</>
<dim> common 2 following A</>
<dim> common 3 following A</>
<dim> common 4 following A</>
<dim> common 4 preceding B</>
<dim> common 3 preceding B</>
<dim> common 2 preceding B</>
<dim> common 1 preceding B</>
<green>- change <inverse>expect</>ed B</>
<red>+ change <inverse>receiv</>ed B</>
<red>+ insert line</>
<dim> common 1 following B</>
<dim> common 2 following B</>
<dim> common 3 between B and C</>
<dim> common 2 preceding C</>
<dim> common 1 preceding C</>
<green>- change <inverse>expect</>ed C</>
<red>+ change <inverse>receiv</>ed C</>
<dim> common 1 following C</>
<dim> common 2 following C</>
<dim> common 3 following C</>
<dim> common 5 following C</>"
`;
exports[`joinAlignedDiffsNoExpand patch 1 with context 4 and last line is empty common 1`] = `
"<yellow>@@ -1,24 +1,24 @@</>
<dim> common 2 preceding A</>
<dim> common 1 preceding A</>
<green>- delete line</>
<green>- change <inverse>expect</>ed A</>
<red>+ change <inverse>receiv</>ed A</>
<dim> common 1 following A</>
<dim> common 2 following A</>
<dim> common 3 following A</>
<dim> common 4 following A</>
<dim> common 4 preceding B</>
<dim> common 3 preceding B</>
<dim> common 2 preceding B</>
<dim> common 1 preceding B</>
<green>- change <inverse>expect</>ed B</>
<red>+ change <inverse>receiv</>ed B</>
<red>+ insert line</>
<dim> common 1 following B</>
<dim> common 2 following B</>
<dim> common 3 between B and C</>
<dim> common 2 preceding C</>
<dim> common 1 preceding C</>
<green>- change <inverse>expect</>ed C</>
<red>+ change <inverse>receiv</>ed C</>
<dim> common 1 following C</>
<dim> common 2 following C</>
<dim> common 3 following C</>
<dim> ↵</>"
`;
exports[`joinAlignedDiffsNoExpand patch 2 with context 3 1`] = `
"<yellow>@@ -1,8 +1,7 @@</>
<dim> common 2 preceding A</>
<dim> common 1 preceding A</>
<green>- delete line</>
<green>- change <inverse>expect</>ed A</>
<red>+ change <inverse>receiv</>ed A</>
<dim> common 1 following A</>
<dim> common 2 following A</>
<dim> common 3 following A</>
<yellow>@@ -11,13 +10,14 @@</>
<dim> common 3 preceding B</>
<dim> common 2 preceding B</>
<dim> common 1 preceding B</>
<green>- change <inverse>expect</>ed B</>
<red>+ change <inverse>receiv</>ed B</>
<red>+ insert line</>
<dim> common 1 following B</>
<dim> common 2 following B</>
<dim> common 3 between B and C</>
<dim> common 2 preceding C</>
<dim> common 1 preceding C</>
<green>- change <inverse>expect</>ed C</>
<red>+ change <inverse>receiv</>ed C</>
<dim> common 1 following C</>
<dim> common 2 following C</>
<dim> common 3 following C</>"
`;
exports[`joinAlignedDiffsNoExpand patch 3 with context 2 and omit excess common at start 1`] = `
"<yellow>@@ -2,6 +2,5 @@</>
<dim> common 2 preceding A</>
<dim> common 1 preceding A</>
<green>- delete line</>
<green>- change <inverse>expect</>ed A</>
<red>+ change <inverse>receiv</>ed A</>
<dim> common 1 following A</>
<dim> common 2 following A</>
<yellow>@@ -12,5 +11,6 @@</>
<dim> common 2 preceding B</>
<dim> common 1 preceding B</>
<green>- change <inverse>expect</>ed B</>
<red>+ change <inverse>receiv</>ed B</>
<red>+ insert line</>
<dim> common 1 following B</>
<dim> common 2 following B</>
<yellow>@@ -18,5 +18,5 @@</>
<dim> common 2 preceding C</>
<dim> common 1 preceding C</>
<green>- change <inverse>expect</>ed C</>
<red>+ change <inverse>receiv</>ed C</>
<dim> common 1 following C</>
<dim> common 2 following C</>"
`;
@@ -0,0 +1,171 @@
/**
* 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 {computeStringDiffs, printMultilineStringDiffs} from '../printDiffs';
const testAlignedDiffs = (a: string, b: string): string => {
const {diffs} = computeStringDiffs(a, b);
return printMultilineStringDiffs(diffs, true);
};
describe('getAlignedDiffs', () => {
describe('lines', () => {
test('change preceding and following common', () => {
const a = 'delete\ncommon between changes\nprev';
const b = 'insert\ncommon between changes\nnext';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('common preceding and following change', () => {
const a = 'common preceding\ndelete\ncommon following';
const b = 'common preceding\ninsert\ncommon following';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('common at end when both current change lines are empty', () => {
const a = 'delete\ncommon at end';
const b = 'common at end';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('common between delete and insert', () => {
const a = 'delete\ncommon between changes';
const b = 'common between changes\ninsert';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('common between insert and delete', () => {
const a = 'common between changes\ndelete';
const b = 'insert\ncommon between changes';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
describe('newline', () => {
test('delete only', () => {
const a = 'preceding\nfollowing';
const b = 'precedingfollowing';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('insert only', () => {
const a = 'precedingfollowing';
const b = 'preceding\nfollowing';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('delete with adjacent change', () => {
const a = 'preceding\nfollowing';
const b = 'precededfollowing';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('insert with adjacent changes', () => {
const a = 'precededfollowing';
const b = 'preceding\nFollowing';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('change from space', () => {
const a = 'preceding following';
const b = 'preceding\nfollowing';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('change to space', () => {
const a = 'preceding\nfollowing';
const b = 'preceding following';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
describe('substrings first', () => {
test('common when both current change lines are empty', () => {
const a = 'first\nmiddle\nlast prev';
const b = 'insert\nfirst\nmiddle\nlast next';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('common when either current change line is non-empty', () => {
const a = 'expected first\n\nlast';
const b = 'first\n\nlast';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('delete completes the current line', () => {
const a = 'common preceding first\nmiddle\nlast and following';
const b = 'common preceding and following';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('insert completes the current line', () => {
const a = 'common preceding';
const b = 'common preceding first\nmiddle\n';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
describe('substrings middle', () => {
test('is empty in delete between common', () => {
const a = 'common at start precedes delete\n\nexpected common at end';
const b = 'common at start precedes received common at end';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('is empty in insert at start', () => {
const a = 'expected common at end';
const b = 'insert line\n\nreceived common at end';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('is non-empty in delete at end', () => {
const a = 'common at start precedes delete\nnon-empty line\nnext';
const b = 'common at start precedes prev';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('is non-empty in insert between common', () => {
const a = 'common at start precedes delete expected';
const b = 'common at start precedes insert\nnon-empty\nreceived';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
describe('substrings last', () => {
test('is empty in delete at end', () => {
const a = 'common string preceding prev\n';
const b = 'common string preceding next';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('is empty in insert at end', () => {
const a = 'common string preceding prev';
const b = 'common string preceding next\n';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('is non-empty in common not at end', () => {
const a = 'common first\nlast expected';
const b = 'common first\nlast received';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
describe('strings', () => {
test('change at start and delete or insert at end', () => {
const a = 'prev change common delete\nunchanged\nexpected change common';
const b = 'next change common\nunchanged\nreceived change common insert';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
test('delete or insert at start and change at end', () => {
const a = 'common change prev\nunchanged\ndelete common change this';
const b = 'insert common change next\nunchanged\ncommon change that';
expect(testAlignedDiffs(a, b)).toMatchSnapshot();
});
});
});
@@ -0,0 +1,80 @@
/**
* 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_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from '../cleanupSemantic';
import {INVERTED_COLOR} from '../printDiffs';
import {
joinAlignedDiffsExpand,
joinAlignedDiffsNoExpand,
} from '../joinAlignedDiffs';
const diffsCommonStartEnd = [
new Diff(DIFF_EQUAL, ''),
new Diff(DIFF_EQUAL, 'common 2 preceding A'),
new Diff(DIFF_EQUAL, 'common 1 preceding A'),
new Diff(DIFF_DELETE, 'delete line'),
new Diff(DIFF_DELETE, ['change ', INVERTED_COLOR('expect'), 'ed A'].join('')),
new Diff(DIFF_INSERT, ['change ', INVERTED_COLOR('receiv'), 'ed A'].join('')),
new Diff(DIFF_EQUAL, 'common 1 following A'),
new Diff(DIFF_EQUAL, 'common 2 following A'),
new Diff(DIFF_EQUAL, 'common 3 following A'),
new Diff(DIFF_EQUAL, 'common 4 following A'),
new Diff(DIFF_EQUAL, 'common 4 preceding B'),
new Diff(DIFF_EQUAL, 'common 3 preceding B'),
new Diff(DIFF_EQUAL, 'common 2 preceding B'),
new Diff(DIFF_EQUAL, 'common 1 preceding B'),
new Diff(DIFF_DELETE, ['change ', INVERTED_COLOR('expect'), 'ed B'].join('')),
new Diff(DIFF_INSERT, ['change ', INVERTED_COLOR('receiv'), 'ed B'].join('')),
new Diff(DIFF_INSERT, 'insert line'),
new Diff(DIFF_EQUAL, 'common 1 following B'),
new Diff(DIFF_EQUAL, 'common 2 following B'),
new Diff(DIFF_EQUAL, 'common 3 between B and C'),
new Diff(DIFF_EQUAL, 'common 2 preceding C'),
new Diff(DIFF_EQUAL, 'common 1 preceding C'),
new Diff(DIFF_DELETE, ['change ', INVERTED_COLOR('expect'), 'ed C'].join('')),
new Diff(DIFF_INSERT, ['change ', INVERTED_COLOR('receiv'), 'ed C'].join('')),
new Diff(DIFF_EQUAL, 'common 1 following C'),
new Diff(DIFF_EQUAL, 'common 2 following C'),
new Diff(DIFF_EQUAL, 'common 3 following C'),
new Diff(DIFF_EQUAL, ''),
new Diff(DIFF_EQUAL, 'common 5 following C'),
];
const diffsChangeStartEnd = [
new Diff(DIFF_DELETE, 'delete'),
new Diff(DIFF_EQUAL, 'common following delete'),
new Diff(DIFF_EQUAL, 'common preceding insert'),
new Diff(DIFF_INSERT, 'insert'),
];
describe('joinAlignedDiffsExpand', () => {
test('first line is empty common', () => {
expect(joinAlignedDiffsExpand(diffsCommonStartEnd)).toMatchSnapshot();
});
});
describe('joinAlignedDiffsNoExpand', () => {
test('patch 0 with context 1 and change at start and end', () => {
expect(joinAlignedDiffsNoExpand(diffsChangeStartEnd, 1)).toMatchSnapshot();
});
test('patch 0 with context 5 and first line is empty common', () => {
expect(joinAlignedDiffsNoExpand(diffsCommonStartEnd)).toMatchSnapshot();
});
test('patch 1 with context 4 and last line is empty common', () => {
expect(joinAlignedDiffsNoExpand(diffsCommonStartEnd, 4)).toMatchSnapshot();
});
test('patch 2 with context 3', () => {
expect(joinAlignedDiffsNoExpand(diffsCommonStartEnd, 3)).toMatchSnapshot();
});
test('patch 3 with context 2 and omit excess common at start', () => {
expect(joinAlignedDiffsNoExpand(diffsCommonStartEnd, 2)).toMatchSnapshot();
});
});
+323
View File
@@ -0,0 +1,323 @@
/**
* 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 chalk, {Chalk} from 'chalk';
import diff, {Callbacks} from 'diff-sequences';
import {NO_DIFF_MESSAGE} from './constants';
import {createPatchMark, printAnnotation} from './printDiffs';
import {DiffOptions} from './types';
const DIFF_CONTEXT_DEFAULT = 5;
type Original = {
a: string;
b: string;
};
const fgDelete = chalk.green;
const fgInsert = chalk.red;
const fgCommon = chalk.dim; // common lines (even indentation same)
const fgIndent = chalk.cyan; // common lines (only indentation different)
const bgCommon = chalk.bgYellow; // edge spaces in common line (even indentation same)
const bgInverse = chalk.inverse; // edge spaces in any other lines
// ONLY trailing if expected value is snapshot or multiline string.
const highlightTrailingSpaces = (line: string, bgColor: Chalk): string =>
line.replace(/\s+$/, bgColor('$&'));
// BOTH leading AND trailing if expected value is data structure.
const highlightLeadingTrailingSpaces = (line: string, bgColor: Chalk): string =>
// If line consists of ALL spaces: highlight all of them.
highlightTrailingSpaces(line, bgColor).replace(
// If line has an ODD length of leading spaces: highlight only the LAST.
/^(\s\s)*(\s)(?=[^\s])/,
'$1' + bgColor('$2'),
);
type Highlight = (line: string, bgColor: Chalk) => string;
const getHighlightSpaces = (bothEdges: boolean): Highlight =>
bothEdges ? highlightLeadingTrailingSpaces : highlightTrailingSpaces;
type Put = (line: string) => void;
// Given index interval in expected lines, put formatted delete lines.
const formatDelete = (
aStart: number,
aEnd: number,
aLinesUn: Array<string>,
aLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(aLinesUn !== aLinesIn);
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(fgDelete('- ' + indentation + highlightSpaces(aLineUn, bgInverse)));
}
};
// Given index interval in received lines, put formatted insert lines.
const formatInsert = (
bStart: number,
bEnd: number,
bLinesUn: Array<string>,
bLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(bLinesUn !== bLinesIn);
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(fgInsert('+ ' + indentation + highlightSpaces(bLineUn, bgInverse)));
}
};
// Given the number of items and starting indexes of a common subsequence,
// put formatted common lines.
const formatCommon = (
nCommon: number,
aCommon: number,
bCommon: number,
// aLinesUn has lines that are equal to bLinesUn within a common subsequence
aLinesIn: Array<string>,
bLinesUn: Array<string>,
bLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(bLinesUn !== bLinesIn);
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);
// Color shows whether expected and received line has same indentation.
const hasSameIndentation = aLinesIn[aCommon].length === bLineInLength;
const fg = hasSameIndentation ? fgCommon : fgIndent;
const bg = hasSameIndentation ? bgCommon : bgInverse;
put(fg(' ' + indentation + highlightSpaces(bLineUn, bg)));
}
};
// jest --expand
// Return formatted diff as joined string of all lines.
const diffExpand = (
aLinesUn: Array<string>,
bLinesUn: Array<string>,
aLinesIn: Array<string>,
bLinesIn: Array<string>,
): string => {
const isCommon: Callbacks['isCommon'] = (aIndex, bIndex) =>
aLinesUn[aIndex] === bLinesUn[bIndex];
const array: Array<string> = [];
const put = (line: string) => {
array.push(line);
};
let aStart = 0;
let bStart = 0;
const foundSubsequence: Callbacks['foundSubsequence'] = (
nCommon,
aCommon,
bCommon,
) => {
formatDelete(aStart, aCommon, aLinesUn, aLinesIn, put);
formatInsert(bStart, bCommon, bLinesUn, bLinesIn, put);
formatCommon(nCommon, aCommon, bCommon, aLinesIn, bLinesUn, bLinesIn, 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.
formatDelete(aStart, aLength, aLinesUn, aLinesIn, put);
formatInsert(bStart, bLength, bLinesUn, bLinesIn, put);
return array.join('\n');
};
const getContextLines = (options?: DiffOptions): number =>
options &&
typeof options.contextLines === 'number' &&
options.contextLines >= 0
? options.contextLines
: DIFF_CONTEXT_DEFAULT;
// 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<string>,
bLinesUn: Array<string>,
aLinesIn: Array<string>,
bLinesIn: Array<string>,
nContextLines: number,
): 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 nContextLines2 = nContextLines + nContextLines;
// 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd = aEndCommon;
bEnd = bEndCommon;
return;
}
// Format preceding change lines.
formatDelete(aEnd, aStartCommon, aLinesUn, aLinesIn, put);
formatInsert(bEnd, bStartCommon, bLinesUn, bLinesIn, 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd += nCommon;
bEnd += nCommon;
return;
}
// The patch ends because context is less than number of common lines.
formatCommon(nContextLines, aEnd, bEnd, aLinesIn, bLinesUn, bLinesIn, put);
aEnd += nContextLines;
bEnd += nContextLines;
array[iPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd);
// 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd = aEndCommon;
bEnd = bEndCommon;
}
};
diff(aLength, bLength, isCommon, foundSubsequence);
// If no common subsequence or last was not at end, format remaining change lines.
if (!isAtEnd) {
formatDelete(aEnd, aLength, aLinesUn, aLinesIn, put);
formatInsert(bEnd, bLength, bLinesUn, bLinesIn, 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);
}
return array.join('\n');
};
export default (
a: string,
b: string,
options?: DiffOptions,
original?: Original,
): string => {
if (a === b) {
return NO_DIFF_MESSAGE;
}
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;
}
}
return (
printAnnotation(options) +
(options && options.expand === false
? diffNoExpand(
aLinesUn,
bLinesUn,
aLinesIn,
bLinesIn,
getContextLines(options),
)
: diffExpand(aLinesUn, bLinesUn, aLinesIn, bLinesIn))
);
};
+31 -322
View File
@@ -5,336 +5,45 @@
* LICENSE file in the root directory of this source tree.
*/
import chalk, {Chalk} from 'chalk';
import diff, {Callbacks} from 'diff-sequences';
import {NO_DIFF_MESSAGE} from './constants';
import {DiffOptions} from './types';
import diffSequences from 'diff-sequences';
const DIFF_CONTEXT_DEFAULT = 5;
import {Diff, DIFF_EQUAL, DIFF_DELETE, DIFF_INSERT} from './cleanupSemantic';
type Original = {
a: string;
b: string;
};
const diffStrings = (a: string, b: string): Array<Diff> => {
const isCommon = (aIndex: number, bIndex: number) => a[aIndex] === b[bIndex];
const fgPatchMark = chalk.yellow;
const fgDelete = chalk.green;
const fgInsert = chalk.red;
const fgCommon = chalk.dim; // common lines (even indentation same)
const fgIndent = chalk.cyan; // common lines (only indentation different)
const bgCommon = chalk.bgYellow; // edge spaces in common line (even indentation same)
const bgInverse = chalk.inverse; // edge spaces in any other lines
let aIndex = 0;
let bIndex = 0;
const diffs: Array<Diff> = [];
// ONLY trailing if expected value is snapshot or multiline string.
const highlightTrailingSpaces = (line: string, bgColor: Chalk): string =>
line.replace(/\s+$/, bgColor('$&'));
// BOTH leading AND trailing if expected value is data structure.
const highlightLeadingTrailingSpaces = (line: string, bgColor: Chalk): string =>
// If line consists of ALL spaces: highlight all of them.
highlightTrailingSpaces(line, bgColor).replace(
// If line has an ODD length of leading spaces: highlight only the LAST.
/^(\s\s)*(\s)(?=[^\s])/,
'$1' + bgColor('$2'),
);
type Highlight = (line: string, bgColor: Chalk) => string;
const getHighlightSpaces = (bothEdges: boolean): Highlight =>
bothEdges ? highlightLeadingTrailingSpaces : highlightTrailingSpaces;
const getAnnotation = (options?: DiffOptions): string =>
fgDelete('- ' + ((options && options.aAnnotation) || 'Expected')) +
'\n' +
fgInsert('+ ' + ((options && options.bAnnotation) || 'Received')) +
'\n\n';
type Put = (line: string) => void;
// Given index interval in expected lines, put formatted delete lines.
const formatDelete = (
aStart: number,
aEnd: number,
aLinesUn: Array<string>,
aLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(aLinesUn !== aLinesIn);
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(fgDelete('- ' + indentation + highlightSpaces(aLineUn, bgInverse)));
}
};
// Given index interval in received lines, put formatted insert lines.
const formatInsert = (
bStart: number,
bEnd: number,
bLinesUn: Array<string>,
bLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(bLinesUn !== bLinesIn);
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(fgInsert('+ ' + indentation + highlightSpaces(bLineUn, bgInverse)));
}
};
// Given the number of items and starting indexes of a common subsequence,
// put formatted common lines.
const formatCommon = (
nCommon: number,
aCommon: number,
bCommon: number,
// aLinesUn has lines that are equal to bLinesUn within a common subsequence
aLinesIn: Array<string>,
bLinesUn: Array<string>,
bLinesIn: Array<string>,
put: Put,
) => {
const highlightSpaces = getHighlightSpaces(bLinesUn !== bLinesIn);
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);
// Color shows whether expected and received line has same indentation.
const hasSameIndentation = aLinesIn[aCommon].length === bLineInLength;
const fg = hasSameIndentation ? fgCommon : fgIndent;
const bg = hasSameIndentation ? bgCommon : bgInverse;
put(fg(' ' + indentation + highlightSpaces(bLineUn, bg)));
}
};
// jest --expand
// Return formatted diff as joined string of all lines.
const diffExpand = (
aLinesUn: Array<string>,
bLinesUn: Array<string>,
aLinesIn: Array<string>,
bLinesIn: Array<string>,
): string => {
const isCommon: Callbacks['isCommon'] = (aIndex, bIndex) =>
aLinesUn[aIndex] === bLinesUn[bIndex];
const array: Array<string> = [];
const put = (line: string) => {
array.push(line);
};
let aStart = 0;
let bStart = 0;
const foundSubsequence: Callbacks['foundSubsequence'] = (
nCommon,
aCommon,
bCommon,
const foundSubsequence = (
nCommon: number,
aCommon: number,
bCommon: number,
) => {
formatDelete(aStart, aCommon, aLinesUn, aLinesIn, put);
formatInsert(bStart, bCommon, bLinesUn, bLinesIn, put);
formatCommon(nCommon, aCommon, bCommon, aLinesIn, bLinesUn, bLinesIn, put);
aStart = aCommon + nCommon;
bStart = bCommon + nCommon;
if (aIndex !== aCommon) {
diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex, aCommon)));
}
if (bIndex !== bCommon) {
diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex, bCommon)));
}
aIndex = aCommon + nCommon; // number of characters compared in a
bIndex = bCommon + nCommon; // number of characters compared in b
diffs.push(new Diff(DIFF_EQUAL, b.slice(bCommon, bIndex)));
};
const aLength = aLinesUn.length;
const bLength = bLinesUn.length;
diffSequences(a.length, b.length, isCommon, foundSubsequence);
diff(aLength, bLength, isCommon, foundSubsequence);
// After the last common subsequence, push remaining change items.
if (aIndex !== a.length) {
diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex)));
}
if (bIndex !== b.length) {
diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex)));
}
// After the last common subsequence, format remaining change lines.
formatDelete(aStart, aLength, aLinesUn, aLinesIn, put);
formatInsert(bStart, bLength, bLinesUn, bLinesIn, put);
return array.join('\n');
return diffs;
};
// In GNU diff format, indexes are one-based instead of zero-based.
const createPatchMark = (
aStart: number,
aEnd: number,
bStart: number,
bEnd: number,
): string =>
fgPatchMark(
`@@ -${aStart + 1},${aEnd - aStart} +${bStart + 1},${bEnd - bStart} @@`,
);
const getContextLines = (options?: DiffOptions): number =>
options &&
typeof options.contextLines === 'number' &&
options.contextLines >= 0
? options.contextLines
: DIFF_CONTEXT_DEFAULT;
// 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<string>,
bLinesUn: Array<string>,
aLinesIn: Array<string>,
bLinesIn: Array<string>,
nContextLines: number,
): 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 nContextLines2 = nContextLines + nContextLines;
// 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd = aEndCommon;
bEnd = bEndCommon;
return;
}
// Format preceding change lines.
formatDelete(aEnd, aStartCommon, aLinesUn, aLinesIn, put);
formatInsert(bEnd, bStartCommon, bLinesUn, bLinesIn, 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd += nCommon;
bEnd += nCommon;
return;
}
// The patch ends because context is less than number of common lines.
formatCommon(nContextLines, aEnd, bEnd, aLinesIn, bLinesUn, bLinesIn, put);
aEnd += nContextLines;
bEnd += nContextLines;
array[iPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd);
// 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, aLinesIn, bLinesUn, bLinesIn, put);
aEnd = aEndCommon;
bEnd = bEndCommon;
}
};
diff(aLength, bLength, isCommon, foundSubsequence);
// If no common subsequence or last was not at end, format remaining change lines.
if (!isAtEnd) {
formatDelete(aEnd, aLength, aLinesUn, aLinesIn, put);
formatInsert(bEnd, bLength, bLinesUn, bLinesIn, 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);
}
return array.join('\n');
};
export default (
a: string,
b: string,
options?: DiffOptions,
original?: Original,
): string => {
if (a === b) {
return NO_DIFF_MESSAGE;
}
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;
}
}
return (
getAnnotation(options) +
(options && options.expand === false
? diffNoExpand(
aLinesUn,
bLinesUn,
aLinesIn,
bLinesIn,
getContextLines(options),
)
: diffExpand(aLinesUn, bLinesUn, aLinesIn, bLinesIn))
);
};
export default diffStrings;
+198
View File
@@ -0,0 +1,198 @@
/**
* 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, DIFF_DELETE, DIFF_INSERT} from './cleanupSemantic';
import {MULTILINE_REGEXP, getHighlightedString} from './printDiffs';
// Encapsulate change lines until either a common newline or the end.
class ChangeBuffer {
private op: number;
private line: Array<Diff>; // incomplete line
private lines: Array<Diff>; // complete lines
constructor(op: number) {
this.op = op;
this.line = [];
this.lines = [];
}
private pushSubstring(substring: string): void {
this.pushDiff(new Diff(this.op, substring));
}
private pushLine(): void {
// Assume call only if line has at least one diff,
// therefore an empty line must have a diff which has an empty string.
this.lines.push(
new Diff(this.op, getHighlightedString(this.op, this.line)),
);
this.line.length = 0;
}
isLineEmpty() {
return this.line.length === 0;
}
// Minor input to buffer.
pushDiff(diff: Diff): void {
this.line.push(diff);
}
// Main input to buffer.
align(diff: Diff): void {
const string = diff[1];
if (MULTILINE_REGEXP.test(string)) {
const substrings = string.split('\n');
const iLast = substrings.length - 1;
substrings.forEach((substring, i) => {
if (i < iLast) {
// The first substring completes the current change line.
// A middle substring is a change line.
this.pushSubstring(substring);
this.pushLine();
} else if (substring.length !== 0) {
// The last substring starts a change line, if it is not empty.
// Important: This non-empty condition also automatically omits
// the newline appended to the end of expected and received strings.
this.pushSubstring(substring);
}
});
} else {
// Append non-multiline string to current change line.
this.pushDiff(diff);
}
}
// Output from buffer.
moveLinesTo(lines: Array<Diff>): void {
if (!this.isLineEmpty()) {
this.pushLine();
}
lines.push(...this.lines);
this.lines.length = 0;
}
}
// Encapsulate common and change lines.
class CommonBuffer {
private deleteBuffer: ChangeBuffer;
private insertBuffer: ChangeBuffer;
private lines: Array<Diff>;
constructor(deleteBuffer: ChangeBuffer, insertBuffer: ChangeBuffer) {
this.deleteBuffer = deleteBuffer;
this.insertBuffer = insertBuffer;
this.lines = [];
}
private pushDiffCommonLine(diff: Diff): void {
this.lines.push(diff);
}
private pushDiffChangeLines(diff: Diff): void {
const isDiffEmpty = diff[1].length === 0;
// An empty diff string is redundant, unless a change line is empty.
if (!isDiffEmpty || this.deleteBuffer.isLineEmpty()) {
this.deleteBuffer.pushDiff(diff);
}
if (!isDiffEmpty || this.insertBuffer.isLineEmpty()) {
this.insertBuffer.pushDiff(diff);
}
}
private flushChangeLines(): void {
this.deleteBuffer.moveLinesTo(this.lines);
this.insertBuffer.moveLinesTo(this.lines);
}
// Input to buffer.
align(diff: Diff): void {
const op = diff[0];
const string = diff[1];
if (MULTILINE_REGEXP.test(string)) {
const substrings = string.split('\n');
const iLast = substrings.length - 1;
substrings.forEach((substring, i) => {
if (i === 0) {
const subdiff = new Diff(op, substring);
if (
this.deleteBuffer.isLineEmpty() &&
this.insertBuffer.isLineEmpty()
) {
// If both current change lines are empty,
// then the first substring is a common line.
this.flushChangeLines();
this.pushDiffCommonLine(subdiff);
} else {
// If either current change line is non-empty,
// then the first substring completes the change lines.
this.pushDiffChangeLines(subdiff);
this.flushChangeLines();
}
} else if (i < iLast) {
// A middle substring is a common line.
this.pushDiffCommonLine(new Diff(op, substring));
} else if (substring.length !== 0) {
// The last substring starts a change line, if it is not empty.
// Important: This non-empty condition also automatically omits
// the newline appended to the end of expected and received strings.
this.pushDiffChangeLines(new Diff(op, substring));
}
});
} else {
// Append non-multiline string to current change lines.
// Important: It cannot be at the end following empty change lines,
// because newline appended to the end of expected and received strings.
this.pushDiffChangeLines(diff);
}
}
// Output from buffer.
getLines(): Array<Diff> {
this.flushChangeLines();
return this.lines;
}
}
// Given diffs from expected and received strings,
// return new array of diffs split or joined into lines.
//
// To correctly align a change line at the end, the algorithm:
// * assumes that a newline was appended to the strings
// * omits the last newline from the output array
//
// Assume the function is not called:
// * if either expected or received is empty string
// * if neither expected nor received is multiline string
const getAlignedDiffs = (diffs: Array<Diff>): Array<Diff> => {
const deleteBuffer = new ChangeBuffer(DIFF_DELETE);
const insertBuffer = new ChangeBuffer(DIFF_INSERT);
const commonBuffer = new CommonBuffer(deleteBuffer, insertBuffer);
diffs.forEach(diff => {
switch (diff[0]) {
case DIFF_DELETE:
deleteBuffer.align(diff);
break;
case DIFF_INSERT:
insertBuffer.align(diff);
break;
default:
commonBuffer.align(diff);
}
});
return commonBuffer.getLines();
};
export default getAlignedDiffs;
+8 -5
View File
@@ -8,7 +8,8 @@
import prettyFormat from 'pretty-format';
import chalk from 'chalk';
import getType from 'jest-get-type';
import diffStrings from './diffStrings';
import diffLines from './diffLines';
import {getStringDiff} from './printDiffs';
import {NO_DIFF_MESSAGE, SIMILAR_MESSAGE} from './constants';
import {DiffOptions as JestDiffOptions} from './types';
@@ -79,7 +80,7 @@ function diff(a: any, b: any, options?: JestDiffOptions): string | null {
switch (aType) {
case 'string':
return diffStrings(a, b, options);
return diffLines(a, b, options);
case 'boolean':
case 'number':
return comparePrimitive(a, b, options);
@@ -97,7 +98,7 @@ function comparePrimitive(
b: number | boolean,
options?: JestDiffOptions,
) {
return diffStrings(
return diffLines(
prettyFormat(a, FORMAT_OPTIONS),
prettyFormat(b, FORMAT_OPTIONS),
options,
@@ -121,7 +122,7 @@ function compareObjects(
let hasThrown = false;
try {
diffMessage = diffStrings(
diffMessage = diffLines(
prettyFormat(a, FORMAT_OPTIONS_0),
prettyFormat(b, FORMAT_OPTIONS_0),
options,
@@ -137,7 +138,7 @@ function compareObjects(
// 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 = diffStrings(
diffMessage = diffLines(
prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0),
prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0),
options,
@@ -159,4 +160,6 @@ namespace diff {
export type DiffOptions = JestDiffOptions;
}
diff.getStringDiff = getStringDiff;
export = diff;
+208
View File
@@ -0,0 +1,208 @@
/**
* 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_DELETE, DIFF_EQUAL, DIFF_INSERT, Diff} from './cleanupSemantic';
import {
createPatchMark,
printCommonLine,
printDeleteLine,
printInsertLine,
} from './printDiffs';
const DIFF_CONTEXT_DEFAULT = 5; // same as diffLines
// jest --no-expand
//
// Given array of aligned strings with inverse highlight formatting,
// return joined lines with diff formatting (and patch marks, if needed).
export const joinAlignedDiffsNoExpand = (
diffs: Array<Diff>,
nContextLines: number = DIFF_CONTEXT_DEFAULT,
): string => {
const iLength = diffs.length;
const nContextLines2 = nContextLines + nContextLines;
// First pass: count output lines and see if it has patches.
let jLength = iLength;
let hasExcessAtStartOrEnd = false;
let nExcessesBetweenChanges = 0;
let i = 0;
while (i !== iLength) {
const iStart = i;
while (i !== iLength && diffs[i][0] === DIFF_EQUAL) {
i += 1;
}
if (iStart !== i) {
if (iStart === 0) {
// at start
if (i > nContextLines) {
jLength -= i - nContextLines; // subtract excess common lines
hasExcessAtStartOrEnd = true;
}
} else if (i === iLength) {
// at end
const n = i - iStart;
if (n > nContextLines) {
jLength -= n - nContextLines; // subtract excess common lines
hasExcessAtStartOrEnd = true;
}
} else {
// between changes
const n = i - iStart;
if (n > nContextLines2) {
jLength -= n - nContextLines2; // subtract excess common lines
nExcessesBetweenChanges += 1;
}
}
}
while (i !== iLength && diffs[i][0] !== DIFF_EQUAL) {
i += 1;
}
}
const hasPatch = nExcessesBetweenChanges !== 0 || hasExcessAtStartOrEnd;
if (nExcessesBetweenChanges !== 0) {
jLength += nExcessesBetweenChanges + 1; // add patch lines
} else if (hasExcessAtStartOrEnd) {
jLength += 1; // add patch line
}
const jLast = jLength - 1;
const lines: Array<string> = [];
let jPatchMark = 0; // index of placeholder line for current patch mark
if (hasPatch) {
lines.push(''); // placeholder line for first patch mark
}
// Indexes of expected or received lines in current patch:
let aStart = 0;
let bStart = 0;
let aEnd = 0;
let bEnd = 0;
const pushCommonLine = (line: string): void => {
const j = lines.length;
lines.push(printCommonLine(line, j === 0 || j === jLast));
aEnd += 1;
bEnd += 1;
};
const pushDeleteLine = (line: string): void => {
lines.push(printDeleteLine(line));
aEnd += 1;
};
const pushInsertLine = (line: string): void => {
lines.push(printInsertLine(line));
bEnd += 1;
};
// Second pass: push lines with diff formatting (and patch marks, if needed).
i = 0;
while (i !== iLength) {
let iStart = i;
while (i !== iLength && diffs[i][0] === DIFF_EQUAL) {
i += 1;
}
if (iStart !== i) {
if (iStart === 0) {
// at beginning
if (i > nContextLines) {
iStart = i - nContextLines;
aStart = iStart;
bStart = iStart;
aEnd = aStart;
bEnd = bStart;
}
for (let iCommon = iStart; iCommon !== i; iCommon += 1) {
pushCommonLine(diffs[iCommon][1]);
}
} else if (i === iLength) {
// at end
const iEnd = i - iStart > nContextLines ? iStart + nContextLines : i;
for (let iCommon = iStart; iCommon !== iEnd; iCommon += 1) {
pushCommonLine(diffs[iCommon][1]);
}
} else {
// between changes
const nCommon = i - iStart;
if (nCommon > nContextLines2) {
const iEnd = iStart + nContextLines;
for (let iCommon = iStart; iCommon !== iEnd; iCommon += 1) {
pushCommonLine(diffs[iCommon][1]);
}
lines[jPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd);
jPatchMark = lines.length;
lines.push(''); // placeholder line for next patch mark
const nOmit = nCommon - nContextLines2;
aStart = aEnd + nOmit;
bStart = bEnd + nOmit;
aEnd = aStart;
bEnd = bStart;
for (let iCommon = i - nContextLines; iCommon !== i; iCommon += 1) {
pushCommonLine(diffs[iCommon][1]);
}
} else {
for (let iCommon = iStart; iCommon !== i; iCommon += 1) {
pushCommonLine(diffs[iCommon][1]);
}
}
}
}
while (i !== iLength && diffs[i][0] === DIFF_DELETE) {
pushDeleteLine(diffs[i][1]);
i += 1;
}
while (i !== iLength && diffs[i][0] === DIFF_INSERT) {
pushInsertLine(diffs[i][1]);
i += 1;
}
}
if (hasPatch) {
lines[jPatchMark] = createPatchMark(aStart, aEnd, bStart, bEnd);
}
return lines.join('\n');
};
// jest --expand
//
// Given array of aligned strings with inverse highlight formatting,
// return joined lines with diff formatting.
export const joinAlignedDiffsExpand = (diffs: Array<Diff>) =>
diffs
.map(
(diff: Diff, i: number, diffs: Array<Diff>): string => {
const line = diff[1];
switch (diff[0]) {
case DIFF_DELETE:
return printDeleteLine(line);
case DIFF_INSERT:
return printInsertLine(line);
default:
return printCommonLine(line, i === 0 || i === diffs.length - 1);
}
},
)
.join('\n');
+181
View File
@@ -0,0 +1,181 @@
/**
* 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 chalk from 'chalk';
import {
cleanupSemantic,
DIFF_EQUAL,
DIFF_DELETE,
DIFF_INSERT,
Diff,
} from './cleanupSemantic';
import diffStrings from './diffStrings';
import getAlignedDiffs from './getAlignedDiffs';
import {
joinAlignedDiffsExpand,
joinAlignedDiffsNoExpand,
} from './joinAlignedDiffs';
import {DiffOptions} from './types';
export const DIM_COLOR = chalk.dim;
export const EXPECTED_COLOR = chalk.green;
export const INVERTED_COLOR = chalk.inverse;
export const RECEIVED_COLOR = chalk.red;
const PATCH_COLOR = chalk.yellow;
// Given change op and array of diffs, return concatenated string:
// * include common strings
// * include change strings which have argument op (inverse highlight)
// * exclude change strings which have opposite op
export const getHighlightedString = (op: number, diffs: Array<Diff>): string =>
diffs.reduce(
(reduced: string, diff: Diff): string =>
reduced +
(diff[0] === DIFF_EQUAL
? diff[1]
: diff[0] === op
? INVERTED_COLOR(diff[1])
: ''),
'',
);
export const getExpectedString = (diffs: Array<Diff>): string =>
getHighlightedString(DIFF_DELETE, diffs);
export const getReceivedString = (diffs: Array<Diff>): string =>
getHighlightedString(DIFF_INSERT, diffs);
export const MULTILINE_REGEXP = /\n/;
const NEWLINE_SYMBOL = '\u{21B5}'; // downwards arrow with corner leftwards
const SPACE_SYMBOL = '\u{00B7}'; // middle dot
// Instead of inverse highlight which now implies a change,
// replace common spaces with middle dot at the end of the line.
const replaceSpacesAtEnd = (line: string): string =>
line.replace(/\s+$/, spaces => SPACE_SYMBOL.repeat(spaces.length));
export const printDeleteLine = (line: string) =>
EXPECTED_COLOR(line.length !== 0 ? '- ' + replaceSpacesAtEnd(line) : '-');
export const printInsertLine = (line: string) =>
RECEIVED_COLOR(line.length !== 0 ? '+ ' + replaceSpacesAtEnd(line) : '+');
// Prevent visually ambiguous empty line as the first or the last.
export const printCommonLine = (line: string, isFirstOrLast: boolean = false) =>
line.length !== 0
? DIM_COLOR(' ' + replaceSpacesAtEnd(line))
: isFirstOrLast
? DIM_COLOR(' ' + NEWLINE_SYMBOL)
: '';
export const computeStringDiffs = (expected: string, received: string) => {
const isMultiline =
MULTILINE_REGEXP.test(expected) || MULTILINE_REGEXP.test(received);
// getAlignedDiffs assumes that a newline was appended to the strings.
if (isMultiline) {
expected += '\n';
received += '\n';
}
const diffs = diffStrings(expected, received);
cleanupSemantic(diffs); // impure function
return {diffs, isMultiline};
};
export const hasCommonDiff = (diffs: Array<Diff>, isMultiline: boolean) => {
if (isMultiline) {
// Important: Ignore common newline that was appended to multiline strings!
const iLast = diffs.length - 1;
return diffs.some(
(diff, i) => diff[0] === DIFF_EQUAL && (i !== iLast || diff[1] !== '\n'),
);
}
return diffs.some(diff => diff[0] === DIFF_EQUAL);
};
export const printAnnotation = (options?: DiffOptions): string =>
EXPECTED_COLOR('- ' + ((options && options.aAnnotation) || 'Expected')) +
'\n' +
RECEIVED_COLOR('+ ' + ((options && options.bAnnotation) || 'Received')) +
'\n\n';
// In GNU diff format, indexes are one-based instead of zero-based.
export const createPatchMark = (
aStart: number,
aEnd: number,
bStart: number,
bEnd: number,
): string =>
PATCH_COLOR(
`@@ -${aStart + 1},${aEnd - aStart} +${bStart + 1},${bEnd - bStart} @@`,
);
// Return formatted diff lines without labels.
export const printMultilineStringDiffs = (
diffs: Array<Diff>,
expand: boolean,
): string => {
const lines = getAlignedDiffs(diffs);
return expand
? joinAlignedDiffsExpand(lines)
: joinAlignedDiffsNoExpand(lines);
};
const MAX_DIFF_STRING_LENGTH = 20000;
type StringDiffResult =
| {isMultiline: true; annotatedDiff: string}
| {isMultiline: false; a: string; b: string}
| null;
// Print specific substring diff for strings only:
// * if strings are not equal
// * if neither string is empty
// * if neither string is too long
// * if there is a common string after semantic cleanup
export const getStringDiff = (
expected: string,
received: string,
options?: DiffOptions,
): StringDiffResult => {
if (
expected === received ||
expected.length === 0 ||
received.length === 0 ||
expected.length > MAX_DIFF_STRING_LENGTH ||
received.length > MAX_DIFF_STRING_LENGTH
) {
return null;
}
const {diffs, isMultiline} = computeStringDiffs(expected, received);
if (!hasCommonDiff(diffs, isMultiline)) {
return null;
}
return isMultiline
? {
annotatedDiff:
printAnnotation(options) +
printMultilineStringDiffs(
diffs,
options === undefined || options.expand !== false,
),
isMultiline,
}
: {
a: getExpectedString(diffs),
b: getReceivedString(diffs),
isMultiline,
};
};
-1
View File
@@ -14,7 +14,6 @@
"main": "build/index.js",
"dependencies": {
"chalk": "^2.0.1",
"diff-sequences": "^24.3.0",
"jest-diff": "^24.8.0",
"jest-get-type": "^24.8.0",
"pretty-format": "^24.8.0"
@@ -0,0 +1,29 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`printDiffOrStringify expected and received are multi line with trailing spaces 1`] = `
"<green>- Expected</>
<red>+ Received</>
<green>- <inverse>delete</>·</>
<red>+ <inverse>insert</>·</>
<green>- common <inverse>expect</>ed common</>
<red>+ common <inverse>receiv</>ed common</>
<green>- <inverse>prev</>·</>
<red>+ <inverse>next</>·</>"
`;
exports[`printDiffOrStringify expected and received are single line with multiple changes 1`] = `
"Expected: <green>\\"<inverse>delete</> common <inverse>expect</>ed common <inverse>prev</>\\"</>
Received: <red>\\"<inverse>insert</> common <inverse>receiv</>ed common <inverse>next</>\\"</>"
`;
exports[`printDiffOrStringify expected is empty and received is single line 1`] = `
"Expected: <green>\\"\\"</>
Received: <red>\\"single line\\"</>"
`;
exports[`printDiffOrStringify expected is multi line and received is empty 1`] = `
"Expected: <green>\\"multi</>
<green>line\\"</>
Received: <red>\\"\\"</>"
`;
@@ -0,0 +1,50 @@
/**
* 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 {EXPECTED_COLOR, INVERTED_COLOR, printDiffOrStringify} from '../index';
describe('printDiffOrStringify', () => {
const testDiffOrStringify = (expected: string, received: string): string =>
printDiffOrStringify(expected, received, 'Expected', 'Received', true);
test('expected is empty and received is single line', () => {
const expected = '';
const received = 'single line';
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
});
test('expected is multi line and received is empty', () => {
const expected = 'multi\nline';
const received = '';
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
});
test('expected and received are single line with multiple changes', () => {
const expected = 'delete common expected common prev';
const received = 'insert common received common next';
expect(testDiffOrStringify(expected, received)).toMatchSnapshot();
});
test('expected and received are multi line with trailing spaces', () => {
const expected = 'delete \ncommon expected common\nprev ';
const received = 'insert \ncommon received common\nnext ';
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
const test = testDiffOrStringify(expected, received);
// It is a generic line diff:
expect(test).toContain(EXPECTED_COLOR('- line'));
// It is not a specific substring diff
expect(test).not.toContain(EXPECTED_COLOR('- ' + INVERTED_COLOR('line')));
});
});
@@ -1,58 +0,0 @@
/**
* 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 diffSequences from 'diff-sequences';
import {
cleanupSemantic,
Diff,
DIFF_EQUAL,
DIFF_DELETE,
DIFF_INSERT,
} from './cleanupSemantic';
const diffStrings = (a: string, b: string): Array<Diff> | null => {
const isCommon = (aIndex: number, bIndex: number) => a[aIndex] === b[bIndex];
let aIndex = 0;
let bIndex = 0;
const diffs: Array<Diff> = [];
const foundSubsequence = (
nCommon: number,
aCommon: number,
bCommon: number,
) => {
if (aIndex !== aCommon) {
diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex, aCommon)));
}
if (bIndex !== bCommon) {
diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex, bCommon)));
}
aIndex = aCommon + nCommon; // number of characters compared in a
bIndex = bCommon + nCommon; // number of characters compared in b
diffs.push(new Diff(DIFF_EQUAL, b.slice(bCommon, bIndex)));
};
diffSequences(a.length, b.length, isCommon, foundSubsequence);
// After the last common subsequence, push remaining change items.
if (aIndex !== a.length) {
diffs.push(new Diff(DIFF_DELETE, a.slice(aIndex)));
}
if (bIndex !== b.length) {
diffs.push(new Diff(DIFF_INSERT, b.slice(bIndex)));
}
cleanupSemantic(diffs);
// Assume it has a change string, but does it have a common string?
return diffs.some(diff => diff[0] === DIFF_EQUAL) ? diffs : null;
};
export default diffStrings;
+45 -45
View File
@@ -6,17 +6,10 @@
*/
import chalk from 'chalk';
import jestDiff, {DiffOptions} from 'jest-diff';
import jestDiff, {DiffOptions, getStringDiff} from 'jest-diff';
import getType, {isPrimitive} from 'jest-get-type';
import prettyFormat from 'pretty-format';
import diffStrings from './diffStrings';
import {
MULTILINE_REGEXP,
getExpectedString,
getReceivedString,
} from './printDiff';
const {
AsymmetricMatcher,
DOMCollection,
@@ -51,6 +44,9 @@ export const INVERTED_COLOR = chalk.inverse;
export const BOLD_WEIGHT = chalk.bold;
export const DIM_COLOR = chalk.dim;
const MULTILINE_REGEXP = /\n/;
const SPACE_SYMBOL = '\u{00B7}'; // middle dot
const NUMBERS = [
'zero',
'one',
@@ -99,10 +95,15 @@ export const stringify = (object: unknown, maxDepth: number = 10): string => {
export const highlightTrailingWhitespace = (text: string): string =>
text.replace(/\s+$/gm, chalk.inverse('$&'));
// Instead of inverse highlight which now implies a change,
// replace common spaces with middle dot at the end of any line.
const replaceTrailingSpaces = (text: string): string =>
text.replace(/\s+$/gm, spaces => SPACE_SYMBOL.repeat(spaces.length));
export const printReceived = (object: unknown) =>
RECEIVED_COLOR(highlightTrailingWhitespace(stringify(object)));
RECEIVED_COLOR(replaceTrailingSpaces(stringify(object)));
export const printExpected = (value: unknown) =>
EXPECTED_COLOR(highlightTrailingWhitespace(stringify(value)));
EXPECTED_COLOR(replaceTrailingSpaces(stringify(value)));
export const printWithType = (
name: string, // 'Expected' or 'Received'
@@ -206,7 +207,7 @@ export const ensureExpectedIsNonNegativeInteger = (
}
};
const isDiffable = (expected: unknown, received: unknown): boolean => {
const isLineDiffable = (expected: unknown, received: unknown): boolean => {
const expectedType = getType(expected);
const receivedType = getType(received);
@@ -215,12 +216,15 @@ const isDiffable = (expected: unknown, received: unknown): boolean => {
}
if (isPrimitive(expected)) {
// Print diff only if both strings have more than one line.
// Print generic line diff for strings only:
// * if neither string is empty
// * if either string has more than one line
return (
typeof expected === 'string' &&
typeof received === 'string' &&
MULTILINE_REGEXP.test(expected) &&
MULTILINE_REGEXP.test(received)
expected.length !== 0 &&
received.length !== 0 &&
(MULTILINE_REGEXP.test(expected) || MULTILINE_REGEXP.test(received))
);
}
@@ -260,30 +264,27 @@ export const printDiffOrStringify = (
receivedLabel: string,
expand: boolean, // CLI options: true if `--expand` or false if `--no-expand`
): string => {
const printLabel = getLabelPrinter(expectedLabel, receivedLabel);
if (typeof expected === 'string' && typeof received === 'string') {
const result = getStringDiff(expected, received, {
aAnnotation: expectedLabel,
bAnnotation: receivedLabel,
expand,
});
if (
typeof expected === 'string' &&
typeof received === 'string' &&
expected.length !== 0 &&
received.length !== 0 &&
!MULTILINE_REGEXP.test(expected) &&
!MULTILINE_REGEXP.test(received)
) {
const diffs = diffStrings(expected, received);
if (result !== null) {
if (result.isMultiline) {
return result.annotatedDiff;
}
if (Array.isArray(diffs)) {
const expectedLine =
printLabel(expectedLabel) + printExpected(getExpectedString(diffs));
const receivedLine =
printLabel(receivedLabel) + printReceived(getReceivedString(diffs));
const printLabel = getLabelPrinter(expectedLabel, receivedLabel);
const expectedLine = printLabel(expectedLabel) + printExpected(result.a);
const receivedLine = printLabel(receivedLabel) + printReceived(result.b);
return expectedLine + '\n' + receivedLine;
}
} else if (isDiffable(expected, received)) {
// Cannot use same serialization as shortcut to avoid diff,
// because stringify (that is, pretty-format with min option)
// omits constructor name for array or object, too bad so sad :(
}
if (isLineDiffable(expected, received)) {
const difference = jestDiff(expected, received, {
aAnnotation: expectedLabel,
bAnnotation: receivedLabel,
@@ -299,17 +300,15 @@ export const printDiffOrStringify = (
}
}
// Cannot reuse value of stringify(received) in report string,
// because printReceived does inverse highlight space at end of line,
// but RECEIVED_COLOR does not (it refers to a plain chalk method).
return (
`${printLabel(expectedLabel)}${printExpected(expected)}\n` +
`${printLabel(receivedLabel)}${
stringify(expected) === stringify(received)
? 'serializes to the same string'
: printReceived(received)
}`
);
const printLabel = getLabelPrinter(expectedLabel, receivedLabel);
const expectedLine = printLabel(expectedLabel) + printExpected(expected);
const receivedLine =
printLabel(receivedLabel) +
(stringify(expected) === stringify(received)
? 'serializes to the same string'
: printReceived(received));
return expectedLine + '\n' + receivedLine;
};
// Sometimes, e.g. when comparing two numbers, the output from jest-diff
@@ -324,7 +323,8 @@ const shouldPrintDiff = (actual: unknown, expected: unknown) => {
}
return true;
};
export const diff: typeof jestDiff = (a, b, options) =>
export const diff = (a: any, b: any, options?: DiffOptions): string | null =>
shouldPrintDiff(a, b) ? jestDiff(a, b, options) : null;
export const pluralize = (word: string, count: number) =>
@@ -1,36 +0,0 @@
/**
* 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, DIFF_EQUAL, DIFF_DELETE, DIFF_INSERT} from './cleanupSemantic';
import {INVERTED_COLOR} from './index';
export const getDiffString = (diffs: Array<Diff>, op: number): string => {
const hasEqual = diffs.some(
diff => diff[0] === DIFF_EQUAL && diff[1].length !== 0,
);
return diffs.reduce(
(reduced: string, diff: Diff): string =>
reduced +
(diff[0] === DIFF_EQUAL
? diff[1]
: diff[0] !== op
? ''
: hasEqual
? INVERTED_COLOR(diff[1])
: diff[1]),
'',
);
};
export const getExpectedString = (diffs: Array<Diff>): string =>
getDiffString(diffs, DIFF_DELETE);
export const getReceivedString = (diffs: Array<Diff>): string =>
getDiffString(diffs, DIFF_INSERT);
export const MULTILINE_REGEXP = /\n/;
@@ -5,7 +5,6 @@
"outDir": "build"
},
"references": [
{"path": "../diff-sequences"},
{"path": "../jest-diff"},
{"path": "../jest-get-type"},
{"path": "../pretty-format"}
+1 -1
View File
@@ -101,7 +101,7 @@ const CUSTOM_IGNORED_PATTERNS = [
'^flow-typed/.*',
'^packages/expect/src/jasmineUtils\\.ts$',
'^packages/jest-config/src/vendor/jsonlint\\.js$',
'^packages/jest-matcher-utils/src/cleanupSemantic\\.ts$',
'^packages/jest-diff/src/cleanupSemantic\\.ts$',
].map(createRegExp);
const IGNORED_PATTERNS = [