diff --git a/CHANGELOG.md b/CHANGELOG.md index 60010fe140fc..1df79e17699c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -37,6 +37,7 @@ - `[jest-snapshot]` Remove only the added newlines in multiline snapshots ([#8859](https://github.com/facebook/jest/pull/8859)) - `[jest-snapshot]` Distinguish empty string from external snapshot not written ([#8880](https://github.com/facebook/jest/pull/8880)) - `[jest-snapshot]` [**BREAKING**] Distinguish empty string from internal snapshot not written ([#8898](https://github.com/facebook/jest/pull/8898)) +- `[jest-snapshot]` [**BREAKING**] Remove `report` method and throw matcher errors ([#9049](https://github.com/facebook/jest/pull/9049)) - `[jest-transform]` Properly cache transformed files across tests ([#8890](https://github.com/facebook/jest/pull/8890)) ### Chore & Maintenance diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index a051fd99db72..59336c948bce 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -196,7 +196,7 @@ test('handles invalid property matchers', () => { `, }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Property matchers must be an object.'); + expect(stderr).toMatch('Expected properties must be an object'); expect(exitCode).toBe(1); } { @@ -207,9 +207,9 @@ test('handles invalid property matchers', () => { `, }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Property matchers must be an object.'); + expect(stderr).toMatch('Expected properties must be an object'); expect(stderr).toMatch( - 'To provide a snapshot test name without property matchers, use: toMatchSnapshot("name")', + `To provide a hint without properties: toMatchSnapshot('hint')`, ); expect(exitCode).toBe(1); } @@ -221,9 +221,9 @@ test('handles invalid property matchers', () => { `, }); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Property matchers must be an object.'); + expect(stderr).toMatch('Expected properties must be an object'); expect(stderr).toMatch( - 'To provide a snapshot test name without property matchers, use: toMatchSnapshot("name")', + `To provide a hint without properties: toMatchSnapshot('hint')`, ); expect(exitCode).toBe(1); } diff --git a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts index 6ccdcdbd5e56..c795a8aeb14f 100644 --- a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts @@ -74,14 +74,16 @@ test('cannot be used with .not', () => { const filename = 'cannot-be-used-with-not.test.js'; const template = makeTemplate(` test('cannot be used with .not', () => { - expect('').not.toThrowErrorMatchingInlineSnapshot(); + expect(() => { throw new Error('apple'); }) + .not + .toThrowErrorMatchingInlineSnapshot(); }); `); { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('.not cannot be used with snapshot matchers'); + expect(stderr).toMatch('Snapshot matchers cannot be used with not'); expect(exitCode).toBe(1); } }); diff --git a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts index 1a9f398a65a4..ed579226a02f 100644 --- a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts @@ -67,14 +67,16 @@ test('accepts custom snapshot name', () => { test('cannot be used with .not', () => { const filename = 'cannot-be-used-with-not.test.js'; const template = makeTemplate(`test('cannot be used with .not', () => { - expect('').not.toThrowErrorMatchingSnapshot(); + expect(() => { throw new Error('apple'); }) + .not + .toThrowErrorMatchingSnapshot(); }); `); { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, exitCode} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('.not cannot be used with snapshot matchers'); + expect(stderr).toMatch('Snapshot matchers cannot be used with not'); expect(exitCode).toBe(1); } }); diff --git a/e2e/snapshot/__tests__/snapshot.test.js b/e2e/snapshot/__tests__/snapshot.test.js index f9d79bbd3ca2..22722a34f77d 100644 --- a/e2e/snapshot/__tests__/snapshot.test.js +++ b/e2e/snapshot/__tests__/snapshot.test.js @@ -30,7 +30,7 @@ describe('snapshot', () => { it('cannot be used with .not', () => { expect(() => expect('').not.toMatchSnapshot()).toThrow( - '.not cannot be used with snapshot matchers' + 'Snapshot matchers cannot be used with not' ); }); diff --git a/packages/jest-matcher-utils/src/index.ts b/packages/jest-matcher-utils/src/index.ts index 8229292934b6..2b0c470e1854 100644 --- a/packages/jest-matcher-utils/src/index.ts +++ b/packages/jest-matcher-utils/src/index.ts @@ -415,8 +415,11 @@ export const getLabelPrinter = (...strings: Array): PrintLabel => { export const matcherErrorMessage = ( hint: string, // assertion returned from call to matcherHint generic: string, // condition which correct value must fulfill - specific: string, // incorrect value returned from call to printWithType -) => `${hint}\n\n${chalk.bold('Matcher error')}: ${generic}\n\n${specific}`; + specific?: string, // incorrect value returned from call to printWithType +) => + `${hint}\n\n${chalk.bold('Matcher error')}: ${generic}${ + typeof specific === 'string' ? '\n\n' + specific : '' + }`; // Display assertion for the report when a test fails. // New format: rejects/resolves, not, and matcher name have black color diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap deleted file mode 100644 index 1a5d74cefabb..000000000000 --- a/packages/jest-snapshot/src/__tests__/__snapshots__/printDiffOrStringified.test.ts.snap +++ /dev/null @@ -1,295 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`backtick single line expected and received 1`] = ` -Snapshot: "var foo = \`backtick\`;" -Received: "var foo = tag\`backtick\`;" -`; - -exports[`empty string expected and received single line 1`] = ` -Snapshot: "" -Received: "single line string" -`; - -exports[`empty string received and expected multi line 1`] = ` -- Snapshot - 3 -+ Received + 0 - -- multi -- line -- string -`; - -exports[`escape backslash in multi line string 1`] = ` -- Snapshot - 1 -+ Received + 2 - -- Forward / slash and back \\ slash -+ Forward / slash -+ Back \\ slash -`; - -exports[`escape backslash in single line string 1`] = ` -Snapshot: "forward / slash and back \\\\ slash" -Received: "Forward / slash and back \\\\ slash" -`; - -exports[`escape double quote marks in string 1`] = ` -Snapshot: "What does \\"oobleck\\" mean?" -Received: "What does \\"ewbleck\\" mean?" -`; - -exports[`escape regexp 1`] = ` -Snapshot: /\\\\\\\\\\("\\)/g -Received: /\\\\\\\\\\("\\)/ -`; - -exports[`expand false 1`] = ` -- Snapshot - 1 -+ Received + 3 - -@@ -12,7 +12,9 @@ - ? "number" - : T extends boolean - ? "boolean" - : T extends undefined - ? "undefined" -- : T extends Function ? "function" : "object"; -+ : T extends Function -+ ? "function" -+ : "object"; - ↵ -`; - -exports[`expand true 1`] = ` -- Snapshot - 1 -+ Received + 3 - - type TypeName = - T extends string ? "string" : - T extends number ? "number" : - T extends boolean ? "boolean" : - T extends undefined ? "undefined" : - T extends Function ? "function" : - "object"; - ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - type TypeName = T extends string - ? "string" - : T extends number - ? "number" - : T extends boolean - ? "boolean" - : T extends undefined - ? "undefined" -- : T extends Function ? "function" : "object"; -+ : T extends Function -+ ? "function" -+ : "object"; - ↵ -`; - -exports[`fallback to line diff 1`] = ` -- Snapshot - 1 -+ Received + 8 - -+ ====================================options===================================== -+ parsers: ["flow", "typescript"] -+ printWidth: 80 -+ | printWidth -+ =====================================input====================================== - [...a, ...b,]; - [...a, ...b]; -- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -+ -+ =====================================output===================================== - [...a, ...b]; - [...a, ...b]; - -+ ================================================================================ -`; - -exports[`has no common after clean up chaff array 1`] = ` -- Snapshot - 2 -+ Received + 2 - - Array [ -- "delete", -- "two", -+ "insert", -+ "2", - ] -`; - -exports[`has no common after clean up chaff string single line 1`] = ` -Snapshot: "delete" -Received: "insert" -`; - -exports[`isLineDiffable false asymmetric matcher 1`] = ` -Snapshot: null -Received: Object { - "asymmetricMatch": [Function], -} -`; - -exports[`isLineDiffable false boolean 1`] = ` -Snapshot: true -Received: false -`; - -exports[`isLineDiffable false date 1`] = ` -Snapshot: 2019-09-19T00:00:00.000Z -Received: 2019-09-20T00:00:00.000Z -`; - -exports[`isLineDiffable false error 1`] = ` -Snapshot: [Error: Cannot spread fragment "NameAndAppearances" within itself.] -Received: [Error: Cannot spread fragment "NameAndAppearancesAndFriends" within itself.] -`; - -exports[`isLineDiffable false function 1`] = ` -Snapshot: undefined -Received: [Function] -`; - -exports[`isLineDiffable false number 1`] = ` -Snapshot: -0 -Received: NaN -`; - -exports[`isLineDiffable true array 1`] = ` -- Snapshot - 0 -+ Received + 2 - - Array [ - Object { -+ "_id": "b14680dec683e744ada1f2fe08614086", - "code": 4011, - "weight": 2.13, - }, - Object { -+ "_id": "7fc63ff01769c4fa7d9279e97e307829", - "code": 4019, - "count": 4, - }, - ] -`; - -exports[`isLineDiffable true object 1`] = ` -- Snapshot - 2 -+ Received + 3 - - Object { - "props": Object { -- "className": "logo", -- "src": "/img/jest.png", -+ "alt": "Jest logo", -+ "class": "logo", -+ "src": "/img/jest.svg", - }, - "type": "img", - } -`; - -exports[`isLineDiffable true single line expected and multi line received 1`] = ` -- Snapshot - 1 -+ Received + 3 - -- Array [] -+ Array [ -+ 0, -+ ] -`; - -exports[`isLineDiffable true single line expected and received 1`] = ` -- Snapshot - 1 -+ Received + 1 - -- Array [] -+ Object {} -`; - -exports[`multi line small change in one line and other is unchanged 1`] = ` -- Snapshot - 1 -+ Received + 1 - -- There is no route defined for key 'Settings'. -+ There is no route defined for key Settings. - Must be one of: 'Home' -`; - -exports[`multi line small changes 1`] = ` -- Snapshot - 7 -+ Received + 7 - -- 69 | -+ 68 | -- 70 | test('assert.doesNotThrow', () => { -+ 69 | test('assert.doesNotThrow', () => { -- > 71 | assert.doesNotThrow(() => { -+ > 70 | assert.doesNotThrow(() => { - | ^ -- 72 | throw Error('err!'); -+ 71 | throw Error('err!'); -- 73 | }); -+ 72 | }); -- 74 | }); -+ 73 | }); -- at Object.doesNotThrow (__tests__/assertionError.test.js:71:10) -+ at Object.doesNotThrow (__tests__/assertionError.test.js:70:10) -`; - -exports[`single line large changes 1`] = ` -Snapshot: "Array length must be a finite positive integer" -Received: "Invalid array length" -`; - -exports[`without serialize backtick single line expected and multi line received 1`] = ` -- Snapshot - 1 -+ Received + 2 - -- var foo = \`backtick\`; -+ var foo = \`back -+ tick\`; -`; - -exports[`without serialize backtick single line expected and received 1`] = ` -- Snapshot - 1 -+ Received + 1 - -- var foo = \`backtick\`; -+ var foo = \`back\${x}tick\`; -`; - -exports[`without serialize has no common after clean up chaff multi line 1`] = ` -- Snapshot - 2 -+ Received + 2 - -- delete -- two -+ insert -+ 2 -`; - -exports[`without serialize has no common after clean up chaff single line 1`] = ` -- Snapshot - 1 -+ Received + 1 - -- delete -+ insert -`; - -exports[`without serialize prettier/pull/5590 1`] = ` -- Snapshot - 1 -+ Received + 1 - -@@ -4,8 +4,8 @@ - | printWidth - =====================================input====================================== - John "ShotGun" Nelson - - =====================================output===================================== -- <i"John "ShotGun" Nelson" /> -+ <i'John "ShotGun" Nelson' /> - - ================================================================================ -`; diff --git a/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap new file mode 100644 index 000000000000..9d9621f48717 --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/__snapshots__/printSnapshot.test.ts.snap @@ -0,0 +1,494 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`matcher error toMatchInlineSnapshot Expected properties must be an object (non-null) without snapshot 1`] = ` +expect(received).toMatchInlineSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has type: boolean +Expected properties has value: false +`; + +exports[`matcher error toMatchInlineSnapshot Expected properties must be an object (null) with snapshot 1`] = ` +expect(received).toMatchInlineSnapshot(properties, snapshot) + +Matcher error: Expected properties must be an object + +Expected properties has value: null +`; + +exports[`matcher error toMatchInlineSnapshot Inline snapshot must be a string 1`] = ` +expect(received).resolves.toMatchInlineSnapshot(properties, snapshot) + +Matcher error: Inline snapshot must be a string + +Inline snapshot has type: symbol +Inline snapshot has value: Symbol(is not a string) +`; + +exports[`matcher error toMatchInlineSnapshot Snapshot matchers cannot be used with not 1`] = ` +expect(received).not.toMatchInlineSnapshot(snapshot) + +Matcher error: Snapshot matchers cannot be used with not +`; + +exports[`matcher error toMatchSnapshot Expected properties must be an object (non-null) 1`] = ` +expect(received).toMatchSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has type: function +Expected properties has value: [Function properties] +`; + +exports[`matcher error toMatchSnapshot Expected properties must be an object (null) with hint 1`] = ` +expect(received).toMatchSnapshot(properties, hint) + +Matcher error: Expected properties must be an object + +Expected properties has value: null + +To provide a hint without properties: toMatchSnapshot('hint') +`; + +exports[`matcher error toMatchSnapshot Expected properties must be an object (null) without hint 1`] = ` +expect(received).toMatchSnapshot(properties) + +Matcher error: Expected properties must be an object + +Expected properties has value: null +`; + +exports[`matcher error toMatchSnapshot Snapshot state must be initialized 1`] = ` +expect(received).resolves.toMatchSnapshot(hint) + +Snapshot state must be initialized + +Snapshot state has value: undefined +`; + +exports[`matcher error toThrowErrorMatchingInlineSnapshot Inline snapshot must be a string 1`] = ` +expect(received).toThrowErrorMatchingInlineSnapshot(snapshot) + +Matcher error: Inline snapshot must be a string + +Inline snapshot has type: number +Inline snapshot has value: 404 +`; + +exports[`matcher error toThrowErrorMatchingInlineSnapshot Snapshot state must be initialized 1`] = ` +expect(received).rejects.toThrowErrorMatchingInlineSnapshot(snapshot) + +Snapshot state must be initialized + +Snapshot state has value: undefined +`; + +exports[`matcher error toThrowErrorMatchingSnapshot Received value must be a function 1`] = ` +expect(received).toThrowErrorMatchingSnapshot() + +Matcher error: received value must be a function + +Received has type: number +Received has value: 13 +`; + +exports[`matcher error toThrowErrorMatchingSnapshot Snapshot matchers cannot be used with not 1`] = ` +expect(received).not.toThrowErrorMatchingSnapshot(hint) + +Matcher error: Snapshot matchers cannot be used with not +`; + +exports[`other error toThrowErrorMatchingSnapshot Received function did not throw 1`] = ` +expect(received).toThrowErrorMatchingSnapshot() + +Received function did not throw +`; + +exports[`pass false toMatchInlineSnapshot with properties equals false with snapshot 1`] = ` +expect(received).toMatchInlineSnapshot(properties, snapshot) + +Snapshot name: \`with properties 1\` + +Expected properties: {"id": "abcdef"} +Received value: {"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"} +`; + +exports[`pass false toMatchInlineSnapshot with properties equals false without snapshot 1`] = ` +expect(received).toMatchInlineSnapshot(properties) + +Snapshot name: \`with properties 1\` + +Expected properties: {"id": "abcdef"} +Received value: {"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"} +`; + +exports[`pass false toMatchInlineSnapshot with properties equals true 1`] = ` +expect(received).toMatchInlineSnapshot(properties, snapshot) + +Snapshot name: \`with properties 1\` + +- Snapshot - 1 ++ Received + 1 + + Object { + "id": "abcdef", +- "text": "inline snapshot", ++ "text": "received", + "type": "ADD_ITEM", + } +`; + +exports[`pass false toMatchSnapshot New snapshot was not written (multi line) 1`] = ` +expect(received).toMatchSnapshot(hint) + +Snapshot name: \`New snapshot was not written: (CI) 1\` + +New snapshot was not written. The update flag must be explicitly passed to write a new snapshot. + +This is likely because this test is run in a continuous integration (CI) environment in which snapshots are not written by default. + +Received: +"To write or not to write, +that is the question." +`; + +exports[`pass false toMatchSnapshot New snapshot was not written (single line) 1`] = ` +expect(received).toMatchSnapshot(hint) + +Snapshot name: \`New snapshot was not written: (CI) 2\` + +New snapshot was not written. The update flag must be explicitly passed to write a new snapshot. + +This is likely because this test is run in a continuous integration (CI) environment in which snapshots are not written by default. + +Received: "Write me if you can!" +`; + +exports[`pass false toMatchSnapshot with properties equals false 1`] = ` +expect(received).toMatchSnapshot(properties) + +Snapshot name: \`with properties 1\` + +Expected properties: {"id": "abcdef"} +Received value: {"id": "abcdefg", "text": "Increase code coverage", "type": "ADD_ITEM"} +`; + +exports[`pass false toMatchSnapshot with properties equals true 1`] = ` +expect(received).toMatchSnapshot(properties, hint) + +Snapshot name: \`with properties: change text value 1\` + +- Snapshot - 1 ++ Received + 1 + + Object { + "id": "abcdef", +- "text": "snapshot", ++ "text": "received", + "type": "ADD_ITEM", + } +`; + +exports[`pass false toThrowErrorMatchingInlineSnapshot with snapshot 1`] = ` +expect(received).toThrowErrorMatchingInlineSnapshot(snapshot) + +Snapshot name: \`with snapshot 1\` + +Snapshot: "inline snapshot" +Received: "received" +`; + +exports[`printDiffOrStringified backtick single line expected and received 1`] = ` +Snapshot: "var foo = \`backtick\`;" +Received: "var foo = tag\`backtick\`;" +`; + +exports[`printDiffOrStringified empty string expected and received single line 1`] = ` +Snapshot: "" +Received: "single line string" +`; + +exports[`printDiffOrStringified empty string received and expected multi line 1`] = ` +- Snapshot - 3 ++ Received + 0 + +- multi +- line +- string +`; + +exports[`printDiffOrStringified escape backslash in multi line string 1`] = ` +- Snapshot - 1 ++ Received + 2 + +- Forward / slash and back \\ slash ++ Forward / slash ++ Back \\ slash +`; + +exports[`printDiffOrStringified escape backslash in single line string 1`] = ` +Snapshot: "forward / slash and back \\\\ slash" +Received: "Forward / slash and back \\\\ slash" +`; + +exports[`printDiffOrStringified escape double quote marks in string 1`] = ` +Snapshot: "What does \\"oobleck\\" mean?" +Received: "What does \\"ewbleck\\" mean?" +`; + +exports[`printDiffOrStringified escape regexp 1`] = ` +Snapshot: /\\\\\\\\\\("\\)/g +Received: /\\\\\\\\\\("\\)/ +`; + +exports[`printDiffOrStringified expand false 1`] = ` +- Snapshot - 1 ++ Received + 3 + +@@ -12,7 +12,9 @@ + ? "number" + : T extends boolean + ? "boolean" + : T extends undefined + ? "undefined" +- : T extends Function ? "function" : "object"; ++ : T extends Function ++ ? "function" ++ : "object"; + ↵ +`; + +exports[`printDiffOrStringified expand true 1`] = ` +- Snapshot - 1 ++ Received + 3 + + type TypeName = + T extends string ? "string" : + T extends number ? "number" : + T extends boolean ? "boolean" : + T extends undefined ? "undefined" : + T extends Function ? "function" : + "object"; + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + type TypeName = T extends string + ? "string" + : T extends number + ? "number" + : T extends boolean + ? "boolean" + : T extends undefined + ? "undefined" +- : T extends Function ? "function" : "object"; ++ : T extends Function ++ ? "function" ++ : "object"; + ↵ +`; + +exports[`printDiffOrStringified fallback to line diff 1`] = ` +- Snapshot - 1 ++ Received + 8 + ++ ====================================options===================================== ++ parsers: ["flow", "typescript"] ++ printWidth: 80 ++ | printWidth ++ =====================================input====================================== + [...a, ...b,]; + [...a, ...b]; +- ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ++ ++ =====================================output===================================== + [...a, ...b]; + [...a, ...b]; + ++ ================================================================================ +`; + +exports[`printDiffOrStringified has no common after clean up chaff array 1`] = ` +- Snapshot - 2 ++ Received + 2 + + Array [ +- "delete", +- "two", ++ "insert", ++ "2", + ] +`; + +exports[`printDiffOrStringified has no common after clean up chaff string single line 1`] = ` +Snapshot: "delete" +Received: "insert" +`; + +exports[`printDiffOrStringified isLineDiffable false asymmetric matcher 1`] = ` +Snapshot: null +Received: Object { + "asymmetricMatch": [Function], +} +`; + +exports[`printDiffOrStringified isLineDiffable false boolean 1`] = ` +Snapshot: true +Received: false +`; + +exports[`printDiffOrStringified isLineDiffable false date 1`] = ` +Snapshot: 2019-09-19T00:00:00.000Z +Received: 2019-09-20T00:00:00.000Z +`; + +exports[`printDiffOrStringified isLineDiffable false error 1`] = ` +Snapshot: [Error: Cannot spread fragment "NameAndAppearances" within itself.] +Received: [Error: Cannot spread fragment "NameAndAppearancesAndFriends" within itself.] +`; + +exports[`printDiffOrStringified isLineDiffable false function 1`] = ` +Snapshot: undefined +Received: [Function] +`; + +exports[`printDiffOrStringified isLineDiffable false number 1`] = ` +Snapshot: -0 +Received: NaN +`; + +exports[`printDiffOrStringified isLineDiffable true array 1`] = ` +- Snapshot - 0 ++ Received + 2 + + Array [ + Object { ++ "_id": "b14680dec683e744ada1f2fe08614086", + "code": 4011, + "weight": 2.13, + }, + Object { ++ "_id": "7fc63ff01769c4fa7d9279e97e307829", + "code": 4019, + "count": 4, + }, + ] +`; + +exports[`printDiffOrStringified isLineDiffable true object 1`] = ` +- Snapshot - 2 ++ Received + 3 + + Object { + "props": Object { +- "className": "logo", +- "src": "/img/jest.png", ++ "alt": "Jest logo", ++ "class": "logo", ++ "src": "/img/jest.svg", + }, + "type": "img", + } +`; + +exports[`printDiffOrStringified isLineDiffable true single line expected and multi line received 1`] = ` +- Snapshot - 1 ++ Received + 3 + +- Array [] ++ Array [ ++ 0, ++ ] +`; + +exports[`printDiffOrStringified isLineDiffable true single line expected and received 1`] = ` +- Snapshot - 1 ++ Received + 1 + +- Array [] ++ Object {} +`; + +exports[`printDiffOrStringified multi line small change in one line and other is unchanged 1`] = ` +- Snapshot - 1 ++ Received + 1 + +- There is no route defined for key 'Settings'. ++ There is no route defined for key Settings. + Must be one of: 'Home' +`; + +exports[`printDiffOrStringified multi line small changes 1`] = ` +- Snapshot - 7 ++ Received + 7 + +- 69 | ++ 68 | +- 70 | test('assert.doesNotThrow', () => { ++ 69 | test('assert.doesNotThrow', () => { +- > 71 | assert.doesNotThrow(() => { ++ > 70 | assert.doesNotThrow(() => { + | ^ +- 72 | throw Error('err!'); ++ 71 | throw Error('err!'); +- 73 | }); ++ 72 | }); +- 74 | }); ++ 73 | }); +- at Object.doesNotThrow (__tests__/assertionError.test.js:71:10) ++ at Object.doesNotThrow (__tests__/assertionError.test.js:70:10) +`; + +exports[`printDiffOrStringified single line large changes 1`] = ` +Snapshot: "Array length must be a finite positive integer" +Received: "Invalid array length" +`; + +exports[`printDiffOrStringified without serialize backtick single line expected and multi line received 1`] = ` +- Snapshot - 1 ++ Received + 2 + +- var foo = \`backtick\`; ++ var foo = \`back ++ tick\`; +`; + +exports[`printDiffOrStringified without serialize backtick single line expected and received 1`] = ` +- Snapshot - 1 ++ Received + 1 + +- var foo = \`backtick\`; ++ var foo = \`back\${x}tick\`; +`; + +exports[`printDiffOrStringified without serialize has no common after clean up chaff multi line 1`] = ` +- Snapshot - 2 ++ Received + 2 + +- delete +- two ++ insert ++ 2 +`; + +exports[`printDiffOrStringified without serialize has no common after clean up chaff single line 1`] = ` +- Snapshot - 1 ++ Received + 1 + +- delete ++ insert +`; + +exports[`printDiffOrStringified without serialize prettier/pull/5590 1`] = ` +- Snapshot - 1 ++ Received + 1 + +@@ -4,8 +4,8 @@ + | printWidth + =====================================input====================================== + John "ShotGun" Nelson + + =====================================output===================================== +- <i"John "ShotGun" Nelson" /> ++ <i'John "ShotGun" Nelson' /> + + ================================================================================ +`; diff --git a/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts b/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts deleted file mode 100644 index 3651bd201834..000000000000 --- a/packages/jest-snapshot/src/__tests__/printDiffOrStringified.test.ts +++ /dev/null @@ -1,530 +0,0 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - */ - -import ansiRegex = require('ansi-regex'); -import * as style from 'ansi-styles'; -import chalk from 'chalk'; -import {printDiffOrStringified} from '../print'; -import {stringify} from '../utils'; - -// This is an experiment to read snapshots more easily: -// * to avoid first line misaligned because of opening double quote mark, -// return string without calling print function to serialize it, -// which also reduces extra escape sequences which is a subject of the tests! -// * to align lines, return tags which have same width -// * to see inline markup, return matching and tags -// * to see unexpected escape codes, do not return empty string as default - -const convertStyles = (val: string): string => - val.replace(ansiRegex(), match => { - switch (match) { - case style.inverse.open: - return ''; - case style.inverse.close: - return ''; - - case style.dim.open: - return ''; - case style.green.open: - return ''; - case style.red.open: - return ''; - case style.yellow.open: - return ''; - case style.bgYellow.open: - return ''; - - case style.dim.close: - case style.green.close: - case style.red.close: - case style.yellow.close: - case style.bgYellow.close: - return ''; - - default: - return match; - } - }); - -expect.addSnapshotSerializer({ - serialize(val: string): string { - return val; - }, - test(val: any): val is string { - return typeof val === 'string'; - }, -}); - -// Simulate default serialization. -const testWithStringify = ( - expected: unknown, - received: unknown, - expand: boolean, -): string => - convertStyles( - printDiffOrStringified( - stringify(expected), - stringify(received), - received, - expand, - ), - ); - -// Simulate custom raw string serialization. -const testWithoutStringify = ( - expected: string, - received: string, - expand: boolean, -): string => - convertStyles(printDiffOrStringified(expected, received, received, expand)); - -describe('backtick', () => { - test('single line expected and received', () => { - const expected = 'var foo = `backtick`;'; - const received = 'var foo = tag`backtick`;'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); -}); - -describe('empty string', () => { - test('expected and received single line', () => { - const expected = ''; - const received = 'single line string'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('received and expected multi line', () => { - const expected = 'multi\nline\nstring'; - const received = ''; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); -}); - -describe('escape', () => { - test('double quote marks in string', () => { - const expected = 'What does "oobleck" mean?'; - const received = 'What does "ewbleck" mean?'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('backslash in multi line string', () => { - const expected = 'Forward / slash and back \\ slash'; - const received = 'Forward / slash\nBack \\ slash'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('backslash in single line string', () => { - const expected = 'forward / slash and back \\ slash'; - const received = 'Forward / slash and back \\ slash'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('regexp', () => { - const expected = /\\(")/g; - const received = /\\(")/; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); -}); - -describe('expand', () => { - // prettier/pull/5272 - const expected = [ - 'type TypeName =', - 'T extends string ? "string" :', - 'T extends number ? "number" :', - 'T extends boolean ? "boolean" :', - 'T extends undefined ? "undefined" :', - 'T extends Function ? "function" :', - '"object";', - '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', - 'type TypeName = T extends string', - '? "string"', - ': T extends number', - '? "number"', - ': T extends boolean', - '? "boolean"', - ': T extends undefined', - '? "undefined"', - ': T extends Function ? "function" : "object";', - '', - ].join('\n'); - const received = [ - 'type TypeName =', - 'T extends string ? "string" :', - 'T extends number ? "number" :', - 'T extends boolean ? "boolean" :', - 'T extends undefined ? "undefined" :', - 'T extends Function ? "function" :', - '"object";', - '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', - 'type TypeName = T extends string', - '? "string"', - ': T extends number', - '? "number"', - ': T extends boolean', - '? "boolean"', - ': T extends undefined', - '? "undefined"', - ': T extends Function', - '? "function"', - ': "object";', - '', - ].join('\n'); - - test('false', () => { - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('true', () => { - expect(testWithStringify(expected, received, true)).toMatchSnapshot(); - }); -}); - -test('fallback to line diff', () => { - const expected = [ - '[...a, ...b,];', - '[...a, ...b];', - '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', - '[...a, ...b];', - '[...a, ...b];', - '', - ].join('\n'); - const received = [ - '====================================options=====================================', - 'parsers: ["flow", "typescript"]', - 'printWidth: 80', - ' | printWidth', - '=====================================input======================================', - '[...a, ...b,];', - '[...a, ...b];', - '', - '=====================================output=====================================', - '[...a, ...b];', - '[...a, ...b];', - '', - '================================================================================', - ].join('\n'); - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); -}); - -describe('has no common after clean up chaff', () => { - test('array', () => { - const expected = ['delete', 'two']; - const received = ['insert', '2']; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('string single line', () => { - const expected = 'delete'; - const received = 'insert'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); -}); - -describe('MAX_DIFF_STRING_LENGTH', () => { - describe('unquoted', () => { - // Do not call diffStringsUnified if either string is longer than max. - const lessChange = chalk.inverse('single '); - const less = 'single line'; - const more = 'multi line' + '\n123456789'.repeat(2000); // 10 + 20K chars - - test('both are less', () => { - const less2 = 'multi\nline'; - const difference = printDiffOrStringified(less2, less, less, true); - - expect(difference).toMatch('- multi'); - expect(difference).toMatch('- line'); - expect(difference).toMatch(lessChange); - expect(difference).not.toMatch('+ single line'); - }); - - test('expected is more', () => { - const difference = printDiffOrStringified(more, less, less, true); - - expect(difference).toMatch('- multi line'); - expect(difference).toMatch('+ single line'); - expect(difference).not.toMatch(lessChange); - }); - - test('received is more', () => { - const difference = printDiffOrStringified(less, more, more, true); - - expect(difference).toMatch('- single line'); - expect(difference).toMatch('+ multi line'); - expect(difference).not.toMatch(lessChange); - }); - }); - - describe('quoted', () => { - // Do not call diffStringsRaw if either string is longer than max. - const lessChange = chalk.inverse('no'); - const less = 'no numbers'; - const more = 'many numbers' + ' 123456789'.repeat(2000); // 12 + 20K chars - const lessQuoted = '"' + less + '"'; - const moreQuoted = '"' + more + '"'; - - test('both are less', () => { - const lessQuoted2 = '"0 numbers"'; - const stringified = printDiffOrStringified( - lessQuoted2, - lessQuoted, - less, - true, - ); - - expect(stringified).toMatch('Received:'); - expect(stringified).toMatch(lessChange); - expect(stringified).not.toMatch('+ Received'); - }); - - test('expected is more', () => { - const stringified = printDiffOrStringified( - moreQuoted, - lessQuoted, - less, - true, - ); - - expect(stringified).toMatch('Received:'); - expect(stringified).toMatch(less); - expect(stringified).not.toMatch('+ Received'); - expect(stringified).not.toMatch(lessChange); - }); - - test('received is more', () => { - const stringified = printDiffOrStringified( - lessQuoted, - moreQuoted, - more, - true, - ); - - expect(stringified).toMatch('Snapshot:'); - expect(stringified).toMatch(less); - expect(stringified).not.toMatch('- Snapshot'); - expect(stringified).not.toMatch(lessChange); - }); - }); -}); - -describe('isLineDiffable', () => { - describe('false', () => { - test('asymmetric matcher', () => { - const expected = null; - const received = {asymmetricMatch: () => {}}; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('boolean', () => { - const expected = true; - const received = false; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('date', () => { - const expected = new Date('2019-09-19'); - const received = new Date('2019-09-20'); - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('error', () => { - const expected = new Error( - 'Cannot spread fragment "NameAndAppearances" within itself.', - ); - const received = new Error( - 'Cannot spread fragment "NameAndAppearancesAndFriends" within itself.', - ); - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('function', () => { - const expected = undefined; - const received = () => {}; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('number', () => { - const expected = -0; - const received = NaN; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - }); - - describe('true', () => { - test('array', () => { - const expected0 = { - code: 4011, - weight: 2.13, - }; - const expected1 = { - code: 4019, - count: 4, - }; - - const expected = [expected0, expected1]; - const received = [ - {_id: 'b14680dec683e744ada1f2fe08614086', ...expected0}, - {_id: '7fc63ff01769c4fa7d9279e97e307829', ...expected1}, - ]; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('object', () => { - const type = 'img'; - const expected = { - props: { - className: 'logo', - src: '/img/jest.png', - }, - type, - }; - const received = { - props: { - alt: 'Jest logo', - class: 'logo', - src: '/img/jest.svg', - }, - type, - }; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('single line expected and received', () => { - const expected = []; - const received = {}; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('single line expected and multi line received', () => { - const expected = []; - const received = [0]; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); - }); - }); -}); - -test('multi line small change in one line and other is unchanged', () => { - const expected = - "There is no route defined for key 'Settings'.\nMust be one of: 'Home'"; - const received = - "There is no route defined for key Settings.\nMust be one of: 'Home'"; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); -}); - -test('multi line small changes', () => { - const expected = [ - ' 69 | ', - " 70 | test('assert.doesNotThrow', () => {", - ' > 71 | assert.doesNotThrow(() => {', - ' | ^', - " 72 | throw Error('err!');", - ' 73 | });', - ' 74 | });', - ' at Object.doesNotThrow (__tests__/assertionError.test.js:71:10)', - ].join('\n'); - const received = [ - ' 68 | ', - " 69 | test('assert.doesNotThrow', () => {", - ' > 70 | assert.doesNotThrow(() => {', - ' | ^', - " 71 | throw Error('err!');", - ' 72 | });', - ' 73 | });', - ' at Object.doesNotThrow (__tests__/assertionError.test.js:70:10)', - ].join('\n'); - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); -}); - -test('single line large changes', () => { - const expected = 'Array length must be a finite positive integer'; - const received = 'Invalid array length'; - - expect(testWithStringify(expected, received, false)).toMatchSnapshot(); -}); - -describe('without serialize', () => { - test('backtick single line expected and received', () => { - const expected = 'var foo = `backtick`;'; - const received = 'var foo = `back${x}tick`;'; - - expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('backtick single line expected and multi line received', () => { - const expected = 'var foo = `backtick`;'; - const received = 'var foo = `back\ntick`;'; - - expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('has no common after clean up chaff multi line', () => { - const expected = 'delete\ntwo'; - const received = 'insert\n2'; - - expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('has no common after clean up chaff single line', () => { - const expected = 'delete'; - const received = 'insert'; - - expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); - }); - - test('prettier/pull/5590', () => { - const expected = [ - '====================================options=====================================', - 'parsers: ["html"]', - 'printWidth: 80', - ' | printWidth', - '=====================================input======================================', - `John "ShotGun" Nelson`, - '', - '=====================================output=====================================', - 'John "ShotGun" Nelson', - '', - '================================================================================', - ].join('\n'); - const received = [ - '====================================options=====================================', - 'parsers: ["html"]', - 'printWidth: 80', - ' | printWidth', - '=====================================input======================================', - `John "ShotGun" Nelson`, - '', - '=====================================output=====================================', - `John "ShotGun" Nelson`, - '', - '================================================================================', - ].join('\n'); - - expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); - }); -}); diff --git a/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts new file mode 100644 index 000000000000..7f4898a756bd --- /dev/null +++ b/packages/jest-snapshot/src/__tests__/printSnapshot.test.ts @@ -0,0 +1,1034 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. All Rights Reserved. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + */ + +import ansiRegex = require('ansi-regex'); +import * as styles from 'ansi-styles'; +import chalk from 'chalk'; +import format = require('pretty-format'); + +import jestSnapshot = require('../index'); +import {printDiffOrStringified} from '../printSnapshot'; +import {stringify} from '../utils'; + +const convertAnsi = (val: string): string => + val.replace(ansiRegex(), match => { + switch (match) { + case styles.inverse.open: + return ''; + case styles.inverse.close: + return ''; + + case styles.bold.open: + return ''; + case styles.dim.open: + return ''; + case styles.green.open: + return ''; + case styles.red.open: + return ''; + case styles.yellow.open: + return ''; + case styles.bgYellow.open: + return ''; + + case styles.bold.close: + case styles.dim.close: + case styles.green.close: + case styles.red.close: + case styles.yellow.close: + case styles.bgYellow.close: + return ''; + + default: + return match; + } + }); + +expect.addSnapshotSerializer({ + serialize(val: string): string { + return convertAnsi(val); + }, + test(val: any): val is string { + return typeof val === 'string'; + }, +}); + +const { + toMatchInlineSnapshot, + toMatchSnapshot, + toThrowErrorMatchingInlineSnapshot, + toThrowErrorMatchingSnapshot, +} = jestSnapshot; + +describe('matcher error', () => { + describe('toMatchInlineSnapshot', () => { + const received = { + id: 'abcdef', + text: 'Throw matcher error', + type: 'ADD_ITEM', + }; + + test('Expected properties must be an object (non-null) without snapshot', () => { + const context = { + isNot: false, + promise: '', + }; + const properties = false; + + expect(() => { + toMatchInlineSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (null) with snapshot', () => { + const context = { + isNot: false, + promise: '', + }; + const properties = null; + const snapshot = ''; + + expect(() => { + toMatchInlineSnapshot.call(context, received, properties, snapshot); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Inline snapshot must be a string', () => { + const context = { + isNot: false, + promise: 'resolves', + }; + const properties = {}; + const snapshot = Symbol('is not a string'); + + expect(() => { + toMatchInlineSnapshot.call(context, received, properties, snapshot); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Snapshot matchers cannot be used with not', () => { + const context = { + isNot: true, + promise: '', + }; + const received = -13; + const snapshot = '13'; + + expect(() => { + toMatchInlineSnapshot.call(context, received, snapshot); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('toMatchSnapshot', () => { + const received = { + id: 'abcdef', + text: 'Throw matcher error', + type: 'ADD_ITEM', + }; + + test('Expected properties must be an object (non-null)', () => { + const context = { + isNot: false, + promise: '', + }; + const properties = () => {}; + + expect(() => { + toMatchSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (null) with hint', () => { + const context = { + isNot: false, + promise: '', + }; + const properties = null; + const hint = 'reminder'; + + expect(() => { + toMatchSnapshot.call(context, received, properties, hint); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Expected properties must be an object (null) without hint', () => { + const context = { + isNot: false, + promise: '', + }; + const properties = null; + + expect(() => { + toMatchSnapshot.call(context, received, properties); + }).toThrowErrorMatchingSnapshot(); + }); + + // Future test: Snapshot hint must be a string + + test('Snapshot state must be initialized', () => { + const context = { + isNot: false, + promise: 'resolves', + }; + const hint = 'initialize me'; + + expect(() => { + toMatchSnapshot.call(context, received, hint); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('toThrowErrorMatchingInlineSnapshot', () => { + test('Inline snapshot must be a string', () => { + const context = { + isNot: false, + promise: '', + }; + const received = () => { + throw new Error('Not found'); + }; + const snapshot = 404; + const fromPromise = false; + + expect(() => { + toThrowErrorMatchingInlineSnapshot.call( + context, + received, + snapshot, + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Snapshot state must be initialized', () => { + const context = { + isNot: false, + promise: 'rejects', + }; + const received = new Error('404'); + const snapshot = '"Not found"'; + const fromPromise = true; + + expect(() => { + toThrowErrorMatchingInlineSnapshot.call( + context, + received, + snapshot, + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + }); + + describe('toThrowErrorMatchingSnapshot', () => { + test('Received value must be a function', () => { + const context = { + isNot: false, + promise: '', + }; + const received = 13; + const fromPromise = false; + + expect(() => { + toThrowErrorMatchingSnapshot.call( + context, + received, + undefined, + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + + test('Snapshot matchers cannot be used with not', () => { + const context = { + isNot: true, + promise: '', + }; + const received = new Error('received'); + const hint = 'reminder'; + const fromPromise = true; + + expect(() => { + toThrowErrorMatchingSnapshot.call(context, received, hint, fromPromise); + }).toThrowErrorMatchingSnapshot(); + }); + + // Future test: Snapshot hint must be a string + }); +}); + +describe('other error', () => { + describe('toThrowErrorMatchingSnapshot', () => { + test('Received function did not throw', () => { + const context = { + isNot: false, + promise: '', + }; + const received = () => {}; + const fromPromise = false; + + expect(() => { + toThrowErrorMatchingSnapshot.call( + context, + received, + undefined, + fromPromise, + ); + }).toThrowErrorMatchingSnapshot(); + }); + }); +}); + +describe('pass false', () => { + describe('toMatchInlineSnapshot', () => { + describe('with properties', () => { + const id = 'abcdef'; + const properties = {id}; + const type = 'ADD_ITEM'; + + describe('equals false', () => { + const context = { + currentTestName: 'with properties', + equals: () => false, + isNot: false, + promise: '', + snapshotState: { + fail: fullTestName => fullTestName + ' 1', + }, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + }; + const received = { + id: 'abcdefg', + text: 'Increase code coverage', + type, + }; + + test('with snapshot', () => { + const snapshot = ''; + const {message, pass} = toMatchInlineSnapshot.call( + context, + received, + properties, + snapshot, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + + test('without snapshot', () => { + const {message, pass} = toMatchInlineSnapshot.call( + context, + received, + properties, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + }); + + test('equals true', () => { + const context = { + currentTestName: 'with properties', + equals: () => true, + isNot: false, + promise: '', + snapshotState: { + expand: false, + match({inlineSnapshot, received}) { + return { + actual: format(received), + count: 1, + expected: inlineSnapshot, + pass: false, + }; + }, + }, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + }; + const received = { + id, + text: 'received', + type, + }; + const snapshot = format({ + id, + text: 'inline snapshot', + type, + }); + + const {message, pass} = toMatchInlineSnapshot.call( + context, + received, + properties, + snapshot, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + }); + }); + + describe('toMatchSnapshot', () => { + test('New snapshot was not written (multi line)', () => { + const context = { + currentTestName: 'New snapshot was not written', + isNot: false, + promise: '', + snapshotState: { + match({received}) { + return { + actual: format(received), + count: 1, + expected: undefined, + pass: false, + }; + }, + }, + }; + const received = 'To write or not to write,\nthat is the question.'; + const hint = '(CI)'; + + const {message, pass} = toMatchSnapshot.call(context, received, hint); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + + test('New snapshot was not written (single line)', () => { + const context = { + currentTestName: 'New snapshot was not written', + isNot: false, + promise: '', + snapshotState: { + match({received}) { + return { + actual: format(received), + count: 2, + expected: undefined, + pass: false, + }; + }, + }, + }; + const received = 'Write me if you can!'; + const hint = '(CI)'; + + const {message, pass} = toMatchSnapshot.call(context, received, hint); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + + describe('with properties', () => { + const id = 'abcdef'; + const properties = {id}; + const type = 'ADD_ITEM'; + + test('equals false', () => { + const context = { + currentTestName: 'with properties', + equals: () => false, + isNot: false, + promise: '', + snapshotState: { + fail: fullTestName => fullTestName + ' 1', + }, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + }; + const received = { + id: 'abcdefg', + text: 'Increase code coverage', + type, + }; + + const {message, pass} = toMatchSnapshot.call( + context, + received, + properties, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + + test('equals true', () => { + const context = { + currentTestName: 'with properties', + equals: () => true, + isNot: false, + promise: '', + snapshotState: { + expand: false, + match({received}) { + return { + actual: format(received), + count: 1, + expected: format({ + id, + text: 'snapshot', + type, + }), + pass: false, + }; + }, + }, + utils: { + iterableEquality: () => {}, + subsetEquality: () => {}, + }, + }; + const received = { + id, + text: 'received', + type, + }; + const hint = 'change text value'; + + const {message, pass} = toMatchSnapshot.call( + context, + received, + properties, + hint, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + }); + }); + + describe('toThrowErrorMatchingInlineSnapshot', () => { + test('with snapshot', () => { + const context = { + currentTestName: 'with snapshot', + isNot: false, + promise: '', + snapshotState: { + expand: false, + match({inlineSnapshot, received}) { + return { + actual: format(received), + count: 1, + expected: inlineSnapshot, + pass: false, + }; + }, + }, + }; + const received = new Error('received'); + const snapshot = '"inline snapshot"'; + const fromPromise = true; + + const {message, pass} = toThrowErrorMatchingInlineSnapshot.call( + context, + received, + snapshot, + fromPromise, + ); + expect(pass).toBe(false); + expect(message()).toMatchSnapshot(); + }); + }); +}); + +describe('pass true', () => { + describe('toMatchSnapshot', () => { + test('without properties', () => { + const context = { + isNot: false, + promise: '', + snapshotState: { + match() { + return { + expected: '', + pass: true, + }; + }, + }, + }; + const received = 7; + + const {pass} = toMatchSnapshot.call(context, received); + expect(pass).toBe(true); + }); + }); +}); + +describe('printDiffOrStringified', () => { + // Simulate default serialization. + const testWithStringify = ( + expected: unknown, + received: unknown, + expand: boolean, + ): string => + printDiffOrStringified( + stringify(expected), + stringify(received), + received, + expand, + ); + + // Simulate custom raw string serialization. + const testWithoutStringify = ( + expected: string, + received: string, + expand: boolean, + ): string => printDiffOrStringified(expected, received, received, expand); + + describe('backtick', () => { + test('single line expected and received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = tag`backtick`;'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('empty string', () => { + test('expected and received single line', () => { + const expected = ''; + const received = 'single line string'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('received and expected multi line', () => { + const expected = 'multi\nline\nstring'; + const received = ''; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('escape', () => { + test('double quote marks in string', () => { + const expected = 'What does "oobleck" mean?'; + const received = 'What does "ewbleck" mean?'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('backslash in multi line string', () => { + const expected = 'Forward / slash and back \\ slash'; + const received = 'Forward / slash\nBack \\ slash'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('backslash in single line string', () => { + const expected = 'forward / slash and back \\ slash'; + const received = 'Forward / slash and back \\ slash'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('regexp', () => { + const expected = /\\(")/g; + const received = /\\(")/; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('expand', () => { + // prettier/pull/5272 + const expected = [ + 'type TypeName =', + 'T extends string ? "string" :', + 'T extends number ? "number" :', + 'T extends boolean ? "boolean" :', + 'T extends undefined ? "undefined" :', + 'T extends Function ? "function" :', + '"object";', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + 'type TypeName = T extends string', + '? "string"', + ': T extends number', + '? "number"', + ': T extends boolean', + '? "boolean"', + ': T extends undefined', + '? "undefined"', + ': T extends Function ? "function" : "object";', + '', + ].join('\n'); + const received = [ + 'type TypeName =', + 'T extends string ? "string" :', + 'T extends number ? "number" :', + 'T extends boolean ? "boolean" :', + 'T extends undefined ? "undefined" :', + 'T extends Function ? "function" :', + '"object";', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + 'type TypeName = T extends string', + '? "string"', + ': T extends number', + '? "number"', + ': T extends boolean', + '? "boolean"', + ': T extends undefined', + '? "undefined"', + ': T extends Function', + '? "function"', + ': "object";', + '', + ].join('\n'); + + test('false', () => { + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('true', () => { + expect(testWithStringify(expected, received, true)).toMatchSnapshot(); + }); + }); + + test('fallback to line diff', () => { + const expected = [ + '[...a, ...b,];', + '[...a, ...b];', + '~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~', + '[...a, ...b];', + '[...a, ...b];', + '', + ].join('\n'); + const received = [ + '====================================options=====================================', + 'parsers: ["flow", "typescript"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + '[...a, ...b,];', + '[...a, ...b];', + '', + '=====================================output=====================================', + '[...a, ...b];', + '[...a, ...b];', + '', + '================================================================================', + ].join('\n'); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + describe('has no common after clean up chaff', () => { + test('array', () => { + const expected = ['delete', 'two']; + const received = ['insert', '2']; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('string single line', () => { + const expected = 'delete'; + const received = 'insert'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('MAX_DIFF_STRING_LENGTH', () => { + describe('unquoted', () => { + // Do not call diffStringsUnified if either string is longer than max. + const lessChange = chalk.inverse('single '); + const less = 'single line'; + const more = 'multi line' + '\n123456789'.repeat(2000); // 10 + 20K chars + + test('both are less', () => { + const less2 = 'multi\nline'; + const difference = printDiffOrStringified(less2, less, less, true); + + expect(difference).toMatch('- multi'); + expect(difference).toMatch('- line'); + expect(difference).toMatch(lessChange); + expect(difference).not.toMatch('+ single line'); + }); + + test('expected is more', () => { + const difference = printDiffOrStringified(more, less, less, true); + + expect(difference).toMatch('- multi line'); + expect(difference).toMatch('+ single line'); + expect(difference).not.toMatch(lessChange); + }); + + test('received is more', () => { + const difference = printDiffOrStringified(less, more, more, true); + + expect(difference).toMatch('- single line'); + expect(difference).toMatch('+ multi line'); + expect(difference).not.toMatch(lessChange); + }); + }); + + describe('quoted', () => { + // Do not call diffStringsRaw if either string is longer than max. + const lessChange = chalk.inverse('no'); + const less = 'no numbers'; + const more = 'many numbers' + ' 123456789'.repeat(2000); // 12 + 20K chars + const lessQuoted = '"' + less + '"'; + const moreQuoted = '"' + more + '"'; + + test('both are less', () => { + const lessQuoted2 = '"0 numbers"'; + const stringified = printDiffOrStringified( + lessQuoted2, + lessQuoted, + less, + true, + ); + + expect(stringified).toMatch('Received:'); + expect(stringified).toMatch(lessChange); + expect(stringified).not.toMatch('+ Received'); + }); + + test('expected is more', () => { + const stringified = printDiffOrStringified( + moreQuoted, + lessQuoted, + less, + true, + ); + + expect(stringified).toMatch('Received:'); + expect(stringified).toMatch(less); + expect(stringified).not.toMatch('+ Received'); + expect(stringified).not.toMatch(lessChange); + }); + + test('received is more', () => { + const stringified = printDiffOrStringified( + lessQuoted, + moreQuoted, + more, + true, + ); + + expect(stringified).toMatch('Snapshot:'); + expect(stringified).toMatch(less); + expect(stringified).not.toMatch('- Snapshot'); + expect(stringified).not.toMatch(lessChange); + }); + }); + }); + + describe('isLineDiffable', () => { + describe('false', () => { + test('asymmetric matcher', () => { + const expected = null; + const received = {asymmetricMatch: () => {}}; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('boolean', () => { + const expected = true; + const received = false; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('date', () => { + const expected = new Date('2019-09-19'); + const received = new Date('2019-09-20'); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('error', () => { + const expected = new Error( + 'Cannot spread fragment "NameAndAppearances" within itself.', + ); + const received = new Error( + 'Cannot spread fragment "NameAndAppearancesAndFriends" within itself.', + ); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('function', () => { + const expected = undefined; + const received = () => {}; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('number', () => { + const expected = -0; + const received = NaN; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + + describe('true', () => { + test('array', () => { + const expected0 = { + code: 4011, + weight: 2.13, + }; + const expected1 = { + code: 4019, + count: 4, + }; + + const expected = [expected0, expected1]; + const received = [ + {_id: 'b14680dec683e744ada1f2fe08614086', ...expected0}, + {_id: '7fc63ff01769c4fa7d9279e97e307829', ...expected1}, + ]; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('object', () => { + const type = 'img'; + const expected = { + props: { + className: 'logo', + src: '/img/jest.png', + }, + type, + }; + const received = { + props: { + alt: 'Jest logo', + class: 'logo', + src: '/img/jest.svg', + }, + type, + }; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('single line expected and received', () => { + const expected = []; + const received = {}; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('single line expected and multi line received', () => { + const expected = []; + const received = [0]; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + }); + }); + + test('multi line small change in one line and other is unchanged', () => { + const expected = + "There is no route defined for key 'Settings'.\nMust be one of: 'Home'"; + const received = + "There is no route defined for key Settings.\nMust be one of: 'Home'"; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('multi line small changes', () => { + const expected = [ + ' 69 | ', + " 70 | test('assert.doesNotThrow', () => {", + ' > 71 | assert.doesNotThrow(() => {', + ' | ^', + " 72 | throw Error('err!');", + ' 73 | });', + ' 74 | });', + ' at Object.doesNotThrow (__tests__/assertionError.test.js:71:10)', + ].join('\n'); + const received = [ + ' 68 | ', + " 69 | test('assert.doesNotThrow', () => {", + ' > 70 | assert.doesNotThrow(() => {', + ' | ^', + " 71 | throw Error('err!');", + ' 72 | });', + ' 73 | });', + ' at Object.doesNotThrow (__tests__/assertionError.test.js:70:10)', + ].join('\n'); + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('single line large changes', () => { + const expected = 'Array length must be a finite positive integer'; + const received = 'Invalid array length'; + + expect(testWithStringify(expected, received, false)).toMatchSnapshot(); + }); + + describe('without serialize', () => { + test('backtick single line expected and received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = `back${x}tick`;'; + + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('backtick single line expected and multi line received', () => { + const expected = 'var foo = `backtick`;'; + const received = 'var foo = `back\ntick`;'; + + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('has no common after clean up chaff multi line', () => { + const expected = 'delete\ntwo'; + const received = 'insert\n2'; + + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('has no common after clean up chaff single line', () => { + const expected = 'delete'; + const received = 'insert'; + + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); + }); + + test('prettier/pull/5590', () => { + const expected = [ + '====================================options=====================================', + 'parsers: ["html"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + `John "ShotGun" Nelson`, + '', + '=====================================output=====================================', + 'John "ShotGun" Nelson', + '', + '================================================================================', + ].join('\n'); + const received = [ + '====================================options=====================================', + 'parsers: ["html"]', + 'printWidth: 80', + ' | printWidth', + '=====================================input======================================', + `John "ShotGun" Nelson`, + '', + '=====================================output=====================================', + `John "ShotGun" Nelson`, + '', + '================================================================================', + ].join('\n'); + + expect(testWithoutStringify(expected, received, false)).toMatchSnapshot(); + }); + }); +}); diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index 3e587f1a89b3..b83d1b1bd93b 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -8,13 +8,18 @@ import * as fs from 'fs'; import {Config} from '@jest/types'; import {FS as HasteFS} from 'jest-haste-map'; // eslint-disable-line import/no-extraneous-dependencies -import {MatcherState} from 'expect'; import { BOLD_WEIGHT, + EXPECTED_COLOR, MatcherHintOptions, RECEIVED_COLOR, + matcherErrorMessage, matcherHint, + printExpected, + printReceived, + printWithType, + stringify, } from 'jest-matcher-utils'; import { EXTENSION, @@ -24,39 +29,26 @@ import { } from './snapshot_resolver'; import SnapshotState from './State'; import {addSerializer, getSerializers} from './plugins'; -import {printDiffOrStringified} from './print'; +import { + PROPERTIES_ARG, + SNAPSHOT_ARG, + matcherHintFromConfig, + noColor, + printDiffOrStringified, +} from './printSnapshot'; +import {Context, MatchSnapshotConfig} from './types'; import * as utils from './utils'; -type Context = MatcherState & { - snapshotState: SnapshotState; -}; - -type MatchSnapshotConfig = { - context: Context; - expectedArgument: string; - hint?: string; - inlineSnapshot?: string; - isInline: boolean; - matcherName: string; - options: MatcherHintOptions; - propertyMatchers?: any; - received: any; -}; - const DID_NOT_THROW = 'Received function did not throw'; // same as toThrow -const NOT_SNAPSHOT_MATCHERS = `.${BOLD_WEIGHT( +const NOT_SNAPSHOT_MATCHERS = `Snapshot matchers cannot be used with ${BOLD_WEIGHT( 'not', -)} cannot be used with snapshot matchers`; +)}`; -const HINT_ARG = 'hint'; -const HINT_COLOR = BOLD_WEIGHT; -const INLINE_SNAPSHOT_ARG = 'snapshot'; -const PROPERTY_MATCHERS_ARG = 'properties'; const INDENTATION_REGEX = /^([^\S\n]*)\S/m; // Display name in report when matcher fails same as in snapshot file, // but with optional hint argument in bold weight. -const printName = ( +const printSnapshotName = ( concatenatedBlockNames = '', hint = '', count: number, @@ -65,7 +57,7 @@ const printName = ( const hasHint = hint.length !== 0; return ( - '`' + + 'Snapshot name: `' + (hasNames ? utils.escapeBacktickString(concatenatedBlockNames) : '') + (hasNames && hasHint ? ': ' : '') + (hasHint ? BOLD_WEIGHT(utils.escapeBacktickString(hint)) : '') + @@ -163,52 +155,57 @@ const cleanup = ( const toMatchSnapshot = function( this: Context, received: any, - propertyMatchers?: any, + propertiesOrHint?: object | Config.Path, hint?: Config.Path, ) { const matcherName = 'toMatchSnapshot'; - let expectedArgument = ''; - let secondArgument = ''; + let properties; + + const length = arguments.length; + if (length === 2 && typeof propertiesOrHint === 'string') { + hint = propertiesOrHint; + } else if (length >= 2) { + if (typeof propertiesOrHint !== 'object' || propertiesOrHint === null) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + let printedWithType = printWithType( + 'Expected properties', + propertiesOrHint, + printExpected, + ); + + if (length === 3) { + options.secondArgument = 'hint'; + options.secondArgumentColor = BOLD_WEIGHT; + + if (propertiesOrHint == null) { + printedWithType += `\n\nTo provide a hint without properties: toMatchSnapshot('hint')`; + } + } - if (typeof propertyMatchers === 'object' && propertyMatchers !== null) { - expectedArgument = PROPERTY_MATCHERS_ARG; - if (typeof hint === 'string' && hint.length !== 0) { - secondArgument = HINT_ARG; + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('properties')} must be an object`, + printedWithType, + ), + ); } - } else if ( - typeof propertyMatchers === 'string' && - propertyMatchers.length !== 0 - ) { - expectedArgument = HINT_ARG; - } - const options: MatcherHintOptions = { - isNot: this.isNot, - promise: this.promise, - secondArgument, - }; - - if (expectedArgument === HINT_ARG) { - options.expectedColor = HINT_COLOR; - } - if (secondArgument === HINT_ARG) { - options.secondArgumentColor = HINT_COLOR; - } + // Future breaking change: Snapshot hint must be a string + // if (arguments.length === 3 && typeof hint !== 'string') {} - if (arguments.length === 3 && !propertyMatchers) { - throw new Error( - 'Property matchers must be an object.\n\nTo provide a snapshot test name without property matchers, use: toMatchSnapshot("name")', - ); + properties = propertiesOrHint; } return _toMatchSnapshot({ context: this, - expectedArgument, hint, isInline: false, matcherName, - options, - propertyMatchers, + properties, received, }); }; @@ -216,81 +213,100 @@ const toMatchSnapshot = function( const toMatchInlineSnapshot = function( this: Context, received: any, - propertyMatchersOrInlineSnapshot?: any, + propertiesOrSnapshot?: object | string, inlineSnapshot?: string, ) { const matcherName = 'toMatchInlineSnapshot'; - let expectedArgument = ''; - let secondArgument = ''; - - if (typeof propertyMatchersOrInlineSnapshot === 'string') { - expectedArgument = INLINE_SNAPSHOT_ARG; - } else if ( - typeof propertyMatchersOrInlineSnapshot === 'object' && - propertyMatchersOrInlineSnapshot !== null - ) { - expectedArgument = PROPERTY_MATCHERS_ARG; - if (typeof inlineSnapshot === 'string') { - secondArgument = INLINE_SNAPSHOT_ARG; + let properties; + + const length = arguments.length; + if (length === 2 && typeof propertiesOrSnapshot === 'string') { + inlineSnapshot = propertiesOrSnapshot; + } else if (length >= 2) { + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + }; + if (length === 3) { + options.secondArgument = SNAPSHOT_ARG; + options.secondArgumentColor = noColor; } - } - const options: MatcherHintOptions = { - isNot: this.isNot, - promise: this.promise, - secondArgument, - }; + if ( + typeof propertiesOrSnapshot !== 'object' || + propertiesOrSnapshot === null + ) { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Expected ${EXPECTED_COLOR('properties')} must be an object`, + printWithType( + 'Expected properties', + propertiesOrSnapshot, + printExpected, + ), + ), + ); + } - let propertyMatchers; - if (typeof propertyMatchersOrInlineSnapshot === 'string') { - inlineSnapshot = propertyMatchersOrInlineSnapshot; - } else { - propertyMatchers = propertyMatchersOrInlineSnapshot; + if (length === 3 && typeof inlineSnapshot !== 'string') { + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, PROPERTIES_ARG, options), + `Inline snapshot must be a string`, + printWithType('Inline snapshot', inlineSnapshot, stringify), + ), + ); + } + + properties = propertiesOrSnapshot; } return _toMatchSnapshot({ context: this, - expectedArgument, inlineSnapshot: inlineSnapshot !== undefined ? stripAddedIndentation(inlineSnapshot) : undefined, isInline: true, matcherName, - options, - propertyMatchers, + properties, received, }); }; -const _toMatchSnapshot = ({ - context, - expectedArgument, - hint, - inlineSnapshot, - isInline, - matcherName, - options, - propertyMatchers, - received, -}: MatchSnapshotConfig) => { +const _toMatchSnapshot = (config: MatchSnapshotConfig) => { + const { + context, + hint, + inlineSnapshot, + isInline, + matcherName, + properties, + } = config; + let {received} = config; + context.dontThrow && context.dontThrow(); - hint = typeof propertyMatchers === 'string' ? propertyMatchers : hint; const {currentTestName, isNot, snapshotState} = context; if (isNot) { throw new Error( - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\n' + + matcherErrorMessage( + matcherHintFromConfig(config, false), NOT_SNAPSHOT_MATCHERS, + ), ); } - if (!snapshotState) { + if (snapshotState == null) { + // Because the state is the problem, this is not a matcher error. throw new Error( - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\nsnapshot state must be initialized', + matcherHintFromConfig(config, false) + + '\n\n' + + `Snapshot state must be initialized` + + '\n\n' + + printWithType('Snapshot state', snapshotState, stringify), ); } @@ -299,11 +315,8 @@ const _toMatchSnapshot = ({ ? `${currentTestName}: ${hint}` : currentTestName || ''; // future BREAKING change: || hint - if (typeof propertyMatchers === 'object') { - if (propertyMatchers === null) { - throw new Error(`Property matchers must be an object.`); - } - const propertyPass = context.equals(received, propertyMatchers, [ + if (typeof properties === 'object') { + const propertyPass = context.equals(received, properties, [ context.utils.iterableEquality, context.utils.subsetEquality, ]); @@ -313,25 +326,21 @@ const _toMatchSnapshot = ({ const matched = /(\d+)$/.exec(key); const count = matched === null ? 1 : Number(matched[1]); - const report = () => - `Snapshot name: ${printName(currentTestName, hint, count)}\n` + - '\n' + - `Expected properties: ${context.utils.printExpected( - propertyMatchers, - )}\n` + - `Received value: ${context.utils.printReceived(received)}`; + const message = () => + matcherHintFromConfig(config, false) + + '\n\n' + + printSnapshotName(currentTestName, hint, count) + + '\n\n' + + `Expected properties: ${printExpected(properties)}\n` + + `Received value: ${printReceived(received)}`; return { - message: () => - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\n' + - report(), + message, name: matcherName, pass: false, - report, }; } else { - received = utils.deepMerge(received, propertyMatchers); + received = utils.deepMerge(received, properties); } } @@ -342,39 +351,47 @@ const _toMatchSnapshot = ({ received, testName: fullTestName, }); - const {count, pass} = result; - const {actual, expected} = result; + const {actual, count, expected, pass} = result; - let report: () => string; if (pass) { return {message: () => '', pass: true}; - } else if (expected === undefined) { - report = () => - `New snapshot was ${RECEIVED_COLOR('not written')}. The update flag ` + - `must be explicitly passed to write a new snapshot.\n\n` + - `This is likely because this test is run in a continuous integration ` + - `(CI) environment in which snapshots are not written by default.\n\n` + - `${RECEIVED_COLOR('Received value')} ` + - `${actual}`; - } else { - report = () => - `Snapshot name: ${printName(currentTestName, hint, count)}\n\n` + - printDiffOrStringified(expected, actual, received, snapshotState.expand); } + const message = + expected === undefined + ? () => + matcherHintFromConfig(config, true) + + '\n\n' + + printSnapshotName(currentTestName, hint, count) + + '\n\n' + + `New snapshot was ${BOLD_WEIGHT('not written')}. The update flag ` + + `must be explicitly passed to write a new snapshot.\n\n` + + `This is likely because this test is run in a continuous integration ` + + `(CI) environment in which snapshots are not written by default.\n\n` + + `Received:${actual.includes('\n') ? '\n' : ' '}${RECEIVED_COLOR( + actual, + )}` + : () => + matcherHintFromConfig(config, true) + + '\n\n' + + printSnapshotName(currentTestName, hint, count) + + '\n\n' + + printDiffOrStringified( + expected, + actual, + received, + snapshotState.expand, + ); + // Passing the actual and expected objects so that a custom reporter // could access them, for example in order to display a custom visual diff, // or create a different error message return { actual, expected, - message: () => - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\n' + - report(), + message, name: matcherName, pass: false, - report, }; }; @@ -385,23 +402,16 @@ const toThrowErrorMatchingSnapshot = function( fromPromise: boolean, ) { const matcherName = 'toThrowErrorMatchingSnapshot'; - const expectedArgument = - typeof hint === 'string' && hint.length !== 0 ? HINT_ARG : ''; - const options = { - expectedColor: HINT_COLOR, - isNot: this.isNot, - promise: this.promise, - secondArgument: '', - }; + + // Future breaking change: Snapshot hint must be a string + // if (hint !== undefined && typeof hint !== string) {} return _toThrowErrorMatchingSnapshot( { context: this, - expectedArgument, hint, isInline: false, matcherName, - options, received, }, fromPromise, @@ -415,22 +425,29 @@ const toThrowErrorMatchingInlineSnapshot = function( fromPromise?: boolean, ) { const matcherName = 'toThrowErrorMatchingInlineSnapshot'; - const expectedArgument = - typeof inlineSnapshot === 'string' ? INLINE_SNAPSHOT_ARG : ''; - const options: MatcherHintOptions = { - isNot: this.isNot, - promise: this.promise, - secondArgument: '', - }; + + if (inlineSnapshot !== undefined && typeof inlineSnapshot !== 'string') { + const options: MatcherHintOptions = { + expectedColor: noColor, + isNot: this.isNot, + promise: this.promise, + }; + + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, SNAPSHOT_ARG, options), + `Inline snapshot must be a string`, + printWithType('Inline snapshot', inlineSnapshot, stringify), + ), + ); + } return _toThrowErrorMatchingSnapshot( { context: this, - expectedArgument, inlineSnapshot, isInline: true, matcherName, - options, received, }, fromPromise, @@ -438,26 +455,42 @@ const toThrowErrorMatchingInlineSnapshot = function( }; const _toThrowErrorMatchingSnapshot = ( - { + config: MatchSnapshotConfig, + fromPromise?: boolean, +) => { + const { context, - expectedArgument, + hint, inlineSnapshot, isInline, matcherName, - options, received, - hint, - }: MatchSnapshotConfig, - fromPromise?: boolean, -) => { + } = config; + context.dontThrow && context.dontThrow(); - const {isNot} = context; + + const {isNot, promise} = context; + + if (!fromPromise) { + if (typeof received !== 'function') { + const options: MatcherHintOptions = {isNot, promise}; + + throw new Error( + matcherErrorMessage( + matcherHint(matcherName, undefined, '', options), + `${RECEIVED_COLOR('received')} value must be a function`, + printWithType('Received', received, printReceived), + ), + ); + } + } if (isNot) { throw new Error( - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\n' + + matcherErrorMessage( + matcherHintFromConfig(config, false), NOT_SNAPSHOT_MATCHERS, + ), ); } @@ -474,21 +507,18 @@ const _toThrowErrorMatchingSnapshot = ( } if (error === undefined) { + // Because the received value is a function, this is not a matcher error. throw new Error( - matcherHint(matcherName, undefined, expectedArgument, options) + - '\n\n' + - DID_NOT_THROW, + matcherHintFromConfig(config, false) + '\n\n' + DID_NOT_THROW, ); } return _toMatchSnapshot({ context, - expectedArgument, hint, inlineSnapshot, isInline, matcherName, - options, received: error.message, }); }; diff --git a/packages/jest-snapshot/src/print.ts b/packages/jest-snapshot/src/printSnapshot.ts similarity index 75% rename from packages/jest-snapshot/src/print.ts rename to packages/jest-snapshot/src/printSnapshot.ts index b277c5fd51a8..853678217c2b 100644 --- a/packages/jest-snapshot/src/print.ts +++ b/packages/jest-snapshot/src/printSnapshot.ts @@ -17,14 +17,68 @@ import { } from 'jest-diff'; import getType = require('jest-get-type'); import { + BOLD_WEIGHT, EXPECTED_COLOR, INVERTED_COLOR, + MatcherHintOptions, RECEIVED_COLOR, getLabelPrinter, + matcherHint, } from 'jest-matcher-utils'; import prettyFormat = require('pretty-format'); +import {MatchSnapshotConfig} from './types'; import {unstringifyString} from './utils'; +export const noColor = (string: string) => string; + +export const HINT_ARG = 'hint'; +export const SNAPSHOT_ARG = 'snapshot'; +export const PROPERTIES_ARG = 'properties'; + +export const matcherHintFromConfig = ( + { + context: {isNot, promise}, + hint, + inlineSnapshot, + matcherName, + properties, + }: MatchSnapshotConfig, + isUpdatable: boolean, +): string => { + const options: MatcherHintOptions = {isNot, promise}; + + let expectedArgument = ''; + + if (typeof properties === 'object') { + expectedArgument = PROPERTIES_ARG; + if (isUpdatable) { + options.expectedColor = noColor; + } + + if (typeof hint === 'string' && hint.length !== 0) { + options.secondArgument = HINT_ARG; + options.secondArgumentColor = BOLD_WEIGHT; + } else if (typeof inlineSnapshot === 'string') { + options.secondArgument = SNAPSHOT_ARG; + if (!isUpdatable) { + options.secondArgumentColor = noColor; + } + } + } else { + if (typeof hint === 'string' && hint.length !== 0) { + expectedArgument = HINT_ARG; + options.expectedColor = BOLD_WEIGHT; + } else if (typeof inlineSnapshot === 'string') { + expectedArgument = SNAPSHOT_ARG; + if (!isUpdatable) { + options.expectedColor = noColor; + } + } + } + + return matcherHint(matcherName, undefined, expectedArgument, options); +}; + // Given array of diffs, return string: // * include common substrings // * exclude change substrings which have opposite op diff --git a/packages/jest-snapshot/src/types.ts b/packages/jest-snapshot/src/types.ts index 4cbc30eb7d8a..1b7d6e284b05 100644 --- a/packages/jest-snapshot/src/types.ts +++ b/packages/jest-snapshot/src/types.ts @@ -5,4 +5,21 @@ * LICENSE file in the root directory of this source tree. */ +import {MatcherState} from 'expect'; +import SnapshotState from './State'; + +export type Context = MatcherState & { + snapshotState: SnapshotState; +}; + +export type MatchSnapshotConfig = { + context: Context; + hint?: string; + inlineSnapshot?: string; + isInline: boolean; + matcherName: string; + properties?: object; + received: any; +}; + export type SnapshotData = Record;