mirror of
https://github.com/hanzoai/jest.git
synced 2026-09-24 16:59:57 +00:00
expect: Highlight substring differences when matcher fails, part 2 (#8528)
This commit is contained in:
committed by
Simen Bekkhus
parent
6da622fb31
commit
00df15ea32
+1
-1
@@ -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
@@ -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
|
||||
|
||||
@@ -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>()</>
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
@@ -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))
|
||||
);
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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,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;
|
||||
|
||||
@@ -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');
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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"
|
||||
|
||||
+29
@@ -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;
|
||||
@@ -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"}
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
Reference in New Issue
Block a user