From 57706fa0ed723a037f72057af48078c417b63d14 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Mon, 1 May 2017 15:36:40 -0400 Subject: [PATCH 01/14] Ignore indentation in jest-diff --- packages/jest-diff/src/__tests__/diff-test.js | 232 ++++++++++++++++-- packages/jest-diff/src/diffStrings.js | 101 +++++++- 2 files changed, 316 insertions(+), 17 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff-test.js b/packages/jest-diff/src/__tests__/diff-test.js index 2ded42f88849..dd4b1d7cf274 100644 --- a/packages/jest-diff/src/__tests__/diff-test.js +++ b/packages/jest-diff/src/__tests__/diff-test.js @@ -81,8 +81,6 @@ describe('no visual difference', () => { test('oneline strings', () => { // oneline strings don't produce a diff currently. expect(stripAnsi(diff('ab', 'aa'))).toBe(null); - expect(diff('a', 'a')).toMatch(/no visual difference/); - expect(stripAnsi(diff('123456789', '234567890'))).toBe(null); }); test('falls back to not call toJSON if objects look identical', () => { @@ -129,6 +127,23 @@ test('booleans', () => { expect(result).toBe(null); }); +test('collapses big diffs to patch format', () => { + const result = diff( + {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, + {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, + {expand: false}, + ); + + expect(result).toMatchSnapshot(); +}); + +// Some of the following assertions seem complex, but compare to alternatives: +// * toMatch instead of toMatchSnapshot: +// * to avoid visual complexity of escaped quotes in expected string +// * to omit Expected/Received heading which is an irrelevant detail +// * join lines of expected string instead of multiline string: +// * to avoid ambiguity about indentation in diff lines + test('React elements', () => { const result = diff( { @@ -148,19 +163,208 @@ test('React elements', () => { type: 'div', }, ); - expect(stripAnsi(result)).toMatch(//); - expect(stripAnsi(result)).toMatch(/\-\s+Hello/); - expect(stripAnsi(result)).toMatch(/\+\s+Goodbye/); -}); -test('collapses big diffs to patch format', () => { - const result = diff( - {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, - {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, - {expand: false}, + expect(stripAnsi(result)).toMatch( + [ + '- ', + '- Hello', + '+
', + '+ Goodbye', + '
', + ].join('\n'), ); +}); - expect(result).toMatchSnapshot(); +describe('does ignore indentation in JavaScript structures', () => { + const less = { + searching: '', + sorting: { + descending: false, + fieldKey: 'what', + }, + }; + const more = { + searching: '', + sorting: [ + { + descending: false, + fieldKey: 'what', + }, + ], + }; + + test('from less to more', () => { + // Replace unchanged chunk in the middle with received lines, + // which are more indented. + expect(stripAnsi(diff(less, more))).toMatch( + [ + ' Object {', + ' "searching": "",', + '- "sorting": Object {', + '+ "sorting": Array [', + '+ Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '+ ],', + ' }', + ].join('\n'), + ); + }); + + test('from more to less', () => { + // Replace unchanged chunk in the middle with received lines, + // which are less indented. + expect(stripAnsi(diff(more, less))).toMatch( + [ + ' Object {', + ' "searching": "",', + '- "sorting": Array [', + '- Object {', + '+ "sorting": Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '- ],', + ' }', + ].join('\n'), + ); + }); +}); + +describe('multiline string as property of JavaScript object', () => { + // Without indentation is safest in multiline strings: + const expectedWithout = { + id: 'J', + points: `0.5,0.460 +0.25,0.875`, + }; + const receivedWithout = { + id: 'J', + points: `0.5,0.460 +0.5,0.875 +0.25,0.875`, + }; + + // With indentation is confusing, as this test demonstrates :( + // What looks like one indent level under points is really two levels, + // because the test itself is indented one level! + const expectedWith = { + id: 'J', + points: `0.5,0.460 + 0.25,0.875`, + }; + const receivedWith = { + id: 'J', + points: `0.5,0.460 + 0.5,0.875 + 0.25,0.875`, + }; + + test('without indentation', () => { + expect(stripAnsi(diff(expectedWithout, receivedWithout))).toMatch( + [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', + ].join('\n'), + ); + }); + + test('with indentation', () => { + expect(stripAnsi(diff(expectedWith, receivedWith))).toMatch( + [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', + ].join('\n'), + ); + }); + + test('without to with indentation', () => { + // Don’t ignore changes to indentation in a multiline string. + expect(stripAnsi(diff(expectedWithout, receivedWith))).toMatch( + [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '- 0.25,0.875",', + '+ 0.5,0.875', + '+ 0.25,0.875",', + ' }', + ].join('\n'), + ); + }); +}); + +describe('does ignore indentation in React elements', () => { + const leaf = { + $$typeof: Symbol.for('react.element'), + props: { + children: ['text'], + }, + type: 'span', + }; + const less = { + $$typeof: Symbol.for('react.element'), + props: { + children: [leaf], + }, + type: 'span', + }; + const more = { + $$typeof: Symbol.for('react.element'), + props: { + children: [ + { + $$typeof: Symbol.for('react.element'), + props: { + children: [leaf], + }, + type: 'strong', + }, + ], + }, + type: 'span', + }; + + test('from less to more', () => { + // Replace unchanged chunk in the middle with received lines, + // which are more indented. + expect(stripAnsi(diff(less, more))).toMatch( + [ + ' ', + '+ ', + ' ', + ' text', + ' ', + '+ ', + ' ', + ].join('\n'), + ); + }); + + test('from more to less', () => { + // Replace unchanged chunk in the middle with received lines, + // which are less indented. + expect(stripAnsi(diff(more, less))).toMatch( + [ + ' ', + '- ', + ' ', + ' text', + ' ', + '- ', + ' ', + ].join('\n'), + ); + }); }); diff --git a/packages/jest-diff/src/diffStrings.js b/packages/jest-diff/src/diffStrings.js index 787d930eb680..e339234dd5ff 100644 --- a/packages/jest-diff/src/diffStrings.js +++ b/packages/jest-diff/src/diffStrings.js @@ -47,14 +47,109 @@ const getAnnotation = (options: ?DiffOptions): string => chalk.red('+ ' + ((options && options.bAnnotation) || 'Received')) + '\n\n'; +// Given string, return array of its lines including newline characters. +// Note: split-lines package doesn’t allow newline at end of last line :( +const regexpNewline = /\n/g; +function splitIntoLines(string: string): Array { + const lines = []; + + regexpNewline.lastIndex = 0; + let prevIndex = regexpNewline.lastIndex; + regexpNewline.exec(string); + + while (regexpNewline.lastIndex !== 0) { + lines.push(string.slice(prevIndex, regexpNewline.lastIndex)); + prevIndex = regexpNewline.lastIndex; + regexpNewline.exec(string); + } + + if (prevIndex < string.length || prevIndex === 0) { + lines.push(string.slice(prevIndex)); + } + + return lines; +} + +// Return whether line has an odd number of unescaped quotes. +function oddCountOfQuotes(line) { + let oddBackslashes = false; + let oddQuotes = false; + // eslint-disable-next-line prefer-const + for (let char of line) { + if (char === '\\') { + oddBackslashes = !oddBackslashes; + } else { + if (char === '"' && !oddBackslashes) { + oddQuotes = !oddQuotes; + } + oddBackslashes = false; + } + } + return oddQuotes; +} + +// Given array of lines, return lines without indentation, +// except in multiline strings. +const regexpIndentation = /^[ ]*/; +function unindentLines(lines) { + let inMultilineString = false; + + return lines.map(line => { + const oddCount = oddCountOfQuotes(line); + + if (inMultilineString) { + inMultilineString = !oddCount; + return line; + } + + inMultilineString = oddCount; + return line.replace(regexpIndentation, ''); + }); +} + +// Given expected and received after an assertion fails, +// return chunks from diff.diffLines with original indentation, +// but ignoring indentation except in multiline strings. +// diff.diffLines ignoreWhitespace option doesn’t work for 3 reasons: +// It ignores indentation in multiline strings. +// It ignores whitespace at the end of lines. +// It returns chunks without original indentation. +function diffUnindentedLines(a, b) { + const aLines = splitIntoLines(a); + const bLines = splitIntoLines(b); + const chunks = diff.diffLines( + unindentLines(aLines).join(''), + unindentLines(bLines).join(''), + ); + + let aIndex = 0; + let bIndex = 0; + chunks.forEach(chunk => { + const count = chunk.count; // number of lines in chunk + + // Replace unindented lines with original lines. + // Assume that a is expected and b is received. + if (chunk.removed) { + chunk.value = aLines.slice(aIndex, (aIndex += count)).join(''); + } else { + chunk.value = bLines.slice(bIndex, (bIndex += count)).join(''); + if (!chunk.added) { + aIndex += count; // increment both if chunk is unchanged + } + } + }); + + return chunks; +} + const diffLines = (a: string, b: string): Diff => { + const chunks = diffUnindentedLines(a, b); let isDifferent = false; return { - diff: diff - .diffLines(a, b) + diff: chunks .map(part => { const {added, removed} = part; - if (part.added || part.removed) { + if (added || removed) { isDifferent = true; } From 78c830882d7df02256b5a0e3d13d1599bfe65fa5 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 3 May 2017 12:31:07 -0400 Subject: [PATCH 02/14] Ignore indentation in structuredPatch and test it more --- packages/jest-diff/src/__tests__/diff-test.js | 415 +++++++++++------- packages/jest-diff/src/diffStrings.js | 50 ++- 2 files changed, 306 insertions(+), 159 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff-test.js b/packages/jest-diff/src/__tests__/diff-test.js index dd4b1d7cf274..e271e24ca20c 100644 --- a/packages/jest-diff/src/__tests__/diff-test.js +++ b/packages/jest-diff/src/__tests__/diff-test.js @@ -13,6 +13,17 @@ const diff = require('../'); const stripAnsi = require('strip-ansi'); +// diff has no space between mark and content when {expand: false} +// which is the default for Jest CLI but not for jest-diff +const optFalse = {expand: false}; +const unexpanded = lines => + lines.map(line => line[0] + line.slice(2)).join('\n'); + +// diff has a space between mark and content when {expand: true} +// which requires --expand object for Jest CLI but is default for jest-diff +const optTrue = {expand: true}; +const expanded = lines => lines.join('\n'); + const toJSON = function toJSON() { return 'apple'; }; @@ -95,26 +106,58 @@ test('prints a fallback message if two objects truly look identical', () => { expect(diff(a, b)).toMatchSnapshot(); }); -test('multiline strings', () => { - const result = diff( - `line 1 +// Some of the following assertions seem complex, but compare to alternatives: +// * toMatch instead of toMatchSnapshot: +// * to avoid visual complexity of escaped quotes in expected string +// * to omit Expected/Received heading which is an irrelevant detail +// * join lines of expected string instead of multiline string: +// * to avoid ambiguity about indentation in diff lines + +describe('multiline strings', () => { + const a = `line 1 line 2 line 3 -line 4`, - `line 1 +line 4`; + const b = `line 1 line 2 line 3 -line 4`, - ); - - expect(stripAnsi(result)).toMatch(/\- line 2/); - expect(stripAnsi(result)).toMatch(/\+ line {2}2/); +line 4`; + const expected = [ + ' line 1', + '- line 2', + '+ line 2', + ' line 3', + ' line 4', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); }); -test('objects', () => { - const result = stripAnsi(diff({a: {b: {c: 5}}}, {a: {b: {c: 6}}})); - expect(result).toMatch(/\-\s+\"c\"\: 5/); - expect(result).toMatch(/\+\s+\"c\"\: 6/); +describe('objects', () => { + const a = {a: {b: {c: 5}}}; + const b = {a: {b: {c: 6}}}; + const expected = [ + ' Object {', + ' "a": Object {', + ' "b": Object {', + '- "c": 5,', + '+ "c": 6,', + ' },', + ' },', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); }); test('numbers', () => { @@ -127,65 +170,96 @@ test('booleans', () => { expect(result).toBe(null); }); +describe('multiline string snapshot', () => { + // For example, CLI output + // Like a snapshot test of a string which has double quotes. + const a = ` +" +Options: +--help, -h Show help [boolean] +--bail, -b Exit the test suite immediately upon the first + failing test. [boolean]" +`; + const b = ` +" +Options: + --help, -h Show help [boolean] + --bail, -b Exit the test suite immediately upon the first + failing test. [boolean]" +`; + const expected = [ + ' "', + ' Options:', + '- --help, -h Show help [boolean]', + '- --bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]"', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]"', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); +}); + +describe('React elements', () => { + const a = { + $$typeof: Symbol.for('react.element'), + props: { + children: 'Hello', + className: 'fun', + }, + type: 'div', + }; + const b = { + $$typeof: Symbol.for('react.element'), + className: 'fun', + props: { + children: 'Goodbye', + }, + type: 'div', + }; + const expected = [ + '- ', + '- Hello', + '+
', + '+ Goodbye', + '
', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); +}); + test('collapses big diffs to patch format', () => { const result = diff( {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, - {expand: false}, + optFalse, ); expect(result).toMatchSnapshot(); }); -// Some of the following assertions seem complex, but compare to alternatives: -// * toMatch instead of toMatchSnapshot: -// * to avoid visual complexity of escaped quotes in expected string -// * to omit Expected/Received heading which is an irrelevant detail -// * join lines of expected string instead of multiline string: -// * to avoid ambiguity about indentation in diff lines - -test('React elements', () => { - const result = diff( - { - $$typeof: Symbol.for('react.element'), - props: { - children: 'Hello', - className: 'fun', - }, - type: 'div', - }, - { - $$typeof: Symbol.for('react.element'), - className: 'fun', - props: { - children: 'Goodbye', - }, - type: 'div', - }, - ); - - expect(stripAnsi(result)).toMatch( - [ - '- ', - '- Hello', - '+
', - '+ Goodbye', - '
', - ].join('\n'), - ); -}); - describe('does ignore indentation in JavaScript structures', () => { - const less = { + const a = { searching: '', sorting: { descending: false, fieldKey: 'what', }, }; - const more = { + const b = { searching: '', sorting: [ { @@ -195,113 +269,138 @@ describe('does ignore indentation in JavaScript structures', () => { ], }; - test('from less to more', () => { + describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, // which are more indented. - expect(stripAnsi(diff(less, more))).toMatch( - [ - ' Object {', - ' "searching": "",', - '- "sorting": Object {', - '+ "sorting": Array [', - '+ Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '+ ],', - ' }', - ].join('\n'), - ); + const expected = [ + ' Object {', + ' "searching": "",', + '- "sorting": Object {', + '+ "sorting": Array [', + '+ Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '+ ],', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); }); - test('from more to less', () => { + describe('from more to less', () => { // Replace unchanged chunk in the middle with received lines, // which are less indented. - expect(stripAnsi(diff(more, less))).toMatch( - [ - ' Object {', - ' "searching": "",', - '- "sorting": Array [', - '- Object {', - '+ "sorting": Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '- ],', - ' }', - ].join('\n'), - ); + const expected = [ + ' Object {', + ' "searching": "",', + '- "sorting": Array [', + '- Object {', + '+ "sorting": Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '- ],', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(b, a, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(b, a, optTrue))).toMatch(expanded(expected)); + }); }); }); describe('multiline string as property of JavaScript object', () => { - // Without indentation is safest in multiline strings: - const expectedWithout = { + // Unindented is safest in multiline strings: + const a = { id: 'J', points: `0.5,0.460 0.25,0.875`, }; - const receivedWithout = { + const b = { id: 'J', points: `0.5,0.460 0.5,0.875 0.25,0.875`, }; - // With indentation is confusing, as this test demonstrates :( + // Indented is confusing, as this test demonstrates :( // What looks like one indent level under points is really two levels, // because the test itself is indented one level! - const expectedWith = { + const aIn = { id: 'J', points: `0.5,0.460 0.25,0.875`, }; - const receivedWith = { + const bIn = { id: 'J', points: `0.5,0.460 0.5,0.875 0.25,0.875`, }; - test('without indentation', () => { - expect(stripAnsi(diff(expectedWithout, receivedWithout))).toMatch( - [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+ 0.5,0.875', - ' 0.25,0.875",', - ' }', - ].join('\n'), - ); + describe('unindented', () => { + const expected = [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); }); - test('with indentation', () => { - expect(stripAnsi(diff(expectedWith, receivedWith))).toMatch( - [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+ 0.5,0.875', - ' 0.25,0.875",', - ' }', - ].join('\n'), - ); + describe('indented', () => { + const expected = [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(aIn, bIn, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(aIn, bIn, optTrue))).toMatch(expanded(expected)); + }); }); - test('without to with indentation', () => { + describe('unindented to indented', () => { // Don’t ignore changes to indentation in a multiline string. - expect(stripAnsi(diff(expectedWithout, receivedWith))).toMatch( - [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '- 0.25,0.875",', - '+ 0.5,0.875', - '+ 0.25,0.875",', - ' }', - ].join('\n'), - ); + const expected = [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '- 0.25,0.875",', + '+ 0.5,0.875', + '+ 0.25,0.875",', + ' }', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, bIn, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, bIn, optTrue))).toMatch(expanded(expected)); + }); }); }); @@ -313,14 +412,14 @@ describe('does ignore indentation in React elements', () => { }, type: 'span', }; - const less = { + const a = { $$typeof: Symbol.for('react.element'), props: { children: [leaf], }, type: 'span', }; - const more = { + const b = { $$typeof: Symbol.for('react.element'), props: { children: [ @@ -336,35 +435,45 @@ describe('does ignore indentation in React elements', () => { type: 'span', }; - test('from less to more', () => { + describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, // which are more indented. - expect(stripAnsi(diff(less, more))).toMatch( - [ - ' ', - '+ ', - ' ', - ' text', - ' ', - '+ ', - ' ', - ].join('\n'), - ); + const expected = [ + ' ', + '+ ', + ' ', + ' text', + ' ', + '+ ', + ' ', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); }); - test('from more to less', () => { + describe('from more to less', () => { // Replace unchanged chunk in the middle with received lines, // which are less indented. - expect(stripAnsi(diff(more, less))).toMatch( - [ - ' ', - '- ', - ' ', - ' text', - ' ', - '- ', - ' ', - ].join('\n'), - ); + const expected = [ + ' ', + '- ', + ' ', + ' text', + ' ', + '- ', + ' ', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(b, a, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(b, a, optTrue))).toMatch(expanded(expected)); + }); }); }); diff --git a/packages/jest-diff/src/diffStrings.js b/packages/jest-diff/src/diffStrings.js index e339234dd5ff..864189bb4212 100644 --- a/packages/jest-diff/src/diffStrings.js +++ b/packages/jest-diff/src/diffStrings.js @@ -114,7 +114,7 @@ function unindentLines(lines) { // It ignores indentation in multiline strings. // It ignores whitespace at the end of lines. // It returns chunks without original indentation. -function diffUnindentedLines(a, b) { +function diffChunksUnindented(a, b) { const aLines = splitIntoLines(a); const bLines = splitIntoLines(b); const chunks = diff.diffLines( @@ -143,7 +143,7 @@ function diffUnindentedLines(a, b) { } const diffLines = (a: string, b: string): Diff => { - const chunks = diffUnindentedLines(a, b); + const chunks = diffChunksUnindented(a, b); let isDifferent = false; return { diff: chunks @@ -164,7 +164,7 @@ const diffLines = (a: string, b: string): Diff => { return lines .map(line => { const highlightedLine = highlightTrailingWhitespace(line, bgColor); - const mark = color(part.added ? '+' : part.removed ? '-' : ' '); + const mark = color(added ? '+' : removed ? '-' : ' '); return mark + ' ' + color(highlightedLine) + '\n'; }) .join(''); @@ -189,6 +189,44 @@ const createPatchMark = (hunk: Hunk): string => { return chalk.yellow(`@@ ${markOld} ${markNew} @@\n`); }; +// Given expected and received after an assertion fails, +// return hunks from diff.structuredPatch with original indentation, +// but ignoring indentation except in multiline strings. +function diffHunksUnindented(a, b, options) { + const aLines = splitIntoLines(a); + const bLines = splitIntoLines(b); + const hunks = diff.structuredPatch( + '', + '', + unindentLines(aLines).join(''), + unindentLines(bLines).join(''), + '', + '', + options, + ).hunks; + + hunks.forEach(hunk => { + // hunk has one-based line numbers + let aIndex = hunk.oldStart - 1; + let bIndex = hunk.newStart - 1; + + // Replace unindented lines with original lines. + // Assume that a is expected and b is received. + hunk.lines = hunk.lines.map(line => { + const mark = line[0]; + if (mark === ' ') { + aIndex += 1; // increment both if line is unchanged + } + return ( + mark + + (mark === '-' ? aLines[aIndex++] : bLines[bIndex++]).replace('\n', '') + ); + }); + }); + + return hunks; +} + const structuredPatch = (a: string, b: string): Diff => { const options = {context: DIFF_CONTEXT}; let isDifferent = false; @@ -201,11 +239,11 @@ const structuredPatch = (a: string, b: string): Diff => { } const oldLinesCount = (a.match(/\n/g) || []).length; + const hunks = diffHunksUnindented(a, b, options); return { - diff: diff - .structuredPatch('', '', a, b, '', '', options) - .hunks.map((hunk: Hunk) => { + diff: hunks + .map((hunk: Hunk) => { const lines = hunk.lines .map(line => { const added = line[0] === '+'; From b1980c0f58af45663183212516a307061c45eb6f Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 3 May 2017 16:28:57 -0400 Subject: [PATCH 03/14] Fix regression for multiline string from toBe or toEqual --- packages/jest-diff/src/__tests__/diff-test.js | 35 ++++++++++++++++++- packages/jest-diff/src/diffStrings.js | 18 ++++++---- packages/jest-diff/src/index.js | 6 +++- 3 files changed, 50 insertions(+), 9 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff-test.js b/packages/jest-diff/src/__tests__/diff-test.js index e271e24ca20c..88977d8ed131 100644 --- a/packages/jest-diff/src/__tests__/diff-test.js +++ b/packages/jest-diff/src/__tests__/diff-test.js @@ -170,9 +170,42 @@ test('booleans', () => { expect(result).toBe(null); }); +describe('multiline string non-snapshot', () => { + // For example, CLI output + // toBe or toEqual for a string isn’t enclosed in double quotes. + const a = ` +Options: +--help, -h Show help [boolean] +--bail, -b Exit the test suite immediately upon the first + failing test. [boolean] +`; + const b = ` +Options: + --help, -h Show help [boolean] + --bail, -b Exit the test suite immediately upon the first + failing test. [boolean] +`; + const expected = [ + ' Options:', + '- --help, -h Show help [boolean]', + '- --bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]', + ]; + + test('expand: false', () => { + expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + }); + test('expand: true', () => { + expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + }); +}); + describe('multiline string snapshot', () => { // For example, CLI output - // Like a snapshot test of a string which has double quotes. + // A snapshot of a string is enclosed in double quotes. const a = ` " Options: diff --git a/packages/jest-diff/src/diffStrings.js b/packages/jest-diff/src/diffStrings.js index 8480dd1f7160..ce9cf8030e34 100644 --- a/packages/jest-diff/src/diffStrings.js +++ b/packages/jest-diff/src/diffStrings.js @@ -142,8 +142,10 @@ function diffChunksUnindented(a, b) { return chunks; } -const diffLines = (a: string, b: string): Diff => { - const chunks = diffChunksUnindented(a, b); +const diffLines = (a: string, b: string, multilineString?: boolean): Diff => { + const chunks = multilineString + ? diff.diffLines(a, b) + : diffChunksUnindented(a, b); let isDifferent = false; return { diff: chunks @@ -227,7 +229,7 @@ function diffHunksUnindented(a, b, options) { return hunks; } -const structuredPatch = (a: string, b: string): Diff => { +const structuredPatch = (a: string, b: string, multilineString?: boolean): Diff => { const options = {context: DIFF_CONTEXT}; let isDifferent = false; // Make sure the strings end with a newline. @@ -239,7 +241,9 @@ const structuredPatch = (a: string, b: string): Diff => { } const oldLinesCount = (a.match(/\n/g) || []).length; - const hunks = diffHunksUnindented(a, b, options); + const hunks = multilineString + ? diff.structuredPatch('', '', a, b, '', '', options).hunks + : diffHunksUnindented(a, b, options); return { diff: hunks @@ -268,14 +272,14 @@ const structuredPatch = (a: string, b: string): Diff => { }; }; -function diffStrings(a: string, b: string, options: ?DiffOptions): string { +function diffStrings(a: string, b: string, options: ?DiffOptions, multilineString?: boolean): string { // `diff` uses the Myers LCS diff algorithm which runs in O(n+d^2) time // (where "d" is the edit distance) and can get very slow for large edit // distances. Mitigate the cost by switching to a lower-resolution diff // whenever linebreaks are involved. const result = options && options.expand === false - ? structuredPatch(a, b) - : diffLines(a, b); + ? structuredPatch(a, b, multilineString) + : diffLines(a, b, multilineString); if (result.isDifferent) { return getAnnotation(options) + result.diff; diff --git a/packages/jest-diff/src/index.js b/packages/jest-diff/src/index.js index 73a957f65fb3..ee51e5fb9cbc 100644 --- a/packages/jest-diff/src/index.js +++ b/packages/jest-diff/src/index.js @@ -83,7 +83,11 @@ function diff(a: any, b: any, options: ?DiffOptions): ?string { case 'string': const multiline = a.match(/[\r\n]/) !== -1 && b.indexOf('\n') !== -1; if (multiline) { - return diffStrings(String(a), String(b), options); + // Need explicit arg not to ignore indentation for multiline string + // from toBe or toEqual assertion (which isn’t enclosed in quotes) + // unlike a snapshot (which is enclosed in quotes). + const multilineString = !options || options.aAnnotation !== 'Snapshot'; + return diffStrings(a, b, options, multilineString); } return null; case 'number': From 18063a3227b20467a482ada96904cad0105cacae Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Mon, 8 May 2017 11:39:17 -0400 Subject: [PATCH 04/14] Add snapshot option and make spacing consistent for --expand --- .../__snapshots__/failures-test.js.snap | 34 +- .../__tests__/__snapshots__/diff-test.js.snap | 25 +- packages/jest-diff/src/__tests__/diff-test.js | 491 +++++++++++------- packages/jest-diff/src/diffStrings.js | 162 ++---- packages/jest-diff/src/index.js | 70 ++- packages/jest-snapshot/src/index.js | 1 + 6 files changed, 465 insertions(+), 318 deletions(-) diff --git a/integration_tests/__tests__/__snapshots__/failures-test.js.snap b/integration_tests/__tests__/__snapshots__/failures-test.js.snap index 6f61df20fc78..f802fcb4035a 100644 --- a/integration_tests/__tests__/__snapshots__/failures-test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures-test.js.snap @@ -205,13 +205,13 @@ Object { + Received Object { - \\"a\\": Object { - \\"b\\": Object { - - \\"c\\": 6, - + \\"c\\": 5, - }, - }, - } + \\"a\\": Object { + \\"b\\": Object { + - \\"c\\": 6, + + \\"c\\": 5, + }, + }, + } at Object. (__tests__/node-assertion-error-test.js:40:10) @@ -233,13 +233,13 @@ Object { + Received Object { - \\"a\\": Object { - \\"b\\": Object { - - \\"c\\": 7, - + \\"c\\": 5, - }, - }, - } + \\"a\\": Object { + \\"b\\": Object { + - \\"c\\": 7, + + \\"c\\": 5, + }, + }, + } at Object. (__tests__/node-assertion-error-test.js:44:10) @@ -302,9 +302,9 @@ Object { + Received Object { - - \\"a\\": 2, - + \\"a\\": 1, - } + - \\"a\\": 2, + + \\"a\\": 1, + } at Object. (__tests__/node-assertion-error-test.js:60:10) diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff-test.js.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff-test.js.snap index e01e56d8cf13..f9046cb8f2e3 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff-test.js.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff-test.js.snap @@ -17,6 +17,21 @@ exports[`collapses big diffs to patch format 1`] = ` }" `; +exports[`falls back to not call toJSON 1`] = ` +"Compared values serialize to the same structure. +Printing internal object structure without calling \`toJSON\` instead. + +- Expected ++ Received + + Object { ++ \\"key\\": Object { + \\"line\\": 2, ++ }, + \\"toJSON\\": [Function toJSON], + }" +`; + exports[`falls back to not call toJSON if objects look identical 1`] = ` "Compared values serialize to the same structure. Printing internal object structure without calling \`toJSON\` instead. @@ -24,11 +39,11 @@ Printing internal object structure without calling \`toJSON\` instead. - Expected + Received - Object { -- \\"line\\": 1, -+ \\"line\\": 2, - \\"toJSON\\": [Function toJSON], - }" + Object { +- \\"line\\": 1, ++ \\"line\\": 2, + \\"toJSON\\": [Function toJSON], + }" `; exports[`prints a fallback message if two objects truly look identical 1`] = `"Compared values have no visual difference."`; diff --git a/packages/jest-diff/src/__tests__/diff-test.js b/packages/jest-diff/src/__tests__/diff-test.js index 88977d8ed131..1ef565668bcf 100644 --- a/packages/jest-diff/src/__tests__/diff-test.js +++ b/packages/jest-diff/src/__tests__/diff-test.js @@ -13,16 +13,15 @@ const diff = require('../'); const stripAnsi = require('strip-ansi'); -// diff has no space between mark and content when {expand: false} -// which is the default for Jest CLI but not for jest-diff -const optFalse = {expand: false}; -const unexpanded = lines => - lines.map(line => line[0] + line.slice(2)).join('\n'); +function received(a, b, options, replacement) { + return stripAnsi(diff(a, b, options, replacement)); +} -// diff has a space between mark and content when {expand: true} -// which requires --expand object for Jest CLI but is default for jest-diff -const optTrue = {expand: true}; -const expanded = lines => lines.join('\n'); +const snapshot = {snapshot: true}; +const unexpanded = {expand: false}; +const unexpandedSnap = Object.assign({}, unexpanded, snapshot); +const expanded = {expand: true}; +const expandedSnap = Object.assign({}, expanded, snapshot); const toJSON = function toJSON() { return 'apple'; @@ -42,7 +41,7 @@ describe('different types', () => { const typeB = values[3]; test(`'${a}' and '${b}'`, () => { - expect(stripAnsi(diff(a, b))).toBe( + expect(received(a, b)).toBe( ' Comparing two different types of values. ' + `Expected ${typeA} but received ${typeB}.`, ); @@ -64,7 +63,7 @@ describe('no visual difference', () => { [{a: {b: 5}}, {a: {b: 5}}], ].forEach(values => { test(`'${JSON.stringify(values[0])}' and '${JSON.stringify(values[1])}'`, () => { - expect(stripAnsi(diff(values[0], values[1]))).toBe( + expect(received(values[0], values[1])).toBe( 'Compared values have no visual difference.', ); }); @@ -74,7 +73,7 @@ describe('no visual difference', () => { const arg1 = new Map([[1, 'foo'], [2, 'bar']]); const arg2 = new Map([[2, 'bar'], [1, 'foo']]); - expect(stripAnsi(diff(arg1, arg2))).toBe( + expect(received(arg1, arg2)).toBe( 'Compared values have no visual difference.', ); }); @@ -83,7 +82,7 @@ describe('no visual difference', () => { const arg1 = new Set([1, 2]); const arg2 = new Set([2, 1]); - expect(stripAnsi(diff(arg1, arg2))).toBe( + expect(received(arg1, arg2)).toBe( 'Compared values have no visual difference.', ); }); @@ -91,7 +90,7 @@ describe('no visual difference', () => { test('oneline strings', () => { // oneline strings don't produce a diff currently. - expect(stripAnsi(diff('ab', 'aa'))).toBe(null); + expect(received('ab', 'aa')).toBe(null); }); test('falls back to not call toJSON if objects look identical', () => { @@ -113,6 +112,27 @@ test('prints a fallback message if two objects truly look identical', () => { // * join lines of expected string instead of multiline string: // * to avoid ambiguity about indentation in diff lines +test('falls back to not call toJSON', () => { + const a = {line: 2, toJSON}; + const b = {key: {line: 2}, toJSON}; + expect(diff(a, b)).toMatchSnapshot(); + const expected = [ + ' Object {', + '- "key": Object {', + ' "line": 2,', + '- },', + ' "toJSON": [Function toJSON],', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); + }); +}); + describe('multiline strings', () => { const a = `line 1 line 2 @@ -123,18 +143,18 @@ line 2 line 3 line 4`; const expected = [ - ' line 1', - '- line 2', - '+ line 2', - ' line 3', - ' line 4', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' line 1', + '-line 2', + '+line 2', + ' line 3', + ' line 4', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); }); }); @@ -142,21 +162,21 @@ describe('objects', () => { const a = {a: {b: {c: 5}}}; const b = {a: {b: {c: 6}}}; const expected = [ - ' Object {', - ' "a": Object {', - ' "b": Object {', - '- "c": 5,', - '+ "c": 6,', - ' },', - ' },', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "a": Object {', + ' "b": Object {', + '- "c": 5,', + '+ "c": 6,', + ' },', + ' },', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); }); }); @@ -186,20 +206,20 @@ Options: failing test. [boolean] `; const expected = [ - ' Options:', - '- --help, -h Show help [boolean]', - '- --bail, -b Exit the test suite immediately upon the first', - '- failing test. [boolean]', - '+ --help, -h Show help [boolean]', - '+ --bail, -b Exit the test suite immediately upon the first', - '+ failing test. [boolean]', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' Options:', + '---help, -h Show help [boolean]', + '---bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); }); }); @@ -221,21 +241,21 @@ Options: failing test. [boolean]" `; const expected = [ - ' "', - ' Options:', - '- --help, -h Show help [boolean]', - '- --bail, -b Exit the test suite immediately upon the first', - '- failing test. [boolean]"', - '+ --help, -h Show help [boolean]', - '+ --bail, -b Exit the test suite immediately upon the first', - '+ failing test. [boolean]"', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' "', + ' Options:', + '---help, -h Show help [boolean]', + '---bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]"', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]"', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpandedSnap)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(a, b, expandedSnap)).toMatch(expected); }); }); @@ -250,27 +270,27 @@ describe('React elements', () => { }; const b = { $$typeof: Symbol.for('react.element'), - className: 'fun', + className: 'fun', // ignored by serializer props: { children: 'Goodbye', }, type: 'div', }; const expected = [ - '- ', - '- Hello', - '+
', - '+ Goodbye', - '
', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + '-', + '- Hello', + '+
', + '+ Goodbye', + '
', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); }); }); @@ -278,7 +298,7 @@ test('collapses big diffs to patch format', () => { const result = diff( {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, - optFalse, + unexpanded, ); expect(result).toMatchSnapshot(); @@ -301,28 +321,55 @@ describe('does ignore indentation in JavaScript structures', () => { }, ], }; + const aSnap = [ + 'Object {', + ' "searching": "",', + ' "sorting": Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '}', + ].join('\n'); + const bSnap = [ + 'Object {', + ' "searching": "",', + ' "sorting": Array [', + ' Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + ' ],', + '}', + ].join('\n'); describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, // which are more indented. const expected = [ - ' Object {', - ' "searching": "",', - '- "sorting": Object {', - '+ "sorting": Array [', - '+ Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '+ ],', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "searching": "",', + '- "sorting": Object {', + '+ "sorting": Array [', + '+ Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '+ ],', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(aSnap, bSnap, unexpandedSnap)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded snapshot', () => { + expect(received(aSnap, bSnap, expandedSnap)).toMatch(expected); }); }); @@ -330,40 +377,62 @@ describe('does ignore indentation in JavaScript structures', () => { // Replace unchanged chunk in the middle with received lines, // which are less indented. const expected = [ - ' Object {', - ' "searching": "",', - '- "sorting": Array [', - '- Object {', - '+ "sorting": Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '- ],', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(b, a, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "searching": "",', + '- "sorting": Array [', + '- Object {', + '+ "sorting": Object {', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '- ],', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(b, a, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(b, a, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(b, a, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(bSnap, aSnap, unexpandedSnap)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(bSnap, aSnap, expandedSnap)).toMatch(expected); }); }); }); describe('multiline string as property of JavaScript object', () => { // Unindented is safest in multiline strings: - const a = { + const aUn = { id: 'J', points: `0.5,0.460 0.25,0.875`, }; - const b = { + const bUn = { id: 'J', points: `0.5,0.460 0.5,0.875 0.25,0.875`, }; + const aUnSnap = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '0.25,0.875",', + '}', + ].join('\n'); + const bUnSnap = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '0.5,0.875', + '0.25,0.875",', + '}', + ].join('\n'); // Indented is confusing, as this test demonstrates :( // What looks like one indent level under points is really two levels, @@ -379,60 +448,96 @@ describe('multiline string as property of JavaScript object', () => { 0.5,0.875 0.25,0.875`, }; + const aInSnap = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + ' 0.25,0.875",', + '}', + ].join('\n'); + const bInSnap = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + ' 0.5,0.875', + ' 0.25,0.875",', + '}', + ].join('\n'); describe('unindented', () => { const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+ 0.5,0.875', - ' 0.25,0.875",', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+0.5,0.875', + ' 0.25,0.875",', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(aUn, bUn, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(aUn, bUn, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(aUnSnap, bUnSnap, unexpandedSnap)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded snapshot', () => { + expect(received(aUnSnap, bUnSnap, expandedSnap)).toMatch(expected); }); }); describe('indented', () => { const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+ 0.5,0.875', - ' 0.25,0.875",', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(aIn, bIn, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(aIn, bIn, unexpanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(aIn, bIn, optTrue))).toMatch(expanded(expected)); + test('expanded', () => { + expect(received(aIn, bIn, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(aInSnap, bInSnap, unexpandedSnap)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(aInSnap, bInSnap, expandedSnap)).toMatch(expected); }); }); describe('unindented to indented', () => { // Don’t ignore changes to indentation in a multiline string. const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '- 0.25,0.875",', - '+ 0.5,0.875', - '+ 0.25,0.875",', - ' }', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, bIn, optFalse))).toMatch(unexpanded(expected)); + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '-0.25,0.875",', + '+ 0.5,0.875', + '+ 0.25,0.875",', + ' }', + ].join('\n'); + + test('unexpanded', () => { + expect(received(aUn, bIn, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(aUn, bIn, expanded)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, bIn, optTrue))).toMatch(expanded(expected)); + + test('unexpanded snapshot', () => { + expect(received(aUnSnap, bInSnap, unexpandedSnap)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(aUnSnap, bInSnap, expandedSnap)).toMatch(expected); }); }); }); @@ -467,25 +572,48 @@ describe('does ignore indentation in React elements', () => { }, type: 'span', }; + const aSnap = [ + '', + ' ', + ' text', + ' ', + '', + ].join('\n'); + const bSnap = [ + '', + ' ', + ' ', + ' text', + ' ', + ' ', + '', + ].join('\n'); describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, // which are more indented. const expected = [ - ' ', - '+ ', - ' ', - ' text', - ' ', - '+ ', - ' ', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(a, b, optFalse))).toMatch(unexpanded(expected)); + ' ', + '+ ', + ' ', + ' text', + ' ', + '+ ', + ' ', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(aSnap, bSnap, unexpandedSnap)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(a, b, optTrue))).toMatch(expanded(expected)); + test('expanded snapshot', () => { + expect(received(aSnap, bSnap, expandedSnap)).toMatch(expected); }); }); @@ -493,20 +621,27 @@ describe('does ignore indentation in React elements', () => { // Replace unchanged chunk in the middle with received lines, // which are less indented. const expected = [ - ' ', - '- ', - ' ', - ' text', - ' ', - '- ', - ' ', - ]; - - test('expand: false', () => { - expect(stripAnsi(diff(b, a, optFalse))).toMatch(unexpanded(expected)); + ' ', + '- ', + ' ', + ' text', + ' ', + '- ', + ' ', + ].join('\n'); + + test('unexpanded', () => { + expect(received(b, a, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(b, a, expanded)).toMatch(expected); + }); + + test('unexpanded snapshot', () => { + expect(received(bSnap, aSnap, unexpandedSnap)).toMatch(expected); }); - test('expand: true', () => { - expect(stripAnsi(diff(b, a, optTrue))).toMatch(expanded(expected)); + test('expanded snapshot', () => { + expect(received(bSnap, aSnap, expandedSnap)).toMatch(expected); }); }); }); diff --git a/packages/jest-diff/src/diffStrings.js b/packages/jest-diff/src/diffStrings.js index ce9cf8030e34..c53dc22f7073 100644 --- a/packages/jest-diff/src/diffStrings.js +++ b/packages/jest-diff/src/diffStrings.js @@ -20,6 +20,12 @@ export type DiffOptions = {| aAnnotation?: string, bAnnotation?: string, expand?: boolean, + snapshot?: boolean, +|}; + +export type Replacement = {| + a: string, + b: string, |}; type Diff = {diff: string, isDifferent: boolean}; @@ -47,92 +53,38 @@ const getAnnotation = (options: ?DiffOptions): string => chalk.red('+ ' + ((options && options.bAnnotation) || 'Received')) + '\n\n'; -// Given string, return array of its lines including newline characters. -// Note: split-lines package doesn’t allow newline at end of last line :( -const regexpNewline = /\n/g; -function splitIntoLines(string: string): Array { - const lines = []; - - regexpNewline.lastIndex = 0; - let prevIndex = regexpNewline.lastIndex; - regexpNewline.exec(string); +// Given string, return array of its lines. +function splitIntoLines(string) { + const lines = string.split('\n'); - while (regexpNewline.lastIndex !== 0) { - lines.push(string.slice(prevIndex, regexpNewline.lastIndex)); - prevIndex = regexpNewline.lastIndex; - regexpNewline.exec(string); - } - - if (prevIndex < string.length || prevIndex === 0) { - lines.push(string.slice(prevIndex)); + if (lines.length !== 0 && lines[lines.length - 1] === '') { + lines.pop(); } return lines; } -// Return whether line has an odd number of unescaped quotes. -function oddCountOfQuotes(line) { - let oddBackslashes = false; - let oddQuotes = false; - // eslint-disable-next-line prefer-const - for (let char of line) { - if (char === '\\') { - oddBackslashes = !oddBackslashes; - } else { - if (char === '"' && !oddBackslashes) { - oddQuotes = !oddQuotes; - } - oddBackslashes = false; - } - } - return oddQuotes; -} - -// Given array of lines, return lines without indentation, -// except in multiline strings. -const regexpIndentation = /^[ ]*/; -function unindentLines(lines) { - let inMultilineString = false; - - return lines.map(line => { - const oddCount = oddCountOfQuotes(line); - - if (inMultilineString) { - inMultilineString = !oddCount; - return line; - } - - inMultilineString = oddCount; - return line.replace(regexpIndentation, ''); - }); -} - -// Given expected and received after an assertion fails, -// return chunks from diff.diffLines with original indentation, -// but ignoring indentation except in multiline strings. -// diff.diffLines ignoreWhitespace option doesn’t work for 3 reasons: -// It ignores indentation in multiline strings. -// It ignores whitespace at the end of lines. -// It returns chunks without original indentation. -function diffChunksUnindented(a, b) { - const aLines = splitIntoLines(a); - const bLines = splitIntoLines(b); - const chunks = diff.diffLines( - unindentLines(aLines).join(''), - unindentLines(bLines).join(''), - ); +// Replace unindented lines with original lines. +function replaceValueOfChunks(chunks, replacement) { + const aLines = splitIntoLines(replacement.a); + const bLines = splitIntoLines(replacement.b); let aIndex = 0; let bIndex = 0; chunks.forEach(chunk => { const count = chunk.count; // number of lines in chunk - // Replace unindented lines with original lines. // Assume that a is expected and b is received. if (chunk.removed) { - chunk.value = aLines.slice(aIndex, (aIndex += count)).join(''); + chunk.value = aLines + .slice(aIndex, (aIndex += count)) + .map(line => line + '\n') + .join(''); } else { - chunk.value = bLines.slice(bIndex, (bIndex += count)).join(''); + chunk.value = bLines + .slice(bIndex, (bIndex += count)) + .map(line => line + '\n') + .join(''); if (!chunk.added) { aIndex += count; // increment both if chunk is unchanged } @@ -142,32 +94,29 @@ function diffChunksUnindented(a, b) { return chunks; } -const diffLines = (a: string, b: string, multilineString?: boolean): Diff => { - const chunks = multilineString - ? diff.diffLines(a, b) - : diffChunksUnindented(a, b); +const diffLines = (a: string, b: string, replacement?: Replacement): Diff => { + let chunks = diff.diffLines(a, b); + if (replacement) { + chunks = replaceValueOfChunks(chunks, replacement); + } let isDifferent = false; return { diff: chunks .map(part => { - const {added, removed} = part; + const {added, removed, value} = part; if (added || removed) { isDifferent = true; } - const lines = part.value.split('\n'); + const lines = splitIntoLines(value); const color = getColor(added, removed); const bgColor = getBgColor(added, removed); - if (lines[lines.length - 1] === '') { - lines.pop(); - } - return lines .map(line => { const highlightedLine = highlightTrailingWhitespace(line, bgColor); const mark = color(added ? '+' : removed ? '-' : ' '); - return mark + ' ' + color(highlightedLine) + '\n'; + return mark + color(highlightedLine) + '\n'; }) .join(''); }) @@ -191,45 +140,34 @@ const createPatchMark = (hunk: Hunk): string => { return chalk.yellow(`@@ ${markOld} ${markNew} @@\n`); }; -// Given expected and received after an assertion fails, -// return hunks from diff.structuredPatch with original indentation, -// but ignoring indentation except in multiline strings. -function diffHunksUnindented(a, b, options) { - const aLines = splitIntoLines(a); - const bLines = splitIntoLines(b); - const hunks = diff.structuredPatch( - '', - '', - unindentLines(aLines).join(''), - unindentLines(bLines).join(''), - '', - '', - options, - ).hunks; +// Replace unindented lines with original lines. +function replaceLinesInHunks(hunks, replacement) { + const aLines = splitIntoLines(replacement.a); + const bLines = splitIntoLines(replacement.b); hunks.forEach(hunk => { // hunk has one-based line numbers let aIndex = hunk.oldStart - 1; let bIndex = hunk.newStart - 1; - // Replace unindented lines with original lines. // Assume that a is expected and b is received. hunk.lines = hunk.lines.map(line => { const mark = line[0]; if (mark === ' ') { aIndex += 1; // increment both if line is unchanged } - return ( - mark + - (mark === '-' ? aLines[aIndex++] : bLines[bIndex++]).replace('\n', '') - ); + return mark + (mark === '-' ? aLines[aIndex++] : bLines[bIndex++]); }); }); return hunks; } -const structuredPatch = (a: string, b: string, multilineString?: boolean): Diff => { +const structuredPatch = ( + a: string, + b: string, + replacement?: Replacement, +): Diff => { const options = {context: DIFF_CONTEXT}; let isDifferent = false; // Make sure the strings end with a newline. @@ -241,9 +179,10 @@ const structuredPatch = (a: string, b: string, multilineString?: boolean): Diff } const oldLinesCount = (a.match(/\n/g) || []).length; - const hunks = multilineString - ? diff.structuredPatch('', '', a, b, '', '', options).hunks - : diffHunksUnindented(a, b, options); + let hunks = diff.structuredPatch('', '', a, b, '', '', options).hunks; + if (replacement) { + hunks = replaceLinesInHunks(hunks, replacement); + } return { diff: hunks @@ -272,14 +211,19 @@ const structuredPatch = (a: string, b: string, multilineString?: boolean): Diff }; }; -function diffStrings(a: string, b: string, options: ?DiffOptions, multilineString?: boolean): string { +function diffStrings( + a: string, + b: string, + options: ?DiffOptions, + replacement?: Replacement, +): string { // `diff` uses the Myers LCS diff algorithm which runs in O(n+d^2) time // (where "d" is the edit distance) and can get very slow for large edit // distances. Mitigate the cost by switching to a lower-resolution diff // whenever linebreaks are involved. const result = options && options.expand === false - ? structuredPatch(a, b, multilineString) - : diffLines(a, b, multilineString); + ? structuredPatch(a, b, replacement) + : diffLines(a, b, replacement); if (result.isDifferent) { return getAnnotation(options) + result.diff; diff --git a/packages/jest-diff/src/index.js b/packages/jest-diff/src/index.js index ee51e5fb9cbc..d601b9615d35 100644 --- a/packages/jest-diff/src/index.js +++ b/packages/jest-diff/src/index.js @@ -36,11 +36,57 @@ const PLUGINS = [ const FORMAT_OPTIONS = { plugins: PLUGINS, }; +const FORMAT_OPTIONS_0 = Object.assign({}, FORMAT_OPTIONS, { + indent: 0, +}); const FALLBACK_FORMAT_OPTIONS = { callToJSON: false, maxDepth: 10, plugins: PLUGINS, }; +const FALLBACK_FORMAT_OPTIONS_0 = Object.assign({}, FALLBACK_FORMAT_OPTIONS, { + indent: 0, +}); + +// Return whether line has an odd number of unescaped quotes. +function oddCountOfQuotes(line) { + let oddBackslashes = false; + let oddQuotes = false; + // eslint-disable-next-line prefer-const + for (let char of line) { + if (char === '\\') { + oddBackslashes = !oddBackslashes; + } else { + if (char === '"' && !oddBackslashes) { + oddQuotes = !oddQuotes; + } + oddBackslashes = false; + } + } + return oddQuotes; +} + +// Given array of lines, return lines without indentation, +// except in multiline strings. +const regexpIndentation = /^[ ]*/; +function unindent(string) { + let inMultilineString = false; + + return string + .split('\n') + .map(line => { + const oddCount = oddCountOfQuotes(line); + + if (inMultilineString) { + inMultilineString = !oddCount; + return line; + } + + inMultilineString = oddCount; + return line.replace(regexpIndentation, ''); + }) + .join('\n'); +} // Generate a string that will highlight the difference between two values // with green and red. (similar to how github does code diffing) @@ -83,11 +129,9 @@ function diff(a: any, b: any, options: ?DiffOptions): ?string { case 'string': const multiline = a.match(/[\r\n]/) !== -1 && b.indexOf('\n') !== -1; if (multiline) { - // Need explicit arg not to ignore indentation for multiline string - // from toBe or toEqual assertion (which isn’t enclosed in quotes) - // unlike a snapshot (which is enclosed in quotes). - const multilineString = !options || options.aAnnotation !== 'Snapshot'; - return diffStrings(a, b, options, multilineString); + return options && options.snapshot + ? diffStrings(unindent(a), unindent(b), options, {a, b}) + : diffStrings(a, b, options); } return null; case 'number': @@ -116,9 +160,13 @@ function compareObjects(a: Object, b: Object, options: ?DiffOptions) { try { diffMessage = diffStrings( - prettyFormat(a, FORMAT_OPTIONS), - prettyFormat(b, FORMAT_OPTIONS), + prettyFormat(a, FORMAT_OPTIONS_0), + prettyFormat(b, FORMAT_OPTIONS_0), options, + { + a: prettyFormat(a, FORMAT_OPTIONS), + b: prettyFormat(b, FORMAT_OPTIONS), + }, ); } catch (e) { hasThrown = true; @@ -128,9 +176,13 @@ function compareObjects(a: Object, b: Object, options: ?DiffOptions) { // without calling `toJSON`. It's also possible that toJSON might throw. if (!diffMessage || diffMessage === NO_DIFF_MESSAGE) { diffMessage = diffStrings( - prettyFormat(a, FALLBACK_FORMAT_OPTIONS), - prettyFormat(b, FALLBACK_FORMAT_OPTIONS), + prettyFormat(a, FALLBACK_FORMAT_OPTIONS_0), + prettyFormat(b, FALLBACK_FORMAT_OPTIONS_0), options, + { + a: prettyFormat(a, FALLBACK_FORMAT_OPTIONS), + b: prettyFormat(b, FALLBACK_FORMAT_OPTIONS), + }, ); if (diffMessage !== NO_DIFF_MESSAGE && !hasThrown) { diffMessage = SIMILAR_MESSAGE + '\n\n' + diffMessage; diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 7dc1990baafa..93d15aaf1867 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -89,6 +89,7 @@ const toMatchSnapshot = function(received: any, testName?: string) { aAnnotation: 'Snapshot', bAnnotation: 'Received', expand: snapshotState.expand, + snapshot: true, }); const report = () => From 9096b7bcb6bff8ec5e181efdaea55ebf2bdfb7ab Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 28 Jun 2017 12:33:58 -0400 Subject: [PATCH 05/14] Ignore indentation only in compareObjects not in snapshots --- packages/jest-diff/src/__tests__/diff.test.js | 296 ++++++++++++++---- packages/jest-diff/src/index.js | 44 +-- packages/jest-snapshot/src/index.js | 1 - 3 files changed, 228 insertions(+), 113 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index 0ede580fa19d..0c1cab9aab1f 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -15,11 +15,8 @@ function received(a, b, options, replacement) { return stripAnsi(diff(a, b, options, replacement)); } -const snapshot = {snapshot: true}; const unexpanded = {expand: false}; -const unexpandedSnap = Object.assign({}, unexpanded, snapshot); const expanded = {expand: true}; -const expandedSnap = Object.assign({}, expanded, snapshot); const toJSON = function toJSON() { return 'apple'; @@ -250,10 +247,10 @@ Options: ].join('\n'); test('unexpanded', () => { - expect(received(a, b, unexpandedSnap)).toMatch(expected); + expect(received(a, b, unexpanded)).toMatch(expected); }); test('expanded', () => { - expect(received(a, b, expandedSnap)).toMatch(expected); + expect(received(a, b, expanded)).toMatch(expected); }); }); @@ -302,7 +299,7 @@ test('collapses big diffs to patch format', () => { expect(result).toMatchSnapshot(); }); -describe('does ignore indentation in JavaScript structures', () => { +describe('indentation in JavaScript structures', () => { const a = { searching: '', sorting: { @@ -319,26 +316,6 @@ describe('does ignore indentation in JavaScript structures', () => { }, ], }; - const aSnap = [ - 'Object {', - ' "searching": "",', - ' "sorting": Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '}', - ].join('\n'); - const bSnap = [ - 'Object {', - ' "searching": "",', - ' "sorting": Array [', - ' Object {', - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - ' ],', - '}', - ].join('\n'); describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, @@ -363,12 +340,7 @@ describe('does ignore indentation in JavaScript structures', () => { expect(received(a, b, expanded)).toMatch(expected); }); - test('unexpanded snapshot', () => { - expect(received(aSnap, bSnap, unexpandedSnap)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(aSnap, bSnap, expandedSnap)).toMatch(expected); - }); + // Snapshots cannot distinguish indentation from leading spaces in text :( }); describe('from more to less', () => { @@ -394,12 +366,7 @@ describe('does ignore indentation in JavaScript structures', () => { expect(received(b, a, expanded)).toMatch(expected); }); - test('unexpanded snapshot', () => { - expect(received(bSnap, aSnap, unexpandedSnap)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(bSnap, aSnap, expandedSnap)).toMatch(expected); - }); + // Snapshots cannot distinguish indentation from leading spaces in text :( }); }); @@ -479,11 +446,12 @@ describe('multiline string as property of JavaScript object', () => { expect(received(aUn, bUn, expanded)).toMatch(expected); }); + // Proposed serialization for multiline string value in objects. test('unexpanded snapshot', () => { - expect(received(aUnSnap, bUnSnap, unexpandedSnap)).toMatch(expected); + expect(received(aUnSnap, bUnSnap, unexpanded)).toMatch(expected); }); test('expanded snapshot', () => { - expect(received(aUnSnap, bUnSnap, expandedSnap)).toMatch(expected); + expect(received(aUnSnap, bUnSnap, expanded)).toMatch(expected); }); }); @@ -504,11 +472,13 @@ describe('multiline string as property of JavaScript object', () => { expect(received(aIn, bIn, expanded)).toMatch(expected); }); + // If change to serialization for multiline string value in objects, + // then review the relevance of the following tests: test('unexpanded snapshot', () => { - expect(received(aInSnap, bInSnap, unexpandedSnap)).toMatch(expected); + expect(received(aInSnap, bInSnap, unexpanded)).toMatch(expected); }); test('expanded snapshot', () => { - expect(received(aInSnap, bInSnap, expandedSnap)).toMatch(expected); + expect(received(aInSnap, bInSnap, expanded)).toMatch(expected); }); }); @@ -531,16 +501,18 @@ describe('multiline string as property of JavaScript object', () => { expect(received(aUn, bIn, expanded)).toMatch(expected); }); + // If change to serialization for multiline string value in objects, + // then review the relevance of the following tests: test('unexpanded snapshot', () => { - expect(received(aUnSnap, bInSnap, unexpandedSnap)).toMatch(expected); + expect(received(aUnSnap, bInSnap, unexpanded)).toMatch(expected); }); test('expanded snapshot', () => { - expect(received(aUnSnap, bInSnap, expandedSnap)).toMatch(expected); + expect(received(aUnSnap, bInSnap, expanded)).toMatch(expected); }); }); }); -describe('does ignore indentation in React elements', () => { +describe('indentation in React elements', () => { const leaf = { $$typeof: Symbol.for('react.element'), props: { @@ -570,22 +542,6 @@ describe('does ignore indentation in React elements', () => { }, type: 'span', }; - const aSnap = [ - '', - ' ', - ' text', - ' ', - '', - ].join('\n'); - const bSnap = [ - '', - ' ', - ' ', - ' text', - ' ', - ' ', - '', - ].join('\n'); describe('from less to more', () => { // Replace unchanged chunk in the middle with received lines, @@ -607,12 +563,7 @@ describe('does ignore indentation in React elements', () => { expect(received(a, b, expanded)).toMatch(expected); }); - test('unexpanded snapshot', () => { - expect(received(aSnap, bSnap, unexpandedSnap)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(aSnap, bSnap, expandedSnap)).toMatch(expected); - }); + // Snapshots cannot distinguish indentation from leading spaces in text :( }); describe('from more to less', () => { @@ -635,11 +586,218 @@ describe('does ignore indentation in React elements', () => { expect(received(b, a, expanded)).toMatch(expected); }); + // Snapshots cannot distinguish indentation from leading spaces in text :( + }); +}); + +describe('spaces as text in React elements', () => { + const value = 2; + const unit = { + $$typeof: Symbol.for('react.element'), + props: { + children: ['m'], + title: 'meters', + }, + type: 'abbr', + }; + const a = { + $$typeof: Symbol.for('react.element'), + props: { + children: [value, ' ', unit], + }, + type: 'span', + }; + const b = { + $$typeof: Symbol.for('react.element'), + props: { + children: [value, ' ', unit], + }, + type: 'span', + }; + const aSnap = [ + '', + ' 2', + ' ', + ' ', + ' m', + ' ', + '', + ].join('\n'); + const bSnap = [ + '', + ' 2', + ' ', + ' ', + ' m', + ' ', + '', + ].join('\n'); + + describe('from less to more', () => { + // Replace one space with two spaces. + const expected = [ + ' ', + ' 2', + '- ', + '+ ', + ' ', + ' m', + ' ', + //' ', // unexpanded does not include this line + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); + }); + + // Snapshots must display differences of leading spaces in text. + test('unexpanded snapshot', () => { + expect(received(aSnap, bSnap, unexpanded)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(aSnap, bSnap, expanded)).toMatch(expected); + }); + }); + + describe('from more to less', () => { + // Replace two spaces with one space. + const expected = [ + ' ', + ' 2', + '- ', + '+ ', + ' ', + ' m', + ' ', + //' ', // unexpanded does not include this line + ].join('\n'); + + test('unexpanded', () => { + expect(received(b, a, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(b, a, expanded)).toMatch(expected); + }); + + // Snapshots must display differences of leading spaces in text. + test('unexpanded snapshot', () => { + expect(received(bSnap, aSnap, unexpanded)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(bSnap, aSnap, expanded)).toMatch(expected); + }); + }); +}); + +describe('spaces at beginning or end of text in React elements', () => { + const em = { + $$typeof: Symbol.for('react.element'), + props: { + children: ['already'], + }, + type: 'em', + }; + const a = { + $$typeof: Symbol.for('react.element'), + props: { + children: ['Jest is', em, 'configured'], + }, + type: 'p', + }; + const b = { + $$typeof: Symbol.for('react.element'), + props: { + children: ['Jest is ', em, ' configured'], + }, + type: 'p', + }; + const aSnap = [ + '

', + ' Jest is', + ' ', + ' already', + ' ', + ' configured', + '

', + ].join('\n'); + const bSnap = [ + '

', + ' Jest is ', + ' ', + ' already', + ' ', + ' configured', + '

', + ].join('\n'); + + describe('from less to more', () => { + // Replace no space with one space at edge of text nodes. + const expected = [ + '

', + '- Jest is', + '+ Jest is ', + ' ', + ' already', + ' ', + '- configured', + '+ configured', + '

', + ].join('\n'); + + test('unexpanded', () => { + expect(received(a, b, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(a, b, expanded)).toMatch(expected); + }); + + // Snapshots must display differences of leading spaces in text. + test('unexpanded snapshot', () => { + expect(received(aSnap, bSnap, unexpanded)).toMatch(expected); + }); + test('expanded snapshot', () => { + expect(received(aSnap, bSnap, expanded)).toMatch(expected); + }); + }); + + describe('from more to less', () => { + // Replace one space with no space at edge of text nodes. + const expected = [ + '

', + '- Jest is ', + '+ Jest is', + ' ', + ' already', + ' ', + '- configured', + '+ configured', + '

', + ].join('\n'); + + test('unexpanded', () => { + expect(received(b, a, unexpanded)).toMatch(expected); + }); + test('expanded', () => { + expect(received(b, a, expanded)).toMatch(expected); + }); + + // Snapshots must display differences of leading spaces in text. test('unexpanded snapshot', () => { - expect(received(bSnap, aSnap, unexpandedSnap)).toMatch(expected); + expect(received(bSnap, aSnap, unexpanded)).toMatch(expected); }); test('expanded snapshot', () => { - expect(received(bSnap, aSnap, expandedSnap)).toMatch(expected); + expect(received(bSnap, aSnap, expanded)).toMatch(expected); }); }); }); diff --git a/packages/jest-diff/src/index.js b/packages/jest-diff/src/index.js index c3135fd7574f..8b5a8a689188 100644 --- a/packages/jest-diff/src/index.js +++ b/packages/jest-diff/src/index.js @@ -45,46 +45,6 @@ const FALLBACK_FORMAT_OPTIONS_0 = Object.assign({}, FALLBACK_FORMAT_OPTIONS, { indent: 0, }); -// Return whether line has an odd number of unescaped quotes. -function oddCountOfQuotes(line) { - let oddBackslashes = false; - let oddQuotes = false; - // eslint-disable-next-line prefer-const - for (let char of line) { - if (char === '\\') { - oddBackslashes = !oddBackslashes; - } else { - if (char === '"' && !oddBackslashes) { - oddQuotes = !oddQuotes; - } - oddBackslashes = false; - } - } - return oddQuotes; -} - -// Given array of lines, return lines without indentation, -// except in multiline strings. -const regexpIndentation = /^[ ]*/; -function unindent(string) { - let inMultilineString = false; - - return string - .split('\n') - .map(line => { - const oddCount = oddCountOfQuotes(line); - - if (inMultilineString) { - inMultilineString = !oddCount; - return line; - } - - inMultilineString = oddCount; - return line.replace(regexpIndentation, ''); - }) - .join('\n'); -} - // Generate a string that will highlight the difference between two values // with green and red. (similar to how github does code diffing) function diff(a: any, b: any, options: ?DiffOptions): ?string { @@ -126,9 +86,7 @@ function diff(a: any, b: any, options: ?DiffOptions): ?string { case 'string': const multiline = a.match(/[\r\n]/) !== -1 && b.indexOf('\n') !== -1; if (multiline) { - return options && options.snapshot - ? diffStrings(unindent(a), unindent(b), options, {a, b}) - : diffStrings(a, b, options); + return diffStrings(a, b, options); } return null; case 'number': diff --git a/packages/jest-snapshot/src/index.js b/packages/jest-snapshot/src/index.js index 5cfab57397c1..30021363a678 100644 --- a/packages/jest-snapshot/src/index.js +++ b/packages/jest-snapshot/src/index.js @@ -90,7 +90,6 @@ const toMatchSnapshot = function(received: any, testName?: string) { aAnnotation: 'Snapshot', bAnnotation: 'Received', expand: snapshotState.expand, - snapshot: true, }); report = () => From 6931b9c3e61b088501ba74c4829e6989a7be8988 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 22 Aug 2017 18:23:23 -0400 Subject: [PATCH 06/14] Format lines that are equal except for indentation in cyan color --- .../__tests__/__snapshots__/diff.test.js.snap | 2 +- packages/jest-diff/src/diff_strings.js | 227 +++++++++--------- .../__snapshots__/react.test.js.snap | 36 +-- .../src/__tests__/convert_ansi.test.js | 2 +- .../pretty-format/src/plugins/convert_ansi.js | 12 + 5 files changed, 150 insertions(+), 129 deletions(-) diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap index 7e6c5638143d..005c6d91a4e7 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap @@ -96,7 +96,7 @@ exports[`falls back to not call toJSON 1`] = ` Object { + \\"key\\": Object { - \\"line\\": 2, + \\"line\\": 2, + }, \\"toJSON\\": [Function toJSON], }" diff --git a/packages/jest-diff/src/diff_strings.js b/packages/jest-diff/src/diff_strings.js index 5e70a104f4b1..1c74f70c32ab 100644 --- a/packages/jest-diff/src/diff_strings.js +++ b/packages/jest-diff/src/diff_strings.js @@ -21,7 +21,7 @@ export type DiffOptions = {| contextLines?: number, |}; -export type Replacement = {| +export type Original = {| a: string, b: string, |}; @@ -36,15 +36,34 @@ type Hunk = {| oldStart: number, |}; -const getColor = (added: boolean, removed: boolean) => - added ? chalk.red : removed ? chalk.green : chalk.dim; +type DIFF_D = -1 | 1 | 0; // diff digit: removed | added | equal -const getBgColor = (added: boolean, removed: boolean) => - added ? chalk.bgRed : removed ? chalk.bgGreen : chalk.dim; +// Given a chunk, return the diff character. +const getC = (chunk): string => (chunk.removed ? '-' : chunk.added ? '+' : ' '); +// Given a diff character, return the diff digit. +const getD = (c: string): DIFF_D => (c === '-' ? -1 : c === '+' ? 1 : 0); + +// Return foreground color for line. +// If compared lines are equal and were formatted with `indent: 0` option, +// then delta is difference in length of original lines. +const getColor = (d: DIFF_D, delta?: number) => + d === 1 ? chalk.red : d === -1 ? chalk.green : delta ? chalk.cyan : chalk.dim; + +// Return background color for leading or trailing spaces. +// Do NOT call if original lines are equal. +const getBgColor = (d: DIFF_D) => + d === 1 ? chalk.bgRed : d === -1 ? chalk.bgGreen : chalk.bgCyan; + +// Highlight trailing ONLY if compared lines from snapshot or multiline string. const highlightTrailingWhitespace = (line: string, bgColor: Function): string => line.replace(/\s+$/, bgColor('$&')); +// Highlight BOTH leading AND trailing if compared lines from data structure, +// therefore expected and received were formatted with `indent: 0` option. +const highlightWhitespace = (line: string, bgColor: Function): string => + highlightTrailingWhitespace(line.replace(/^\s+/, bgColor('$&')), bgColor); + const getAnnotation = (options: ?DiffOptions): string => chalk.green('- ' + ((options && options.aAnnotation) || 'Expected')) + '\n' + @@ -52,7 +71,7 @@ const getAnnotation = (options: ?DiffOptions): string => '\n\n'; // Given string, return array of its lines. -function splitIntoLines(string) { +const splitIntoLines = string => { const lines = string.split('\n'); if (lines.length !== 0 && lines[lines.length - 1] === '') { @@ -60,69 +79,81 @@ function splitIntoLines(string) { } return lines; -} - -// Replace unindented lines with original lines. -function replaceValueOfChunks(chunks, replacement) { - const aLines = splitIntoLines(replacement.a); - const bLines = splitIntoLines(replacement.b); - - let aIndex = 0; - let bIndex = 0; - chunks.forEach(chunk => { - const count = chunk.count; // number of lines in chunk - - // Assume that a is expected and b is received. - if (chunk.removed) { - chunk.value = aLines - .slice(aIndex, (aIndex += count)) - .map(line => line + '\n') - .join(''); - } else { - chunk.value = bLines - .slice(bIndex, (bIndex += count)) - .map(line => line + '\n') - .join(''); - if (!chunk.added) { - aIndex += count; // increment both if chunk is unchanged - } - } - }); - - return chunks; -} +}; -const diffLinesWithReplacement = ( - a: string, - b: string, - replacement?: Replacement, -): Diff => { - let chunks = diffLines(a, b); - if (replacement) { - chunks = replaceValueOfChunks(chunks, replacement); +// Given a diff character and the corresponding compared line, return a line. +// There is a getOriginal callback function if from data structure, +// therefore expected and received were formatted with `indent: 0` option. +const formatLine = ( + c: string, + lineCompared: string, + getOriginal?: Function, +) => { + const d = getD(c); + + if (getOriginal) { + const gotOriginal = getOriginal(d); + const lineOriginal = d === 0 ? gotOriginal[1] : gotOriginal; + const indentation = lineOriginal.slice( + 0, + lineOriginal.length - lineCompared.length, + ); + // If compared lines are equal, + // then delta is difference in length of original lines. + const delta = d === 0 ? gotOriginal[1].length - gotOriginal[0].length : 0; + return getColor(d, delta)( + c + + indentation + + (d === 0 && delta === 0 + ? lineCompared + : highlightWhitespace(lineCompared, getBgColor(d))), + ); } + + return getColor(d)( + c + + (d === 0 + ? lineCompared + : highlightTrailingWhitespace(lineCompared, getBgColor(d))), + ); +}; + +// Given original lines, return callback function which given diff digit, +// returns either the corresponding expected OR received line, +// or if compared lines are equal, array of expected AND received line. +const getterForChunks = (original: Original) => { + const linesExpected = splitIntoLines(original.a); + const linesReceived = splitIntoLines(original.b); + + let indexExpected = 0; + let indexReceived = 0; + + return (d: DIFF_D) => + d === -1 + ? linesExpected[indexExpected++] + : d === 1 + ? linesReceived[indexReceived++] + : [linesExpected[indexExpected++], linesReceived[indexReceived++]]; +}; + +const formatChunks = (a: string, b: string, original?: Original): Diff => { + const getOriginal = original && getterForChunks(original); let isDifferent = false; + return { - diff: chunks - .map(part => { - const {added, removed, value} = part; + diff: diffLines(a, b) + .map(chunk => { + const {added, removed, value} = chunk; if (added || removed) { isDifferent = true; } + const c = getC(chunk); - const lines = splitIntoLines(value); - const color = getColor(added, removed); - const bgColor = getBgColor(added, removed); - - return lines - .map(line => { - const highlightedLine = highlightTrailingWhitespace(line, bgColor); - const mark = added ? '+' : removed ? '-' : ' '; - return color(mark + highlightedLine) + '\n'; - }) - .join(''); + return splitIntoLines(value) + .map(lineCompared => formatLine(c, lineCompared, getOriginal)) + .join('\n'); }) - .join('') + .join('\n') .trim(), isDifferent, }; @@ -142,35 +173,29 @@ const createPatchMark = (hunk: Hunk): string => { return chalk.yellow(`@@ ${markOld} ${markNew} @@\n`); }; -// Replace unindented lines with original lines. -function replaceLinesInHunks(hunks, replacement) { - const aLines = splitIntoLines(replacement.a); - const bLines = splitIntoLines(replacement.b); - - hunks.forEach(hunk => { - // hunk has one-based line numbers - let aIndex = hunk.oldStart - 1; - let bIndex = hunk.newStart - 1; - - // Assume that a is expected and b is received. - hunk.lines = hunk.lines.map(line => { - const mark = line[0]; - if (mark === ' ') { - aIndex += 1; // increment both if line is unchanged - } - return mark + (mark === '-' ? aLines[aIndex++] : bLines[bIndex++]); - }); - }); - - return hunks; -} +// Given original lines, return callback function which given indexes for hunk, +// returns another callback function which given diff digit, +// returns either the corresponding expected OR received line, +// or if compared lines are equal, array of expected AND received line. +const getterForHunks = (original: Original) => { + const linesExpected = splitIntoLines(original.a); + const linesReceived = splitIntoLines(original.b); + + return (indexExpected: number, indexReceived: number) => (d: DIFF_D) => + d === -1 + ? linesExpected[indexExpected++] + : d === 1 + ? linesReceived[indexReceived++] + : [linesExpected[indexExpected++], linesReceived[indexReceived++]]; +}; -const structuredPatchWithReplacement = ( +const formatHunks = ( a: string, b: string, contextLines?: number, - replacement?: Replacement, + original?: Original, ): Diff => { + const getter = original && getterForHunks(original); const options = { context: typeof contextLines === 'number' && contextLines >= 0 @@ -187,33 +212,22 @@ const structuredPatchWithReplacement = ( } const oldLinesCount = (a.match(/\n/g) || []).length; - let hunks = structuredPatch('', '', a, b, '', '', options).hunks; - if (replacement) { - hunks = replaceLinesInHunks(hunks, replacement); - } return { - diff: hunks + diff: structuredPatch('', '', a, b, '', '', options).hunks .map((hunk: Hunk) => { + const getOriginal = + getter && getter(hunk.oldStart - 1, hunk.newStart - 1); const lines = hunk.lines - .map(line => { - const added = line[0] === '+'; - const removed = line[0] === '-'; - - const color = getColor(added, removed); - const bgColor = getBgColor(added, removed); - - const highlightedLine = highlightTrailingWhitespace(line, bgColor); - return color(highlightedLine) + '\n'; - }) - .join(''); + .map(line => formatLine(line[0], line.slice(1), getOriginal)) + .join('\n'); isDifferent = true; return shouldShowPatchMarks(hunk, oldLinesCount) ? createPatchMark(hunk) + lines : lines; }) - .join('') + .join('\n') .trim(), isDifferent, }; @@ -223,7 +237,7 @@ function diffStrings( a: string, b: string, options: ?DiffOptions, - replacement?: Replacement, + original?: Original, ): string { // `diff` uses the Myers LCS diff algorithm which runs in O(n+d^2) time // (where "d" is the edit distance) and can get very slow for large edit @@ -231,13 +245,8 @@ function diffStrings( // whenever linebreaks are involved. const result = options && options.expand === false - ? structuredPatchWithReplacement( - a, - b, - options && options.contextLines, - replacement, - ) - : diffLinesWithReplacement(a, b, replacement); + ? formatHunks(a, b, options && options.contextLines, original) + : formatChunks(a, b, original); if (result.isDifferent) { return getAnnotation(options) + result.diff; diff --git a/packages/pretty-format/src/__tests__/__snapshots__/react.test.js.snap b/packages/pretty-format/src/__tests__/__snapshots__/react.test.js.snap index 827d083ea5a2..480acab20cc9 100644 --- a/packages/pretty-format/src/__tests__/__snapshots__/react.test.js.snap +++ b/packages/pretty-format/src/__tests__/__snapshots__/react.test.js.snap @@ -1,43 +1,43 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP exports[`ReactElement plugin highlights syntax 1`] = ` -" +" prop={ -
+
mouse - + rat - -
+ +
} -/>" +/>" `; exports[`ReactElement plugin highlights syntax with color from theme option 1`] = ` -" +" style=\\"color:red\\" -> +> Hello, Mouse! -" +" `; exports[`ReactTestComponent plugin highlights syntax 1`] = ` -" +" prop={ -
+
mouse - + rat - -
+ +
} -/>" +/>" `; exports[`ReactTestComponent plugin highlights syntax with color from theme option 1`] = ` -" +" style=\\"color:red\\" -> +> Hello, Mouse! -" +" `; diff --git a/packages/pretty-format/src/__tests__/convert_ansi.test.js b/packages/pretty-format/src/__tests__/convert_ansi.test.js index d4ab0344e1a4..cba4652afb0f 100644 --- a/packages/pretty-format/src/__tests__/convert_ansi.test.js +++ b/packages/pretty-format/src/__tests__/convert_ansi.test.js @@ -56,6 +56,6 @@ describe('ConvertAnsi plugin', () => { }); it('does not support other colors', () => { - expect(prettyFormatResult(`${ansiStyle.cyan.open}`)).toEqual('""'); + expect(prettyFormatResult(`${ansiStyle.blue.open}`)).toEqual('""'); }); }); diff --git a/packages/pretty-format/src/plugins/convert_ansi.js b/packages/pretty-format/src/plugins/convert_ansi.js index 2b34eb51f10b..61c5478c543e 100644 --- a/packages/pretty-format/src/plugins/convert_ansi.js +++ b/packages/pretty-format/src/plugins/convert_ansi.js @@ -18,6 +18,10 @@ const toHumanReadableAnsi = text => { switch (match) { case style.red.close: case style.green.close: + case style.cyan.close: + case style.bgRed.close: + case style.bgGreen.close: + case style.bgCyan.close: case style.reset.open: case style.reset.close: return ''; @@ -25,6 +29,14 @@ const toHumanReadableAnsi = text => { return ''; case style.green.open: return ''; + case style.cyan.open: + return ''; + case style.bgRed.open: + return ''; + case style.bgGreen.open: + return ''; + case style.bgCyan.open: + return ''; case style.dim.open: return ''; case style.bold.open: From 87209b9e03e7f5f66ed8d6b14ccd726f948c5ac1 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 23 Aug 2017 16:41:07 -0400 Subject: [PATCH 07/14] Edit code after proof reading --- packages/jest-diff/src/diff_strings.js | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/packages/jest-diff/src/diff_strings.js b/packages/jest-diff/src/diff_strings.js index 1c74f70c32ab..e55ae5d86521 100644 --- a/packages/jest-diff/src/diff_strings.js +++ b/packages/jest-diff/src/diff_strings.js @@ -44,23 +44,23 @@ const getC = (chunk): string => (chunk.removed ? '-' : chunk.added ? '+' : ' '); // Given a diff character, return the diff digit. const getD = (c: string): DIFF_D => (c === '-' ? -1 : c === '+' ? 1 : 0); -// Return foreground color for line. +// Text color for line. // If compared lines are equal and were formatted with `indent: 0` option, // then delta is difference in length of original lines. const getColor = (d: DIFF_D, delta?: number) => d === 1 ? chalk.red : d === -1 ? chalk.green : delta ? chalk.cyan : chalk.dim; -// Return background color for leading or trailing spaces. -// Do NOT call if original lines are equal. +// Do NOT color leading or trailing spaces if original lines are equal: + +// Background color for leading or trailing spaces. const getBgColor = (d: DIFF_D) => d === 1 ? chalk.bgRed : d === -1 ? chalk.bgGreen : chalk.bgCyan; -// Highlight trailing ONLY if compared lines from snapshot or multiline string. +// Trailing ONLY if expected is snapshot or multiline string. const highlightTrailingWhitespace = (line: string, bgColor: Function): string => line.replace(/\s+$/, bgColor('$&')); -// Highlight BOTH leading AND trailing if compared lines from data structure, -// therefore expected and received were formatted with `indent: 0` option. +// BOTH leading AND trailing if expected and received are data structures. const highlightWhitespace = (line: string, bgColor: Function): string => highlightTrailingWhitespace(line.replace(/^\s+/, bgColor('$&')), bgColor); @@ -82,8 +82,6 @@ const splitIntoLines = string => { }; // Given a diff character and the corresponding compared line, return a line. -// There is a getOriginal callback function if from data structure, -// therefore expected and received were formatted with `indent: 0` option. const formatLine = ( c: string, lineCompared: string, @@ -92,6 +90,7 @@ const formatLine = ( const d = getD(c); if (getOriginal) { + // getOriginal callback function if expected and received are data structures. const gotOriginal = getOriginal(d); const lineOriginal = d === 0 ? gotOriginal[1] : gotOriginal; const indentation = lineOriginal.slice( @@ -110,6 +109,7 @@ const formatLine = ( ); } + // Format compared line from snapshot or multiline string. return getColor(d)( c + (d === 0 @@ -150,7 +150,7 @@ const formatChunks = (a: string, b: string, original?: Original): Diff => { const c = getC(chunk); return splitIntoLines(value) - .map(lineCompared => formatLine(c, lineCompared, getOriginal)) + .map(line => formatLine(c, line, getOriginal)) .join('\n'); }) .join('\n') @@ -216,6 +216,7 @@ const formatHunks = ( return { diff: structuredPatch('', '', a, b, '', '', options).hunks .map((hunk: Hunk) => { + // oldStart and newStart are one-based but args are zero-based const getOriginal = getter && getter(hunk.oldStart - 1, hunk.newStart - 1); const lines = hunk.lines From c6b3a21f85b90bf684f95b26e841967ca6f50d6e Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Thu, 24 Aug 2017 09:31:36 -0400 Subject: [PATCH 08/14] Fix misplaced className prop in React elements test --- packages/jest-diff/src/__tests__/diff.test.js | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index 06b2f6b2a333..d95a297601e3 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -251,18 +251,17 @@ describe('React elements', () => { }; const b = { $$typeof: Symbol.for('react.element'), - className: 'fun', // ignored by serializer props: { children: 'Goodbye', + className: 'fun', }, type: 'div', }; const expected = [ - '-', + ' ', '- Hello', - '+
', '+ Goodbye', '
', ].join('\n'); From 5f2afb429561c1cc5468eb77b65a74c57af30856 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Thu, 24 Aug 2017 09:55:36 -0400 Subject: [PATCH 09/14] Edit code and comments after rereading --- packages/jest-diff/src/diff_strings.js | 25 ++++++++++++------------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/packages/jest-diff/src/diff_strings.js b/packages/jest-diff/src/diff_strings.js index c77b1b10fed7..68cdd7da4646 100644 --- a/packages/jest-diff/src/diff_strings.js +++ b/packages/jest-diff/src/diff_strings.js @@ -38,14 +38,14 @@ type Hunk = {| type DIFF_D = -1 | 1 | 0; // diff digit: removed | added | equal -// Given a chunk, return the diff character. +// Given chunk, return diff character. const getC = (chunk): string => (chunk.removed ? '-' : chunk.added ? '+' : ' '); -// Given a diff character, return the diff digit. +// Given diff character by getC from chunk or line from hunk, return diff digit. const getD = (c: string): DIFF_D => (c === '-' ? -1 : c === '+' ? 1 : 0); -// Text color for line. -// If compared lines are equal and were formatted with `indent: 0` option, +// Color for text of line. +// If compared lines are equal and expected and received are data structures, // then delta is difference in length of original lines. const getColor = (d: DIFF_D, delta?: number) => d === 1 ? chalk.red : d === -1 ? chalk.green : delta ? chalk.cyan : chalk.dim; @@ -56,7 +56,7 @@ const getColor = (d: DIFF_D, delta?: number) => const getBgColor = (d: DIFF_D) => d === 1 ? chalk.bgRed : d === -1 ? chalk.bgGreen : chalk.bgCyan; -// Trailing ONLY if expected is snapshot or multiline string. +// ONLY trailing if expected is snapshot or multiline string. const highlightTrailingWhitespace = (line: string, bgColor: Function): string => line.replace(/\s+$/, bgColor('$&')); @@ -81,7 +81,8 @@ const splitIntoLines = string => { return lines; }; -// Given a diff character and the corresponding compared line, return a line. +// Given diff character and corresponding compared line, return line with colors +// and original indentation if compared with `indent: 0` option. const formatLine = ( c: string, lineCompared: string, @@ -93,16 +94,14 @@ const formatLine = ( // getOriginal callback function if expected and received are data structures. const gotOriginal = getOriginal(d); const lineOriginal = d === 0 ? gotOriginal[1] : gotOriginal; - const indentation = lineOriginal.slice( - 0, - lineOriginal.length - lineCompared.length, - ); + const lengthOriginal = lineOriginal.length; // If compared lines are equal, // then delta is difference in length of original lines. - const delta = d === 0 ? gotOriginal[1].length - gotOriginal[0].length : 0; + const delta = d === 0 ? lengthOriginal - gotOriginal[0].length : 0; return getColor(d, delta)( c + - indentation + + // Line was compared without its original indentation. + lineOriginal.slice(0, lengthOriginal - lineCompared.length) + (d === 0 && delta === 0 ? lineCompared : highlightWhitespace(lineCompared, getBgColor(d))), @@ -194,13 +193,13 @@ const formatHunks = ( contextLines?: number, original?: Original, ): Diff => { - const getter = original && getterForHunks(original); const options = { context: typeof contextLines === 'number' && contextLines >= 0 ? contextLines : DIFF_CONTEXT_DEFAULT, }; + const getter = original && getterForHunks(original); let isDifferent = false; // Make sure the strings end with a newline. if (!a.endsWith('\n')) { From b0276018401df0b577b0e872554717390bff4b2f Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Mon, 28 Aug 2017 10:19:50 -0400 Subject: [PATCH 10/14] Factor elementSymbol out of diff test cases --- packages/jest-diff/src/__tests__/diff.test.js | 26 ++++++++++--------- 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index d95a297601e3..2d45db7f715d 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -18,6 +18,8 @@ function received(a, b, options) { const unexpanded = {expand: false}; const expanded = {expand: true}; +const elementSymbol = Symbol.for('react.element'); + const toJSON = function toJSON() { return 'apple'; }; @@ -242,7 +244,7 @@ Options: describe('React elements', () => { const a = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: 'Hello', className: 'fun', @@ -250,7 +252,7 @@ describe('React elements', () => { type: 'div', }; const b = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: 'Goodbye', className: 'fun', @@ -499,25 +501,25 @@ describe('multiline string as property of JavaScript object', () => { describe('indentation in React elements', () => { const leaf = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: ['text'], }, type: 'span', }; const a = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: [leaf], }, type: 'span', }; const b = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: [ { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: [leaf], }, @@ -578,7 +580,7 @@ describe('indentation in React elements', () => { describe('spaces as text in React elements', () => { const value = 2; const unit = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: ['m'], title: 'meters', @@ -586,14 +588,14 @@ describe('spaces as text in React elements', () => { type: 'abbr', }; const a = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: [value, ' ', unit], }, type: 'span', }; const b = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: [value, ' ', unit], }, @@ -687,21 +689,21 @@ describe('spaces as text in React elements', () => { describe('spaces at beginning or end of text in React elements', () => { const em = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: ['already'], }, type: 'em', }; const a = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: ['Jest is', em, 'configured'], }, type: 'p', }; const b = { - $$typeof: Symbol.for('react.element'), + $$typeof: elementSymbol, props: { children: ['Jest is ', em, ' configured'], }, From bebf76adfc536029515acf0f9ee9149c22bc38d6 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Mon, 28 Aug 2017 16:49:57 -0400 Subject: [PATCH 11/14] Add tests for color of text and background color of spaces --- .../__tests__/__snapshots__/diff.test.js.snap | 164 +++++ packages/jest-diff/src/__tests__/diff.test.js | 624 +++++++----------- 2 files changed, 415 insertions(+), 373 deletions(-) diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap index 3684a5e8707d..aff6028f16cf 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap @@ -1,5 +1,137 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP +exports[`background color of spaces (expanded) added is red 1`] = ` +"- Expected ++ Received + +
+ +- ++ following string consists of a space: ++ ++ line has preceding space only ++ line has both preceding and following space ++ line has following space only + +
" +`; + +exports[`background color of spaces (expanded) inchanged is cyan 1`] = ` +"- Expected ++ Received + +
++

+ + following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only + ++

+
" +`; + +exports[`background color of spaces (expanded) removed is green 1`] = ` +"- Expected ++ Received + +
+ +- following string consists of a space: +- +- line has preceding space only +- line has both preceding and following space +- line has following space only ++ + +
" +`; + +exports[`background color of spaces (expanded) unchanged has no color 1`] = ` +"- Expected ++ Received + +
+- ++

+ following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only +- ++

+
" +`; + +exports[`background color of spaces (unexpanded) added is red 1`] = ` +"- Expected ++ Received + +
+ +- ++ following string consists of a space: ++ ++ line has preceding space only ++ line has both preceding and following space ++ line has following space only + +
" +`; + +exports[`background color of spaces (unexpanded) inchanged is cyan 1`] = ` +"- Expected ++ Received + +
++

+ + following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only + ++

+
" +`; + +exports[`background color of spaces (unexpanded) removed is green 1`] = ` +"- Expected ++ Received + +
+ +- following string consists of a space: +- +- line has preceding space only +- line has both preceding and following space +- line has following space only ++ + +
" +`; + +exports[`background color of spaces (unexpanded) unchanged has no color 1`] = ` +"- Expected ++ Received + +
+- ++

+ following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only +- ++

+
" +`; + exports[`collapses big diffs to patch format 1`] = ` "- Expected + Received @@ -17,6 +149,38 @@ exports[`collapses big diffs to patch format 1`] = ` }" `; +exports[`color of text (expanded) 1`] = ` +"- Expected ++ Received + + Object { + \\"searching\\": \\"\\", +- \\"sorting\\": Object { ++ \\"sorting\\": Array [ ++ Object { + \\"descending\\": false, + \\"fieldKey\\": \\"what\\", + }, ++ ], + }" +`; + +exports[`color of text (unexpanded) 1`] = ` +"- Expected ++ Received + + Object { + \\"searching\\": \\"\\", +- \\"sorting\\": Object { ++ \\"sorting\\": Array [ ++ Object { + \\"descending\\": false, + \\"fieldKey\\": \\"what\\", + }, ++ ], + }" +`; + exports[`context number of lines: -1 (5 default) 1`] = ` "- Expected + Received diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index 2d45db7f715d..fc1771b570ad 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -11,9 +11,7 @@ const stripAnsi = require('strip-ansi'); const diff = require('../'); -function received(a, b, options) { - return stripAnsi(diff(a, b, options)); -} +const stripped = (a, b, options) => stripAnsi(diff(a, b, options)); const unexpanded = {expand: false}; const expanded = {expand: true}; @@ -38,7 +36,7 @@ describe('different types', () => { const typeB = values[3]; test(`'${String(a)}' and '${String(b)}'`, () => { - expect(received(a, b)).toBe( + expect(stripped(a, b)).toBe( ' Comparing two different types of values. ' + `Expected ${typeA} but received ${typeB}.`, ); @@ -62,7 +60,7 @@ describe('no visual difference', () => { test(`'${JSON.stringify(values[0])}' and '${JSON.stringify( values[1], )}'`, () => { - expect(received(values[0], values[1])).toBe( + expect(stripped(values[0], values[1])).toBe( 'Compared values have no visual difference.', ); }); @@ -72,7 +70,7 @@ describe('no visual difference', () => { const arg1 = new Map([[1, 'foo'], [2, 'bar']]); const arg2 = new Map([[2, 'bar'], [1, 'foo']]); - expect(received(arg1, arg2)).toBe( + expect(stripped(arg1, arg2)).toBe( 'Compared values have no visual difference.', ); }); @@ -81,7 +79,7 @@ describe('no visual difference', () => { const arg1 = new Set([1, 2]); const arg2 = new Set([2, 1]); - expect(received(arg1, arg2)).toBe( + expect(stripped(arg1, arg2)).toBe( 'Compared values have no visual difference.', ); }); @@ -133,11 +131,11 @@ line 4`; ' line 4', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); @@ -155,11 +153,11 @@ describe('objects', () => { ' }', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); @@ -198,11 +196,11 @@ Options: '+ failing test. [boolean]', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); @@ -234,11 +232,11 @@ Options: '+ failing test. [boolean]"', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); @@ -268,51 +266,89 @@ describe('React elements', () => { ' ', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); -test('collapses big diffs to patch format', () => { - const result = diff( - {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, - {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, - unexpanded, - ); +describe('multiline string as value of object property', () => { + const expected = [ + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+0.5,0.875', + ' 0.25,0.875",', + ' }', + ].join('\n'); - expect(result).toMatchSnapshot(); + describe('(non-snapshot)', () => { + const a = { + id: 'J', + points: '0.5,0.460\n0.25,0.875', + }; + const b = { + id: 'J', + points: '0.5,0.460\n0.5,0.875\n0.25,0.875', + }; + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); + }); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); + }); + }); + + describe('(snapshot)', () => { + const a = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '0.25,0.875",', + '}', + ].join('\n'); + const b = [ + 'Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '0.5,0.875', + '0.25,0.875",', + '}', + ].join('\n'); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); + }); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); + }); + }); }); describe('indentation in JavaScript structures', () => { + const searching = ''; + const object = { + descending: false, + fieldKey: 'what', + }; const a = { - searching: '', - sorting: { - descending: false, - fieldKey: 'what', - }, + searching, + sorting: object, }; const b = { - searching: '', - sorting: [ - { - descending: false, - fieldKey: 'what', - }, - ], + searching, + sorting: [object], }; describe('from less to more', () => { - // Replace unchanged chunk in the middle with received lines, - // which are more indented. const expected = [ ' Object {', ' "searching": "",', '- "sorting": Object {', '+ "sorting": Array [', '+ Object {', + // following 3 lines are unchanged, except for more indentation ' "descending": false,', ' "fieldKey": "what",', ' },', @@ -320,25 +356,22 @@ describe('indentation in JavaScript structures', () => { ' }', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); - - // Snapshots cannot distinguish indentation from leading spaces in text :( }); describe('from more to less', () => { - // Replace unchanged chunk in the middle with received lines, - // which are less indented. const expected = [ ' Object {', ' "searching": "",', '- "sorting": Array [', '- Object {', '+ "sorting": Object {', + // following 3 lines are unchanged, except for less indentation ' "descending": false,', ' "fieldKey": "what",', ' },', @@ -346,160 +379,39 @@ describe('indentation in JavaScript structures', () => { ' }', ].join('\n'); - test('unexpanded', () => { - expect(received(b, a, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(b, a, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(b, a, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(b, a, expanded)).toMatch(expected); }); - - // Snapshots cannot distinguish indentation from leading spaces in text :( }); }); -describe('multiline string as property of JavaScript object', () => { - // Unindented is safest in multiline strings: - const aUn = { - id: 'J', - points: `0.5,0.460 -0.25,0.875`, +describe('color of text', () => { + const searching = ''; + const object = { + descending: false, + fieldKey: 'what', }; - const bUn = { - id: 'J', - points: `0.5,0.460 -0.5,0.875 -0.25,0.875`, - }; - const aUnSnap = [ - 'Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '0.25,0.875",', - '}', - ].join('\n'); - const bUnSnap = [ - 'Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '0.5,0.875', - '0.25,0.875",', - '}', - ].join('\n'); - - // Indented is confusing, as this test demonstrates :( - // What looks like one indent level under points is really two levels, - // because the test itself is indented one level! - const aIn = { - id: 'J', - points: `0.5,0.460 - 0.25,0.875`, + const a = { + searching, + sorting: object, }; - const bIn = { - id: 'J', - points: `0.5,0.460 - 0.5,0.875 - 0.25,0.875`, + const b = { + searching, + sorting: [object], }; - const aInSnap = [ - 'Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - ' 0.25,0.875",', - '}', - ].join('\n'); - const bInSnap = [ - 'Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - ' 0.5,0.875', - ' 0.25,0.875",', - '}', - ].join('\n'); - - describe('unindented', () => { - const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+0.5,0.875', - ' 0.25,0.875",', - ' }', - ].join('\n'); - - test('unexpanded', () => { - expect(received(aUn, bUn, unexpanded)).toMatch(expected); - }); - test('expanded', () => { - expect(received(aUn, bUn, expanded)).toMatch(expected); - }); - // Proposed serialization for multiline string value in objects. - test('unexpanded snapshot', () => { - expect(received(aUnSnap, bUnSnap, unexpanded)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(aUnSnap, bUnSnap, expanded)).toMatch(expected); - }); + test('(unexpanded)', () => { + expect(diff(a, b, unexpanded)).toMatchSnapshot(); }); - - describe('indented', () => { - const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+ 0.5,0.875', - ' 0.25,0.875",', - ' }', - ].join('\n'); - - test('unexpanded', () => { - expect(received(aIn, bIn, unexpanded)).toMatch(expected); - }); - test('expanded', () => { - expect(received(aIn, bIn, expanded)).toMatch(expected); - }); - - // If change to serialization for multiline string value in objects, - // then review the relevance of the following tests: - test('unexpanded snapshot', () => { - expect(received(aInSnap, bInSnap, unexpanded)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(aInSnap, bInSnap, expanded)).toMatch(expected); - }); - }); - - describe('unindented to indented', () => { - // Don’t ignore changes to indentation in a multiline string. - const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '-0.25,0.875",', - '+ 0.5,0.875', - '+ 0.25,0.875",', - ' }', - ].join('\n'); - - test('unexpanded', () => { - expect(received(aUn, bIn, unexpanded)).toMatch(expected); - }); - test('expanded', () => { - expect(received(aUn, bIn, expanded)).toMatch(expected); - }); - - // If change to serialization for multiline string value in objects, - // then review the relevance of the following tests: - test('unexpanded snapshot', () => { - expect(received(aUnSnap, bInSnap, unexpanded)).toMatch(expected); - }); - test('expanded snapshot', () => { - expect(received(aUnSnap, bInSnap, expanded)).toMatch(expected); - }); + test('(expanded)', () => { + expect(diff(a, b, expanded)).toMatchSnapshot(); }); }); -describe('indentation in React elements', () => { +describe('indentation in React elements (non-snapshot)', () => { const leaf = { $$typeof: elementSymbol, props: { @@ -531,11 +443,10 @@ describe('indentation in React elements', () => { }; describe('from less to more', () => { - // Replace unchanged chunk in the middle with received lines, - // which are more indented. const expected = [ ' ', '+ ', + // following 3 lines are unchanged, except for more indentation ' ', ' text', ' ', @@ -543,22 +454,19 @@ describe('indentation in React elements', () => { ' ', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); - - // Snapshots cannot distinguish indentation from leading spaces in text :( }); describe('from more to less', () => { - // Replace unchanged chunk in the middle with received lines, - // which are less indented. const expected = [ ' ', '- ', + // following 3 lines are unchanged, except for less indentation ' ', ' text', ' ', @@ -566,229 +474,199 @@ describe('indentation in React elements', () => { ' ', ].join('\n'); - test('unexpanded', () => { - expect(received(b, a, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(b, a, unexpanded)).toMatch(expected); }); - test('expanded', () => { - expect(received(b, a, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(b, a, expanded)).toMatch(expected); }); - - // Snapshots cannot distinguish indentation from leading spaces in text :( }); }); -describe('spaces as text in React elements', () => { - const value = 2; - const unit = { - $$typeof: elementSymbol, - props: { - children: ['m'], - title: 'meters', - }, - type: 'abbr', - }; - const a = { - $$typeof: elementSymbol, - props: { - children: [value, ' ', unit], - }, - type: 'span', - }; - const b = { - $$typeof: elementSymbol, - props: { - children: [value, ' ', unit], - }, - type: 'span', - }; - const aSnap = [ +describe('indentation in React elements (snapshot)', () => { + // prettier-ignore + const a = [ '', - ' 2', - ' ', - ' ', - ' m', - ' ', + ' ', + ' text', + ' ', '', ].join('\n'); - const bSnap = [ + const b = [ '', - ' 2', - ' ', - ' ', - ' m', - ' ', + ' ', + ' ', + ' text', + ' ', + ' ', '', ].join('\n'); describe('from less to more', () => { - // Replace one space with two spaces. + // We intend to improve snapshot diff in the next version of Jest. const expected = [ ' ', - ' 2', - '- ', - '+ ', - ' ', - ' m', - ' ', - //' ', // unexpanded does not include this line + '- ', + '- text', + '- ', + '+ ', + '+ ', + '+ text', + '+ ', + '+ ', + ' ', ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); - }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); - }); - - // Snapshots must display differences of leading spaces in text. - test('unexpanded snapshot', () => { - expect(received(aSnap, bSnap, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(a, b, unexpanded)).toMatch(expected); }); - test('expanded snapshot', () => { - expect(received(aSnap, bSnap, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(a, b, expanded)).toMatch(expected); }); }); describe('from more to less', () => { - // Replace two spaces with one space. + // We intend to improve snapshot diff in the next version of Jest. const expected = [ ' ', - ' 2', - '- ', - '+ ', - ' ', - ' m', - ' ', - //' ', // unexpanded does not include this line + '- ', + '- ', + '- text', + '- ', + '- ', + '+ ', + '+ text', + '+ ', + ' ', ].join('\n'); - test('unexpanded', () => { - expect(received(b, a, unexpanded)).toMatch(expected); - }); - test('expanded', () => { - expect(received(b, a, expanded)).toMatch(expected); - }); - - // Snapshots must display differences of leading spaces in text. - test('unexpanded snapshot', () => { - expect(received(bSnap, aSnap, unexpanded)).toMatch(expected); + test('(unexpanded)', () => { + expect(stripped(b, a, unexpanded)).toMatch(expected); }); - test('expanded snapshot', () => { - expect(received(bSnap, aSnap, expanded)).toMatch(expected); + test('(expanded)', () => { + expect(stripped(b, a, expanded)).toMatch(expected); }); }); }); -describe('spaces at beginning or end of text in React elements', () => { - const em = { +describe('background color of spaces', () => { + const baseline = { $$typeof: elementSymbol, props: { - children: ['already'], + children: [ + { + $$typeof: elementSymbol, + props: { + children: [''], + }, + type: 'span', + }, + ], }, - type: 'em', + type: 'div', }; - const a = { + const lines = [ + 'following string consists of a space:', + ' ', + ' line has preceding space only', + ' line has both preceding and following space ', + 'line has following space only ', + ]; + const examples = { $$typeof: elementSymbol, props: { - children: ['Jest is', em, 'configured'], + children: [ + { + $$typeof: elementSymbol, + props: { + children: lines, + }, + type: 'span', + }, + ], }, - type: 'p', + type: 'div', }; - const b = { + const unchanged = { $$typeof: elementSymbol, props: { - children: ['Jest is ', em, ' configured'], + children: [ + { + $$typeof: elementSymbol, + props: { + children: lines, + }, + type: 'p', + }, + ], }, - type: 'p', + type: 'div', + }; + const inchanged = { + $$typeof: elementSymbol, + props: { + children: [ + { + $$typeof: elementSymbol, + props: { + children: [ + { + $$typeof: elementSymbol, + props: { + children: [lines], + }, + type: 'span', + }, + ], + }, + type: 'p', + }, + ], + }, + type: 'div', }; - const aSnap = [ - '

', - ' Jest is', - ' ', - ' already', - ' ', - ' configured', - '

', - ].join('\n'); - const bSnap = [ - '

', - ' Jest is ', - ' ', - ' already', - ' ', - ' configured', - '

', - ].join('\n'); - - describe('from less to more', () => { - // Replace no space with one space at edge of text nodes. - const expected = [ - '

', - '- Jest is', - '+ Jest is ', - ' ', - ' already', - ' ', - '- configured', - '+ configured', - '

', - ].join('\n'); - test('unexpanded', () => { - expect(received(a, b, unexpanded)).toMatch(expected); + describe('(unexpanded)', () => { + test('added is red', () => { + expect(diff(baseline, examples, unexpanded)).toMatchSnapshot(); }); - test('expanded', () => { - expect(received(a, b, expanded)).toMatch(expected); + test('removed is green', () => { + expect(diff(examples, baseline, unexpanded)).toMatchSnapshot(); }); - - // Snapshots must display differences of leading spaces in text. - test('unexpanded snapshot', () => { - expect(received(aSnap, bSnap, unexpanded)).toMatch(expected); + test('unchanged has no color', () => { + expect(diff(examples, unchanged, unexpanded)).toMatchSnapshot(); }); - test('expanded snapshot', () => { - expect(received(aSnap, bSnap, expanded)).toMatch(expected); + test('inchanged is cyan', () => { + expect(diff(examples, inchanged, unexpanded)).toMatchSnapshot(); }); }); - describe('from more to less', () => { - // Replace one space with no space at edge of text nodes. - const expected = [ - '

', - '- Jest is ', - '+ Jest is', - ' ', - ' already', - ' ', - '- configured', - '+ configured', - '

', - ].join('\n'); - - test('unexpanded', () => { - expect(received(b, a, unexpanded)).toMatch(expected); + describe('(expanded)', () => { + test('added is red', () => { + expect(diff(baseline, examples, expanded)).toMatchSnapshot(); }); - test('expanded', () => { - expect(received(b, a, expanded)).toMatch(expected); + test('removed is green', () => { + expect(diff(examples, baseline, expanded)).toMatchSnapshot(); }); - - // Snapshots must display differences of leading spaces in text. - test('unexpanded snapshot', () => { - expect(received(bSnap, aSnap, unexpanded)).toMatch(expected); + test('unchanged has no color', () => { + expect(diff(examples, unchanged, expanded)).toMatchSnapshot(); }); - test('expanded snapshot', () => { - expect(received(bSnap, aSnap, expanded)).toMatch(expected); + test('inchanged is cyan', () => { + expect(diff(examples, inchanged, expanded)).toMatchSnapshot(); }); }); }); +test('collapses big diffs to patch format', () => { + const result = diff( + {test: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]}, + {test: [1, 2, 3, 4, 5, 6, 7, 8, 10, 9]}, + unexpanded, + ); + + expect(result).toMatchSnapshot(); +}); + describe('context', () => { const testDiffContextLines = (contextLines?: number) => { test(`number of lines: ${typeof contextLines === 'number' From f178db01a4859d031de6a0aaf47f5cd620097fd3 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 29 Aug 2017 11:31:09 -0400 Subject: [PATCH 12/14] Rewrite tests for color to eliminate duplicate snapshots --- .../__tests__/__snapshots__/diff.test.js.snap | 90 +------------------ packages/jest-diff/src/__tests__/diff.test.js | 56 +++++++----- 2 files changed, 37 insertions(+), 109 deletions(-) diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap index aff6028f16cf..4172d56d2b5d 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap @@ -1,22 +1,6 @@ // Jest Snapshot v1, https://goo.gl/fbAQLP -exports[`background color of spaces (expanded) added is red 1`] = ` -"- Expected -+ Received - -
- -- -+ following string consists of a space: -+ -+ line has preceding space only -+ line has both preceding and following space -+ line has following space only - -
" -`; - -exports[`background color of spaces (expanded) inchanged is cyan 1`] = ` +exports[`background color of spaces cyan for inchanged (expanded) 1`] = ` "- Expected + Received @@ -33,7 +17,7 @@ exports[`background color of spaces (expanded) inchanged is cyan 1`] = ` " `; -exports[`background color of spaces (expanded) removed is green 1`] = ` +exports[`background color of spaces green for removed (expanded) 1`] = ` "- Expected + Received @@ -49,7 +33,7 @@ exports[`background color of spaces (expanded) removed is green 1`] = ` " `; -exports[`background color of spaces (expanded) unchanged has no color 1`] = ` +exports[`background color of spaces no color for unchanged (expanded) 1`] = ` "- Expected + Received @@ -66,7 +50,7 @@ exports[`background color of spaces (expanded) unchanged has no color 1`] = ` " `; -exports[`background color of spaces (unexpanded) added is red 1`] = ` +exports[`background color of spaces red for added (expanded) 1`] = ` "- Expected + Received @@ -82,56 +66,6 @@ exports[`background color of spaces (unexpanded) added is red 1`] = ` " `; -exports[`background color of spaces (unexpanded) inchanged is cyan 1`] = ` -"- Expected -+ Received - -
-+

- - following string consists of a space: - - line has preceding space only - line has both preceding and following space - line has following space only - -+

-
" -`; - -exports[`background color of spaces (unexpanded) removed is green 1`] = ` -"- Expected -+ Received - -
- -- following string consists of a space: -- -- line has preceding space only -- line has both preceding and following space -- line has following space only -+ - -
" -`; - -exports[`background color of spaces (unexpanded) unchanged has no color 1`] = ` -"- Expected -+ Received - -
-- -+

- following string consists of a space: - - line has preceding space only - line has both preceding and following space - line has following space only -- -+

-
" -`; - exports[`collapses big diffs to patch format 1`] = ` "- Expected + Received @@ -165,22 +99,6 @@ exports[`color of text (expanded) 1`] = ` }" `; -exports[`color of text (unexpanded) 1`] = ` -"- Expected -+ Received - - Object { - \\"searching\\": \\"\\", -- \\"sorting\\": Object { -+ \\"sorting\\": Array [ -+ Object { - \\"descending\\": false, - \\"fieldKey\\": \\"what\\", - }, -+ ], - }" -`; - exports[`context number of lines: -1 (5 default) 1`] = ` "- Expected + Received diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index fc1771b570ad..125565b5cf79 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -402,12 +402,14 @@ describe('color of text', () => { searching, sorting: [object], }; + const received = diff(a, b, expanded); - test('(unexpanded)', () => { - expect(diff(a, b, unexpanded)).toMatchSnapshot(); - }); test('(expanded)', () => { - expect(diff(a, b, expanded)).toMatchSnapshot(); + expect(received).toMatchSnapshot(); + }); + test('(unexpanded)', () => { + // Expect same result, unless diff is long enough to require patch marks. + expect(diff(a, b, unexpanded)).toBe(received); }); }); @@ -626,33 +628,41 @@ describe('background color of spaces', () => { type: 'div', }; - describe('(unexpanded)', () => { - test('added is red', () => { - expect(diff(baseline, examples, unexpanded)).toMatchSnapshot(); + // Expect same results, unless diff is long enough to require patch marks. + describe('cyan for inchanged', () => { + const received = diff(examples, inchanged, expanded); + test('(expanded)', () => { + expect(received).toMatchSnapshot(); }); - test('removed is green', () => { - expect(diff(examples, baseline, unexpanded)).toMatchSnapshot(); + test('(unexpanded)', () => { + expect(diff(examples, inchanged, unexpanded)).toBe(received); }); - test('unchanged has no color', () => { - expect(diff(examples, unchanged, unexpanded)).toMatchSnapshot(); + }); + describe('green for removed', () => { + const received = diff(examples, baseline, expanded); + test('(expanded)', () => { + expect(received).toMatchSnapshot(); }); - test('inchanged is cyan', () => { - expect(diff(examples, inchanged, unexpanded)).toMatchSnapshot(); + test('(unexpanded)', () => { + expect(diff(examples, baseline, unexpanded)).toBe(received); }); }); - - describe('(expanded)', () => { - test('added is red', () => { - expect(diff(baseline, examples, expanded)).toMatchSnapshot(); + describe('no color for unchanged', () => { + const received = diff(examples, unchanged, expanded); + test('(expanded)', () => { + expect(received).toMatchSnapshot(); }); - test('removed is green', () => { - expect(diff(examples, baseline, expanded)).toMatchSnapshot(); + test('(unexpanded)', () => { + expect(diff(examples, unchanged, unexpanded)).toBe(received); }); - test('unchanged has no color', () => { - expect(diff(examples, unchanged, expanded)).toMatchSnapshot(); + }); + describe('red for added', () => { + const received = diff(baseline, examples, expanded); + test('(expanded)', () => { + expect(received).toMatchSnapshot(); }); - test('inchanged is cyan', () => { - expect(diff(examples, inchanged, expanded)).toMatchSnapshot(); + test('(unexpanded)', () => { + expect(diff(baseline, examples, unexpanded)).toBe(received); }); }); }); From 787e124ba80bb085f11d8ffd3921110d56af8bc5 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 30 Aug 2017 14:50:19 -0400 Subject: [PATCH 13/14] Make requested changes especially space in second column --- .../__snapshots__/failures.test.js.snap | 40 +- .../__snapshots__/matchers.test.js.snap | 362 +++++++++--------- .../__tests__/__snapshots__/diff.test.js.snap | 202 +++++----- packages/jest-diff/src/__tests__/diff.test.js | 186 ++++----- packages/jest-diff/src/diff_strings.js | 137 ++++--- .../__snapshots__/index.test.js.snap | 10 +- 6 files changed, 483 insertions(+), 454 deletions(-) diff --git a/integration_tests/__tests__/__snapshots__/failures.test.js.snap b/integration_tests/__tests__/__snapshots__/failures.test.js.snap index 8cc32259583c..3180aa0bea2d 100644 --- a/integration_tests/__tests__/__snapshots__/failures.test.js.snap +++ b/integration_tests/__tests__/__snapshots__/failures.test.js.snap @@ -150,14 +150,14 @@ exports[`works with node assert 1`] = ` - Expected + Received - Object { - \\"a\\": Object { - \\"b\\": Object { - - \\"c\\": 6, - + \\"c\\": 5, - }, - }, - } + Object { + \\"a\\": Object { + \\"b\\": Object { + - \\"c\\": 6, + + \\"c\\": 5, + }, + }, + } at __tests__/node_assertion_error.test.js:40:10 @@ -178,14 +178,14 @@ exports[`works with node assert 1`] = ` - Expected + Received - Object { - \\"a\\": Object { - \\"b\\": Object { - - \\"c\\": 7, - + \\"c\\": 5, - }, - }, - } + Object { + \\"a\\": Object { + \\"b\\": Object { + - \\"c\\": 7, + + \\"c\\": 5, + }, + }, + } at __tests__/node_assertion_error.test.js:44:10 @@ -247,10 +247,10 @@ exports[`works with node assert 1`] = ` - Expected + Received - Object { - - \\"a\\": 2, - + \\"a\\": 1, - } + Object { + - \\"a\\": 2, + + \\"a\\": 1, + } at __tests__/node_assertion_error.test.js:60:10 diff --git a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap index 69632c8a590c..9ad0022d6ade 100644 --- a/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap +++ b/packages/expect/src/__tests__/__snapshots__/matchers.test.js.snap @@ -151,10 +151,10 @@ Difference: - Expected + Received --Object {} -+Object { -+ \\"circular\\": [Circular], -+}" +- Object {} ++ Object { ++ \\"circular\\": [Circular], ++ }" `; exports[`.toBe() fails for '"a"' with '.not' 1`] = ` @@ -268,10 +268,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": 5, -+ \\"a\\": 1, - }" + Object { +- \\"a\\": 5, ++ \\"a\\": 1, + }" `; exports[`.toBe() fails for: {} and {} 1`] = ` @@ -1967,12 +1967,12 @@ Difference: - Expected + Received --ArrayContaining [ -+Array [ - 1, -- 2, -+ 3, - ]" +- ArrayContaining [ ++ Array [ + 1, +- 2, ++ 3, + ]" `; exports[`.toEqual() {pass: false} expect([Function anonymous]).not.toEqual(Any) 1`] = ` @@ -2015,12 +2015,12 @@ Difference: - Expected + Received --ObjectContaining { -- \\"a\\": 2, -+Object { -+ \\"a\\": 1, -+ \\"b\\": 2, - }" +- ObjectContaining { +- \\"a\\": 2, ++ Object { ++ \\"a\\": 1, ++ \\"b\\": 2, + }" `; exports[`.toEqual() {pass: false} expect({"a": 5}).toEqual({"b": 6}) 1`] = ` @@ -2036,10 +2036,10 @@ Difference: - Expected + Received - Object { -- \\"b\\": 6, -+ \\"a\\": 5, - }" + Object { +- \\"b\\": 6, ++ \\"a\\": 5, + }" `; exports[`.toEqual() {pass: false} expect({"a": 99}).not.toEqual({"a": 99}) 1`] = ` @@ -2126,10 +2126,10 @@ Difference: - Expected + Received - Map { - 1 => \\"one\\", -+ 2 => \\"two\\", - }" + Map { + 1 => \\"one\\", ++ 2 => \\"two\\", + }" `; exports[`.toEqual() {pass: false} expect(Set {1, 2}).not.toEqual(Set {1, 2}) 1`] = ` @@ -2163,11 +2163,11 @@ Difference: - Expected + Received --Set {} -+Set { -+ 1, -+ 2, -+}" +- Set {} ++ Set { ++ 1, ++ 2, ++ }" `; exports[`.toEqual() {pass: false} expect(Set {1, 2}).toEqual(Set {1, 2, 3}) 1`] = ` @@ -2183,11 +2183,11 @@ Difference: - Expected + Received - Set { - 1, - 2, -- 3, - }" + Set { + 1, + 2, +- 3, + }" `; exports[`.toEqual() {pass: false} expect(false).toEqual(ObjectContaining {"a": 2}) 1`] = ` @@ -2523,10 +2523,10 @@ Difference: - Expected + Received - Object { -- \\"c\\": 4, -+ \\"c\\": 5, - }" + Object { +- \\"c\\": 4, ++ \\"c\\": 5, + }" `; exports[`.toHaveProperty() {pass: false} expect({"a": {"b": 3}}).toHaveProperty('a.b', undefined) 1`] = ` @@ -2852,10 +2852,10 @@ Difference: - Expected + Received - Array [ -- -0, -+ 0, - ]" + Array [ +- -0, ++ 0, + ]" `; exports[`toMatchObject() {pass: false} expect([1, 2, 3]).toMatchObject([1, 2, 2]) 1`] = ` @@ -2869,12 +2869,12 @@ Difference: - Expected + Received - Array [ - 1, - 2, -- 2, -+ 3, - ]" + Array [ + 1, + 2, +- 2, ++ 3, + ]" `; exports[`toMatchObject() {pass: false} expect([1, 2, 3]).toMatchObject([2, 3, 1]) 1`] = ` @@ -2888,12 +2888,12 @@ Difference: - Expected + Received - Array [ -+ 1, - 2, - 3, -- 1, - ]" + Array [ ++ 1, + 2, + 3, +- 1, + ]" `; exports[`toMatchObject() {pass: false} expect([1, 2]).toMatchObject([1, 3]) 1`] = ` @@ -2907,11 +2907,11 @@ Difference: - Expected + Received - Array [ - 1, -- 3, -+ 2, - ]" + Array [ + 1, +- 3, ++ 2, + ]" `; exports[`toMatchObject() {pass: false} expect([Error: foo]).toMatchObject([Error: bar]) 1`] = ` @@ -2925,8 +2925,8 @@ Difference: - Expected + Received --[Error: bar] -+[Error: foo]" +- [Error: bar] ++ [Error: foo]" `; exports[`toMatchObject() {pass: false} expect({"a": "a", "c": "d"}).toMatchObject({"a": Any}) 1`] = ` @@ -2940,10 +2940,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": Any, -+ \\"a\\": \\"a\\", - }" + Object { +- \\"a\\": Any, ++ \\"a\\": \\"a\\", + }" `; exports[`toMatchObject() {pass: false} expect({"a": "b", "c": "d"}).toMatchObject({"a": "b!", "c": "d"}) 1`] = ` @@ -2957,11 +2957,11 @@ Difference: - Expected + Received - Object { -- \\"a\\": \\"b!\\", -+ \\"a\\": \\"b\\", - \\"c\\": \\"d\\", - }" + Object { +- \\"a\\": \\"b!\\", ++ \\"a\\": \\"b\\", + \\"c\\": \\"d\\", + }" `; exports[`toMatchObject() {pass: false} expect({"a": "b", "c": "d"}).toMatchObject({"e": "b"}) 1`] = ` @@ -2975,11 +2975,11 @@ Difference: - Expected + Received - Object { -- \\"e\\": \\"b\\", -+ \\"a\\": \\"b\\", -+ \\"c\\": \\"d\\", - }" + Object { +- \\"e\\": \\"b\\", ++ \\"a\\": \\"b\\", ++ \\"c\\": \\"d\\", + }" `; exports[`toMatchObject() {pass: false} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"a": "b", "t": {"z": [3]}}) 1`] = ` @@ -2993,15 +2993,15 @@ Difference: - Expected + Received - Object { - \\"a\\": \\"b\\", - \\"t\\": Object { -- \\"z\\": Array [ -- 3, -- ], -+ \\"z\\": \\"z\\", - }, - }" + Object { + \\"a\\": \\"b\\", + \\"t\\": Object { +- \\"z\\": Array [ +- 3, +- ], ++ \\"z\\": \\"z\\", + }, + }" `; exports[`toMatchObject() {pass: false} expect({"a": "b", "t": {"x": {"r": "r"}, "z": "z"}}).toMatchObject({"t": {"l": {"r": "r"}}}) 1`] = ` @@ -3015,15 +3015,15 @@ Difference: - Expected + Received - Object { - \\"t\\": Object { -- \\"l\\": Object { -+ \\"x\\": Object { - \\"r\\": \\"r\\", - }, -+ \\"z\\": \\"z\\", - }, - }" + Object { + \\"t\\": Object { +- \\"l\\": Object { ++ \\"x\\": Object { + \\"r\\": \\"r\\", + }, ++ \\"z\\": \\"z\\", + }, + }" `; exports[`toMatchObject() {pass: false} expect({"a": [{"a": "a", "b": "b"}]}).toMatchObject({"a": [{"a": "c"}]}) 1`] = ` @@ -3037,14 +3037,14 @@ Difference: - Expected + Received - Object { - \\"a\\": Array [ - Object { -- \\"a\\": \\"c\\", -+ \\"a\\": \\"a\\", - }, - ], - }" + Object { + \\"a\\": Array [ + Object { +- \\"a\\": \\"c\\", ++ \\"a\\": \\"a\\", + }, + ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, "v"], "b": "b"}).toMatchObject({"a": ["v"]}) 1`] = ` @@ -3058,13 +3058,13 @@ Difference: - Expected + Received - Object { - \\"a\\": Array [ -+ 3, -+ 4, - \\"v\\", - ], - }" + Object { + \\"a\\": Array [ ++ 3, ++ 4, + \\"v\\", + ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": [3, 4, 5, 6]}) 1`] = ` @@ -3078,14 +3078,14 @@ Difference: - Expected + Received - Object { - \\"a\\": Array [ - 3, - 4, - 5, -- 6, - ], - }" + Object { + \\"a\\": Array [ + 3, + 4, + 5, +- 6, + ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": [3, 4]}) 1`] = ` @@ -3099,13 +3099,13 @@ Difference: - Expected + Received - Object { - \\"a\\": Array [ - 3, - 4, -+ 5, - ], - }" + Object { + \\"a\\": Array [ + 3, + 4, ++ 5, + ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": {"b": 4}}) 1`] = ` @@ -3119,16 +3119,16 @@ Difference: - Expected + Received - Object { -- \\"a\\": Object { -- \\"b\\": 4, -- }, -+ \\"a\\": Array [ -+ 3, -+ 4, -+ 5, -+ ], - }" + Object { +- \\"a\\": Object { +- \\"b\\": 4, +- }, ++ \\"a\\": Array [ ++ 3, ++ 4, ++ 5, ++ ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": [3, 4, 5], "b": "b"}).toMatchObject({"a": {"b": Any}}) 1`] = ` @@ -3142,16 +3142,16 @@ Difference: - Expected + Received - Object { -- \\"a\\": Object { -- \\"b\\": Any, -- }, -+ \\"a\\": Array [ -+ 3, -+ 4, -+ 5, -+ ], - }" + Object { +- \\"a\\": Object { +- \\"b\\": Any, +- }, ++ \\"a\\": Array [ ++ 3, ++ 4, ++ 5, ++ ], + }" `; exports[`toMatchObject() {pass: false} expect({"a": 1, "b": 1, "c": 1, "d": {"e": {"f": 555}}}).toMatchObject({"d": {"e": {"f": 222}}}) 1`] = ` @@ -3165,14 +3165,14 @@ Difference: - Expected + Received - Object { - \\"d\\": Object { - \\"e\\": Object { -- \\"f\\": 222, -+ \\"f\\": 555, - }, - }, - }" + Object { + \\"d\\": Object { + \\"e\\": Object { +- \\"f\\": 222, ++ \\"f\\": 555, + }, + }, + }" `; exports[`toMatchObject() {pass: false} expect({"a": 2015-11-30T00:00:00.000Z, "b": "b"}).toMatchObject({"a": 2015-10-10T00:00:00.000Z}) 1`] = ` @@ -3186,10 +3186,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": 2015-10-10T00:00:00.000Z, -+ \\"a\\": 2015-11-30T00:00:00.000Z, - }" + Object { +- \\"a\\": 2015-10-10T00:00:00.000Z, ++ \\"a\\": 2015-11-30T00:00:00.000Z, + }" `; exports[`toMatchObject() {pass: false} expect({"a": null, "b": "b"}).toMatchObject({"a": "4"}) 1`] = ` @@ -3203,10 +3203,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": \\"4\\", -+ \\"a\\": null, - }" + Object { +- \\"a\\": \\"4\\", ++ \\"a\\": null, + }" `; exports[`toMatchObject() {pass: false} expect({"a": null, "b": "b"}).toMatchObject({"a": undefined}) 1`] = ` @@ -3220,10 +3220,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": undefined, -+ \\"a\\": null, - }" + Object { +- \\"a\\": undefined, ++ \\"a\\": null, + }" `; exports[`toMatchObject() {pass: false} expect({"a": undefined}).toMatchObject({"a": null}) 1`] = ` @@ -3237,10 +3237,10 @@ Difference: - Expected + Received - Object { -- \\"a\\": null, -+ \\"a\\": undefined, - }" + Object { +- \\"a\\": null, ++ \\"a\\": undefined, + }" `; exports[`toMatchObject() {pass: false} expect({}).toMatchObject({"a": undefined}) 1`] = ` @@ -3254,10 +3254,10 @@ Difference: - Expected + Received --Object { -- \\"a\\": undefined, --} -+Object {}" +- Object { +- \\"a\\": undefined, +- } ++ Object {}" `; exports[`toMatchObject() {pass: false} expect(2015-11-30T00:00:00.000Z).toMatchObject(2015-10-10T00:00:00.000Z) 1`] = ` @@ -3271,8 +3271,8 @@ Difference: - Expected + Received --2015-10-10T00:00:00.000Z -+2015-11-30T00:00:00.000Z" +- 2015-10-10T00:00:00.000Z ++ 2015-11-30T00:00:00.000Z" `; exports[`toMatchObject() {pass: false} expect(Set {1, 2}).toMatchObject(Set {2}) 1`] = ` @@ -3286,10 +3286,10 @@ Difference: - Expected + Received - Set { -+ 1, - 2, - }" + Set { ++ 1, + 2, + }" `; exports[`toMatchObject() {pass: true} expect([]).toMatchObject([]) 1`] = ` diff --git a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap index 4172d56d2b5d..7f1b56e8b384 100644 --- a/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap +++ b/packages/jest-diff/src/__tests__/__snapshots__/diff.test.js.snap @@ -4,66 +4,66 @@ exports[`background color of spaces cyan for inchanged (expanded) 1`] = ` "- Expected + Received -
-+

- - following string consists of a space: - - line has preceding space only - line has both preceding and following space - line has following space only - -+

-
" +
++

+ + following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only + ++

+
" `; exports[`background color of spaces green for removed (expanded) 1`] = ` "- Expected + Received -
- -- following string consists of a space: -- -- line has preceding space only -- line has both preceding and following space -- line has following space only -+ - -
" +
+ +- following string consists of a space: +- +- line has preceding space only +- line has both preceding and following space +- line has following space only ++ + +
" `; exports[`background color of spaces no color for unchanged (expanded) 1`] = ` "- Expected + Received -
-- -+

- following string consists of a space: - - line has preceding space only - line has both preceding and following space - line has following space only -- -+

-
" +
+- ++

+ following string consists of a space: + + line has preceding space only + line has both preceding and following space + line has following space only +- ++

+
" `; exports[`background color of spaces red for added (expanded) 1`] = ` "- Expected + Received -
- -- -+ following string consists of a space: -+ -+ line has preceding space only -+ line has both preceding and following space -+ line has following space only - -
" +
+ +- ++ following string consists of a space: ++ ++ line has preceding space only ++ line has both preceding and following space ++ line has following space only + +
" `; exports[`collapses big diffs to patch format 1`] = ` @@ -71,32 +71,32 @@ exports[`collapses big diffs to patch format 1`] = ` + Received @@ -6,9 +6,9 @@ - 4, - 5, - 6, - 7, - 8, -+ 10, - 9, -- 10, - ], - }" + 4, + 5, + 6, + 7, + 8, ++ 10, + 9, +- 10, + ], + }" `; exports[`color of text (expanded) 1`] = ` "- Expected + Received - Object { - \\"searching\\": \\"\\", -- \\"sorting\\": Object { -+ \\"sorting\\": Array [ -+ Object { - \\"descending\\": false, - \\"fieldKey\\": \\"what\\", - }, -+ ], - }" + Object { + \\"searching\\": \\"\\", +- \\"sorting\\": Object { ++ \\"sorting\\": Array [ ++ Object { + \\"descending\\": false, + \\"fieldKey\\": \\"what\\", + }, ++ ], + }" `; exports[`context number of lines: -1 (5 default) 1`] = ` @@ -104,16 +104,16 @@ exports[`context number of lines: -1 (5 default) 1`] = ` + Received @@ -6,9 +6,9 @@ - 4, - 5, - 6, - 7, - 8, -+ 10, - 9, -- 10, - ], - }" + 4, + 5, + 6, + 7, + 8, ++ 10, + 9, +- 10, + ], + }" `; exports[`context number of lines: 0 1`] = ` @@ -121,9 +121,9 @@ exports[`context number of lines: 0 1`] = ` + Received @@ -11,0 +11,1 @@ -+ 10, ++ 10, @@ -12,1 +13,0 @@ -- 10," +- 10," `; exports[`context number of lines: 1 1`] = ` @@ -131,11 +131,11 @@ exports[`context number of lines: 1 1`] = ` + Received @@ -10,4 +10,4 @@ - 8, -+ 10, - 9, -- 10, - ]," + 8, ++ 10, + 9, +- 10, + ]," `; exports[`context number of lines: 2 1`] = ` @@ -143,13 +143,13 @@ exports[`context number of lines: 2 1`] = ` + Received @@ -9,6 +9,6 @@ - 7, - 8, -+ 10, - 9, -- 10, - ], - }" + 7, + 8, ++ 10, + 9, +- 10, + ], + }" `; exports[`context number of lines: null (5 default) 1`] = ` @@ -157,16 +157,16 @@ exports[`context number of lines: null (5 default) 1`] = ` + Received @@ -6,9 +6,9 @@ - 4, - 5, - 6, - 7, - 8, -+ 10, - 9, -- 10, - ], - }" + 4, + 5, + 6, + 7, + 8, ++ 10, + 9, +- 10, + ], + }" `; exports[`falls back to not call toJSON if objects look identical 1`] = ` @@ -176,11 +176,11 @@ exports[`falls back to not call toJSON if objects look identical 1`] = ` - Expected + Received - Object { -- \\"line\\": 1, -+ \\"line\\": 2, - \\"toJSON\\": [Function toJSON], - }" + Object { +- \\"line\\": 1, ++ \\"line\\": 2, + \\"toJSON\\": [Function toJSON], + }" `; exports[`prints a fallback message if two objects truly look identical 1`] = `"Compared values have no visual difference."`; diff --git a/packages/jest-diff/src/__tests__/diff.test.js b/packages/jest-diff/src/__tests__/diff.test.js index 125565b5cf79..b25b2dd7bf7d 100644 --- a/packages/jest-diff/src/__tests__/diff.test.js +++ b/packages/jest-diff/src/__tests__/diff.test.js @@ -124,11 +124,11 @@ line 2 line 3 line 4`; const expected = [ - ' line 1', - '-line 2', - '+line 2', - ' line 3', - ' line 4', + ' line 1', + '- line 2', + '+ line 2', + ' line 3', + ' line 4', ].join('\n'); test('(unexpanded)', () => { @@ -143,14 +143,14 @@ describe('objects', () => { const a = {a: {b: {c: 5}}}; const b = {a: {b: {c: 6}}}; const expected = [ - ' Object {', - ' "a": Object {', - ' "b": Object {', - '- "c": 5,', - '+ "c": 6,', - ' },', - ' },', - ' }', + ' Object {', + ' "a": Object {', + ' "b": Object {', + '- "c": 5,', + '+ "c": 6,', + ' },', + ' },', + ' }', ].join('\n'); test('(unexpanded)', () => { @@ -187,13 +187,13 @@ Options: failing test. [boolean] `; const expected = [ - ' Options:', - '---help, -h Show help [boolean]', - '---bail, -b Exit the test suite immediately upon the first', - '- failing test. [boolean]', - '+ --help, -h Show help [boolean]', - '+ --bail, -b Exit the test suite immediately upon the first', - '+ failing test. [boolean]', + ' Options:', + '- --help, -h Show help [boolean]', + '- --bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]', ].join('\n'); test('(unexpanded)', () => { @@ -223,13 +223,13 @@ Options: `; const expected = [ ' "', - ' Options:', - '---help, -h Show help [boolean]', - '---bail, -b Exit the test suite immediately upon the first', - '- failing test. [boolean]"', - '+ --help, -h Show help [boolean]', - '+ --bail, -b Exit the test suite immediately upon the first', - '+ failing test. [boolean]"', + ' Options:', + '- --help, -h Show help [boolean]', + '- --bail, -b Exit the test suite immediately upon the first', + '- failing test. [boolean]"', + '+ --help, -h Show help [boolean]', + '+ --bail, -b Exit the test suite immediately upon the first', + '+ failing test. [boolean]"', ].join('\n'); test('(unexpanded)', () => { @@ -258,12 +258,12 @@ describe('React elements', () => { type: 'div', }; const expected = [ - ' ', - '- Hello', - '+ Goodbye', - ' ', + ' ', + '- Hello', + '+ Goodbye', + ' ', ].join('\n'); test('(unexpanded)', () => { @@ -276,12 +276,12 @@ describe('React elements', () => { describe('multiline string as value of object property', () => { const expected = [ - ' Object {', - ' "id": "J",', - ' "points": "0.5,0.460', - '+0.5,0.875', - ' 0.25,0.875",', - ' }', + ' Object {', + ' "id": "J",', + ' "points": "0.5,0.460', + '+ 0.5,0.875', + ' 0.25,0.875",', + ' }', ].join('\n'); describe('(non-snapshot)', () => { @@ -343,17 +343,17 @@ describe('indentation in JavaScript structures', () => { describe('from less to more', () => { const expected = [ - ' Object {', - ' "searching": "",', - '- "sorting": Object {', - '+ "sorting": Array [', - '+ Object {', + ' Object {', + ' "searching": "",', + '- "sorting": Object {', + '+ "sorting": Array [', + '+ Object {', // following 3 lines are unchanged, except for more indentation - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '+ ],', - ' }', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '+ ],', + ' }', ].join('\n'); test('(unexpanded)', () => { @@ -366,17 +366,17 @@ describe('indentation in JavaScript structures', () => { describe('from more to less', () => { const expected = [ - ' Object {', - ' "searching": "",', - '- "sorting": Array [', - '- Object {', - '+ "sorting": Object {', + ' Object {', + ' "searching": "",', + '- "sorting": Array [', + '- Object {', + '+ "sorting": Object {', // following 3 lines are unchanged, except for less indentation - ' "descending": false,', - ' "fieldKey": "what",', - ' },', - '- ],', - ' }', + ' "descending": false,', + ' "fieldKey": "what",', + ' },', + '- ],', + ' }', ].join('\n'); test('(unexpanded)', () => { @@ -446,14 +446,14 @@ describe('indentation in React elements (non-snapshot)', () => { describe('from less to more', () => { const expected = [ - ' ', - '+ ', + ' ', + '+ ', // following 3 lines are unchanged, except for more indentation - ' ', - ' text', - ' ', - '+ ', - ' ', + ' ', + ' text', + ' ', + '+ ', + ' ', ].join('\n'); test('(unexpanded)', () => { @@ -466,14 +466,14 @@ describe('indentation in React elements (non-snapshot)', () => { describe('from more to less', () => { const expected = [ - ' ', - '- ', + ' ', + '- ', // following 3 lines are unchanged, except for less indentation - ' ', - ' text', - ' ', - '- ', - ' ', + ' ', + ' text', + ' ', + '- ', + ' ', ].join('\n'); test('(unexpanded)', () => { @@ -507,16 +507,16 @@ describe('indentation in React elements (snapshot)', () => { describe('from less to more', () => { // We intend to improve snapshot diff in the next version of Jest. const expected = [ - ' ', - '- ', - '- text', - '- ', - '+ ', - '+ ', - '+ text', - '+ ', - '+ ', - ' ', + ' ', + '- ', + '- text', + '- ', + '+ ', + '+ ', + '+ text', + '+ ', + '+ ', + ' ', ].join('\n'); test('(unexpanded)', () => { @@ -530,16 +530,16 @@ describe('indentation in React elements (snapshot)', () => { describe('from more to less', () => { // We intend to improve snapshot diff in the next version of Jest. const expected = [ - ' ', - '- ', - '- ', - '- text', - '- ', - '- ', - '+ ', - '+ text', - '+ ', - ' ', + ' ', + '- ', + '- ', + '- text', + '- ', + '- ', + '+ ', + '+ text', + '+ ', + ' ', ].join('\n'); test('(unexpanded)', () => { diff --git a/packages/jest-diff/src/diff_strings.js b/packages/jest-diff/src/diff_strings.js index 68cdd7da4646..32f03e859ffe 100644 --- a/packages/jest-diff/src/diff_strings.js +++ b/packages/jest-diff/src/diff_strings.js @@ -21,7 +21,7 @@ export type DiffOptions = {| contextLines?: number, |}; -export type Original = {| +type Original = {| a: string, b: string, |}; @@ -36,31 +36,50 @@ type Hunk = {| oldStart: number, |}; -type DIFF_D = -1 | 1 | 0; // diff digit: removed | added | equal +type DIFF_DIGIT = -1 | 1 | 0; // removed | added | equal + +// Given diff digit, return array which consists of: +// if compared line is removed or added: corresponding original line +// if compared line is equal: original received and expected lines +type GetOriginal = (digit: DIFF_DIGIT) => Array; // Given chunk, return diff character. -const getC = (chunk): string => (chunk.removed ? '-' : chunk.added ? '+' : ' '); +const getDiffChar = (chunk): string => + chunk.removed ? '-' : chunk.added ? '+' : ' '; -// Given diff character by getC from chunk or line from hunk, return diff digit. -const getD = (c: string): DIFF_D => (c === '-' ? -1 : c === '+' ? 1 : 0); +// Given diff character in line of hunk or computed from properties of chunk. +const getDiffDigit = (char: string): DIFF_DIGIT => + char === '-' ? -1 : char === '+' ? 1 : 0; // Color for text of line. -// If compared lines are equal and expected and received are data structures, -// then delta is difference in length of original lines. -const getColor = (d: DIFF_D, delta?: number) => - d === 1 ? chalk.red : d === -1 ? chalk.green : delta ? chalk.cyan : chalk.dim; +const getColor = (digit: DIFF_DIGIT, onlyIndentationChanged?: boolean) => { + if (digit === -1) { + return chalk.green; // removed + } + if (digit === 1) { + return chalk.red; // added + } + return onlyIndentationChanged ? chalk.cyan : chalk.dim; +}; // Do NOT color leading or trailing spaces if original lines are equal: // Background color for leading or trailing spaces. -const getBgColor = (d: DIFF_D) => - d === 1 ? chalk.bgRed : d === -1 ? chalk.bgGreen : chalk.bgCyan; +const getBgColor = (digit: DIFF_DIGIT) => { + if (digit === -1) { + return chalk.bgGreen; // removed + } + if (digit === 1) { + return chalk.bgRed; // added + } + return chalk.bgCyan; // only indentation changed +}; -// ONLY trailing if expected is snapshot or multiline string. +// ONLY trailing if expected value is snapshot or multiline string. const highlightTrailingWhitespace = (line: string, bgColor: Function): string => line.replace(/\s+$/, bgColor('$&')); -// BOTH leading AND trailing if expected and received are data structures. +// BOTH leading AND trailing if expected value is data structure. const highlightWhitespace = (line: string, bgColor: Function): string => highlightTrailingWhitespace(line.replace(/^\s+/, bgColor('$&')), bgColor); @@ -81,39 +100,39 @@ const splitIntoLines = string => { return lines; }; -// Given diff character and corresponding compared line, return line with colors -// and original indentation if compared with `indent: 0` option. +// Given diff character and compared line, return original line with colors. const formatLine = ( - c: string, + char: string, lineCompared: string, - getOriginal?: Function, + getOriginal?: GetOriginal, ) => { - const d = getD(c); + const digit = getDiffDigit(char); if (getOriginal) { - // getOriginal callback function if expected and received are data structures. - const gotOriginal = getOriginal(d); - const lineOriginal = d === 0 ? gotOriginal[1] : gotOriginal; - const lengthOriginal = lineOriginal.length; - // If compared lines are equal, - // then delta is difference in length of original lines. - const delta = d === 0 ? lengthOriginal - gotOriginal[0].length : 0; - return getColor(d, delta)( - c + - // Line was compared without its original indentation. - lineOriginal.slice(0, lengthOriginal - lineCompared.length) + - (d === 0 && delta === 0 - ? lineCompared - : highlightWhitespace(lineCompared, getBgColor(d))), + // Compared without indentation if expected value is data structure. + const lineArray = getOriginal(digit); + const lineOriginal = lineArray[0]; + const onlyIndentationChanged = + digit === 0 && lineOriginal.length !== lineArray[1].length; + + return getColor(digit, onlyIndentationChanged)( + char + + ' ' + + // Prepend indentation spaces from original to compared line. + lineOriginal.slice(0, lineOriginal.length - lineCompared.length) + + (digit !== 0 || onlyIndentationChanged + ? highlightWhitespace(lineCompared, getBgColor(digit)) + : lineCompared), ); } // Format compared line when expected is snapshot or multiline string. - return getColor(d)( - c + - (d === 0 - ? lineCompared - : highlightTrailingWhitespace(lineCompared, getBgColor(d))), + return getColor(digit)( + char + + ' ' + + (digit !== 0 + ? highlightTrailingWhitespace(lineCompared, getBgColor(digit)) + : lineCompared), ); }; @@ -124,17 +143,22 @@ const getterForChunks = (original: Original) => { const linesExpected = splitIntoLines(original.a); const linesReceived = splitIntoLines(original.b); - let indexExpected = 0; - let indexReceived = 0; - - return (d: DIFF_D) => - d === -1 - ? linesExpected[indexExpected++] - : d === 1 - ? linesReceived[indexReceived++] - : [linesExpected[indexExpected++], linesReceived[indexReceived++]]; + let iExpected = 0; + let iReceived = 0; + + return (digit: DIFF_DIGIT) => { + if (digit === -1) { + return [linesExpected[iExpected++]]; + } + if (digit === 1) { + return [linesReceived[iReceived++]]; + } + // Because compared line is equal: original received and expected lines. + return [linesReceived[iReceived++], linesExpected[iExpected++]]; + }; }; +// jest --expand const formatChunks = (a: string, b: string, original?: Original): Diff => { const getOriginal = original && getterForChunks(original); let isDifferent = false; @@ -146,10 +170,10 @@ const formatChunks = (a: string, b: string, original?: Original): Diff => { if (added || removed) { isDifferent = true; } - const c = getC(chunk); + const char = getDiffChar(chunk); return splitIntoLines(value) - .map(line => formatLine(c, line, getOriginal)) + .map(line => formatLine(char, line, getOriginal)) .join('\n'); }) .join('\n'), @@ -179,14 +203,19 @@ const getterForHunks = (original: Original) => { const linesExpected = splitIntoLines(original.a); const linesReceived = splitIntoLines(original.b); - return (indexExpected: number, indexReceived: number) => (d: DIFF_D) => - d === -1 - ? linesExpected[indexExpected++] - : d === 1 - ? linesReceived[indexReceived++] - : [linesExpected[indexExpected++], linesReceived[indexReceived++]]; + return (iExpected: number, iReceived: number) => (digit: DIFF_DIGIT) => { + if (digit === -1) { + return [linesExpected[iExpected++]]; + } + if (digit === 1) { + return [linesReceived[iReceived++]]; + } + // Because compared line is equal: original received and expected lines. + return [linesReceived[iReceived++], linesExpected[iExpected++]]; + }; }; +// jest --no-expand const formatHunks = ( a: string, b: string, diff --git a/packages/jest-matcher-utils/src/__tests__/__snapshots__/index.test.js.snap b/packages/jest-matcher-utils/src/__tests__/__snapshots__/index.test.js.snap index a25786610f30..b651fcfbe4ee 100644 --- a/packages/jest-matcher-utils/src/__tests__/__snapshots__/index.test.js.snap +++ b/packages/jest-matcher-utils/src/__tests__/__snapshots__/index.test.js.snap @@ -17,9 +17,9 @@ Difference: - Expected + Received - Object { -- \\"b\\": 1, -+ \\"a\\": 1, - \\"toJSON\\": [Function toJSON], - }" + Object { +- \\"b\\": 1, ++ \\"a\\": 1, + \\"toJSON\\": [Function toJSON], + }" `; From 519019f7700b9626036e8fe2ef8a3b2d71449749 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Wed, 30 Aug 2017 15:28:49 -0400 Subject: [PATCH 14/14] Edit redundant comments --- packages/jest-diff/src/diff_strings.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/packages/jest-diff/src/diff_strings.js b/packages/jest-diff/src/diff_strings.js index 32f03e859ffe..949a26d3cdd6 100644 --- a/packages/jest-diff/src/diff_strings.js +++ b/packages/jest-diff/src/diff_strings.js @@ -136,9 +136,8 @@ const formatLine = ( ); }; -// Given original lines, return callback function which given diff digit, -// returns either the corresponding expected OR received line, -// or if compared lines are equal, array of expected AND received line. +// Given original lines, return callback function +// which given diff digit, returns array. const getterForChunks = (original: Original) => { const linesExpected = splitIntoLines(original.a); const linesReceived = splitIntoLines(original.b); @@ -196,9 +195,7 @@ const createPatchMark = (hunk: Hunk): string => { }; // Given original lines, return callback function which given indexes for hunk, -// returns another callback function which given diff digit, -// returns either the corresponding expected OR received line, -// or if compared lines are equal, array of expected AND received line. +// returns another callback function which given diff digit, returns array. const getterForHunks = (original: Original) => { const linesExpected = splitIntoLines(original.a); const linesReceived = splitIntoLines(original.b);