From e04b034bd7b127238b7dc2e69e731d5651c67640 Mon Sep 17 00:00:00 2001 From: Mark Pedrotti Date: Tue, 19 Mar 2019 11:40:54 -0400 Subject: [PATCH] jest-snapshot: Improve report when matcher fails, part 14 (#8132) --- CHANGELOG.md | 1 + docs/ExpectAPI.md | 8 +- .../__snapshots__/failures.test.ts.snap | 62 ++-- .../watchModeUpdateSnapshot.test.ts.snap | 4 +- e2e/__tests__/failures.test.ts | 4 +- e2e/__tests__/toMatchInlineSnapshot.test.ts | 6 +- e2e/__tests__/toMatchSnapshot.test.ts | 26 +- ...toThrowErrorMatchingInlineSnapshot.test.ts | 4 +- .../toThrowErrorMatchingSnapshot.test.ts | 6 +- .../__snapshots__/snapshotNamed.test.js.snap | 3 - .../snapshotWithHint.test.js.snap | 3 + ...Named.test.js => snapshotWithHint.test.js} | 4 +- e2e/snapshot/__tests__/snapshot.test.js | 2 +- packages/jest-matcher-utils/src/index.ts | 1 + packages/jest-snapshot/src/index.ts | 264 +++++++++++++----- .../versioned_docs/version-24.0/ExpectAPI.md | 8 +- 16 files changed, 263 insertions(+), 143 deletions(-) delete mode 100644 e2e/failures/__tests__/__snapshots__/snapshotNamed.test.js.snap create mode 100644 e2e/failures/__tests__/__snapshots__/snapshotWithHint.test.js.snap rename e2e/failures/__tests__/{snapshotNamed.test.js => snapshotWithHint.test.js} (72%) diff --git a/CHANGELOG.md b/CHANGELOG.md index ea759c69356e..f74e62d52ead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ### Features +- `[jest-snapshot]` Improve report when matcher fails, part 14 ([#8132](https://github.com/facebook/jest/pull/8132)) - `[@jest/reporter]` Display todo and skip test descriptions when verbose is true ([#8038](https://github.com/facebook/jest/pull/8038)) ### Fixes diff --git a/docs/ExpectAPI.md b/docs/ExpectAPI.md index 1137cd20b202..114773883104 100644 --- a/docs/ExpectAPI.md +++ b/docs/ExpectAPI.md @@ -1125,13 +1125,13 @@ test('this house has my desired features', () => { }); ``` -### `.toMatchSnapshot(propertyMatchers?, snapshotName?)` +### `.toMatchSnapshot(propertyMatchers?, hint?)` This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](SnapshotTesting.md) for more information. You can provide an optional `propertyMatchers` object argument, which has asymmetric matchers as values of a subset of expected properties, **if** the received value will be an **object** instance. It is like `toMatchObject` with flexible criteria for a subset of properties, followed by a snapshot test as exact criteria for the rest of the properties. -You can provide an optional `snapshotName` string argument that is appended to the test name. Jest always appends a number at the end of a snapshot key to differentiate snapshots from a single `it` or `test` block. Jest sorts snapshots by key in the corresponding `.snap` file. +You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file. ### `.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)` @@ -1223,11 +1223,11 @@ test('throws on octopus', () => { > Note: You must wrap the code in a function, otherwise the error will not be caught and the assertion will fail. -### `.toThrowErrorMatchingSnapshot(snapshotName?)` +### `.toThrowErrorMatchingSnapshot(hint?)` Use `.toThrowErrorMatchingSnapshot` to test that a function throws an error matching the most recent snapshot when it is called. -You can provide an optional `snapshotName` string argument that is appended to the test name. Jest always appends a number at the end of a snapshot key to differentiate snapshots from a single `it` or `test` block. Jest sorts snapshots by key in the corresponding `.snap` file. +You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file. For example, let's say you have a `drinkFlavor` function that throws whenever the flavor is `'octopus'`, and is coded like this: diff --git a/e2e/__tests__/__snapshots__/failures.test.ts.snap b/e2e/__tests__/__snapshots__/failures.test.ts.snap index d6cdac0e6940..39f7e80eb37d 100644 --- a/e2e/__tests__/__snapshots__/failures.test.ts.snap +++ b/e2e/__tests__/__snapshots__/failures.test.ts.snap @@ -381,35 +381,6 @@ FAIL __tests__/asyncFailures.test.js at Object.test (__tests__/asyncFailures.test.js:23:1) `; -exports[`works with named snapshot failures 1`] = ` -FAIL __tests__/snapshotNamed.test.js - ✕ failing named snapshot - - ● failing named snapshot - - expect(value).toMatchSnapshot() - - Received value does not match stored snapshot "failing named snapshot: snapname 1". - - - Snapshot - + Received - - - "bar" - + "foo" - - 10 | - 11 | test('failing named snapshot', () => { - > 12 | expect('foo').toMatchSnapshot('snapname'); - | ^ - 13 | }); - 14 | - - at Object.toMatchSnapshot (__tests__/snapshotNamed.test.js:12:17) - - › 1 snapshot failed. - -`; - exports[`works with node assert 1`] = ` FAIL __tests__/assertionError.test.js ✕ assert @@ -819,9 +790,9 @@ FAIL __tests__/snapshot.test.js ● failing snapshot - expect(value).toMatchSnapshot() + expect(received).toMatchSnapshot() - Received value does not match stored snapshot "failing snapshot 1". + Snapshot name: \`failing snapshot 1\` - Snapshot + Received @@ -841,3 +812,32 @@ FAIL __tests__/snapshot.test.js › 1 snapshot failed. `; + +exports[`works with snapshot failures with hint 1`] = ` +FAIL __tests__/snapshotWithHint.test.js + ✕ failing snapshot with hint + + ● failing snapshot with hint + + expect(received).toMatchSnapshot(hint) + + Snapshot name: \`failing snapshot with hint: descriptive hint 1\` + + - Snapshot + + Received + + - "bar" + + "foo" + + 10 | + 11 | test('failing snapshot with hint', () => { + > 12 | expect('foo').toMatchSnapshot('descriptive hint'); + | ^ + 13 | }); + 14 | + + at Object.toMatchSnapshot (__tests__/snapshotWithHint.test.js:12:17) + + › 1 snapshot failed. + +`; diff --git a/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap b/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap index 7023c87719c2..db4e6ef9b129 100644 --- a/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap +++ b/e2e/__tests__/__snapshots__/watchModeUpdateSnapshot.test.ts.snap @@ -4,8 +4,8 @@ exports[`can press "u" to update snapshots: test results 1`] = ` FAIL __tests__/bar.spec.js ✕ bar ● bar - expect(value).toMatchSnapshot() - Received value does not match stored snapshot "bar 1". + expect(received).toMatchSnapshot() + Snapshot name: \`bar 1\` - Snapshot + Received - "foo" diff --git a/e2e/__tests__/failures.test.ts b/e2e/__tests__/failures.test.ts index 851bb3d1a39b..1fba31a77fae 100644 --- a/e2e/__tests__/failures.test.ts +++ b/e2e/__tests__/failures.test.ts @@ -173,8 +173,8 @@ test('works with snapshot failures', () => { ).toMatchSnapshot(); }); -test('works with named snapshot failures', () => { - const {stderr} = runJest(dir, ['snapshotNamed.test.js']); +test('works with snapshot failures with hint', () => { + const {stderr} = runJest(dir, ['snapshotWithHint.test.js']); const result = normalizeDots(cleanStderr(stderr)); diff --git a/e2e/__tests__/toMatchInlineSnapshot.test.ts b/e2e/__tests__/toMatchInlineSnapshot.test.ts index 034b928d508d..655a32d42c22 100644 --- a/e2e/__tests__/toMatchInlineSnapshot.test.ts +++ b/e2e/__tests__/toMatchInlineSnapshot.test.ts @@ -52,7 +52,7 @@ test('basic support', () => { }); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); const fileAfter = readFile(filename); - expect(stderr).toMatch('Received value does not match stored snapshot'); + expect(stderr).toMatch('Snapshot name: `inline snapshots 1`'); expect(status).toBe(1); expect(wrap(fileAfter)).toMatchSnapshot('snapshot mismatch'); } @@ -101,9 +101,7 @@ test('handles property matchers', () => { }); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); const fileAfter = readFile(filename); - expect(stderr).toMatch( - 'Received value does not match snapshot properties for "handles property matchers 1".', - ); + expect(stderr).toMatch('Snapshot name: `handles property matchers 1`'); expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); expect(status).toBe(1); expect(wrap(fileAfter)).toMatchSnapshot('snapshot failed'); diff --git a/e2e/__tests__/toMatchSnapshot.test.ts b/e2e/__tests__/toMatchSnapshot.test.ts index 2f9cdc115a45..9ee514bcd5a0 100644 --- a/e2e/__tests__/toMatchSnapshot.test.ts +++ b/e2e/__tests__/toMatchSnapshot.test.ts @@ -42,7 +42,7 @@ test('basic support', () => { [filename]: template(['{apple: "updated value"}']), }); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Received value does not match stored snapshot'); + expect(stderr).toMatch('Snapshot name: `snapshots 1`'); expect(status).toBe(1); } @@ -107,7 +107,7 @@ test('first snapshot fails, second passes', () => { { writeFiles(TESTS_DIR, {[filename]: template([`'kiwi'`, `'banana'`])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch('Received value does not match stored snapshot'); + expect(stderr).toMatch('Snapshot name: `snapshots 1`'); expect(stderr).toMatch('- "apple"\n + "kiwi"'); expect(stderr).not.toMatch('1 obsolete snapshot found'); expect(status).toBe(1); @@ -178,9 +178,7 @@ test('handles property matchers', () => { { writeFiles(TESTS_DIR, {[filename]: template(['"string"'])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch( - 'Received value does not match snapshot properties for "handles property matchers 1".', - ); + expect(stderr).toMatch('Snapshot name: `handles property matchers 1`'); expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); expect(status).toBe(1); } @@ -229,10 +227,10 @@ test('handles invalid property matchers', () => { } }); -test('handles property matchers with custom name', () => { - const filename = 'handle-property-matchers-with-name.test.js'; - const template = makeTemplate(`test('handles property matchers with name', () => { - expect({createdAt: $1}).toMatchSnapshot({createdAt: expect.any(Date)}, 'custom-name'); +test('handles property matchers with hint', () => { + const filename = 'handle-property-matchers-with-hint.test.js'; + const template = makeTemplate(`test('handles property matchers with hint', () => { + expect({createdAt: $1}).toMatchSnapshot({createdAt: expect.any(Date)}, 'descriptive hint'); }); `); @@ -253,9 +251,9 @@ test('handles property matchers with custom name', () => { writeFiles(TESTS_DIR, {[filename]: template(['"string"'])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch( - 'Received value does not match snapshot properties for "handles property matchers with name: custom-name 1".', + 'Snapshot name: `handles property matchers with hint: descriptive hint 1`', ); - expect(stderr).toMatch('Expected snapshot to match properties:'); + expect(stderr).toMatch('Expected properties:'); expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); expect(status).toBe(1); } @@ -285,9 +283,9 @@ test('handles property matchers with deep properties', () => { writeFiles(TESTS_DIR, {[filename]: template(['"string"', '"Jest"'])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch( - 'Received value does not match snapshot properties for "handles property matchers with deep properties 1".', + 'Snapshot name: `handles property matchers with deep properties 1`', ); - expect(stderr).toMatch('Expected snapshot to match properties:'); + expect(stderr).toMatch('Expected properties:'); expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); expect(status).toBe(1); } @@ -296,7 +294,7 @@ test('handles property matchers with deep properties', () => { writeFiles(TESTS_DIR, {[filename]: template(['new Date()', '"CHANGED"'])}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); expect(stderr).toMatch( - 'Received value does not match stored snapshot "handles property matchers with deep properties 1"', + 'Snapshot name: `handles property matchers with deep properties 1`', ); expect(stderr).toMatch('Snapshots: 1 failed, 1 total'); expect(status).toBe(1); diff --git a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts index 5c69136a6ca9..1f41477c78ce 100644 --- a/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingInlineSnapshot.test.ts @@ -81,9 +81,7 @@ test('cannot be used with .not', () => { { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch( - 'Jest: `.not` cannot be used with `.toThrowErrorMatchingInlineSnapshot()`.', - ); + expect(stderr).toMatch('.not cannot be used with snapshot matchers'); expect(status).toBe(1); } }); diff --git a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts index bd2ea58d67a5..8fbc274711e3 100644 --- a/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts +++ b/e2e/__tests__/toThrowErrorMatchingSnapshot.test.ts @@ -43,7 +43,7 @@ test(`throws the error if tested function didn't throw error`, () => { { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch(`Expected the function to throw an error.`); + expect(stderr).toMatch('Received function did not throw'); expect(status).toBe(1); } }); @@ -74,9 +74,7 @@ test('cannot be used with .not', () => { { writeFiles(TESTS_DIR, {[filename]: template()}); const {stderr, status} = runJest(DIR, ['-w=1', '--ci=false', filename]); - expect(stderr).toMatch( - 'Jest: `.not` cannot be used with `.toThrowErrorMatchingSnapshot()`.', - ); + expect(stderr).toMatch('.not cannot be used with snapshot matchers'); expect(status).toBe(1); } }); diff --git a/e2e/failures/__tests__/__snapshots__/snapshotNamed.test.js.snap b/e2e/failures/__tests__/__snapshots__/snapshotNamed.test.js.snap deleted file mode 100644 index 593f8e2e92d5..000000000000 --- a/e2e/failures/__tests__/__snapshots__/snapshotNamed.test.js.snap +++ /dev/null @@ -1,3 +0,0 @@ -// Jest Snapshot v1, https://goo.gl/fbAQLP - -exports[`failing named snapshot: snapname 1`] = `"bar"`; diff --git a/e2e/failures/__tests__/__snapshots__/snapshotWithHint.test.js.snap b/e2e/failures/__tests__/__snapshots__/snapshotWithHint.test.js.snap new file mode 100644 index 000000000000..1d2712c8f4af --- /dev/null +++ b/e2e/failures/__tests__/__snapshots__/snapshotWithHint.test.js.snap @@ -0,0 +1,3 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`failing snapshot with hint: descriptive hint 1`] = `"bar"`; diff --git a/e2e/failures/__tests__/snapshotNamed.test.js b/e2e/failures/__tests__/snapshotWithHint.test.js similarity index 72% rename from e2e/failures/__tests__/snapshotNamed.test.js rename to e2e/failures/__tests__/snapshotWithHint.test.js index 8cd4004187a6..be7ee8efdc53 100644 --- a/e2e/failures/__tests__/snapshotNamed.test.js +++ b/e2e/failures/__tests__/snapshotWithHint.test.js @@ -8,6 +8,6 @@ */ 'use strict'; -test('failing named snapshot', () => { - expect('foo').toMatchSnapshot('snapname'); +test('failing snapshot with hint', () => { + expect('foo').toMatchSnapshot('descriptive hint'); }); diff --git a/e2e/snapshot/__tests__/snapshot.test.js b/e2e/snapshot/__tests__/snapshot.test.js index 48ce35e33937..ec17021fe7f4 100644 --- a/e2e/snapshot/__tests__/snapshot.test.js +++ b/e2e/snapshot/__tests__/snapshot.test.js @@ -31,7 +31,7 @@ describe('snapshot', () => { it('cannot be used with .not', () => { expect(() => expect('').not.toMatchSnapshot()).toThrow( - 'Jest: `.not` cannot be used with `.toMatchSnapshot()`.' + '.not cannot be used with snapshot matchers' ); }); diff --git a/packages/jest-matcher-utils/src/index.ts b/packages/jest-matcher-utils/src/index.ts index 75e4b53bdcf2..89fd60568e6b 100644 --- a/packages/jest-matcher-utils/src/index.ts +++ b/packages/jest-matcher-utils/src/index.ts @@ -40,6 +40,7 @@ export {DiffOptions}; export const EXPECTED_COLOR = chalk.green; export const RECEIVED_COLOR = chalk.red; export const INVERTED_COLOR = chalk.inverse; +export const BOLD_WEIGHT = chalk.bold; const DIM_COLOR = chalk.dim; const NUMBERS = [ diff --git a/packages/jest-snapshot/src/index.ts b/packages/jest-snapshot/src/index.ts index dcb35ebd6daf..d468c7a7dc01 100644 --- a/packages/jest-snapshot/src/index.ts +++ b/packages/jest-snapshot/src/index.ts @@ -11,7 +11,13 @@ import {FS as HasteFS} from 'jest-haste-map'; import {MatcherState} from 'expect'; import diff from 'jest-diff'; -import {EXPECTED_COLOR, matcherHint, RECEIVED_COLOR} from 'jest-matcher-utils'; +import { + BOLD_WEIGHT, + EXPECTED_COLOR, + matcherHint, + MatcherHintOptions, + RECEIVED_COLOR, +} from 'jest-matcher-utils'; import { buildSnapshotResolver, isSnapshotPath, @@ -26,6 +32,47 @@ type Context = MatcherState & { snapshotState: SnapshotState; }; +type MatchSnapshotConfig = { + context: Context; + expectedArgument: string; + hint?: string; + inlineSnapshot?: string; + 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( + 'not', +)} cannot be used with snapshot matchers`; + +const HINT_ARG = BOLD_WEIGHT('hint'); +const INLINE_SNAPSHOT_ARG = 'snapshot'; +const PROPERTY_MATCHERS_ARG = 'properties'; + +// Display name in report when matcher fails same as in snapshot file, +// but with optional hint argument in bold weight. +const printName = ( + concatenatedBlockNames = '', + hint = '', + count: number, +): string => { + const hasNames = concatenatedBlockNames.length !== 0; + const hasHint = hint.length !== 0; + + return ( + '`' + + (hasNames ? utils.escapeBacktickString(concatenatedBlockNames) : '') + + (hasNames && hasHint ? ': ' : '') + + (hasHint ? BOLD_WEIGHT(utils.escapeBacktickString(hint)) : '') + + ' ' + + count + + '`' + ); +}; + const fileExists = (filePath: Config.Path, hasteFS: HasteFS): boolean => hasteFS.exists(filePath) || fs.existsSync(filePath); @@ -56,8 +103,30 @@ const toMatchSnapshot = function( this: Context, received: any, propertyMatchers?: any, - testName?: Config.Path, + hint?: Config.Path, ) { + const matcherName = 'toMatchSnapshot'; + let expectedArgument = ''; + let secondArgument = ''; + + if (typeof propertyMatchers === 'object' && propertyMatchers !== null) { + expectedArgument = PROPERTY_MATCHERS_ARG; + if (typeof hint === 'string' && hint.length !== 0) { + secondArgument = HINT_ARG; + } + } else if ( + typeof propertyMatchers === 'string' && + propertyMatchers.length !== 0 + ) { + expectedArgument = HINT_ARG; + } + + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + secondArgument, + }; + 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")', @@ -66,9 +135,12 @@ const toMatchSnapshot = function( return _toMatchSnapshot({ context: this, + expectedArgument, + hint, + matcherName, + options, propertyMatchers, received, - testName, }); }; @@ -78,15 +150,41 @@ const toMatchInlineSnapshot = function( propertyMatchersOrInlineSnapshot?: any, 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; + } + } + + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + secondArgument, + }; + let propertyMatchers; if (typeof propertyMatchersOrInlineSnapshot === 'string') { inlineSnapshot = propertyMatchersOrInlineSnapshot; } else { propertyMatchers = propertyMatchersOrInlineSnapshot; } + return _toMatchSnapshot({ context: this, + expectedArgument, inlineSnapshot: inlineSnapshot || '', + matcherName, + options, propertyMatchers, received, }); @@ -94,40 +192,38 @@ const toMatchInlineSnapshot = function( const _toMatchSnapshot = ({ context, - received, - propertyMatchers, - testName, + expectedArgument, + hint, inlineSnapshot, -}: { - context: Context; - received: any; - propertyMatchers?: any; - testName?: string; - inlineSnapshot?: string; -}) => { + matcherName, + options, + propertyMatchers, + received, +}: MatchSnapshotConfig) => { context.dontThrow && context.dontThrow(); - testName = typeof propertyMatchers === 'string' ? propertyMatchers : testName; + hint = typeof propertyMatchers === 'string' ? propertyMatchers : hint; const {currentTestName, isNot, snapshotState} = context; if (isNot) { - const matcherName = - typeof inlineSnapshot === 'string' - ? 'toMatchInlineSnapshot' - : 'toMatchSnapshot'; throw new Error( - `Jest: \`.not\` cannot be used with \`.${matcherName}()\`.`, + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\n' + + NOT_SNAPSHOT_MATCHERS, ); } if (!snapshotState) { - throw new Error('Jest: snapshot state must be initialized.'); + throw new Error( + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\nsnapshot state must be initialized', + ); } const fullTestName = - testName && currentTestName - ? `${currentTestName}: ${testName}` - : currentTestName || ''; + currentTestName && hint + ? `${currentTestName}: ${hint}` + : currentTestName || ''; // future BREAKING change: || hint if (typeof propertyMatchers === 'object') { if (propertyMatchers === null) { @@ -140,21 +236,23 @@ const _toMatchSnapshot = ({ if (!propertyPass) { const key = snapshotState.fail(fullTestName, received); + const matched = /(\d+)$/.exec(key); + const count = matched === null ? 1 : Number(matched[1]); const report = () => - `${RECEIVED_COLOR('Received value')} does not match ` + - `${EXPECTED_COLOR(`snapshot properties for "${key}"`)}.\n\n` + - `Expected snapshot to match properties:\n` + - ` ${context.utils.printExpected(propertyMatchers)}` + - `\nReceived:\n` + - ` ${context.utils.printReceived(received)}`; + `Snapshot name: ${printName(currentTestName, hint, count)}\n` + + '\n' + + `Expected properties: ${context.utils.printExpected( + propertyMatchers, + )}\n` + + `Received value: ${context.utils.printReceived(received)}`; return { message: () => - matcherHint('.toMatchSnapshot', 'value', 'properties') + + matcherHint(matcherName, undefined, expectedArgument, options) + '\n\n' + report(), - name: 'toMatchSnapshot', + name: matcherName, pass: false, report, }; @@ -169,7 +267,7 @@ const _toMatchSnapshot = ({ received, testName: fullTestName, }); - const {pass} = result; + const {count, pass} = result; let {actual, expected} = result; let report: () => string; @@ -193,8 +291,7 @@ const _toMatchSnapshot = ({ }); report = () => - `${RECEIVED_COLOR('Received value')} does not match ` + - `${EXPECTED_COLOR(`stored snapshot "${result.key}"`)}.\n\n` + + `Snapshot name: ${printName(currentTestName, hint, count)}\n\n` + (diffMessage || EXPECTED_COLOR('- ' + (expected || '')) + '\n' + @@ -207,8 +304,10 @@ const _toMatchSnapshot = ({ actual, expected, message: () => - matcherHint('.toMatchSnapshot', 'value', '') + '\n\n' + report(), - name: 'toMatchSnapshot', + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\n' + + report(), + name: matcherName, pass: false, report, }; @@ -217,15 +316,29 @@ const _toMatchSnapshot = ({ const toThrowErrorMatchingSnapshot = function( this: Context, received: any, - testName: string | undefined, + hint: string | undefined, // because error TS1016 for hint?: string fromPromise: boolean, ) { - return _toThrowErrorMatchingSnapshot({ - context: this, + const matcherName = 'toThrowErrorMatchingSnapshot'; + const expectedArgument = + typeof hint === 'string' && hint.length !== 0 ? HINT_ARG : ''; + const options = { + isNot: this.isNot, + promise: this.promise, + secondArgument: '', + }; + + return _toThrowErrorMatchingSnapshot( + { + context: this, + expectedArgument, + hint, + matcherName, + options, + received, + }, fromPromise, - received, - testName, - }); + ); }; const toThrowErrorMatchingInlineSnapshot = function( @@ -234,37 +347,48 @@ const toThrowErrorMatchingInlineSnapshot = function( inlineSnapshot?: string, fromPromise?: boolean, ) { - return _toThrowErrorMatchingSnapshot({ - context: this, + const matcherName = 'toThrowErrorMatchingInlineSnapshot'; + const expectedArgument = + typeof inlineSnapshot === 'string' ? INLINE_SNAPSHOT_ARG : ''; + const options: MatcherHintOptions = { + isNot: this.isNot, + promise: this.promise, + secondArgument: '', + }; + + return _toThrowErrorMatchingSnapshot( + { + context: this, + expectedArgument, + inlineSnapshot: inlineSnapshot || '', + matcherName, + options, + received, + }, fromPromise, - inlineSnapshot: inlineSnapshot || '', - received, - }); + ); }; -const _toThrowErrorMatchingSnapshot = ({ - context, - received, - testName, - fromPromise, - inlineSnapshot, -}: { - context: Context; - received: any; - testName?: string; - fromPromise?: boolean; - inlineSnapshot?: string; -}) => { +const _toThrowErrorMatchingSnapshot = ( + { + context, + expectedArgument, + inlineSnapshot, + matcherName, + options, + received, + hint, + }: MatchSnapshotConfig, + fromPromise?: boolean, +) => { context.dontThrow && context.dontThrow(); const {isNot} = context; - const matcherName = - typeof inlineSnapshot === 'string' - ? 'toThrowErrorMatchingInlineSnapshot' - : 'toThrowErrorMatchingSnapshot'; if (isNot) { throw new Error( - `Jest: \`.not\` cannot be used with \`.${matcherName}()\`.`, + matcherHint(matcherName, undefined, expectedArgument, options) + + '\n\n' + + NOT_SNAPSHOT_MATCHERS, ); } @@ -282,18 +406,20 @@ const _toThrowErrorMatchingSnapshot = ({ if (error === undefined) { throw new Error( - matcherHint(`.${matcherName}`, '() => {}', '') + + matcherHint(matcherName, undefined, expectedArgument, options) + '\n\n' + - `Expected the function to throw an error.\n` + - `But it didn't throw anything.`, + DID_NOT_THROW, ); } return _toMatchSnapshot({ context, + expectedArgument, + hint, inlineSnapshot, + matcherName, + options, received: error.message, - testName, }); }; diff --git a/website/versioned_docs/version-24.0/ExpectAPI.md b/website/versioned_docs/version-24.0/ExpectAPI.md index 4de79675d94d..dd6828349c4c 100644 --- a/website/versioned_docs/version-24.0/ExpectAPI.md +++ b/website/versioned_docs/version-24.0/ExpectAPI.md @@ -1126,13 +1126,13 @@ test('this house has my desired features', () => { }); ``` -### `.toMatchSnapshot(propertyMatchers?, snapshotName?)` +### `.toMatchSnapshot(propertyMatchers?, hint?)` This ensures that a value matches the most recent snapshot. Check out [the Snapshot Testing guide](SnapshotTesting.md) for more information. You can provide an optional `propertyMatchers` object argument, which has asymmetric matchers as values of a subset of expected properties, **if** the received value will be an **object** instance. It is like `toMatchObject` with flexible criteria for a subset of properties, followed by a snapshot test as exact criteria for the rest of the properties. -You can provide an optional `snapshotName` string argument that is appended to the test name. Jest always appends a number at the end of a snapshot key to differentiate snapshots from a single `it` or `test` block. Jest sorts snapshots by key in the corresponding `.snap` file. +You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file. ### `.toMatchInlineSnapshot(propertyMatchers?, inlineSnapshot)` @@ -1224,11 +1224,11 @@ test('throws on octopus', () => { > Note: You must wrap the code in a function, otherwise the error will not be caught and the assertion will fail. -### `.toThrowErrorMatchingSnapshot(snapshotName?)` +### `.toThrowErrorMatchingSnapshot(hint?)` Use `.toThrowErrorMatchingSnapshot` to test that a function throws an error matching the most recent snapshot when it is called. -You can provide an optional `snapshotName` string argument that is appended to the test name. Jest always appends a number at the end of a snapshot key to differentiate snapshots from a single `it` or `test` block. Jest sorts snapshots by key in the corresponding `.snap` file. +You can provide an optional `hint` string argument that is appended to the test name. Although Jest always appends a number at the end of a snapshot name, short descriptive hints might be more useful than numbers to differentiate **multiple** snapshots in a **single** `it` or `test` block. Jest sorts snapshots by name in the corresponding `.snap` file. For example, let's say you have a `drinkFlavor` function that throws whenever the flavor is `'octopus'`, and is coded like this: